Upgrade Feynman research runtime and setup

This commit is contained in:
Advait Paliwal
2026-03-20 23:37:38 -07:00
parent 6332c3c67c
commit be97ac7a38
22 changed files with 1271 additions and 60 deletions

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@ node_modules
.feynman .feynman
.pi/npm .pi/npm
.pi/git .pi/git
.pi/schedule-prompts.json
dist dist
*.tgz *.tgz
outputs/* outputs/*

View File

@@ -4,8 +4,14 @@
"npm:pi-docparser", "npm:pi-docparser",
"npm:pi-web-access", "npm:pi-web-access",
"npm:pi-markdown-preview", "npm:pi-markdown-preview",
"npm:@walterra/pi-charts",
"npm:pi-generative-ui",
"npm:pi-mermaid",
"npm:@aliou/pi-processes", "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, "quietStartup": true,
"collapseChangelog": true "collapseChangelog": true

View File

@@ -9,12 +9,14 @@ It keeps the useful parts of a coding agent:
- skills - skills
- custom extensions - custom extensions
But it biases the runtime toward research work: But it biases the runtime toward general research work:
- literature review - literature review
- paper lookup - source discovery and paper lookup
- source comparison - source comparison
- research memo writing - research memo writing
- paper and report drafting - paper and report drafting
- session recall and durable research memory
- recurring and deferred research jobs
- replication planning when relevant - replication planning when relevant
The primary paper backend is `@companion-ai/alpha-hub` and your alphaXiv account. 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: Then authenticate alphaXiv and start the CLI:
```bash ```bash
feynman --alpha-login feynman setup
feynman feynman
``` ```
@@ -43,7 +45,12 @@ 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. 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 ## Commands
@@ -56,10 +63,19 @@ Inside the REPL:
- `/replicate <paper or claim>` expands the replication prompt template - `/replicate <paper or claim>` expands the replication prompt template
- `/reading-list <topic>` expands the reading-list prompt template - `/reading-list <topic>` expands the reading-list prompt template
- `/research-memo <topic>` expands the general research memo prompt template - `/research-memo <topic>` expands the general research memo prompt template
- `/deepresearch <topic>` expands the thorough source-heavy research prompt template
- `/autoresearch <idea>` expands the end-to-end idea-to-paper prompt template
- `/compare-sources <topic>` expands the source comparison prompt template - `/compare-sources <topic>` expands the source comparison prompt template
- `/paper-code-audit <item>` expands the paper/code audit prompt template - `/paper-code-audit <item>` expands the paper/code audit prompt template
- `/paper-draft <topic>` expands the paper-style writing prompt template - `/paper-draft <topic>` expands the paper-style writing prompt template
- `/research-memo <topic>` 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 ## Custom Tools
@@ -71,6 +87,8 @@ The starter extension adds:
- `alpha_annotate_paper` for persistent local notes - `alpha_annotate_paper` for persistent local notes
- `alpha_list_annotations` for recall across sessions - `alpha_list_annotations` for recall across sessions
- `alpha_read_code` for reading a paper repository - `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. 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-docparser` for PDFs, Office docs, spreadsheets, and images
- `pi-web-access` for broader web, GitHub, PDF, and media access - `pi-web-access` for broader web, GitHub, PDF, and media access
- `pi-markdown-preview` for polished Markdown and LaTeX-heavy research writeups - `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 - `@aliou/pi-processes` for long-running experiments and log tails
- `pi-zotero` for citation-library workflows - `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 ## Layout
@@ -95,5 +119,5 @@ feynman/
├── papers/ # Polished paper-style drafts and writeups ├── papers/ # Polished paper-style drafts and writeups
├── prompts/ # Slash-style prompt templates ├── prompts/ # Slash-style prompt templates
├── skills/ # Research workflows ├── skills/ # Research workflows
└── src/ # Minimal REPL wrapper around pi-coding-agent └── src/ # Branded launcher around the embedded Pi TUI
``` ```

View File

@@ -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 { import {
annotatePaper, annotatePaper,
askPaper, askPaper,
@@ -8,14 +14,456 @@ import {
readPaperCode, readPaperCode,
searchPapers, searchPapers,
} from "@companion-ai/alpha-hub/lib"; } 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"; import { Type } from "@sinclair/typebox";
const execFileAsync = promisify(execFile);
function formatToolText(result: unknown): string { function formatToolText(result: unknown): string {
return typeof result === "string" ? result : JSON.stringify(result, null, 2); 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<number>;
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<void> {
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<string> {
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 = `<!doctype html><html><head><meta charset="utf-8" /><base href="${pathToFileURL(dirname(filePath) + "/").href}" /><title>${basename(filePath)}</title><style>
:root{
--bg:#faf7f2;
--paper:#fffdf9;
--border:#d7cec1;
--text:#1f1c18;
--muted:#6c645a;
--code:#f3eee6;
--link:#0f6d8c;
--quote:#8b7f70;
}
@media (prefers-color-scheme: dark){
:root{
--bg:#161311;
--paper:#1d1916;
--border:#3b342d;
--text:#ebe3d6;
--muted:#b4ab9f;
--code:#221d19;
--link:#8ac6d6;
--quote:#a89d8f;
}
}
body{
font-family:Charter,"Iowan Old Style","Palatino Linotype","Book Antiqua",Palatino,Georgia,serif;
margin:0;
background:var(--bg);
color:var(--text);
line-height:1.7;
}
main{
max-width:900px;
margin:2rem auto 4rem;
padding:2.5rem 3rem;
background:var(--paper);
border:1px solid var(--border);
border-radius:18px;
box-shadow:0 12px 40px rgba(0,0,0,.06);
}
h1,h2,h3,h4,h5,h6{
font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;
line-height:1.2;
margin-top:1.5em;
}
h1{font-size:2.2rem;border-bottom:1px solid var(--border);padding-bottom:.35rem;}
h2{font-size:1.6rem;border-bottom:1px solid var(--border);padding-bottom:.25rem;}
p,ul,ol,blockquote,table{margin:1rem 0;}
pre,code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
pre{
background:var(--code);
border:1px solid var(--border);
border-radius:12px;
padding:1rem 1.1rem;
overflow:auto;
}
code{
background:var(--code);
padding:.12rem .28rem;
border-radius:6px;
}
a{color:var(--link);text-decoration:none}
a:hover{text-decoration:underline}
img{max-width:100%}
blockquote{
border-left:4px solid var(--border);
padding-left:1rem;
color:var(--quote);
}
table{border-collapse:collapse;width:100%}
th,td{border:1px solid var(--border);padding:.55rem .7rem;text-align:left}
</style></head><body><main>${stdout}</main></body></html>`;
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<string> {
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 { 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({ pi.registerTool({
name: "alpha_search", name: "alpha_search",
label: "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,
};
},
});
} }

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@companion-ai/feynman", "name": "@companion-ai/feynman",
"version": "0.1.1", "version": "0.1.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@companion-ai/feynman", "name": "@companion-ai/feynman",
"version": "0.1.1", "version": "0.1.5",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@companion-ai/alpha-hub": "^0.1.2", "@companion-ai/alpha-hub": "^0.1.2",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@companion-ai/feynman", "name": "@companion-ai/feynman",
"version": "0.1.1", "version": "0.1.5",
"description": "Research-first CLI agent built on Pi and alphaXiv", "description": "Research-first CLI agent built on Pi and alphaXiv",
"type": "module", "type": "module",
"bin": { "bin": {
@@ -9,9 +9,11 @@
"files": [ "files": [
"bin/", "bin/",
"dist/", "dist/",
".pi/", ".pi/settings.json",
".pi/themes/",
"extensions/", "extensions/",
"prompts/", "prompts/",
"scripts/",
"skills/", "skills/",
"README.md", "README.md",
".env.example" ".env.example"

16
prompts/autoresearch.md Normal file
View File

@@ -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.

View File

@@ -5,6 +5,8 @@ Compare sources for: $@
Requirements: Requirements:
- Identify the strongest relevant primary sources first. - 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. - Inspect the top sources directly before comparing them.
- Build a comparison matrix covering: - Build a comparison matrix covering:
- source - source

13
prompts/deepresearch.md Normal file
View File

@@ -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.

View File

@@ -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: $@ Investigate the following topic as a literature review: $@
Requirements: 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_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. - 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. - Prefer primary sources and note when something appears to be a preprint or secondary summary.

View File

@@ -4,8 +4,9 @@ description: Build a prioritized reading list on a research topic with rationale
Create a research reading list for: $@ Create a research reading list for: $@
Requirements: Requirements:
- Use `alpha_search` with `all` mode. - If the topic is academic, use `alpha_search` with `all` mode.
- Inspect the strongest papers with `alpha_get_paper`. - 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. - 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. - 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. - For each paper, explain why it is on the list.

View File

@@ -5,6 +5,8 @@ Write a research memo about: $@
Requirements: Requirements:
- Start by finding the strongest relevant sources. - 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. - Read or inspect the top sources directly before making strong claims.
- Distinguish facts, interpretations, and open questions. - Distinguish facts, interpretations, and open questions.
- End with a `Sources` section containing direct URLs for every source used. - End with a `Sources` section containing direct URLs for every source used.

View File

@@ -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 { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
@@ -7,6 +8,68 @@ const appRoot = resolve(here, "..");
const piPackageRoot = resolve(appRoot, "node_modules", "@mariozechner", "pi-coding-agent"); const piPackageRoot = resolve(appRoot, "node_modules", "@mariozechner", "pi-coding-agent");
const packageJsonPath = resolve(piPackageRoot, "package.json"); const packageJsonPath = resolve(piPackageRoot, "package.json");
const cliPath = resolve(piPackageRoot, "dist", "cli.js"); 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)) { if (existsSync(packageJsonPath)) {
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8")); 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"); 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");
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -22,12 +22,13 @@ Use this skill when the user has:
- success metrics - success metrics
- baselines - baselines
- constraints - constraints
3. Search for prior work first with `alpha_search` so you do not reinvent an obviously flawed setup. 3. Search for prior work first.
4. Use `alpha_get_paper` and `alpha_ask_paper` on the strongest references. 4. If the setup is tied to current products, APIs, model offerings, pricing, or market behavior, use `web_search` and `fetch_content` first.
5. Prefer the smallest experiment that can meaningfully reduce uncertainty. 5. Use `alpha_search`, `alpha_get_paper`, and `alpha_ask_paper` for academic baselines and prior experiments.
6. List confounders and failure modes up front. 6. Prefer the smallest experiment that can meaningfully reduce uncertainty.
7. If implementation is requested, create the scripts, configs, and logging plan. 7. List confounders and failure modes up front.
8. Write the plan to disk before running expensive work. 8. If implementation is requested, create the scripts, configs, and logging plan.
9. Write the plan to disk before running expensive work.
## Pitfalls ## Pitfalls

View File

@@ -16,24 +16,26 @@ Use this skill when the user wants:
## Procedure ## Procedure
1. Search broadly first with `alpha_search`. 1. Search broadly first.
2. Pick the strongest candidates by direct relevance, recency, citations, and venue quality. 2. If the topic is primarily academic or paper-centric, start with `alpha_search`.
3. Inspect the top papers with `alpha_get_paper` before making concrete claims. 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. Use `alpha_ask_paper` for missing methodological or experimental details. 4. Pick the strongest candidates by direct relevance, recency, citations, venue quality, and source quality.
5. Build a compact evidence table: 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 - title
- year - year
- authors - authors
- venue - venue
- claim or contribution - claim or contribution
- important caveats - important caveats
6. Distinguish: 8. Distinguish:
- what multiple sources agree on - what multiple sources agree on
- where methods or findings differ - where methods or findings differ
- what remains unresolved - what remains unresolved
7. If the user wants a durable artifact, write a markdown brief to disk. 9. 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`. 10. 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. 11. End with a `Sources` section that lists direct URLs, not just titles.
## Pitfalls ## Pitfalls
@@ -41,6 +43,7 @@ Use this skill when the user wants:
- Do not flatten disagreements into fake consensus. - Do not flatten disagreements into fake consensus.
- Do not treat recent preprints as established facts without saying so. - Do not treat recent preprints as established facts without saying so.
- Do not cite secondary commentary when a primary source is available. - 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 ## Output Shape

View File

@@ -15,30 +15,33 @@ Use this skill for:
## Procedure ## Procedure
1. Start with `alpha_search` in `all` mode. 1. Start with source discovery that matches the topic.
2. Inspect the strongest candidates with `alpha_get_paper`. 2. For academic topics, use `alpha_search` in `all` mode.
3. Use `alpha_ask_paper` for fit questions like: 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 problem does this really solve
- what assumptions does it rely on - what assumptions does it rely on
- what prior work does it build on - what prior work does it build on
4. Classify papers into roles: 6. Classify papers or sources into roles:
- foundational - foundational
- key recent advances - key recent advances
- evaluation or benchmark references - evaluation or benchmark references
- critiques or limitations - critiques or limitations
- likely replication targets - likely replication targets
5. Order the list intentionally: 7. Order the list intentionally:
- start with orientation - start with orientation
- move to strongest methods - move to strongest methods
- finish with edges, critiques, or adjacent work - finish with edges, critiques, or adjacent work
6. Write the final list as a durable markdown artifact in `outputs/`. 8. Write the final list as a durable markdown artifact in `outputs/`.
7. For every paper, include a direct URL. 9. For every source, include a direct URL.
## Pitfalls ## Pitfalls
- Do not sort purely by citations. - Do not sort purely by citations.
- Do not over-index on recency when fundamentals matter. - Do not over-index on recency when fundamentals matter.
- Do not include papers you have not inspected at all. - 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 ## Deliverable

View File

@@ -17,20 +17,23 @@ Use this skill for:
## Procedure ## Procedure
1. Find relevant sources first. 1. Find relevant sources first.
2. Inspect the strongest sources directly before synthesizing. 2. If the topic is current, product-oriented, market-facing, or asks about latest developments, use `web_search` and `fetch_content` first.
3. Separate: 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 - established facts
- plausible inferences - plausible inferences
- unresolved questions - unresolved questions
4. Write a memo with clear sections and a concise narrative. 6. Write a memo with clear sections and a concise narrative.
5. End with a `Sources` section containing direct links. 7. End with a `Sources` section containing direct links.
6. Save the memo to `outputs/` when the user wants a durable artifact. 8. Save the memo to `outputs/` when the user wants a durable artifact.
## Pitfalls ## Pitfalls
- Do not summarize from search snippets alone. - Do not summarize from search snippets alone.
- Do not omit the source list. - Do not omit the source list.
- Do not present inference as fact. - Do not present inference as fact.
- Do not rely on paper search alone for latest/current topics.
## Deliverable ## Deliverable

View File

@@ -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: Operating rules:
- Evidence over fluency. - Evidence over fluency.
@@ -9,18 +10,32 @@ Operating rules:
- State uncertainty explicitly. - State uncertainty explicitly.
- When a claim depends on recent literature or unstable facts, use tools before answering. - 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. - 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 alpha-backed research tools for academic paper search, paper reading, paper Q&A, repository inspection, 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 \`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. - 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. - 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. - 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. - 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: - Default artifact locations:
- outputs/ for reviews, reading lists, and summaries - outputs/ for reviews, reading lists, and summaries
- experiments/ for runnable experiment code and result logs - experiments/ for runnable experiment code and result logs
- notes/ for scratch notes and intermediate synthesis - notes/ for scratch notes and intermediate synthesis
- papers/ for polished paper-style drafts and writeups - 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: Default workflow:
1. Clarify the research objective if needed. 1. Clarify the research objective if needed.
@@ -33,4 +48,6 @@ Default workflow:
Style: Style:
- Concise, skeptical, and explicit. - Concise, skeptical, and explicit.
- Avoid fake certainty. - 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.`;
}

View File

@@ -1,9 +1,11 @@
import "dotenv/config"; 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 { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os"; import { homedir } from "node:os";
import { dirname, resolve } from "node:path"; 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 { parseArgs } from "node:util";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
@@ -18,7 +20,7 @@ import {
AuthStorage, AuthStorage,
} from "@mariozechner/pi-coding-agent"; } 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"; type ThinkingLevel = "off" | "low" | "medium" | "high";
@@ -34,6 +36,8 @@ function printHelp(): void {
/replicate <paper> Expand the replication prompt template /replicate <paper> Expand the replication prompt template
/reading-list <topic> Expand the reading list prompt template /reading-list <topic> Expand the reading list prompt template
/research-memo <topic> Expand the general research memo prompt template /research-memo <topic> Expand the general research memo prompt template
/deepresearch <topic> Expand the thorough source-heavy research prompt template
/autoresearch <idea> Expand the idea-to-paper autoresearch prompt template
/compare-sources <topic> Expand the source comparison prompt template /compare-sources <topic> Expand the source comparison prompt template
/paper-code-audit <item> Expand the paper/code audit prompt template /paper-code-audit <item> Expand the paper/code audit prompt template
/paper-draft <topic> Expand the paper-style writing prompt template /paper-draft <topic> Expand the paper-style writing prompt template
@@ -46,7 +50,15 @@ function printHelp(): void {
--model provider:model Force a specific model --model provider:model Force a specific model
--thinking level off | low | medium | high --thinking level off | low | medium | high
--cwd /path/to/workdir Working directory for tools --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) { function parseModelSpec(spec: string, modelRegistry: ModelRegistry) {
@@ -78,9 +90,32 @@ function normalizeThinkingLevel(value: string | undefined): ThinkingLevel | unde
return undefined; 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 { function patchEmbeddedPiBranding(piPackageRoot: string): void {
const packageJsonPath = resolve(piPackageRoot, "package.json"); const packageJsonPath = resolve(piPackageRoot, "package.json");
const cliPath = resolve(piPackageRoot, "dist", "cli.js"); const cliPath = resolve(piPackageRoot, "dist", "cli.js");
const interactiveModePath = resolve(piPackageRoot, "dist", "modes", "interactive", "interactive-mode.js");
if (existsSync(packageJsonPath)) { if (existsSync(packageJsonPath)) {
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { 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"); 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( 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) { if (!settings.defaultThinkingLevel) {
settings.defaultThinkingLevel = defaultThinkingLevel; settings.defaultThinkingLevel = defaultThinkingLevel;
} }
@@ -180,6 +280,217 @@ function normalizeFeynmanSettings(
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8"); writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
} }
function readJson(path: string): Record<string, unknown> {
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<string, unknown> {
return readJson(getWebSearchConfigPath());
}
function saveWebSearchConfig(config: Record<string, unknown>): 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<string> {
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<number> {
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<void> {
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<void> {
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 { function syncFeynmanTheme(appRoot: string, agentDir: string): void {
const sourceThemePath = resolve(appRoot, ".pi", "themes", "feynman.json"); const sourceThemePath = resolve(appRoot, ".pi", "themes", "feynman.json");
const targetThemeDir = resolve(agentDir, "themes"); const targetThemeDir = resolve(agentDir, "themes");
@@ -201,11 +512,13 @@ async function main(): Promise<void> {
const feynmanAgentDir = resolve(homedir(), ".feynman", "agent"); const feynmanAgentDir = resolve(homedir(), ".feynman", "agent");
const bundledSettingsPath = resolve(appRoot, ".pi", "settings.json"); const bundledSettingsPath = resolve(appRoot, ".pi", "settings.json");
patchEmbeddedPiBranding(piPackageRoot); patchEmbeddedPiBranding(piPackageRoot);
patchPackageWorkspace(appRoot);
const { values, positionals } = parseArgs({ const { values, positionals } = parseArgs({
allowPositionals: true, allowPositionals: true,
options: { options: {
cwd: { type: "string" }, cwd: { type: "string" },
doctor: { type: "boolean" },
help: { type: "boolean" }, help: { type: "boolean" },
"alpha-login": { type: "boolean" }, "alpha-login": { type: "boolean" },
"alpha-logout": { type: "boolean" }, "alpha-logout": { type: "boolean" },
@@ -214,6 +527,7 @@ async function main(): Promise<void> {
"new-session": { type: "boolean" }, "new-session": { type: "boolean" },
prompt: { type: "string" }, prompt: { type: "string" },
"session-dir": { type: "string" }, "session-dir": { type: "string" },
"setup-preview": { type: "boolean" },
thinking: { type: "string" }, thinking: { type: "string" },
}, },
}); });
@@ -233,6 +547,21 @@ async function main(): Promise<void> {
const thinkingLevel = normalizeThinkingLevel(values.thinking ?? process.env.FEYNMAN_THINKING) ?? "medium"; const thinkingLevel = normalizeThinkingLevel(values.thinking ?? process.env.FEYNMAN_THINKING) ?? "medium";
normalizeFeynmanSettings(feynmanSettingsPath, bundledSettingsPath, thinkingLevel, feynmanAuthPath); 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"]) { if (values["alpha-login"]) {
const result = await loginAlpha(); const result = await loginAlpha();
normalizeFeynmanSettings(feynmanSettingsPath, bundledSettingsPath, thinkingLevel, feynmanAuthPath); normalizeFeynmanSettings(feynmanSettingsPath, bundledSettingsPath, thinkingLevel, feynmanAuthPath);
@@ -273,6 +602,7 @@ async function main(): Promise<void> {
} }
const oneShotPrompt = values.prompt; const oneShotPrompt = values.prompt;
const initialPrompt = oneShotPrompt ?? (positionals.length > 0 ? positionals.join(" ") : undefined); const initialPrompt = oneShotPrompt ?? (positionals.length > 0 ? positionals.join(" ") : undefined);
const systemPrompt = buildFeynmanSystemPrompt();
const piArgs = [ const piArgs = [
"--session-dir", "--session-dir",
@@ -284,7 +614,7 @@ async function main(): Promise<void> {
"--prompt-template", "--prompt-template",
resolve(appRoot, "prompts"), resolve(appRoot, "prompts"),
"--system-prompt", "--system-prompt",
FEYNMAN_SYSTEM_PROMPT, systemPrompt,
]; ];
if (explicitModelSpec) { if (explicitModelSpec) {
@@ -307,6 +637,33 @@ async function main(): Promise<void> {
...process.env, ...process.env,
PI_CODING_AGENT_DIR: feynmanAgentDir, PI_CODING_AGENT_DIR: feynmanAgentDir,
FEYNMAN_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",
]),
}, },
}); });