Brand embedded Pi TUI as Feynman
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
27
scripts/patch-embedded-pi.mjs
Normal file
27
scripts/patch-embedded-pi.mjs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
224
src/index.ts
224
src/index.ts
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user