Polish Feynman harness and stabilize Pi web runtime

This commit is contained in:
Advait Paliwal
2026-03-22 20:20:26 -07:00
parent 7f0def3a4c
commit 46810f97b7
47 changed files with 3178 additions and 869 deletions

32
src/pi/launch.ts Normal file
View File

@@ -0,0 +1,32 @@
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { buildPiArgs, buildPiEnv, type PiRuntimeOptions, resolvePiPaths } from "./runtime.js";
export async function launchPiChat(options: PiRuntimeOptions): Promise<void> {
const { piCliPath, promisePolyfillPath } = resolvePiPaths(options.appRoot);
if (!existsSync(piCliPath)) {
throw new Error(`Pi CLI not found: ${piCliPath}`);
}
if (!existsSync(promisePolyfillPath)) {
throw new Error(`Promise polyfill not found: ${promisePolyfillPath}`);
}
const child = spawn(process.execPath, ["--import", promisePolyfillPath, piCliPath, ...buildPiArgs(options)], {
cwd: options.workingDir,
stdio: "inherit",
env: buildPiEnv(options),
});
await new Promise<void>((resolvePromise, reject) => {
child.on("error", reject);
child.on("exit", (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
return;
}
process.exitCode = code ?? 0;
resolvePromise();
});
});
}

99
src/pi/runtime.ts Normal file
View File

@@ -0,0 +1,99 @@
import { existsSync } from "node:fs";
import { dirname, resolve } from "node:path";
import {
BROWSER_FALLBACK_PATHS,
MERMAID_FALLBACK_PATHS,
PANDOC_FALLBACK_PATHS,
resolveExecutable,
} from "../system/executables.js";
export type PiRuntimeOptions = {
appRoot: string;
workingDir: string;
sessionDir: string;
feynmanAgentDir: string;
feynmanVersion?: string;
thinkingLevel?: string;
explicitModelSpec?: string;
oneShotPrompt?: string;
initialPrompt?: string;
systemPrompt: string;
};
export function resolvePiPaths(appRoot: string) {
return {
piPackageRoot: resolve(appRoot, "node_modules", "@mariozechner", "pi-coding-agent"),
piCliPath: resolve(appRoot, "node_modules", "@mariozechner", "pi-coding-agent", "dist", "cli.js"),
promisePolyfillPath: resolve(appRoot, "dist", "system", "promise-polyfill.js"),
researchToolsPath: resolve(appRoot, "extensions", "research-tools.ts"),
skillsPath: resolve(appRoot, "skills"),
promptTemplatePath: resolve(appRoot, "prompts"),
piWorkspaceNodeModulesPath: resolve(appRoot, ".pi", "npm", "node_modules"),
};
}
export function validatePiInstallation(appRoot: string): string[] {
const paths = resolvePiPaths(appRoot);
const missing: string[] = [];
if (!existsSync(paths.piCliPath)) missing.push(paths.piCliPath);
if (!existsSync(paths.promisePolyfillPath)) missing.push(paths.promisePolyfillPath);
if (!existsSync(paths.researchToolsPath)) missing.push(paths.researchToolsPath);
if (!existsSync(paths.skillsPath)) missing.push(paths.skillsPath);
if (!existsSync(paths.promptTemplatePath)) missing.push(paths.promptTemplatePath);
return missing;
}
export function buildPiArgs(options: PiRuntimeOptions): string[] {
const paths = resolvePiPaths(options.appRoot);
const args = [
"--session-dir",
options.sessionDir,
"--extension",
paths.researchToolsPath,
"--skill",
paths.skillsPath,
"--prompt-template",
paths.promptTemplatePath,
"--system-prompt",
options.systemPrompt,
];
if (options.explicitModelSpec) {
args.push("--model", options.explicitModelSpec);
}
if (options.thinkingLevel) {
args.push("--thinking", options.thinkingLevel);
}
if (options.oneShotPrompt) {
args.push("-p", options.oneShotPrompt);
} else if (options.initialPrompt) {
args.push(options.initialPrompt);
}
return args;
}
export function buildPiEnv(options: PiRuntimeOptions): NodeJS.ProcessEnv {
const paths = resolvePiPaths(options.appRoot);
return {
...process.env,
PI_CODING_AGENT_DIR: options.feynmanAgentDir,
FEYNMAN_CODING_AGENT_DIR: options.feynmanAgentDir,
FEYNMAN_VERSION: options.feynmanVersion,
FEYNMAN_PI_NPM_ROOT: paths.piWorkspaceNodeModulesPath,
FEYNMAN_SESSION_DIR: options.sessionDir,
PI_SESSION_DIR: options.sessionDir,
FEYNMAN_MEMORY_DIR: resolve(dirname(options.feynmanAgentDir), "memory"),
FEYNMAN_NODE_EXECUTABLE: process.execPath,
FEYNMAN_BIN_PATH: resolve(options.appRoot, "bin", "feynman.js"),
PANDOC_PATH: process.env.PANDOC_PATH ?? resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS),
PI_SKIP_VERSION_CHECK: process.env.PI_SKIP_VERSION_CHECK ?? "1",
MERMAID_CLI_PATH: process.env.MERMAID_CLI_PATH ?? resolveExecutable("mmdc", MERMAID_FALLBACK_PATHS),
PUPPETEER_EXECUTABLE_PATH:
process.env.PUPPETEER_EXECUTABLE_PATH ?? resolveExecutable("google-chrome", BROWSER_FALLBACK_PATHS),
};
}

121
src/pi/settings.ts Normal file
View File

@@ -0,0 +1,121 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
export type ThinkingLevel = "off" | "low" | "medium" | "high";
export function parseModelSpec(spec: string, modelRegistry: ModelRegistry) {
const trimmed = spec.trim();
const separator = trimmed.includes(":") ? ":" : trimmed.includes("/") ? "/" : null;
if (!separator) {
return undefined;
}
const [provider, ...rest] = trimmed.split(separator);
const id = rest.join(separator);
if (!provider || !id) {
return undefined;
}
return modelRegistry.find(provider, id);
}
export function normalizeThinkingLevel(value: string | undefined): ThinkingLevel | undefined {
if (!value) {
return undefined;
}
const normalized = value.toLowerCase();
if (normalized === "off" || normalized === "low" || normalized === "medium" || normalized === "high") {
return normalized;
}
return undefined;
}
function choosePreferredModel(
availableModels: Array<{ provider: string; id: string }>,
): { provider: string; id: string } | undefined {
const preferences = [
{ provider: "anthropic", id: "claude-opus-4-6" },
{ provider: "anthropic", id: "claude-opus-4-5" },
{ provider: "anthropic", id: "claude-sonnet-4-5" },
{ provider: "openai", id: "gpt-5.4" },
{ provider: "openai", id: "gpt-5" },
];
for (const preferred of preferences) {
const match = availableModels.find(
(model) => model.provider === preferred.provider && model.id === preferred.id,
);
if (match) {
return match;
}
}
return availableModels[0];
}
export function readJson(path: string): Record<string, unknown> {
if (!existsSync(path)) {
return {};
}
try {
return JSON.parse(readFileSync(path, "utf8"));
} catch {
return {};
}
}
export function normalizeFeynmanSettings(
settingsPath: string,
bundledSettingsPath: string,
defaultThinkingLevel: ThinkingLevel,
authPath: string,
): void {
let settings: Record<string, unknown> = {};
if (existsSync(settingsPath)) {
try {
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
} catch {
settings = {};
}
} else if (existsSync(bundledSettingsPath)) {
try {
settings = JSON.parse(readFileSync(bundledSettingsPath, "utf8"));
} catch {
settings = {};
}
}
if (!settings.defaultThinkingLevel) {
settings.defaultThinkingLevel = defaultThinkingLevel;
}
if (settings.editorPaddingX === undefined) {
settings.editorPaddingX = 1;
}
settings.theme = "feynman";
settings.quietStartup = true;
settings.collapseChangelog = true;
const authStorage = AuthStorage.create(authPath);
const modelRegistry = new ModelRegistry(authStorage);
const availableModels = modelRegistry.getAvailable().map((model) => ({
provider: model.provider,
id: model.id,
}));
if ((!settings.defaultProvider || !settings.defaultModel) && availableModels.length > 0) {
const preferredModel = choosePreferredModel(availableModels);
if (preferredModel) {
settings.defaultProvider = preferredModel.provider;
settings.defaultModel = preferredModel.id;
}
}
mkdirSync(dirname(settingsPath), { recursive: true });
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
}