diff --git a/.gitignore b/.gitignore index bb7d380..e244717 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules .feynman .pi/npm .pi/git +.pi/schedule-prompts.json dist *.tgz outputs/* diff --git a/.pi/settings.json b/.pi/settings.json index ebc6304..189c195 100644 --- a/.pi/settings.json +++ b/.pi/settings.json @@ -4,8 +4,14 @@ "npm:pi-docparser", "npm:pi-web-access", "npm:pi-markdown-preview", + "npm:@walterra/pi-charts", + "npm:pi-generative-ui", + "npm:pi-mermaid", "npm:@aliou/pi-processes", - "npm:pi-zotero" + "npm:pi-zotero", + "npm:@kaiserlich-dev/pi-session-search", + "npm:pi-schedule-prompt", + "npm:@samfp/pi-memory" ], "quietStartup": true, "collapseChangelog": true diff --git a/README.md b/README.md index d54b9e3..b08431b 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,14 @@ It keeps the useful parts of a coding agent: - skills - custom extensions -But it biases the runtime toward research work: +But it biases the runtime toward general research work: - literature review -- paper lookup +- source discovery and paper lookup - source comparison - research memo writing - paper and report drafting +- session recall and durable research memory +- recurring and deferred research jobs - replication planning when relevant The primary paper backend is `@companion-ai/alpha-hub` and your alphaXiv account. @@ -29,7 +31,7 @@ npm install -g @companion-ai/feynman Then authenticate alphaXiv and start the CLI: ```bash -feynman --alpha-login +feynman setup feynman ``` @@ -43,7 +45,12 @@ 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. +When you run `feynman`, it launches the real Pi interactive TUI with Feynman's research extensions, skills, prompts, package stack, memory snapshot, and branded defaults preloaded. + +Most users should not need slash commands. The intended default is: +- ask naturally +- let Feynman route into the right workflow +- use slash commands only as explicit shortcuts or overrides ## Commands @@ -56,10 +63,19 @@ Inside the REPL: - `/replicate ` expands the replication prompt template - `/reading-list ` expands the reading-list prompt template - `/research-memo ` expands the general research memo prompt template +- `/deepresearch ` expands the thorough source-heavy research prompt template +- `/autoresearch ` expands the end-to-end idea-to-paper prompt template - `/compare-sources ` expands the source comparison prompt template - `/paper-code-audit ` expands the paper/code audit prompt template - `/paper-draft ` expands the paper-style writing prompt template -- `/research-memo ` expands the general research memo prompt template + +Outside the REPL: + +- `feynman setup` configures alpha login, web research, and preview deps +- `feynman --alpha-login` signs in to alphaXiv +- `feynman --alpha-status` checks alphaXiv auth +- `feynman --doctor` checks models, auth, preview dependencies, and branded settings +- `feynman --setup-preview` installs `pandoc` automatically on macOS/Homebrew systems when preview support is missing ## Custom Tools @@ -71,6 +87,8 @@ The starter extension adds: - `alpha_annotate_paper` for persistent local notes - `alpha_list_annotations` for recall across sessions - `alpha_read_code` for reading a paper repository +- `session_search` for recovering prior Feynman work from stored transcripts +- `preview_file` for browser/PDF review of generated artifacts Feynman uses `@companion-ai/alpha-hub` directly in-process rather than shelling out to the CLI. @@ -82,10 +100,16 @@ Feynman loads a lean research stack from [.pi/settings.json](/Users/advaitpaliwa - `pi-docparser` for PDFs, Office docs, spreadsheets, and images - `pi-web-access` for broader web, GitHub, PDF, and media access - `pi-markdown-preview` for polished Markdown and LaTeX-heavy research writeups +- `@walterra/pi-charts` for charts and quantitative visualizations +- `pi-generative-ui` for interactive HTML-style widgets +- `pi-mermaid` for diagrams in the TUI - `@aliou/pi-processes` for long-running experiments and log tails - `pi-zotero` for citation-library workflows +- `@kaiserlich-dev/pi-session-search` for indexed session recall and summarize/resume UI +- `pi-schedule-prompt` for recurring and deferred research jobs +- `@samfp/pi-memory` for automatic preference/correction memory across sessions -The default expectation is source-grounded outputs with explicit `Sources` sections containing direct URLs. +The default expectation is source-grounded outputs with explicit `Sources` sections containing direct URLs and durable artifacts written to `outputs/`, `notes/`, `experiments/`, or `papers/`. ## Layout @@ -95,5 +119,5 @@ feynman/ ├── papers/ # Polished paper-style drafts and writeups ├── prompts/ # Slash-style prompt templates ├── skills/ # Research workflows -└── src/ # Minimal REPL wrapper around pi-coding-agent +└── src/ # Branded launcher around the embedded Pi TUI ``` diff --git a/extensions/research-tools.ts b/extensions/research-tools.ts index f8c6be9..a1aab66 100644 --- a/extensions/research-tools.ts +++ b/extensions/research-tools.ts @@ -1,3 +1,9 @@ +import { execFile, spawn } from "node:child_process"; +import { mkdir, mkdtemp, readFile, readdir, stat, writeFile } from "node:fs/promises"; +import { homedir, tmpdir } from "node:os"; +import { basename, dirname, extname, join, resolve as resolvePath } from "node:path"; +import { pathToFileURL } from "node:url"; +import { promisify } from "node:util"; import { annotatePaper, askPaper, @@ -8,14 +14,456 @@ import { readPaperCode, searchPapers, } from "@companion-ai/alpha-hub/lib"; -import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; +import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; +const execFileAsync = promisify(execFile); + function formatToolText(result: unknown): string { return typeof result === "string" ? result : JSON.stringify(result, null, 2); } +function getFeynmanHome(): string { + const agentDir = process.env.FEYNMAN_CODING_AGENT_DIR ?? + process.env.PI_CODING_AGENT_DIR ?? + resolvePath(homedir(), ".feynman", "agent"); + return dirname(agentDir); +} + +function extractMessageText(message: unknown): string { + if (!message || typeof message !== "object") { + return ""; + } + + const content = (message as { content?: unknown }).content; + if (typeof content === "string") { + return content; + } + if (!Array.isArray(content)) { + return ""; + } + + return content + .map((item) => { + if (!item || typeof item !== "object") { + return ""; + } + const record = item as { type?: string; text?: unknown; arguments?: unknown; name?: unknown }; + if (record.type === "text" && typeof record.text === "string") { + return record.text; + } + if (record.type === "toolCall") { + const name = typeof record.name === "string" ? record.name : "tool"; + const args = + typeof record.arguments === "string" + ? record.arguments + : record.arguments + ? JSON.stringify(record.arguments) + : ""; + return `[tool:${name}] ${args}`; + } + return ""; + }) + .filter(Boolean) + .join("\n"); +} + +function buildExcerpt(text: string, query: string, radius = 180): string { + const normalizedText = text.replace(/\s+/g, " ").trim(); + if (!normalizedText) { + return ""; + } + + const lower = normalizedText.toLowerCase(); + const q = query.toLowerCase(); + const index = lower.indexOf(q); + if (index === -1) { + return normalizedText.slice(0, radius * 2) + (normalizedText.length > radius * 2 ? "..." : ""); + } + + const start = Math.max(0, index - radius); + const end = Math.min(normalizedText.length, index + q.length + radius); + const prefix = start > 0 ? "..." : ""; + const suffix = end < normalizedText.length ? "..." : ""; + return `${prefix}${normalizedText.slice(start, end)}${suffix}`; +} + +async function searchSessionTranscripts(query: string, limit: number): Promise<{ + query: string; + results: Array<{ + sessionId: string; + sessionFile: string; + startedAt?: string; + cwd?: string; + matchCount: number; + topMatches: Array<{ role: string; timestamp?: string; excerpt: string }>; + }>; +}> { + const packageRoot = process.env.FEYNMAN_PI_NPM_ROOT; + if (packageRoot) { + try { + const indexerPath = pathToFileURL( + join(packageRoot, "@kaiserlich-dev", "pi-session-search", "extensions", "indexer.ts"), + ).href; + const indexer = await import(indexerPath) as { + updateIndex?: (onProgress?: (msg: string) => void) => Promise; + search?: (query: string, limit?: number) => Array<{ + sessionPath: string; + project: string; + timestamp: string; + snippet: string; + rank: number; + title: string | null; + }>; + getSessionSnippets?: (sessionPath: string, query: string, limit?: number) => string[]; + }; + + await indexer.updateIndex?.(); + const results = indexer.search?.(query, limit) ?? []; + if (results.length > 0) { + return { + query, + results: results.map((result) => ({ + sessionId: basename(result.sessionPath), + sessionFile: result.sessionPath, + startedAt: result.timestamp, + cwd: result.project, + matchCount: 1, + topMatches: (indexer.getSessionSnippets?.(result.sessionPath, query, 4) ?? [result.snippet]) + .filter(Boolean) + .map((excerpt) => ({ + role: "match", + excerpt, + })), + })), + }; + } + } catch { + // Fall back to direct JSONL scanning below. + } + } + + const sessionDir = join(getFeynmanHome(), "sessions"); + const terms = query + .toLowerCase() + .split(/\s+/) + .map((term) => term.trim()) + .filter((term) => term.length >= 2); + const needle = query.toLowerCase(); + + let files: string[] = []; + try { + files = (await readdir(sessionDir)) + .filter((entry) => entry.endsWith(".jsonl")) + .map((entry) => join(sessionDir, entry)); + } catch { + return { query, results: [] }; + } + + const sessions = []; + for (const file of files) { + const raw = await readFile(file, "utf8").catch(() => ""); + if (!raw) { + continue; + } + + let sessionId = basename(file); + let startedAt: string | undefined; + let cwd: string | undefined; + const matches: Array<{ role: string; timestamp?: string; excerpt: string }> = []; + + for (const line of raw.split("\n")) { + if (!line.trim()) { + continue; + } + try { + const record = JSON.parse(line) as { + type?: string; + id?: string; + timestamp?: string; + cwd?: string; + message?: { role?: string; content?: unknown }; + }; + if (record.type === "session") { + sessionId = record.id ?? sessionId; + startedAt = record.timestamp; + cwd = record.cwd; + continue; + } + if (record.type !== "message" || !record.message) { + continue; + } + + const text = extractMessageText(record.message); + if (!text) { + continue; + } + const lower = text.toLowerCase(); + const matched = lower.includes(needle) || terms.some((term) => lower.includes(term)); + if (!matched) { + continue; + } + matches.push({ + role: record.message.role ?? "unknown", + timestamp: record.timestamp, + excerpt: buildExcerpt(text, query), + }); + } catch { + continue; + } + } + + if (matches.length === 0) { + continue; + } + + let mtime = 0; + try { + mtime = (await stat(file)).mtimeMs; + } catch { + mtime = 0; + } + + sessions.push({ + sessionId, + sessionFile: file, + startedAt, + cwd, + matchCount: matches.length, + topMatches: matches.slice(0, 4), + mtime, + }); + } + + sessions.sort((a, b) => { + if (b.matchCount !== a.matchCount) { + return b.matchCount - a.matchCount; + } + return b.mtime - a.mtime; + }); + + return { + query, + results: sessions.slice(0, limit).map(({ mtime: _mtime, ...session }) => session), + }; +} + +function isMarkdownPath(path: string): boolean { + return [".md", ".markdown", ".txt"].includes(extname(path).toLowerCase()); +} + +function isLatexPath(path: string): boolean { + return extname(path).toLowerCase() === ".tex"; +} + +function wrapCodeAsMarkdown(source: string, filePath: string): string { + const language = extname(filePath).replace(/^\./, "") || "text"; + return `# ${basename(filePath)}\n\n\`\`\`${language}\n${source}\n\`\`\`\n`; +} + +async function openWithDefaultApp(targetPath: string): Promise { + const target = pathToFileURL(targetPath).href; + if (process.platform === "darwin") { + await execFileAsync("open", [target]); + return; + } + if (process.platform === "win32") { + await execFileAsync("cmd", ["/c", "start", "", target]); + return; + } + await execFileAsync("xdg-open", [target]); +} + +async function runCommandWithInput( + command: string, + args: string[], + input: string, +): Promise<{ stdout: string; stderr: string }> { + return await new Promise((resolve, reject) => { + const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] }); + const stdoutChunks: Buffer[] = []; + const stderrChunks: Buffer[] = []; + + child.stdout.on("data", (chunk: Buffer | string) => { + stdoutChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk); + }); + child.stderr.on("data", (chunk: Buffer | string) => { + stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk); + }); + + child.once("error", reject); + child.once("close", (code) => { + const stdout = Buffer.concat(stdoutChunks).toString("utf8"); + const stderr = Buffer.concat(stderrChunks).toString("utf8"); + if (code === 0) { + resolve({ stdout, stderr }); + return; + } + reject(new Error(`${command} failed with exit code ${code}${stderr ? `: ${stderr.trim()}` : ""}`)); + }); + + child.stdin.end(input); + }); +} + +async function renderHtmlPreview(filePath: string): Promise { + const source = await readFile(filePath, "utf8"); + const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc"; + const inputFormat = isLatexPath(filePath) + ? "latex" + : "markdown+lists_without_preceding_blankline+tex_math_dollars+autolink_bare_uris-raw_html"; + const markdown = isLatexPath(filePath) || isMarkdownPath(filePath) ? source : wrapCodeAsMarkdown(source, filePath); + const args = ["-f", inputFormat, "-t", "html5", "--mathml", "--wrap=none", `--resource-path=${dirname(filePath)}`]; + const { stdout } = await runCommandWithInput(pandocCommand, args, markdown); + const html = `${basename(filePath)}
${stdout}
`; + const tempDir = await mkdtemp(join(tmpdir(), "feynman-preview-")); + const htmlPath = join(tempDir, `${basename(filePath)}.html`); + await writeFile(htmlPath, html, "utf8"); + return htmlPath; +} + +async function renderPdfPreview(filePath: string): Promise { + const source = await readFile(filePath, "utf8"); + const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc"; + const pdfEngine = process.env.PANDOC_PDF_ENGINE?.trim() || "xelatex"; + const inputFormat = isLatexPath(filePath) + ? "latex" + : "markdown+lists_without_preceding_blankline+tex_math_dollars+autolink_bare_uris-raw_html"; + const markdown = isLatexPath(filePath) || isMarkdownPath(filePath) ? source : wrapCodeAsMarkdown(source, filePath); + const tempDir = await mkdtemp(join(tmpdir(), "feynman-preview-")); + const pdfPath = join(tempDir, `${basename(filePath)}.pdf`); + const args = [ + "-f", + inputFormat, + "-o", + pdfPath, + `--pdf-engine=${pdfEngine}`, + `--resource-path=${dirname(filePath)}`, + ]; + await runCommandWithInput(pandocCommand, args, markdown); + return pdfPath; +} + export default function researchTools(pi: ExtensionAPI): void { + function installFeynmanHeader(ctx: ExtensionContext): void { + if (!ctx.hasUI) { + return; + } + + ctx.ui.setHeader((_tui, theme) => ({ + render(_width: number): string[] { + return [ + "", + `${theme.fg("accent", theme.bold("Feynman"))}${theme.fg("muted", " research agent")}`, + theme.fg("dim", "sources first • memory on • scheduled research ready"), + "", + ]; + }, + invalidate() {}, + })); + } + + pi.on("session_start", async (_event, ctx) => { + installFeynmanHeader(ctx); + }); + + pi.on("session_switch", async (_event, ctx) => { + installFeynmanHeader(ctx); + }); + + pi.registerTool({ + name: "session_search", + label: "Session Search", + description: "Search prior Feynman session transcripts to recover what was done, said, or written before.", + parameters: Type.Object({ + query: Type.String({ + description: "Search query to look for in past sessions.", + }), + limit: Type.Optional( + Type.Number({ + description: "Maximum number of sessions to return. Defaults to 3.", + }), + ), + }), + async execute(_toolCallId, params) { + const result = await searchSessionTranscripts(params.query, Math.max(1, Math.min(params.limit ?? 3, 8))); + return { + content: [{ type: "text", text: formatToolText(result) }], + details: result, + }; + }, + }); + pi.registerTool({ name: "alpha_search", label: "Alpha Search", @@ -168,4 +616,47 @@ export default function researchTools(pi: ExtensionAPI): void { } }, }); + + pi.registerTool({ + name: "preview_file", + label: "Preview File", + description: "Open a markdown, LaTeX, PDF, or code artifact in the browser or a PDF viewer for human review.", + parameters: Type.Object({ + path: Type.String({ + description: "Path to the file to preview.", + }), + target: Type.Optional( + Type.String({ + description: "Preview target: browser or pdf. Defaults to browser.", + }), + ), + }), + async execute(_toolCallId, params, _signal, _onUpdate, ctx) { + const target = (params.target?.trim().toLowerCase() || "browser"); + if (target !== "browser" && target !== "pdf") { + throw new Error("target must be browser or pdf"); + } + + const resolvedPath = resolvePath(ctx.cwd, params.path); + const openedPath = + extname(resolvedPath).toLowerCase() === ".pdf" && target === "pdf" + ? resolvedPath + : target === "pdf" + ? await renderPdfPreview(resolvedPath) + : await renderHtmlPreview(resolvedPath); + + await mkdir(dirname(openedPath), { recursive: true }).catch(() => {}); + await openWithDefaultApp(openedPath); + + const result = { + sourcePath: resolvedPath, + target, + openedPath, + }; + return { + content: [{ type: "text", text: formatToolText(result) }], + details: result, + }; + }, + }); } diff --git a/package-lock.json b/package-lock.json index 7a97113..b635b2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@companion-ai/feynman", - "version": "0.1.1", + "version": "0.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@companion-ai/feynman", - "version": "0.1.1", + "version": "0.1.5", "hasInstallScript": true, "dependencies": { "@companion-ai/alpha-hub": "^0.1.2", diff --git a/package.json b/package.json index e691ae6..7dfa056 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@companion-ai/feynman", - "version": "0.1.1", + "version": "0.1.5", "description": "Research-first CLI agent built on Pi and alphaXiv", "type": "module", "bin": { @@ -9,9 +9,11 @@ "files": [ "bin/", "dist/", - ".pi/", + ".pi/settings.json", + ".pi/themes/", "extensions/", "prompts/", + "scripts/", "skills/", "README.md", ".env.example" diff --git a/prompts/autoresearch.md b/prompts/autoresearch.md new file mode 100644 index 0000000..240054e --- /dev/null +++ b/prompts/autoresearch.md @@ -0,0 +1,16 @@ +--- +description: Turn a research idea into a paper-oriented end-to-end run with literature, hypotheses, experiments when possible, and a draft artifact. +--- +Run an autoresearch workflow for: $@ + +Requirements: +- Start by clarifying the research objective, scope, and target contribution. +- Search for the strongest relevant primary sources first. +- If the topic is current, product-oriented, market-facing, or asks about latest developments, start with `web_search` and `fetch_content`. +- Use `alpha_search` for academic background or paper-centric parts of the topic, but do not rely on it alone for current topics. +- Build a compact evidence table before committing to a paper narrative. +- If experiments are feasible in the current environment, design and run the smallest experiment that materially reduces uncertainty. +- If experiments are not feasible, produce a paper-style draft that is explicit about missing validation and limitations. +- Save intermediate planning or synthesis artifacts to `notes/` or `outputs/`. +- Save the final paper-style draft to `papers/`. +- End with a `Sources` section containing direct URLs for every source used. diff --git a/prompts/compare-sources.md b/prompts/compare-sources.md index 500bd38..395a4f0 100644 --- a/prompts/compare-sources.md +++ b/prompts/compare-sources.md @@ -5,6 +5,8 @@ Compare sources for: $@ Requirements: - Identify the strongest relevant primary sources first. +- For current or market-facing topics, use `web_search` and `fetch_content` to gather up-to-date primary sources before comparing them. +- For academic claims, use `alpha_search` and inspect the strongest papers directly. - Inspect the top sources directly before comparing them. - Build a comparison matrix covering: - source diff --git a/prompts/deepresearch.md b/prompts/deepresearch.md new file mode 100644 index 0000000..893ef40 --- /dev/null +++ b/prompts/deepresearch.md @@ -0,0 +1,13 @@ +--- +description: Run a thorough, source-heavy investigation on a topic and produce a durable research brief with explicit evidence and source links. +--- +Run a deep research workflow for: $@ + +Requirements: +- If the topic is current, product-oriented, market-facing, regulatory, or asks about latest developments, start with `web_search` and `fetch_content`. +- If the topic has an academic literature component, use `alpha_search`, `alpha_get_paper`, and `alpha_ask_paper` for the strongest papers. +- Do not rely on a single source type when the topic spans both current reality and academic background. +- Build a compact evidence table before synthesizing conclusions. +- Distinguish clearly between established facts, plausible inferences, disagreements, and unresolved questions. +- Produce a durable markdown artifact in `outputs/`. +- End with a `Sources` section containing direct URLs for every source used. diff --git a/prompts/lit-review.md b/prompts/lit-review.md index c7645ea..75c9a30 100644 --- a/prompts/lit-review.md +++ b/prompts/lit-review.md @@ -4,7 +4,8 @@ description: Run a literature review on a topic using paper search and primary-s Investigate the following topic as a literature review: $@ Requirements: -- Use `alpha_search` first. +- If the topic is academic or paper-centric, use `alpha_search` first. +- If the topic is current, product-oriented, market-facing, or asks about latest developments, use `web_search` and `fetch_content` first, then use `alpha_search` only for academic background. - Use `alpha_get_paper` on the most relevant papers before making strong claims. - Use `alpha_ask_paper` for targeted follow-up questions when the report is not enough. - Prefer primary sources and note when something appears to be a preprint or secondary summary. diff --git a/prompts/reading-list.md b/prompts/reading-list.md index 2aef81d..5db85f0 100644 --- a/prompts/reading-list.md +++ b/prompts/reading-list.md @@ -4,8 +4,9 @@ description: Build a prioritized reading list on a research topic with rationale Create a research reading list for: $@ Requirements: -- Use `alpha_search` with `all` mode. -- Inspect the strongest papers with `alpha_get_paper`. +- If the topic is academic, use `alpha_search` with `all` mode. +- If the topic is current, product-oriented, or asks for the latest landscape, use `web_search` and `fetch_content` first, then add `alpha_search` for academic background when relevant. +- Inspect the strongest papers or primary sources directly before recommending them. - Use `alpha_ask_paper` when a paper's fit is unclear. - Group papers by role when useful: foundational, strongest recent work, methods, benchmarks, critiques, replication targets. - For each paper, explain why it is on the list. diff --git a/prompts/research-memo.md b/prompts/research-memo.md index 95708f7..4797d9d 100644 --- a/prompts/research-memo.md +++ b/prompts/research-memo.md @@ -5,6 +5,8 @@ Write a research memo about: $@ Requirements: - Start by finding the strongest relevant sources. +- If the topic is current, market-facing, product-oriented, regulatory, or asks about latest developments, use `web_search` and `fetch_content` first. +- Use `alpha_search` for academic background where relevant, but do not rely on it alone for current topics. - Read or inspect the top sources directly before making strong claims. - Distinguish facts, interpretations, and open questions. - End with a `Sources` section containing direct URLs for every source used. diff --git a/scripts/patch-embedded-pi.mjs b/scripts/patch-embedded-pi.mjs index ce5beb7..8918030 100644 --- a/scripts/patch-embedded-pi.mjs +++ b/scripts/patch-embedded-pi.mjs @@ -1,4 +1,5 @@ -import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -7,6 +8,68 @@ 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"); +const interactiveModePath = resolve(piPackageRoot, "dist", "modes", "interactive", "interactive-mode.js"); +const workspaceRoot = resolve(appRoot, ".pi", "npm", "node_modules"); +const webAccessPath = resolve(workspaceRoot, "pi-web-access", "index.ts"); +const sessionSearchIndexerPath = resolve( + workspaceRoot, + "@kaiserlich-dev", + "pi-session-search", + "extensions", + "indexer.ts", +); +const piMemoryPath = resolve(workspaceRoot, "@samfp", "pi-memory", "src", "index.ts"); +const settingsPath = resolve(appRoot, ".pi", "settings.json"); +const workspaceDir = resolve(appRoot, ".pi", "npm"); +const workspacePackageJsonPath = resolve(workspaceDir, "package.json"); + +function ensurePackageWorkspace() { + if (!existsSync(settingsPath)) { + return; + } + + const settings = JSON.parse(readFileSync(settingsPath, "utf8")); + const packageSpecs = Array.isArray(settings.packages) + ? settings.packages + .filter((value) => typeof value === "string" && value.startsWith("npm:")) + .map((value) => value.slice(4)) + : []; + + if (packageSpecs.length === 0) { + return; + } + + mkdirSync(workspaceDir, { recursive: true }); + + writeFileSync( + workspacePackageJsonPath, + JSON.stringify( + { + name: "pi-extensions", + private: true, + dependencies: Object.fromEntries(packageSpecs.map((spec) => [spec, "latest"])), + }, + null, + 2, + ) + "\n", + "utf8", + ); + + const npmExec = process.env.npm_execpath; + const install = npmExec + ? spawnSync(process.execPath, [npmExec, "install", "--prefix", workspaceDir, ...packageSpecs], { + stdio: "inherit", + }) + : spawnSync("npm", ["install", "--prefix", workspaceDir, ...packageSpecs], { + stdio: "inherit", + }); + + if (install.status !== 0) { + console.warn("[feynman] warning: failed to preinstall default Pi packages into .pi/npm"); + } +} + +ensurePackageWorkspace(); if (existsSync(packageJsonPath)) { const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8")); @@ -25,3 +88,59 @@ if (existsSync(cliPath)) { writeFileSync(cliPath, cliSource.replace('process.title = "pi";', 'process.title = "feynman";'), "utf8"); } } + +if (existsSync(interactiveModePath)) { + const interactiveModeSource = readFileSync(interactiveModePath, "utf8"); + if (interactiveModeSource.includes("`π - ${sessionName} - ${cwdBasename}`")) { + writeFileSync( + interactiveModePath, + interactiveModeSource + .replace("`π - ${sessionName} - ${cwdBasename}`", "`feynman - ${sessionName} - ${cwdBasename}`") + .replace("`π - ${cwdBasename}`", "`feynman - ${cwdBasename}`"), + "utf8", + ); + } +} + +if (existsSync(webAccessPath)) { + const source = readFileSync(webAccessPath, "utf8"); + if (source.includes('pi.registerCommand("search",')) { + writeFileSync( + webAccessPath, + source.replace('pi.registerCommand("search",', 'pi.registerCommand("web-results",'), + "utf8", + ); + } +} + +if (existsSync(sessionSearchIndexerPath)) { + const source = readFileSync(sessionSearchIndexerPath, "utf8"); + const original = 'const sessionsDir = path.join(os.homedir(), ".pi", "agent", "sessions");'; + const replacement = + 'const sessionsDir = process.env.FEYNMAN_SESSION_DIR ?? process.env.PI_SESSION_DIR ?? path.join(os.homedir(), ".pi", "agent", "sessions");'; + if (source.includes(original)) { + writeFileSync(sessionSearchIndexerPath, source.replace(original, replacement), "utf8"); + } +} + +if (existsSync(piMemoryPath)) { + let source = readFileSync(piMemoryPath, "utf8"); + const memoryOriginal = 'const MEMORY_DIR = join(homedir(), ".pi", "memory");'; + const memoryReplacement = + 'const MEMORY_DIR = process.env.FEYNMAN_MEMORY_DIR ?? process.env.PI_MEMORY_DIR ?? join(homedir(), ".pi", "memory");'; + if (source.includes(memoryOriginal)) { + source = source.replace(memoryOriginal, memoryReplacement); + } + const execOriginal = 'const result = await pi.exec("pi", ["-p", prompt, "--print"], {'; + const execReplacement = [ + 'const execBinary = process.env.FEYNMAN_NODE_EXECUTABLE || process.env.FEYNMAN_EXECUTABLE || "pi";', + ' const execArgs = process.env.FEYNMAN_BIN_PATH', + ' ? [process.env.FEYNMAN_BIN_PATH, "--prompt", prompt]', + ' : ["-p", prompt, "--print"];', + ' const result = await pi.exec(execBinary, execArgs, {', + ].join("\n"); + if (source.includes(execOriginal)) { + source = source.replace(execOriginal, execReplacement); + } + writeFileSync(piMemoryPath, source, "utf8"); +} diff --git a/skills/research/autoresearch/SKILL.md b/skills/research/autoresearch/SKILL.md new file mode 100644 index 0000000..284ced4 --- /dev/null +++ b/skills/research/autoresearch/SKILL.md @@ -0,0 +1,56 @@ +--- +name: autoresearch +description: Use this when the user wants an end-to-end idea-to-paper run, from problem framing through literature, experiments if feasible, and a paper-style draft. +--- + +# AutoResearch + +## When To Use + +Use this skill when the user wants: +- an idea turned into a paper-style draft +- a full research workflow, not just a memo or reading list +- autonomous progress from topic framing to deliverable + +## Procedure + +1. Restate the idea as a concrete research question and identify the likely contribution type: + - empirical result + - synthesis or review + - method proposal + - benchmark or audit +2. Search for relevant primary sources first. +3. If the topic is current, product-oriented, market-facing, or asks about latest developments, start with `web_search` and `fetch_content`. +4. Use `alpha_search`, `alpha_get_paper`, and `alpha_ask_paper` for academic background or paper-centric parts of the topic. +5. Build a compact evidence table in `notes/` or `outputs/` before deciding on the paper narrative. +6. Decide whether experiments are feasible in the current environment: + - if yes, design and run the smallest experiment that materially reduces uncertainty + - if no, continue with a literature-grounded or theory-grounded draft and state the limitation clearly +7. Produce at least two artifacts: + - an intermediate artifact (research memo, evidence table, or experiment log) + - a final paper-style draft in `papers/` +8. Structure the final draft with: + - title + - abstract + - introduction + - related work + - method or synthesis + - evidence or experiments + - limitations + - conclusion +9. End with a `Sources` section containing direct URLs for every source used. + +## Pitfalls + +- Do not jump straight to drafting before checking the literature. +- Do not treat a current topic as if papers alone are enough. +- Do not fake experiments when the environment cannot support them. +- Do not present speculative contributions as established results. +- Do not omit limitations or missing validation. + +## Deliverable + +A complete idea-to-paper run should leave behind: +- one intermediate artifact in `notes/` or `outputs/` +- one final paper-style draft in `papers/` +- a source list with direct URLs diff --git a/skills/research/context-recall/SKILL.md b/skills/research/context-recall/SKILL.md new file mode 100644 index 0000000..2086d45 --- /dev/null +++ b/skills/research/context-recall/SKILL.md @@ -0,0 +1,39 @@ +--- +name: context-recall +description: Use this when the user asks what was done before, refers to earlier sessions, wants prior artifacts, or expects Feynman to remember past work. +--- + +# Context Recall + +## When To Use + +Use this skill when the user: +- asks what was done previously +- refers to an earlier paper, memo, or artifact +- expects cross-session continuity +- asks what has already been tried or written + +## Procedure + +1. Read durable memory first with `memory_search` or `memory_lessons`. +2. Search prior sessions with `session_search`. +3. If needed, inspect the current workspace for artifacts in `outputs/`, `notes/`, `experiments/`, and `papers/`. +4. Distinguish clearly between: + - durable remembered facts + - session transcript recall + - currently present files on disk +5. If you find a stable correction or preference that should persist, save it with `memory_remember`. + +## Pitfalls + +- Do not claim to remember something without checking memory or session history. +- Do not confuse durable memory with transient task progress. +- Do not summarize prior work from vague impressions; recover evidence first. + +## Deliverable + +Include: +- what was previously done +- where the evidence came from +- which artifacts or files exist now +- any gaps or uncertainty diff --git a/skills/research/deep-research/SKILL.md b/skills/research/deep-research/SKILL.md new file mode 100644 index 0000000..4e8c148 --- /dev/null +++ b/skills/research/deep-research/SKILL.md @@ -0,0 +1,54 @@ +--- +name: deep-research +description: Use this when the user wants a broad, thorough investigation with strong sourcing, explicit evidence tables, and a durable research brief. +--- + +# Deep Research + +## When To Use + +Use this skill when the user wants: +- a thorough investigation rather than a quick memo +- a broad landscape analysis +- careful source comparison across multiple source types +- a durable research brief with explicit evidence + +## Procedure + +1. Clarify the exact scope and what decision or question the research should support. +2. Choose the right retrieval mix: + - use `web_search` and `fetch_content` first for current, product, market, regulatory, or latest topics + - use `alpha_search`, `alpha_get_paper`, and `alpha_ask_paper` for academic background or paper-centric claims + - use both when the topic spans current reality and academic literature +3. Gather enough high-quality sources before synthesizing. +4. Build an evidence table covering: + - source + - claim + - evidence type + - caveats + - relevance +5. Synthesize: + - strongest findings + - disagreements + - open questions + - what would change the conclusion +6. Save a durable markdown brief to `outputs/`. +7. End with a `Sources` section containing direct URLs for every source used. + +## Pitfalls + +- Do not answer a current topic from papers alone. +- Do not answer an academic topic from search snippets alone. +- Do not collapse disagreement into fake consensus. +- Do not omit the evidence table on broad or high-stakes topics. + +## Deliverable + +Include: +- scope +- evidence table +- key findings +- disagreements or caveats +- open questions +- recommendation or next step +- sources diff --git a/skills/research/experiment-design/SKILL.md b/skills/research/experiment-design/SKILL.md index 7ac7baa..fef14c7 100644 --- a/skills/research/experiment-design/SKILL.md +++ b/skills/research/experiment-design/SKILL.md @@ -22,12 +22,13 @@ Use this skill when the user has: - success metrics - baselines - constraints -3. Search for prior work first with `alpha_search` so you do not reinvent an obviously flawed setup. -4. Use `alpha_get_paper` and `alpha_ask_paper` on the strongest references. -5. Prefer the smallest experiment that can meaningfully reduce uncertainty. -6. List confounders and failure modes up front. -7. If implementation is requested, create the scripts, configs, and logging plan. -8. Write the plan to disk before running expensive work. +3. Search for prior work first. +4. If the setup is tied to current products, APIs, model offerings, pricing, or market behavior, use `web_search` and `fetch_content` first. +5. Use `alpha_search`, `alpha_get_paper`, and `alpha_ask_paper` for academic baselines and prior experiments. +6. Prefer the smallest experiment that can meaningfully reduce uncertainty. +7. List confounders and failure modes up front. +8. If implementation is requested, create the scripts, configs, and logging plan. +9. Write the plan to disk before running expensive work. ## Pitfalls diff --git a/skills/research/literature-review/SKILL.md b/skills/research/literature-review/SKILL.md index ed523c3..2028ad2 100644 --- a/skills/research/literature-review/SKILL.md +++ b/skills/research/literature-review/SKILL.md @@ -16,24 +16,26 @@ Use this skill when the user wants: ## Procedure -1. Search broadly first with `alpha_search`. -2. Pick the strongest candidates by direct relevance, recency, citations, and venue quality. -3. Inspect the top papers with `alpha_get_paper` before making concrete claims. -4. Use `alpha_ask_paper` for missing methodological or experimental details. -5. Build a compact evidence table: +1. Search broadly first. +2. If the topic is primarily academic or paper-centric, start with `alpha_search`. +3. If the topic includes current products, companies, markets, software, or "latest/current" framing, start with `web_search` and `fetch_content`, then use `alpha_search` only for academic background. +4. Pick the strongest candidates by direct relevance, recency, citations, venue quality, and source quality. +5. Inspect the top papers with `alpha_get_paper` before making concrete claims. +6. Use `alpha_ask_paper` for missing methodological or experimental details. +7. Build a compact evidence table: - title - year - authors - venue - claim or contribution - important caveats -6. Distinguish: +8. Distinguish: - what multiple sources agree on - where methods or findings differ - what remains unresolved -7. If the user wants a durable artifact, write a markdown brief to disk. -8. If you discover an important gotcha about a paper, save it with `alpha_annotate_paper`. -9. End with a `Sources` section that lists direct URLs, not just titles. +9. If the user wants a durable artifact, write a markdown brief to disk. +10. If you discover an important gotcha about a paper, save it with `alpha_annotate_paper`. +11. End with a `Sources` section that lists direct URLs, not just titles. ## Pitfalls @@ -41,6 +43,7 @@ Use this skill when the user wants: - Do not flatten disagreements into fake consensus. - Do not treat recent preprints as established facts without saying so. - Do not cite secondary commentary when a primary source is available. +- Do not treat a current product or market topic as if it were a paper-only topic. ## Output Shape diff --git a/skills/research/reading-list/SKILL.md b/skills/research/reading-list/SKILL.md index 65d567e..f89f3d2 100644 --- a/skills/research/reading-list/SKILL.md +++ b/skills/research/reading-list/SKILL.md @@ -15,30 +15,33 @@ Use this skill for: ## Procedure -1. Start with `alpha_search` in `all` mode. -2. Inspect the strongest candidates with `alpha_get_paper`. -3. Use `alpha_ask_paper` for fit questions like: +1. Start with source discovery that matches the topic. +2. For academic topics, use `alpha_search` in `all` mode. +3. For current, product-oriented, or market-facing topics, use `web_search` and `fetch_content` first, then use `alpha_search` for background literature if needed. +4. Inspect the strongest candidates directly before recommending them. +5. Use `alpha_ask_paper` for fit questions like: - what problem does this really solve - what assumptions does it rely on - what prior work does it build on -4. Classify papers into roles: +6. Classify papers or sources into roles: - foundational - key recent advances - evaluation or benchmark references - critiques or limitations - likely replication targets -5. Order the list intentionally: +7. Order the list intentionally: - start with orientation - move to strongest methods - finish with edges, critiques, or adjacent work -6. Write the final list as a durable markdown artifact in `outputs/`. -7. For every paper, include a direct URL. +8. Write the final list as a durable markdown artifact in `outputs/`. +9. For every source, include a direct URL. ## Pitfalls - Do not sort purely by citations. - Do not over-index on recency when fundamentals matter. - Do not include papers you have not inspected at all. +- Do not force everything into papers when the user actually needs current docs, products, or market sources. ## Deliverable diff --git a/skills/research/research-memo/SKILL.md b/skills/research/research-memo/SKILL.md index 21a8346..c0e6bd8 100644 --- a/skills/research/research-memo/SKILL.md +++ b/skills/research/research-memo/SKILL.md @@ -17,20 +17,23 @@ Use this skill for: ## Procedure 1. Find relevant sources first. -2. Inspect the strongest sources directly before synthesizing. -3. Separate: +2. If the topic is current, product-oriented, market-facing, or asks about latest developments, use `web_search` and `fetch_content` first. +3. If there is an academic literature component, use `alpha_search` and inspect the strongest papers directly. +4. Inspect the strongest sources directly before synthesizing. +5. Separate: - established facts - plausible inferences - unresolved questions -4. Write a memo with clear sections and a concise narrative. -5. End with a `Sources` section containing direct links. -6. Save the memo to `outputs/` when the user wants a durable artifact. +6. Write a memo with clear sections and a concise narrative. +7. End with a `Sources` section containing direct links. +8. Save the memo to `outputs/` when the user wants a durable artifact. ## Pitfalls - Do not summarize from search snippets alone. - Do not omit the source list. - Do not present inference as fact. +- Do not rely on paper search alone for latest/current topics. ## Deliverable diff --git a/src/feynman-prompt.ts b/src/feynman-prompt.ts index 11d385d..bf24911 100644 --- a/src/feynman-prompt.ts +++ b/src/feynman-prompt.ts @@ -1,6 +1,7 @@ -export const FEYNMAN_SYSTEM_PROMPT = `You are Feynman, a research-first AI agent. +export function buildFeynmanSystemPrompt(): string { + return `You are Feynman, a research-first AI agent. -Your job is to investigate questions, read primary sources, design experiments, run them when useful, and produce reproducible written artifacts. +Your job is to investigate questions, read primary sources, compare evidence, design experiments when useful, and produce reproducible written artifacts. Operating rules: - Evidence over fluency. @@ -9,18 +10,32 @@ Operating rules: - State uncertainty explicitly. - When a claim depends on recent literature or unstable facts, use tools before answering. - When discussing papers, cite title, year, and identifier or URL when possible. -- Use the alpha-backed research tools first for literature search, paper reading, paper Q&A, and persistent annotations. -- Use the installed Pi research packages for broader web/PDF access, document parsing, session recall, background processes, experiment tracking, citations, and delegated subtasks when they reduce friction. +- Use the alpha-backed research tools for academic paper search, paper reading, paper Q&A, repository inspection, and persistent annotations. +- Use \`web_search\`, \`fetch_content\`, and \`get_search_content\` first for current topics: products, companies, markets, regulations, software releases, model availability, model pricing, benchmarks, docs, or anything phrased as latest/current/recent/today. +- For mixed topics, combine both: use web sources for current reality and paper sources for background literature. +- Never answer a latest/current question from arXiv or alpha-backed paper search alone. +- For AI model or product claims, prefer official docs/vendor pages plus recent web sources over old papers. +- Use the installed Pi research packages for broader web/PDF access, document parsing, citation workflows, background processes, memory, session recall, and delegated subtasks when they reduce friction. +- Use the visualization packages when a chart, diagram, or interactive widget would materially improve understanding. Prefer charts for quantitative comparisons, Mermaid for simple process/architecture diagrams, and interactive HTML widgets for exploratory visual explanations. +- Persistent memory is package-backed. Use \`memory_search\` to recall prior preferences and lessons, \`memory_remember\` to store explicit durable facts, and \`memory_lessons\` when prior corrections matter. +- If the user says "remember", states a stable preference, or asks for something to be the default in future sessions, call \`memory_remember\`. Do not just say you will remember it. +- Session recall is package-backed. Use \`session_search\` when the user references prior work, asks what has been done before, or when you suspect relevant past context exists. +- Feynman is intended to support always-on research work. Use the scheduling package when recurring or deferred work is appropriate instead of telling the user to remember manually. +- Use \`schedule_prompt\` for recurring scans, delayed follow-ups, reminders, and periodic research jobs. +- If the user asks you to remind, check later, run something nightly, or keep watching something over time, call \`schedule_prompt\`. Do not just promise to do it later. +- Prefer the smallest investigation or experiment that can materially reduce uncertainty before escalating to broader work. - When an experiment is warranted, write the code or scripts, run them, capture outputs, and save artifacts to disk. - Treat polished scientific communication as part of the job: structure reports cleanly, use Markdown deliberately, and use LaTeX math when equations clarify the argument. - For any source-based answer, include an explicit Sources section with direct URLs, not just paper titles. - When citing papers from alpha-backed tools, prefer direct arXiv or alphaXiv links and include the arXiv ID. +- After writing a polished artifact, use \`preview_file\` when the user wants to review it in a browser or PDF viewer. +- Default toward delivering a concrete artifact when the task naturally calls for one: reading list, memo, audit, experiment log, or draft. - Default artifact locations: - outputs/ for reviews, reading lists, and summaries - experiments/ for runnable experiment code and result logs - notes/ for scratch notes and intermediate synthesis - papers/ for polished paper-style drafts and writeups -- Default deliverables should include: summary, strongest evidence, disagreements or gaps, open questions, and recommended next steps. +- Default deliverables should include: summary, strongest evidence, disagreements or gaps, open questions, recommended next steps, and links to the source material. Default workflow: 1. Clarify the research objective if needed. @@ -33,4 +48,6 @@ Default workflow: Style: - Concise, skeptical, and explicit. - Avoid fake certainty. -- Do not present unverified claims as facts.`; +- Do not present unverified claims as facts. +- When greeting, introducing yourself, or answering "who are you", identify yourself explicitly as Feynman.`; +} diff --git a/src/index.ts b/src/index.ts index 25f0090..e56cf62 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,11 @@ import "dotenv/config"; -import { spawn } from "node:child_process"; +import { spawn, spawnSync } from "node:child_process"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { dirname, resolve } from "node:path"; +import { stdin as input, stdout as output } from "node:process"; +import { createInterface } from "node:readline/promises"; import { parseArgs } from "node:util"; import { fileURLToPath } from "node:url"; @@ -18,7 +20,7 @@ import { AuthStorage, } from "@mariozechner/pi-coding-agent"; -import { FEYNMAN_SYSTEM_PROMPT } from "./feynman-prompt.js"; +import { buildFeynmanSystemPrompt } from "./feynman-prompt.js"; type ThinkingLevel = "off" | "low" | "medium" | "high"; @@ -34,6 +36,8 @@ function printHelp(): void { /replicate Expand the replication prompt template /reading-list Expand the reading list prompt template /research-memo Expand the general research memo prompt template + /deepresearch Expand the thorough source-heavy research prompt template + /autoresearch Expand the idea-to-paper autoresearch prompt template /compare-sources Expand the source comparison prompt template /paper-code-audit Expand the paper/code audit prompt template /paper-draft Expand the paper-style writing prompt template @@ -46,7 +50,15 @@ function printHelp(): void { --model provider:model Force a specific model --thinking level off | low | medium | high --cwd /path/to/workdir Working directory for tools - --session-dir /path Session storage directory`); + --session-dir /path Session storage directory + --doctor Check Feynman auth, models, preview tools, and paths + --setup-preview Install preview dependencies when possible + +Top-level: + feynman setup Configure alpha login, web search, and preview deps + feynman setup alpha Configure alphaXiv login + feynman setup web Configure web search provider + feynman setup preview Install preview dependencies`); } function parseModelSpec(spec: string, modelRegistry: ModelRegistry) { @@ -78,9 +90,32 @@ function normalizeThinkingLevel(value: string | undefined): ThinkingLevel | unde return undefined; } +function resolveExecutable(name: string, fallbackPaths: string[] = []): string | undefined { + for (const candidate of fallbackPaths) { + if (existsSync(candidate)) { + return candidate; + } + } + + const result = spawnSync("sh", ["-lc", `command -v ${name}`], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + + if (result.status === 0) { + const resolved = result.stdout.trim(); + if (resolved) { + return resolved; + } + } + + return undefined; +} + function patchEmbeddedPiBranding(piPackageRoot: string): void { const packageJsonPath = resolve(piPackageRoot, "package.json"); const cliPath = resolve(piPackageRoot, "dist", "cli.js"); + const interactiveModePath = resolve(piPackageRoot, "dist", "modes", "interactive", "interactive-mode.js"); if (existsSync(packageJsonPath)) { const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { @@ -101,6 +136,77 @@ function patchEmbeddedPiBranding(piPackageRoot: string): void { writeFileSync(cliPath, cliSource.replace('process.title = "pi";', 'process.title = "feynman";'), "utf8"); } } + + if (existsSync(interactiveModePath)) { + const interactiveModeSource = readFileSync(interactiveModePath, "utf8"); + if (interactiveModeSource.includes("`π - ${sessionName} - ${cwdBasename}`")) { + writeFileSync( + interactiveModePath, + interactiveModeSource + .replace("`π - ${sessionName} - ${cwdBasename}`", "`feynman - ${sessionName} - ${cwdBasename}`") + .replace("`π - ${cwdBasename}`", "`feynman - ${cwdBasename}`"), + "utf8", + ); + } + } +} + +function patchPackageWorkspace(appRoot: string): void { + const workspaceRoot = resolve(appRoot, ".pi", "npm", "node_modules"); + const webAccessPath = resolve(workspaceRoot, "pi-web-access", "index.ts"); + const sessionSearchIndexerPath = resolve( + workspaceRoot, + "@kaiserlich-dev", + "pi-session-search", + "extensions", + "indexer.ts", + ); + const piMemoryPath = resolve(workspaceRoot, "@samfp", "pi-memory", "src", "index.ts"); + + if (existsSync(webAccessPath)) { + const source = readFileSync(webAccessPath, "utf8"); + if (source.includes('pi.registerCommand("search",')) { + writeFileSync( + webAccessPath, + source.replace('pi.registerCommand("search",', 'pi.registerCommand("web-results",'), + "utf8", + ); + } + } + + if (existsSync(sessionSearchIndexerPath)) { + const source = readFileSync(sessionSearchIndexerPath, "utf8"); + const original = 'const sessionsDir = path.join(os.homedir(), ".pi", "agent", "sessions");'; + const replacement = + 'const sessionsDir = process.env.FEYNMAN_SESSION_DIR ?? process.env.PI_SESSION_DIR ?? path.join(os.homedir(), ".pi", "agent", "sessions");'; + if (source.includes(original)) { + writeFileSync(sessionSearchIndexerPath, source.replace(original, replacement), "utf8"); + } + } + + if (existsSync(piMemoryPath)) { + let source = readFileSync(piMemoryPath, "utf8"); + const memoryOriginal = 'const MEMORY_DIR = join(homedir(), ".pi", "memory");'; + const memoryReplacement = + 'const MEMORY_DIR = process.env.FEYNMAN_MEMORY_DIR ?? process.env.PI_MEMORY_DIR ?? join(homedir(), ".pi", "memory");'; + if (source.includes(memoryOriginal)) { + source = source.replace(memoryOriginal, memoryReplacement); + } + + const execOriginal = 'const result = await pi.exec("pi", ["-p", prompt, "--print"], {'; + const execReplacement = [ + 'const execBinary = process.env.FEYNMAN_NODE_EXECUTABLE || process.env.FEYNMAN_EXECUTABLE || "pi";', + ' const execArgs = process.env.FEYNMAN_BIN_PATH', + ' ? [process.env.FEYNMAN_BIN_PATH, "--prompt", prompt]', + ' : ["-p", prompt, "--print"];', + ' const result = await pi.exec(execBinary, execArgs, {', + ].join("\n"); + if (source.includes(execOriginal)) { + source = source.replace(execOriginal, execReplacement); + } + + writeFileSync(piMemoryPath, source, "utf8"); + } } function choosePreferredModel( @@ -149,12 +255,6 @@ function normalizeFeynmanSettings( } } - if (Array.isArray(settings.packages)) { - settings.packages = settings.packages.filter( - (entry) => entry !== "npm:@kaiserlich-dev/pi-session-search", - ); - } - if (!settings.defaultThinkingLevel) { settings.defaultThinkingLevel = defaultThinkingLevel; } @@ -180,6 +280,217 @@ function normalizeFeynmanSettings( writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8"); } +function readJson(path: string): Record { + if (!existsSync(path)) { + return {}; + } + + try { + return JSON.parse(readFileSync(path, "utf8")); + } catch { + return {}; + } +} + +function getWebSearchConfigPath(): string { + return resolve(homedir(), ".pi", "web-search.json"); +} + +function loadWebSearchConfig(): Record { + return readJson(getWebSearchConfigPath()); +} + +function saveWebSearchConfig(config: Record): void { + const path = getWebSearchConfigPath(); + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, JSON.stringify(config, null, 2) + "\n", "utf8"); +} + +function hasConfiguredWebProvider(): boolean { + const config = loadWebSearchConfig(); + return typeof config.perplexityApiKey === "string" && config.perplexityApiKey.trim().length > 0 + || typeof config.geminiApiKey === "string" && config.geminiApiKey.trim().length > 0; +} + +async function promptText(question: string, defaultValue = ""): Promise { + if (!input.isTTY || !output.isTTY) { + throw new Error("feynman setup requires an interactive terminal."); + } + const rl = createInterface({ input, output }); + try { + const suffix = defaultValue ? ` [${defaultValue}]` : ""; + const value = (await rl.question(`${question}${suffix}: `)).trim(); + return value || defaultValue; + } finally { + rl.close(); + } +} + +async function promptChoice(question: string, choices: string[], defaultIndex = 0): Promise { + console.log(question); + for (const [index, choice] of choices.entries()) { + const marker = index === defaultIndex ? "*" : " "; + console.log(` ${marker} ${index + 1}. ${choice}`); + } + const answer = await promptText("Select", String(defaultIndex + 1)); + const parsed = Number(answer); + if (!Number.isFinite(parsed) || parsed < 1 || parsed > choices.length) { + return defaultIndex; + } + return parsed - 1; +} + +async function setupWebProvider(): Promise { + const config = loadWebSearchConfig(); + const choices = [ + "Gemini API key", + "Perplexity API key", + "Browser Gemini (manual sign-in only)", + "Skip", + ]; + const selection = await promptChoice("Choose a web search provider for Feynman:", choices, hasConfiguredWebProvider() ? 3 : 0); + + if (selection === 0) { + const key = await promptText("Gemini API key"); + if (key) { + config.geminiApiKey = key; + delete config.perplexityApiKey; + saveWebSearchConfig(config); + console.log("Saved Gemini API key to ~/.pi/web-search.json"); + } + return; + } + + if (selection === 1) { + const key = await promptText("Perplexity API key"); + if (key) { + config.perplexityApiKey = key; + delete config.geminiApiKey; + saveWebSearchConfig(config); + console.log("Saved Perplexity API key to ~/.pi/web-search.json"); + } + return; + } + + if (selection === 2) { + console.log("Sign into gemini.google.com in Chrome, Chromium, Brave, or Edge, then restart Feynman."); + return; + } +} + +async function runSetup( + section: string | undefined, + settingsPath: string, + bundledSettingsPath: string, + authPath: string, + workingDir: string, + sessionDir: string, +): Promise { + if (section === "alpha" || !section) { + if (!isAlphaLoggedIn()) { + await loginAlpha(); + console.log("alphaXiv login complete"); + } else { + console.log("alphaXiv login already configured"); + } + if (section === "alpha") return; + } + + if (section === "web" || !section) { + await setupWebProvider(); + if (section === "web") return; + } + + if (section === "preview" || !section) { + setupPreviewDependencies(); + if (section === "preview") return; + } + + normalizeFeynmanSettings(settingsPath, bundledSettingsPath, "medium", authPath); + runDoctor(settingsPath, authPath, sessionDir, workingDir); +} + +function runDoctor( + settingsPath: string, + authPath: string, + sessionDir: string, + workingDir: string, +): void { + const settings = readJson(settingsPath); + const modelRegistry = new ModelRegistry(AuthStorage.create(authPath)); + const availableModels = modelRegistry.getAvailable(); + const pandocPath = resolveExecutable("pandoc", [ + "/opt/homebrew/bin/pandoc", + "/usr/local/bin/pandoc", + ]); + const browserPath = + process.env.PUPPETEER_EXECUTABLE_PATH ?? + resolveExecutable("google-chrome", [ + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + "/Applications/Chromium.app/Contents/MacOS/Chromium", + "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser", + "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", + ]); + + console.log("Feynman doctor"); + console.log(""); + console.log(`working dir: ${workingDir}`); + console.log(`session dir: ${sessionDir}`); + console.log(""); + console.log(`alphaXiv auth: ${isAlphaLoggedIn() ? "ok" : "missing"}`); + if (isAlphaLoggedIn()) { + const name = getAlphaUserName(); + if (name) { + console.log(` user: ${name}`); + } + } + console.log(`models available: ${availableModels.length}`); + if (availableModels.length > 0) { + const sample = availableModels + .slice(0, 6) + .map((model) => `${model.provider}/${model.id}`) + .join(", "); + console.log(` sample: ${sample}`); + } + console.log( + `default model: ${typeof settings.defaultProvider === "string" && typeof settings.defaultModel === "string" + ? `${settings.defaultProvider}/${settings.defaultModel}` + : "not set"}`, + ); + console.log(`pandoc: ${pandocPath ?? "missing"}`); + console.log(`browser preview runtime: ${browserPath ?? "missing"}`); + console.log(`web research provider: ${hasConfiguredWebProvider() ? "configured" : "missing"}`); + console.log(`quiet startup: ${settings.quietStartup === true ? "enabled" : "disabled"}`); + console.log(`theme: ${typeof settings.theme === "string" ? settings.theme : "not set"}`); + console.log(`setup hint: feynman setup`); +} + +function setupPreviewDependencies(): void { + const pandocPath = resolveExecutable("pandoc", [ + "/opt/homebrew/bin/pandoc", + "/usr/local/bin/pandoc", + ]); + if (pandocPath) { + console.log(`pandoc already installed at ${pandocPath}`); + return; + } + + const brewPath = resolveExecutable("brew", [ + "/opt/homebrew/bin/brew", + "/usr/local/bin/brew", + ]); + if (process.platform === "darwin" && brewPath) { + const result = spawnSync(brewPath, ["install", "pandoc"], { stdio: "inherit" }); + if (result.status !== 0) { + throw new Error("Failed to install pandoc via Homebrew."); + } + console.log("Preview dependency installed: pandoc"); + return; + } + + throw new Error("Automatic preview setup is only supported on macOS with Homebrew right now."); +} + function syncFeynmanTheme(appRoot: string, agentDir: string): void { const sourceThemePath = resolve(appRoot, ".pi", "themes", "feynman.json"); const targetThemeDir = resolve(agentDir, "themes"); @@ -201,11 +512,13 @@ async function main(): Promise { const feynmanAgentDir = resolve(homedir(), ".feynman", "agent"); const bundledSettingsPath = resolve(appRoot, ".pi", "settings.json"); patchEmbeddedPiBranding(piPackageRoot); + patchPackageWorkspace(appRoot); const { values, positionals } = parseArgs({ allowPositionals: true, options: { cwd: { type: "string" }, + doctor: { type: "boolean" }, help: { type: "boolean" }, "alpha-login": { type: "boolean" }, "alpha-logout": { type: "boolean" }, @@ -214,6 +527,7 @@ async function main(): Promise { "new-session": { type: "boolean" }, prompt: { type: "string" }, "session-dir": { type: "string" }, + "setup-preview": { type: "boolean" }, thinking: { type: "string" }, }, }); @@ -233,6 +547,21 @@ async function main(): Promise { const thinkingLevel = normalizeThinkingLevel(values.thinking ?? process.env.FEYNMAN_THINKING) ?? "medium"; normalizeFeynmanSettings(feynmanSettingsPath, bundledSettingsPath, thinkingLevel, feynmanAuthPath); + if (positionals[0] === "setup") { + await runSetup(positionals[1], feynmanSettingsPath, bundledSettingsPath, feynmanAuthPath, workingDir, sessionDir); + return; + } + + if (values.doctor) { + runDoctor(feynmanSettingsPath, feynmanAuthPath, sessionDir, workingDir); + return; + } + + if (values["setup-preview"]) { + setupPreviewDependencies(); + return; + } + if (values["alpha-login"]) { const result = await loginAlpha(); normalizeFeynmanSettings(feynmanSettingsPath, bundledSettingsPath, thinkingLevel, feynmanAuthPath); @@ -273,6 +602,7 @@ async function main(): Promise { } const oneShotPrompt = values.prompt; const initialPrompt = oneShotPrompt ?? (positionals.length > 0 ? positionals.join(" ") : undefined); + const systemPrompt = buildFeynmanSystemPrompt(); const piArgs = [ "--session-dir", @@ -284,7 +614,7 @@ async function main(): Promise { "--prompt-template", resolve(appRoot, "prompts"), "--system-prompt", - FEYNMAN_SYSTEM_PROMPT, + systemPrompt, ]; if (explicitModelSpec) { @@ -307,6 +637,33 @@ async function main(): Promise { ...process.env, PI_CODING_AGENT_DIR: feynmanAgentDir, FEYNMAN_CODING_AGENT_DIR: feynmanAgentDir, + FEYNMAN_PI_NPM_ROOT: resolve(appRoot, ".pi", "npm", "node_modules"), + FEYNMAN_SESSION_DIR: sessionDir, + PI_SESSION_DIR: sessionDir, + FEYNMAN_MEMORY_DIR: resolve(dirname(feynmanAgentDir), "memory"), + FEYNMAN_NODE_EXECUTABLE: process.execPath, + FEYNMAN_BIN_PATH: resolve(appRoot, "bin", "feynman.js"), + PANDOC_PATH: + process.env.PANDOC_PATH ?? + resolveExecutable("pandoc", [ + "/opt/homebrew/bin/pandoc", + "/usr/local/bin/pandoc", + ]), + PI_SKIP_VERSION_CHECK: process.env.PI_SKIP_VERSION_CHECK ?? "1", + MERMAID_CLI_PATH: + process.env.MERMAID_CLI_PATH ?? + resolveExecutable("mmdc", [ + "/opt/homebrew/bin/mmdc", + "/usr/local/bin/mmdc", + ]), + PUPPETEER_EXECUTABLE_PATH: + process.env.PUPPETEER_EXECUTABLE_PATH ?? + resolveExecutable("google-chrome", [ + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + "/Applications/Chromium.app/Contents/MacOS/Chromium", + "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser", + "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", + ]), }, });