Fix Pi package updates and merge feynman-model
This commit is contained in:
@@ -95,3 +95,12 @@ Use this file to track chronology, not release notes. Keep entries short, factua
|
||||
- Failed / learned: An initial local `build:native-bundle` check failed because `npm pack` and `build:native-bundle` were run in parallel, and `prepack` intentionally removes `dist/release`; rerunning `npm run build:native-bundle` sequentially succeeded.
|
||||
- Blockers: None in the repo; publishing still depends on the GitHub workflow running on the bumped version.
|
||||
- Next: Push the `0.2.16` release bump and monitor npm/GitHub release publication.
|
||||
|
||||
### 2026-03-31 10:45 PDT — pi-maintenance-issues-prs
|
||||
|
||||
- Objective: Triage open Pi-related issues/PRs, fix the concrete package update regression, and refresh Pi dependencies against current upstream releases.
|
||||
- Changed: Pinned direct package-manager operations (`feynman update`, `feynman packages install`) to Feynman's npm prefix by exporting `FEYNMAN_NPM_PREFIX`, `NPM_CONFIG_PREFIX`, and `npm_config_prefix` before invoking Pi's `DefaultPackageManager`; bumped `@mariozechner/pi-ai` and `@mariozechner/pi-coding-agent` from `0.62.0` to `0.64.0`; adapted `src/model/registry.ts` to the new `ModelRegistry.create(...)` factory; integrated PR #15's `/feynman-model` command on top of current `main`.
|
||||
- Verified: Ran `npm test`, `npm run typecheck`, and `npm run build` successfully after the dependency bump and PR integration; confirmed upstream `pi-coding-agent@0.64.0` still uses `npm install -g` for user-scope package updates, so the Feynman-side prefix fix is still required.
|
||||
- Failed / learned: PR #14 is a stale branch with no clean merge path against current `main`; the only user-facing delta is the ValiChord prompt/skill addition, and the branch also carries unrelated release churn plus demo-style material, so it was not merged in this pass.
|
||||
- Blockers: None in the local repo state; remote merge/push still depends on repository credentials and branch policy.
|
||||
- Next: If remote write access is available, commit and push the validated maintenance changes, then close issue #22 and resolve PR #15 as merged while leaving PR #14 unmerged pending a cleaned-up, non-promotional resubmission.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
import { registerAlphaTools } from "./research-tools/alpha.js";
|
||||
import { registerFeynmanModelCommand } from "./research-tools/feynman-model.js";
|
||||
import { installFeynmanHeader } from "./research-tools/header.js";
|
||||
import { registerHelpCommand } from "./research-tools/help.js";
|
||||
import { registerInitCommand, registerOutputsCommand } from "./research-tools/project.js";
|
||||
@@ -17,6 +18,7 @@ export default function researchTools(pi: ExtensionAPI): void {
|
||||
});
|
||||
|
||||
registerAlphaTools(pi);
|
||||
registerFeynmanModelCommand(pi);
|
||||
registerHelpCommand(pi);
|
||||
registerInitCommand(pi);
|
||||
registerOutputsCommand(pi);
|
||||
|
||||
309
extensions/research-tools/feynman-model.ts
Normal file
309
extensions/research-tools/feynman-model.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import { type Dirent, existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { basename, join, resolve } from "node:path";
|
||||
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
const FRONTMATTER_PATTERN = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/;
|
||||
const INHERIT_MAIN = "__inherit_main__";
|
||||
|
||||
type FrontmatterDocument = {
|
||||
lines: string[];
|
||||
body: string;
|
||||
eol: string;
|
||||
trailingNewline: boolean;
|
||||
};
|
||||
|
||||
type SubagentModelConfig = {
|
||||
agent: string;
|
||||
model?: string;
|
||||
filePath: string;
|
||||
};
|
||||
|
||||
type SelectOption<T> = {
|
||||
label: string;
|
||||
value: T;
|
||||
};
|
||||
|
||||
type CommandContext = Parameters<Parameters<ExtensionAPI["registerCommand"]>[1]["handler"]>[1];
|
||||
|
||||
type TargetChoice =
|
||||
| { type: "main" }
|
||||
| { type: "subagent"; agent: string; model?: string };
|
||||
|
||||
function expandHomePath(value: string): string {
|
||||
if (value === "~") return homedir();
|
||||
if (value.startsWith("~/")) return resolve(homedir(), value.slice(2));
|
||||
return value;
|
||||
}
|
||||
|
||||
function resolveFeynmanAgentDir(): string {
|
||||
const configured = process.env.PI_CODING_AGENT_DIR ?? process.env.FEYNMAN_CODING_AGENT_DIR;
|
||||
if (configured?.trim()) {
|
||||
return resolve(expandHomePath(configured.trim()));
|
||||
}
|
||||
return resolve(homedir(), ".feynman", "agent");
|
||||
}
|
||||
|
||||
function formatModelSpec(model: { provider: string; id: string }): string {
|
||||
return `${model.provider}/${model.id}`;
|
||||
}
|
||||
|
||||
function detectEol(text: string): string {
|
||||
return text.includes("\r\n") ? "\r\n" : "\n";
|
||||
}
|
||||
|
||||
function normalizeLineEndings(text: string): string {
|
||||
return text.replace(/\r\n/g, "\n");
|
||||
}
|
||||
|
||||
function parseFrontmatterDocument(text: string): FrontmatterDocument | null {
|
||||
const normalized = normalizeLineEndings(text);
|
||||
const match = normalized.match(FRONTMATTER_PATTERN);
|
||||
if (!match) return null;
|
||||
|
||||
return {
|
||||
lines: match[1].split("\n"),
|
||||
body: match[2] ?? "",
|
||||
eol: detectEol(text),
|
||||
trailingNewline: normalized.endsWith("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
function serializeFrontmatterDocument(document: FrontmatterDocument): string {
|
||||
const normalized = `---\n${document.lines.join("\n")}\n---\n${document.body}`;
|
||||
const withTrailingNewline =
|
||||
document.trailingNewline && !normalized.endsWith("\n") ? `${normalized}\n` : normalized;
|
||||
return document.eol === "\n" ? withTrailingNewline : withTrailingNewline.replace(/\n/g, "\r\n");
|
||||
}
|
||||
|
||||
function parseFrontmatterKey(line: string): string | undefined {
|
||||
const match = line.match(/^\s*([A-Za-z0-9_-]+)\s*:/);
|
||||
return match?.[1]?.toLowerCase();
|
||||
}
|
||||
|
||||
function getFrontmatterValue(lines: string[], key: string): string | undefined {
|
||||
const normalizedKey = key.toLowerCase();
|
||||
for (const line of lines) {
|
||||
const parsedKey = parseFrontmatterKey(line);
|
||||
if (parsedKey !== normalizedKey) continue;
|
||||
const separatorIndex = line.indexOf(":");
|
||||
if (separatorIndex === -1) return undefined;
|
||||
const value = line.slice(separatorIndex + 1).trim();
|
||||
return value.length > 0 ? value : undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function upsertFrontmatterValue(lines: string[], key: string, value: string): string[] {
|
||||
const normalizedKey = key.toLowerCase();
|
||||
const nextLines = [...lines];
|
||||
const existingIndex = nextLines.findIndex((line) => parseFrontmatterKey(line) === normalizedKey);
|
||||
const serialized = `${key}: ${value}`;
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
nextLines[existingIndex] = serialized;
|
||||
return nextLines;
|
||||
}
|
||||
|
||||
const descriptionIndex = nextLines.findIndex((line) => parseFrontmatterKey(line) === "description");
|
||||
const nameIndex = nextLines.findIndex((line) => parseFrontmatterKey(line) === "name");
|
||||
const insertIndex = descriptionIndex !== -1 ? descriptionIndex + 1 : nameIndex !== -1 ? nameIndex + 1 : nextLines.length;
|
||||
nextLines.splice(insertIndex, 0, serialized);
|
||||
return nextLines;
|
||||
}
|
||||
|
||||
function removeFrontmatterKey(lines: string[], key: string): string[] {
|
||||
const normalizedKey = key.toLowerCase();
|
||||
return lines.filter((line) => parseFrontmatterKey(line) !== normalizedKey);
|
||||
}
|
||||
|
||||
function normalizeAgentName(name: string): string {
|
||||
return name.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function getAgentsDir(agentDir: string): string {
|
||||
return join(agentDir, "agents");
|
||||
}
|
||||
|
||||
function listAgentFiles(agentsDir: string): string[] {
|
||||
if (!existsSync(agentsDir)) return [];
|
||||
|
||||
return readdirSync(agentsDir, { withFileTypes: true })
|
||||
.filter((entry: Dirent) => (entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(".md"))
|
||||
.filter((entry) => !entry.name.endsWith(".chain.md"))
|
||||
.map((entry) => join(agentsDir, entry.name));
|
||||
}
|
||||
|
||||
function readAgentConfig(filePath: string): SubagentModelConfig {
|
||||
const content = readFileSync(filePath, "utf8");
|
||||
const parsed = parseFrontmatterDocument(content);
|
||||
const fallbackName = basename(filePath, ".md");
|
||||
if (!parsed) return { agent: fallbackName, filePath };
|
||||
|
||||
return {
|
||||
agent: getFrontmatterValue(parsed.lines, "name") ?? fallbackName,
|
||||
model: getFrontmatterValue(parsed.lines, "model"),
|
||||
filePath,
|
||||
};
|
||||
}
|
||||
|
||||
function listSubagentModelConfigs(agentDir: string): SubagentModelConfig[] {
|
||||
return listAgentFiles(getAgentsDir(agentDir))
|
||||
.map((filePath) => readAgentConfig(filePath))
|
||||
.sort((left, right) => left.agent.localeCompare(right.agent));
|
||||
}
|
||||
|
||||
function findAgentConfig(configs: SubagentModelConfig[], agentName: string): SubagentModelConfig | undefined {
|
||||
const normalized = normalizeAgentName(agentName);
|
||||
return (
|
||||
configs.find((config) => normalizeAgentName(config.agent) === normalized) ??
|
||||
configs.find((config) => normalizeAgentName(basename(config.filePath, ".md")) === normalized)
|
||||
);
|
||||
}
|
||||
|
||||
function getAgentConfigOrThrow(agentDir: string, agentName: string): SubagentModelConfig {
|
||||
const configs = listSubagentModelConfigs(agentDir);
|
||||
const target = findAgentConfig(configs, agentName);
|
||||
if (target) return target;
|
||||
|
||||
if (configs.length === 0) {
|
||||
throw new Error(`No subagent definitions found in ${getAgentsDir(agentDir)}.`);
|
||||
}
|
||||
|
||||
const availableAgents = configs.map((config) => config.agent).join(", ");
|
||||
throw new Error(`Unknown subagent: ${agentName}. Available agents: ${availableAgents}`);
|
||||
}
|
||||
|
||||
function setSubagentModel(agentDir: string, agentName: string, modelSpec: string): void {
|
||||
const normalizedModelSpec = modelSpec.trim();
|
||||
if (!normalizedModelSpec) throw new Error("Model spec cannot be empty.");
|
||||
|
||||
const target = getAgentConfigOrThrow(agentDir, agentName);
|
||||
const content = readFileSync(target.filePath, "utf8");
|
||||
const parsed = parseFrontmatterDocument(content);
|
||||
|
||||
if (!parsed) {
|
||||
const eol = detectEol(content);
|
||||
const injected = `---${eol}name: ${target.agent}${eol}model: ${normalizedModelSpec}${eol}---${eol}${content}`;
|
||||
writeFileSync(target.filePath, injected, "utf8");
|
||||
return;
|
||||
}
|
||||
|
||||
const nextLines = upsertFrontmatterValue(parsed.lines, "model", normalizedModelSpec);
|
||||
if (nextLines.join("\n") !== parsed.lines.join("\n")) {
|
||||
writeFileSync(target.filePath, serializeFrontmatterDocument({ ...parsed, lines: nextLines }), "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
function unsetSubagentModel(agentDir: string, agentName: string): void {
|
||||
const target = getAgentConfigOrThrow(agentDir, agentName);
|
||||
const content = readFileSync(target.filePath, "utf8");
|
||||
const parsed = parseFrontmatterDocument(content);
|
||||
if (!parsed) return;
|
||||
|
||||
const nextLines = removeFrontmatterKey(parsed.lines, "model");
|
||||
if (nextLines.join("\n") !== parsed.lines.join("\n")) {
|
||||
writeFileSync(target.filePath, serializeFrontmatterDocument({ ...parsed, lines: nextLines }), "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
async function selectOption<T>(
|
||||
ctx: CommandContext,
|
||||
title: string,
|
||||
options: SelectOption<T>[],
|
||||
): Promise<T | undefined> {
|
||||
const selected = await ctx.ui.select(
|
||||
title,
|
||||
options.map((option) => option.label),
|
||||
);
|
||||
if (!selected) return undefined;
|
||||
return options.find((option) => option.label === selected)?.value;
|
||||
}
|
||||
|
||||
export function registerFeynmanModelCommand(pi: ExtensionAPI): void {
|
||||
pi.registerCommand("feynman-model", {
|
||||
description: "Open Feynman model menu (main + per-subagent overrides).",
|
||||
handler: async (_args, ctx) => {
|
||||
if (!ctx.hasUI) {
|
||||
ctx.ui.notify("feynman-model requires interactive mode.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ctx.modelRegistry.refresh();
|
||||
const availableModels = [...ctx.modelRegistry.getAvailable()].sort((left, right) =>
|
||||
formatModelSpec(left).localeCompare(formatModelSpec(right)),
|
||||
);
|
||||
if (availableModels.length === 0) {
|
||||
ctx.ui.notify("No models available.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const agentDir = resolveFeynmanAgentDir();
|
||||
const subagentConfigs = listSubagentModelConfigs(agentDir);
|
||||
const currentMain = ctx.model ? formatModelSpec(ctx.model) : "(none)";
|
||||
|
||||
const targetOptions: SelectOption<TargetChoice>[] = [
|
||||
{ label: `main (default): ${currentMain}`, value: { type: "main" } },
|
||||
...subagentConfigs.map((config) => ({
|
||||
label: `${config.agent}: ${config.model ?? "default"}`,
|
||||
value: { type: "subagent" as const, agent: config.agent, model: config.model },
|
||||
})),
|
||||
];
|
||||
|
||||
const target = await selectOption(ctx, "Choose target", targetOptions);
|
||||
if (!target) return;
|
||||
|
||||
if (target.type === "main") {
|
||||
const selectedModel = await selectOption(
|
||||
ctx,
|
||||
"Select main model",
|
||||
availableModels.map((model) => {
|
||||
const spec = formatModelSpec(model);
|
||||
const suffix = spec === currentMain ? " (current)" : "";
|
||||
return { label: `${spec}${suffix}`, value: model };
|
||||
}),
|
||||
);
|
||||
if (!selectedModel) return;
|
||||
|
||||
const success = await pi.setModel(selectedModel);
|
||||
if (!success) {
|
||||
ctx.ui.notify(`No API key found for ${selectedModel.provider}.`, "error");
|
||||
return;
|
||||
}
|
||||
ctx.ui.notify(`Main model set to ${formatModelSpec(selectedModel)}.`, "info");
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedSubagentModel = await selectOption(
|
||||
ctx,
|
||||
`Select model for ${target.agent}`,
|
||||
[
|
||||
{
|
||||
label: target.model ? "(inherit main default)" : "(inherit main default) (current)",
|
||||
value: INHERIT_MAIN,
|
||||
},
|
||||
...availableModels.map((model) => {
|
||||
const spec = formatModelSpec(model);
|
||||
const suffix = spec === target.model ? " (current)" : "";
|
||||
return { label: `${spec}${suffix}`, value: spec };
|
||||
}),
|
||||
],
|
||||
);
|
||||
if (!selectedSubagentModel) return;
|
||||
|
||||
if (selectedSubagentModel === INHERIT_MAIN) {
|
||||
unsetSubagentModel(agentDir, target.agent);
|
||||
ctx.ui.notify(`${target.agent} now inherits the main model.`, "info");
|
||||
return;
|
||||
}
|
||||
|
||||
setSubagentModel(agentDir, target.agent, selectedSubagentModel);
|
||||
ctx.ui.notify(`${target.agent} model set to ${selectedSubagentModel}.`, "info");
|
||||
} catch (error) {
|
||||
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -36,6 +36,7 @@ export function readPromptSpecs(appRoot) {
|
||||
|
||||
export const extensionCommandSpecs = [
|
||||
{ name: "help", args: "", section: "Project & Session", description: "Show grouped Feynman commands and prefill the editor with a selected command.", publicDocs: true },
|
||||
{ name: "feynman-model", args: "", section: "Project & Session", description: "Open Feynman model menu (main + per-subagent overrides).", publicDocs: true },
|
||||
{ name: "init", args: "", section: "Project & Session", description: "Bootstrap AGENTS.md and session-log folders for a research project.", publicDocs: true },
|
||||
{ name: "outputs", args: "", section: "Project & Session", description: "Browse all research artifacts (papers, outputs, experiments, notes).", publicDocs: true },
|
||||
];
|
||||
|
||||
38
package-lock.json
generated
38
package-lock.json
generated
@@ -7,11 +7,12 @@
|
||||
"": {
|
||||
"name": "@companion-ai/feynman",
|
||||
"version": "0.2.16",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@companion-ai/alpha-hub": "^0.1.2",
|
||||
"@mariozechner/pi-ai": "^0.62.0",
|
||||
"@mariozechner/pi-coding-agent": "^0.62.0",
|
||||
"@mariozechner/pi-ai": "^0.64.0",
|
||||
"@mariozechner/pi-coding-agent": "^0.64.0",
|
||||
"@sinclair/typebox": "^0.34.48",
|
||||
"dotenv": "^17.3.1"
|
||||
},
|
||||
@@ -1468,21 +1469,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mariozechner/pi-agent-core": {
|
||||
"version": "0.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-agent-core/-/pi-agent-core-0.62.0.tgz",
|
||||
"integrity": "sha512-SBjqgDrgKOaC+IGzFGB3jXQErv9H1QMYnWFvUg6ra6dG0ZgWFBUZb6unidngWLsmaxSDWes6KeKiVFMsr2VSEQ==",
|
||||
"version": "0.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-agent-core/-/pi-agent-core-0.64.0.tgz",
|
||||
"integrity": "sha512-IN/sIxWOD0v1OFVXHB605SGiZhO5XdEWG5dO8EAV08n3jz/p12o4OuYGvhGXmHhU28WXa/FGWC+FO5xiIih8Uw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-ai": "^0.62.0"
|
||||
"@mariozechner/pi-ai": "^0.64.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mariozechner/pi-ai": {
|
||||
"version": "0.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.62.0.tgz",
|
||||
"integrity": "sha512-mJgryZ5RgBQG++tiETMtCQQJoH2MAhKetCfqI98NMvGydu7L9x2qC2JekQlRaAgIlTgv4MRH1UXHMEs4UweE/Q==",
|
||||
"version": "0.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.64.0.tgz",
|
||||
"integrity": "sha512-Z/Jnf+JSVDPLRcxJsa8XhYTJKIqKekNueaCpBLGQHgizL1F9RQ1Rur3rIfZpfXkt2cLu/AIPtOs223ueuoWaWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.73.0",
|
||||
@@ -1507,16 +1508,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mariozechner/pi-coding-agent": {
|
||||
"version": "0.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-coding-agent/-/pi-coding-agent-0.62.0.tgz",
|
||||
"integrity": "sha512-f1NnExqsHuA6w8UVlBtPsvTBhdkMc0h1JD9SzGCdWTLou5GHJr2JIP6DlwV9IKWAnM+sAelaoFez+14wLP2zOQ==",
|
||||
"version": "0.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-coding-agent/-/pi-coding-agent-0.64.0.tgz",
|
||||
"integrity": "sha512-Q4tcqSqFGQtOgCtRyIp1D80Nv2if13Q2pfbnrOlaT/mix90mLcZGML9jKVnT1jGSy5GMYudU1HsS7cx53kxb0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mariozechner/jiti": "^2.6.2",
|
||||
"@mariozechner/pi-agent-core": "^0.62.0",
|
||||
"@mariozechner/pi-ai": "^0.62.0",
|
||||
"@mariozechner/pi-tui": "^0.62.0",
|
||||
"@mariozechner/pi-agent-core": "^0.64.0",
|
||||
"@mariozechner/pi-ai": "^0.64.0",
|
||||
"@mariozechner/pi-tui": "^0.64.0",
|
||||
"@silvia-odwyer/photon-node": "^0.3.4",
|
||||
"ajv": "^8.17.1",
|
||||
"chalk": "^5.5.0",
|
||||
"cli-highlight": "^2.1.11",
|
||||
"diff": "^8.0.2",
|
||||
@@ -1543,9 +1545,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mariozechner/pi-tui": {
|
||||
"version": "0.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.62.0.tgz",
|
||||
"integrity": "sha512-/At11PPe8l319MnUoK4wN5L/uVCU6bDdiIUzH8Ez0stOkjSF6isRXScZ+RMM+6iCKsD4muBTX8Cmcif+3/UWHA==",
|
||||
"version": "0.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.64.0.tgz",
|
||||
"integrity": "sha512-W1qLry9MAuN/V3YJmMv/BJa0VaYv721NkXPg/DGItdqWxuDc+1VdNbyAnRwxblNkIpXVUWL26x64BlyFXpxmkg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mime-types": "^2.1.4",
|
||||
|
||||
@@ -60,8 +60,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@companion-ai/alpha-hub": "^0.1.2",
|
||||
"@mariozechner/pi-ai": "^0.62.0",
|
||||
"@mariozechner/pi-coding-agent": "^0.62.0",
|
||||
"@mariozechner/pi-ai": "^0.64.0",
|
||||
"@mariozechner/pi-coding-agent": "^0.64.0",
|
||||
"@sinclair/typebox": "^0.34.48",
|
||||
"dotenv": "^17.3.1"
|
||||
},
|
||||
|
||||
@@ -18,6 +18,7 @@ import { ensureFeynmanHome, getDefaultSessionDir, getFeynmanAgentDir, getFeynman
|
||||
import { launchPiChat } from "./pi/launch.js";
|
||||
import { CORE_PACKAGE_SOURCES, getOptionalPackagePresetSources, listOptionalPackagePresets } from "./pi/package-presets.js";
|
||||
import { normalizeFeynmanSettings, normalizeThinkingLevel, parseModelSpec } from "./pi/settings.js";
|
||||
import { applyFeynmanPackageManagerEnv } from "./pi/runtime.js";
|
||||
import {
|
||||
authenticateModelProvider,
|
||||
getCurrentModelSpec,
|
||||
@@ -154,6 +155,7 @@ async function handleModelCommand(subcommand: string | undefined, args: string[]
|
||||
}
|
||||
|
||||
async function handleUpdateCommand(workingDir: string, feynmanAgentDir: string, source?: string): Promise<void> {
|
||||
applyFeynmanPackageManagerEnv(feynmanAgentDir);
|
||||
const settingsManager = SettingsManager.create(workingDir, feynmanAgentDir);
|
||||
const packageManager = new DefaultPackageManager({
|
||||
cwd: workingDir,
|
||||
@@ -177,6 +179,7 @@ async function handleUpdateCommand(workingDir: string, feynmanAgentDir: string,
|
||||
}
|
||||
|
||||
async function handlePackagesCommand(subcommand: string | undefined, args: string[], workingDir: string, feynmanAgentDir: string): Promise<void> {
|
||||
applyFeynmanPackageManagerEnv(feynmanAgentDir);
|
||||
const settingsManager = SettingsManager.create(workingDir, feynmanAgentDir);
|
||||
const configuredSources = new Set(
|
||||
settingsManager
|
||||
|
||||
@@ -7,6 +7,5 @@ export function getModelsJsonPath(authPath: string): string {
|
||||
}
|
||||
|
||||
export function createModelRegistry(authPath: string): ModelRegistry {
|
||||
return new ModelRegistry(AuthStorage.create(authPath), getModelsJsonPath(authPath));
|
||||
return ModelRegistry.create(AuthStorage.create(authPath), getModelsJsonPath(authPath));
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,18 @@ export type PiRuntimeOptions = {
|
||||
initialPrompt?: string;
|
||||
};
|
||||
|
||||
export function getFeynmanNpmPrefixPath(feynmanAgentDir: string): string {
|
||||
return resolve(dirname(feynmanAgentDir), "npm-global");
|
||||
}
|
||||
|
||||
export function applyFeynmanPackageManagerEnv(feynmanAgentDir: string): string {
|
||||
const feynmanNpmPrefixPath = getFeynmanNpmPrefixPath(feynmanAgentDir);
|
||||
process.env.FEYNMAN_NPM_PREFIX = feynmanNpmPrefixPath;
|
||||
process.env.NPM_CONFIG_PREFIX = feynmanNpmPrefixPath;
|
||||
process.env.npm_config_prefix = feynmanNpmPrefixPath;
|
||||
return feynmanNpmPrefixPath;
|
||||
}
|
||||
|
||||
export function resolvePiPaths(appRoot: string) {
|
||||
return {
|
||||
piPackageRoot: resolve(appRoot, "node_modules", "@mariozechner", "pi-coding-agent"),
|
||||
@@ -83,8 +95,7 @@ export function buildPiArgs(options: PiRuntimeOptions): string[] {
|
||||
|
||||
export function buildPiEnv(options: PiRuntimeOptions): NodeJS.ProcessEnv {
|
||||
const paths = resolvePiPaths(options.appRoot);
|
||||
const feynmanHome = dirname(options.feynmanAgentDir);
|
||||
const feynmanNpmPrefixPath = resolve(feynmanHome, "npm-global");
|
||||
const feynmanNpmPrefixPath = getFeynmanNpmPrefixPath(options.feynmanAgentDir);
|
||||
const feynmanNpmBinPath = resolve(feynmanNpmPrefixPath, "bin");
|
||||
|
||||
const currentPath = process.env.PATH ?? "";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { buildPiArgs, buildPiEnv, resolvePiPaths } from "../src/pi/runtime.js";
|
||||
import { applyFeynmanPackageManagerEnv, buildPiArgs, buildPiEnv, resolvePiPaths } from "../src/pi/runtime.js";
|
||||
|
||||
test("buildPiArgs includes configured runtime paths and prompt", () => {
|
||||
const args = buildPiArgs({
|
||||
@@ -70,6 +70,37 @@ test("buildPiEnv wires Feynman paths into the Pi environment", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("applyFeynmanPackageManagerEnv pins npm globals to the Feynman prefix", () => {
|
||||
const previousFeynmanPrefix = process.env.FEYNMAN_NPM_PREFIX;
|
||||
const previousUppercasePrefix = process.env.NPM_CONFIG_PREFIX;
|
||||
const previousLowercasePrefix = process.env.npm_config_prefix;
|
||||
|
||||
try {
|
||||
const prefix = applyFeynmanPackageManagerEnv("/home/.feynman/agent");
|
||||
|
||||
assert.equal(prefix, "/home/.feynman/npm-global");
|
||||
assert.equal(process.env.FEYNMAN_NPM_PREFIX, "/home/.feynman/npm-global");
|
||||
assert.equal(process.env.NPM_CONFIG_PREFIX, "/home/.feynman/npm-global");
|
||||
assert.equal(process.env.npm_config_prefix, "/home/.feynman/npm-global");
|
||||
} finally {
|
||||
if (previousFeynmanPrefix === undefined) {
|
||||
delete process.env.FEYNMAN_NPM_PREFIX;
|
||||
} else {
|
||||
process.env.FEYNMAN_NPM_PREFIX = previousFeynmanPrefix;
|
||||
}
|
||||
if (previousUppercasePrefix === undefined) {
|
||||
delete process.env.NPM_CONFIG_PREFIX;
|
||||
} else {
|
||||
process.env.NPM_CONFIG_PREFIX = previousUppercasePrefix;
|
||||
}
|
||||
if (previousLowercasePrefix === undefined) {
|
||||
delete process.env.npm_config_prefix;
|
||||
} else {
|
||||
process.env.npm_config_prefix = previousLowercasePrefix;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("resolvePiPaths includes the Promise.withResolvers polyfill path", () => {
|
||||
const paths = resolvePiPaths("/repo/feynman");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user