From 389baf06f2e10dbf0c096a62f65aade244417b79 Mon Sep 17 00:00:00 2001 From: Advait Paliwal Date: Fri, 20 Mar 2026 11:22:21 -0700 Subject: [PATCH] Brand embedded Pi TUI as Feynman --- .gitignore | 3 + README.md | 1 + package.json | 1 + scripts/patch-embedded-pi.mjs | 27 ++++ src/index.ts | 224 +++++++++++++--------------------- 5 files changed, 120 insertions(+), 136 deletions(-) create mode 100644 scripts/patch-embedded-pi.mjs diff --git a/.gitignore b/.gitignore index de55996..616b199 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ node_modules .env .feynman +.pi/npm +.pi/git dist +*.tgz outputs/* !outputs/.gitkeep diff --git a/README.md b/README.md index 9d2ee55..b9e2261 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ npm run start ``` Feynman uses Pi under the hood, but the user-facing entrypoint is `feynman`, not `pi`. +When you run `feynman`, it launches the real Pi interactive TUI with Feynman's research extensions, skills, prompts, and package stack preloaded. ## Commands diff --git a/package.json b/package.json index 3e26d3c..021c852 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "scripts": { "build": "tsc -p tsconfig.build.json", "dev": "tsx src/index.ts", + "postinstall": "node ./scripts/patch-embedded-pi.mjs", "start": "tsx src/index.ts", "start:dist": "node ./bin/feynman.js", "typecheck": "tsc --noEmit" diff --git a/scripts/patch-embedded-pi.mjs b/scripts/patch-embedded-pi.mjs new file mode 100644 index 0000000..ce5beb7 --- /dev/null +++ b/scripts/patch-embedded-pi.mjs @@ -0,0 +1,27 @@ +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); +const appRoot = resolve(here, ".."); +const piPackageRoot = resolve(appRoot, "node_modules", "@mariozechner", "pi-coding-agent"); +const packageJsonPath = resolve(piPackageRoot, "package.json"); +const cliPath = resolve(piPackageRoot, "dist", "cli.js"); + +if (existsSync(packageJsonPath)) { + const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8")); + if (pkg.piConfig?.name !== "feynman") { + pkg.piConfig = { + ...(pkg.piConfig || {}), + name: "feynman", + }; + writeFileSync(packageJsonPath, JSON.stringify(pkg, null, "\t") + "\n", "utf8"); + } +} + +if (existsSync(cliPath)) { + const cliSource = readFileSync(cliPath, "utf8"); + if (cliSource.includes('process.title = "pi";')) { + writeFileSync(cliPath, cliSource.replace('process.title = "pi";', 'process.title = "feynman";'), "utf8"); + } +} diff --git a/src/index.ts b/src/index.ts index 43fbce9..07040c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,11 @@ import "dotenv/config"; -import { mkdirSync } from "node:fs"; -import { stdin as input, stdout as output } from "node:process"; +import { spawn } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; import { dirname, resolve } from "node:path"; import { parseArgs } from "node:util"; import { fileURLToPath } from "node:url"; -import readline from "node:readline/promises"; import { getUserName as getAlphaUserName, @@ -14,13 +14,8 @@ import { logout as logoutAlpha, } from "@companion-ai/alpha-hub/lib"; import { - AuthStorage, - createAgentSession, - createCodingTools, - DefaultResourceLoader, ModelRegistry, - SessionManager, - SettingsManager, + AuthStorage, } from "@mariozechner/pi-coding-agent"; import { FEYNMAN_SYSTEM_PROMPT } from "./feynman-prompt.js"; @@ -81,9 +76,39 @@ function normalizeThinkingLevel(value: string | undefined): ThinkingLevel | unde return undefined; } +function patchEmbeddedPiBranding(piPackageRoot: string): void { + const packageJsonPath = resolve(piPackageRoot, "package.json"); + const cliPath = resolve(piPackageRoot, "dist", "cli.js"); + + if (existsSync(packageJsonPath)) { + const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { + piConfig?: { name?: string; configDir?: string }; + }; + if (pkg.piConfig?.name !== "feynman") { + pkg.piConfig = { + ...pkg.piConfig, + name: "feynman", + }; + writeFileSync(packageJsonPath, JSON.stringify(pkg, null, "\t") + "\n", "utf8"); + } + } + + if (existsSync(cliPath)) { + const cliSource = readFileSync(cliPath, "utf8"); + if (cliSource.includes('process.title = "pi";')) { + writeFileSync(cliPath, cliSource.replace('process.title = "pi";', 'process.title = "feynman";'), "utf8"); + } + } +} + async function main(): Promise { const here = dirname(fileURLToPath(import.meta.url)); const appRoot = resolve(here, ".."); + const piPackageRoot = resolve(appRoot, "node_modules", "@mariozechner", "pi-coding-agent"); + const piCliPath = resolve(appRoot, "node_modules", "@mariozechner", "pi-coding-agent", "dist", "cli.js"); + const feynmanAgentDir = resolve(homedir(), ".feynman", "agent"); + const bundledSettingsPath = resolve(appRoot, ".pi", "settings.json"); + patchEmbeddedPiBranding(piPackageRoot); const { values, positionals } = parseArgs({ allowPositionals: true, @@ -136,147 +161,74 @@ async function main(): Promise { } const workingDir = resolve(values.cwd ?? process.cwd()); - const sessionDir = resolve(values["session-dir"] ?? resolve(appRoot, ".feynman", "sessions")); + const sessionDir = resolve(values["session-dir"] ?? resolve(homedir(), ".feynman", "sessions")); mkdirSync(sessionDir, { recursive: true }); - const settingsManager = SettingsManager.create(appRoot); - - const authStorage = AuthStorage.create(); - const modelRegistry = new ModelRegistry(authStorage); - const explicitModelSpec = values.model ?? process.env.FEYNMAN_MODEL; - const explicitModel = explicitModelSpec ? parseModelSpec(explicitModelSpec, modelRegistry) : undefined; - - if (explicitModelSpec && !explicitModel) { - throw new Error(`Unknown model: ${explicitModelSpec}`); + mkdirSync(feynmanAgentDir, { recursive: true }); + const feynmanSettingsPath = resolve(feynmanAgentDir, "settings.json"); + if (!existsSync(feynmanSettingsPath) && existsSync(bundledSettingsPath)) { + writeFileSync(feynmanSettingsPath, readFileSync(bundledSettingsPath, "utf8"), "utf8"); } - if (!explicitModel) { - const available = await modelRegistry.getAvailable(); - if (available.length === 0) { - throw new Error( - "No models are available. Configure pi auth or export a provider API key before starting Feynman.", - ); + const explicitModelSpec = values.model ?? process.env.FEYNMAN_MODEL; + if (explicitModelSpec) { + const modelRegistry = new ModelRegistry(AuthStorage.create()); + const explicitModel = parseModelSpec(explicitModelSpec, modelRegistry); + if (!explicitModel) { + throw new Error(`Unknown model: ${explicitModelSpec}`); } } const thinkingLevel = normalizeThinkingLevel(values.thinking ?? process.env.FEYNMAN_THINKING) ?? "medium"; + const oneShotPrompt = values.prompt; + const initialPrompt = oneShotPrompt ?? (positionals.length > 0 ? positionals.join(" ") : undefined); - const resourceLoader = new DefaultResourceLoader({ - cwd: appRoot, - additionalExtensionPaths: [resolve(appRoot, "extensions")], - additionalPromptTemplatePaths: [resolve(appRoot, "prompts")], - additionalSkillPaths: [resolve(appRoot, "skills")], - settingsManager, - systemPromptOverride: () => FEYNMAN_SYSTEM_PROMPT, - appendSystemPromptOverride: () => [], - }); - await resourceLoader.reload(); + const piArgs = [ + "--session-dir", + sessionDir, + "--extension", + resolve(appRoot, "extensions", "research-tools.ts"), + "--skill", + resolve(appRoot, "skills"), + "--prompt-template", + resolve(appRoot, "prompts"), + "--system-prompt", + FEYNMAN_SYSTEM_PROMPT, + ]; - const sessionManager = values["new-session"] - ? SessionManager.create(workingDir, sessionDir) - : SessionManager.continueRecent(workingDir, sessionDir); + if (explicitModelSpec) { + piArgs.push("--model", explicitModelSpec); + } + if (thinkingLevel) { + piArgs.push("--thinking", thinkingLevel); + } + if (oneShotPrompt) { + piArgs.push("-p", oneShotPrompt); + } + else if (initialPrompt) { + piArgs.push(initialPrompt); + } - const { session } = await createAgentSession({ - authStorage, + const child = spawn(process.execPath, [piCliPath, ...piArgs], { cwd: workingDir, - model: explicitModel, - modelRegistry, - resourceLoader, - sessionManager, - settingsManager, - thinkingLevel, - tools: createCodingTools(workingDir), + stdio: "inherit", + env: { + ...process.env, + PI_CODING_AGENT_DIR: feynmanAgentDir, + FEYNMAN_CODING_AGENT_DIR: feynmanAgentDir, + }, }); - session.subscribe((event) => { - if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { - process.stdout.write(event.assistantMessageEvent.delta); - return; - } - - if (event.type === "tool_execution_start") { - process.stderr.write(`\n[tool] ${event.toolName}\n`); - return; - } - - if (event.type === "tool_execution_end" && event.isError) { - process.stderr.write(`[tool-error] ${event.toolName}\n`); - } + await new Promise((resolvePromise, reject) => { + child.on("error", reject); + child.on("exit", (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + return; + } + process.exitCode = code ?? 0; + resolvePromise(); + }); }); - - const initialPrompt = values.prompt ?? (positionals.length > 0 ? positionals.join(" ") : undefined); - - if (initialPrompt) { - await session.prompt(initialPrompt); - process.stdout.write("\n"); - session.dispose(); - return; - } - - console.log("Feynman research agent"); - console.log(`working dir: ${workingDir}`); - console.log(`session dir: ${sessionDir}`); - console.log("type /help for commands"); - - const rl = readline.createInterface({ input, output }); - - try { - while (true) { - const line = (await rl.question("feynman> ")).trim(); - if (!line) { - continue; - } - - if (line === "/exit" || line === "/quit") { - break; - } - - if (line === "/help") { - printHelp(); - continue; - } - - if (line === "/alpha-login") { - const result = await loginAlpha(); - const name = - (result.userInfo && - typeof result.userInfo === "object" && - "name" in result.userInfo && - typeof result.userInfo.name === "string") - ? result.userInfo.name - : getAlphaUserName(); - console.log(name ? `alphaXiv login complete: ${name}` : "alphaXiv login complete"); - continue; - } - - if (line === "/alpha-logout") { - logoutAlpha(); - console.log("alphaXiv auth cleared"); - continue; - } - - if (line === "/alpha-status") { - if (isAlphaLoggedIn()) { - const name = getAlphaUserName(); - console.log(name ? `alphaXiv logged in as ${name}` : "alphaXiv logged in"); - } else { - console.log("alphaXiv not logged in"); - } - continue; - } - - if (line === "/new") { - await session.newSession(); - console.log("started a new session"); - continue; - } - - await session.prompt(line); - process.stdout.write("\n"); - } - } finally { - rl.close(); - session.dispose(); - } } main().catch((error) => {