Brand embedded Pi TUI as Feynman

This commit is contained in:
Advait Paliwal
2026-03-20 11:22:21 -07:00
parent 1e68c872df
commit 389baf06f2
5 changed files with 120 additions and 136 deletions

3
.gitignore vendored
View File

@@ -1,6 +1,9 @@
node_modules node_modules
.env .env
.feynman .feynman
.pi/npm
.pi/git
dist dist
*.tgz
outputs/* outputs/*
!outputs/.gitkeep !outputs/.gitkeep

View File

@@ -42,6 +42,7 @@ npm run start
``` ```
Feynman uses Pi under the hood, but the user-facing entrypoint is `feynman`, not `pi`. 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 ## Commands

View File

@@ -19,6 +19,7 @@
"scripts": { "scripts": {
"build": "tsc -p tsconfig.build.json", "build": "tsc -p tsconfig.build.json",
"dev": "tsx src/index.ts", "dev": "tsx src/index.ts",
"postinstall": "node ./scripts/patch-embedded-pi.mjs",
"start": "tsx src/index.ts", "start": "tsx src/index.ts",
"start:dist": "node ./bin/feynman.js", "start:dist": "node ./bin/feynman.js",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"

View File

@@ -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");
}
}

View File

@@ -1,11 +1,11 @@
import "dotenv/config"; import "dotenv/config";
import { mkdirSync } from "node:fs"; import { spawn } from "node:child_process";
import { stdin as input, stdout as output } from "node:process"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, resolve } from "node:path"; import { dirname, resolve } from "node:path";
import { parseArgs } from "node:util"; import { parseArgs } from "node:util";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import readline from "node:readline/promises";
import { import {
getUserName as getAlphaUserName, getUserName as getAlphaUserName,
@@ -14,13 +14,8 @@ import {
logout as logoutAlpha, logout as logoutAlpha,
} from "@companion-ai/alpha-hub/lib"; } from "@companion-ai/alpha-hub/lib";
import { import {
AuthStorage,
createAgentSession,
createCodingTools,
DefaultResourceLoader,
ModelRegistry, ModelRegistry,
SessionManager, AuthStorage,
SettingsManager,
} from "@mariozechner/pi-coding-agent"; } from "@mariozechner/pi-coding-agent";
import { FEYNMAN_SYSTEM_PROMPT } from "./feynman-prompt.js"; import { FEYNMAN_SYSTEM_PROMPT } from "./feynman-prompt.js";
@@ -81,9 +76,39 @@ function normalizeThinkingLevel(value: string | undefined): ThinkingLevel | unde
return undefined; 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<void> { async function main(): Promise<void> {
const here = dirname(fileURLToPath(import.meta.url)); const here = dirname(fileURLToPath(import.meta.url));
const appRoot = resolve(here, ".."); 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({ const { values, positionals } = parseArgs({
allowPositionals: true, allowPositionals: true,
@@ -136,147 +161,74 @@ async function main(): Promise<void> {
} }
const workingDir = resolve(values.cwd ?? process.cwd()); 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 }); mkdirSync(sessionDir, { recursive: true });
const settingsManager = SettingsManager.create(appRoot); mkdirSync(feynmanAgentDir, { recursive: true });
const feynmanSettingsPath = resolve(feynmanAgentDir, "settings.json");
const authStorage = AuthStorage.create(); if (!existsSync(feynmanSettingsPath) && existsSync(bundledSettingsPath)) {
const modelRegistry = new ModelRegistry(authStorage); writeFileSync(feynmanSettingsPath, readFileSync(bundledSettingsPath, "utf8"), "utf8");
const explicitModelSpec = values.model ?? process.env.FEYNMAN_MODEL;
const explicitModel = explicitModelSpec ? parseModelSpec(explicitModelSpec, modelRegistry) : undefined;
if (explicitModelSpec && !explicitModel) {
throw new Error(`Unknown model: ${explicitModelSpec}`);
} }
if (!explicitModel) { const explicitModelSpec = values.model ?? process.env.FEYNMAN_MODEL;
const available = await modelRegistry.getAvailable(); if (explicitModelSpec) {
if (available.length === 0) { const modelRegistry = new ModelRegistry(AuthStorage.create());
throw new Error( const explicitModel = parseModelSpec(explicitModelSpec, modelRegistry);
"No models are available. Configure pi auth or export a provider API key before starting Feynman.", if (!explicitModel) {
); throw new Error(`Unknown model: ${explicitModelSpec}`);
} }
} }
const thinkingLevel = normalizeThinkingLevel(values.thinking ?? process.env.FEYNMAN_THINKING) ?? "medium"; 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({ const piArgs = [
cwd: appRoot, "--session-dir",
additionalExtensionPaths: [resolve(appRoot, "extensions")], sessionDir,
additionalPromptTemplatePaths: [resolve(appRoot, "prompts")], "--extension",
additionalSkillPaths: [resolve(appRoot, "skills")], resolve(appRoot, "extensions", "research-tools.ts"),
settingsManager, "--skill",
systemPromptOverride: () => FEYNMAN_SYSTEM_PROMPT, resolve(appRoot, "skills"),
appendSystemPromptOverride: () => [], "--prompt-template",
}); resolve(appRoot, "prompts"),
await resourceLoader.reload(); "--system-prompt",
FEYNMAN_SYSTEM_PROMPT,
];
const sessionManager = values["new-session"] if (explicitModelSpec) {
? SessionManager.create(workingDir, sessionDir) piArgs.push("--model", explicitModelSpec);
: SessionManager.continueRecent(workingDir, sessionDir); }
if (thinkingLevel) {
piArgs.push("--thinking", thinkingLevel);
}
if (oneShotPrompt) {
piArgs.push("-p", oneShotPrompt);
}
else if (initialPrompt) {
piArgs.push(initialPrompt);
}
const { session } = await createAgentSession({ const child = spawn(process.execPath, [piCliPath, ...piArgs], {
authStorage,
cwd: workingDir, cwd: workingDir,
model: explicitModel, stdio: "inherit",
modelRegistry, env: {
resourceLoader, ...process.env,
sessionManager, PI_CODING_AGENT_DIR: feynmanAgentDir,
settingsManager, FEYNMAN_CODING_AGENT_DIR: feynmanAgentDir,
thinkingLevel, },
tools: createCodingTools(workingDir),
}); });
session.subscribe((event) => { await new Promise<void>((resolvePromise, reject) => {
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { child.on("error", reject);
process.stdout.write(event.assistantMessageEvent.delta); child.on("exit", (code, signal) => {
return; if (signal) {
} process.kill(process.pid, signal);
return;
if (event.type === "tool_execution_start") { }
process.stderr.write(`\n[tool] ${event.toolName}\n`); process.exitCode = code ?? 0;
return; resolvePromise();
} });
if (event.type === "tool_execution_end" && event.isError) {
process.stderr.write(`[tool-error] ${event.toolName}\n`);
}
}); });
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) => { main().catch((error) => {