Replace Pi tool registrations with skills and CLI integration

- Remove all manually registered Pi tools (alpha_search, alpha_get_paper,
  alpha_ask_paper, alpha_annotate_paper, alpha_list_annotations,
  alpha_read_code, session_search, preview_file) and their wrappers
  (alpha.ts, preview.ts, session-search.ts, alpha-tools.test.ts)
- Add Pi skill files for alpha-research, session-search, preview,
  modal-compute, and runpod-compute in skills/
- Sync skills to ~/.feynman/agent/skills/ on startup via syncBundledAssets
- Add node_modules/.bin to Pi subprocess PATH so alpha CLI is accessible
- Add /outputs extension command to browse research artifacts via dialog
- Add Modal and RunPod as execution environments in /replicate and
  /autoresearch prompts
- Remove redundant /alpha-login /alpha-logout /alpha-status REPL commands
  (feynman alpha CLI still works)
- Update README, researcher agent, metadata, and website docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Advait Paliwal
2026-03-25 00:38:45 -07:00
parent 5fab329ad1
commit 7024a86024
26 changed files with 320 additions and 1009 deletions

View File

@@ -6,3 +6,7 @@ FEYNMAN_THINKING=medium
OPENAI_API_KEY= OPENAI_API_KEY=
ANTHROPIC_API_KEY= ANTHROPIC_API_KEY=
RUNPOD_API_KEY=
MODAL_TOKEN_ID=
MODAL_TOKEN_SECRET=

View File

@@ -9,7 +9,7 @@ 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 for academic paper search, paper reading, paper Q&A, repository inspection, and persistent annotations. - Use the alpha-research skill 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. - 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. - 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. - Never answer a latest/current question from arXiv or alpha-backed paper search alone.
@@ -30,7 +30,6 @@ Operating rules:
- 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. - 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. - 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. - 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. - 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. - 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. - 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.
@@ -38,11 +37,9 @@ Operating rules:
- Prefer the smallest investigation or experiment that can materially reduce uncertainty before escalating to broader work. - 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.
- Before pausing long-running work, update the durable state on disk first: plan artifact, `CHANGELOG.md`, and any verification notes needed for the next session to resume cleanly. - Before pausing long-running work, update the durable state on disk first: plan artifact, `CHANGELOG.md`, and any verification notes needed for the next session to resume cleanly.
- Before recommending an execution environment, consider the system resources shown in the header (CPU, RAM, GPU, Docker availability). Recommend Docker when isolation on the current machine helps, and say explicitly when the workload exceeds local capacity. Do not suggest GPU workloads locally if no GPU is detected.
- 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` only when the user wants review or export. Prefer browser preview by default; use PDF only when explicitly requested.
- Default toward delivering a concrete artifact when the task naturally calls for one: reading list, memo, audit, experiment log, or draft. - Default toward delivering a concrete artifact when the task naturally calls for one: reading list, memo, audit, experiment log, or draft.
- For user-facing workflows, produce exactly one canonical durable Markdown artifact unless the user explicitly asks for multiple deliverables. - For user-facing workflows, produce exactly one canonical durable Markdown artifact unless the user explicitly asks for multiple deliverables.
- Do not create extra user-facing intermediate markdown files just because the workflow has multiple reasoning stages. - Do not create extra user-facing intermediate markdown files just because the workflow has multiple reasoning stages.

View File

@@ -21,7 +21,7 @@ You are Feynman's evidence-gathering subagent.
1. **Start wide.** Begin with short, broad queries to map the landscape. Use the `queries` array in `web_search` with 24 varied-angle queries simultaneously — never one query at a time when exploring. 1. **Start wide.** Begin with short, broad queries to map the landscape. Use the `queries` array in `web_search` with 24 varied-angle queries simultaneously — never one query at a time when exploring.
2. **Evaluate availability.** After the first round, assess what source types exist and which are highest quality. Adjust strategy accordingly. 2. **Evaluate availability.** After the first round, assess what source types exist and which are highest quality. Adjust strategy accordingly.
3. **Progressively narrow.** Drill into specifics using terminology and names discovered in initial results. Refine queries, don't repeat them. 3. **Progressively narrow.** Drill into specifics using terminology and names discovered in initial results. Refine queries, don't repeat them.
4. **Cross-source.** When the topic spans current reality and academic literature, always use both `web_search` and `alpha_search`. 4. **Cross-source.** When the topic spans current reality and academic literature, always use both `web_search` and the `alpha` CLI (`alpha search`).
Use `recencyFilter` on `web_search` for fast-moving topics. Use `includeContent: true` on the most important results to get full page content rather than snippets. Use `recencyFilter` on `web_search` for fast-moving topics. Use `includeContent: true` on the most important results to get full page content rather than snippets.

View File

@@ -62,6 +62,7 @@ Ask naturally or use slash commands as shortcuts.
| `/draft <topic>` | Paper-style draft from research findings | | `/draft <topic>` | Paper-style draft from research findings |
| `/autoresearch <idea>` | Autonomous experiment loop | | `/autoresearch <idea>` | Autonomous experiment loop |
| `/watch <topic>` | Recurring research watch | | `/watch <topic>` | Recurring research watch |
| `/outputs` | Browse all research artifacts |
--- ---
@@ -76,19 +77,21 @@ Four bundled research agents, dispatched automatically.
--- ---
### Tools ### Skills & Tools
- **[AlphaXiv](https://www.alphaxiv.org/)** — paper search, Q&A, code reading, persistent annotations - **[AlphaXiv](https://www.alphaxiv.org/)** — paper search, Q&A, code reading, annotations (via `alpha` CLI)
- **Docker** — isolated container execution for safe experiments on your machine - **Docker** — isolated container execution for safe experiments on your machine
- **Web search** — Gemini or Perplexity, zero-config default - **Web search** — Gemini or Perplexity, zero-config default
- **Session search** — indexed recall across prior research sessions - **Session search** — indexed recall across prior research sessions
- **Preview** — browser and PDF export of generated artifacts - **Preview** — browser and PDF export of generated artifacts
- **Modal** — serverless GPU compute for burst training and inference
- **RunPod** — persistent GPU pods with SSH access for long-running experiments
--- ---
### How it works ### How it works
Built on [Pi](https://github.com/badlogic/pi-mono) for the agent runtime, [alphaXiv](https://www.alphaxiv.org/) for paper search and analysis, and [Docker](https://www.docker.com/) for isolated local execution. Every output is source-grounded — claims link to papers, docs, or repos with direct URLs. Built on [Pi](https://github.com/badlogic/pi-mono) for the agent runtime, [alphaXiv](https://www.alphaxiv.org/) for paper search and analysis, and CLI tools for compute and execution. Capabilities are delivered as [Pi skills](https://github.com/badlogic/pi-skills) — Markdown instruction files synced to `~/.feynman/agent/skills/` on startup. Every output is source-grounded — claims link to papers, docs, or repos with direct URLs.
--- ---

View File

@@ -1,9 +1,8 @@
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { registerAlphaCommands, registerAlphaTools } from "./research-tools/alpha.js";
import { installFeynmanHeader } from "./research-tools/header.js"; import { installFeynmanHeader } from "./research-tools/header.js";
import { registerHelpCommand } from "./research-tools/help.js"; import { registerHelpCommand } from "./research-tools/help.js";
import { registerInitCommand, registerPreviewTool, registerSessionSearchTool } from "./research-tools/project.js"; import { registerInitCommand, registerOutputsCommand } from "./research-tools/project.js";
export default function researchTools(pi: ExtensionAPI): void { export default function researchTools(pi: ExtensionAPI): void {
const cache: { agentSummaryPromise?: Promise<{ agents: string[]; chains: string[] }> } = {}; const cache: { agentSummaryPromise?: Promise<{ agents: string[]; chains: string[] }> } = {};
@@ -16,10 +15,7 @@ export default function researchTools(pi: ExtensionAPI): void {
await installFeynmanHeader(pi, ctx, cache); await installFeynmanHeader(pi, ctx, cache);
}); });
registerAlphaCommands(pi);
registerHelpCommand(pi); registerHelpCommand(pi);
registerInitCommand(pi); registerInitCommand(pi);
registerSessionSearchTool(pi); registerOutputsCommand(pi);
registerAlphaTools(pi);
registerPreviewTool(pi);
} }

View File

@@ -1,380 +0,0 @@
import {
annotatePaper,
askPaper,
clearPaperAnnotation,
disconnect,
getPaper,
getUserName as getAlphaUserName,
isLoggedIn as isAlphaLoggedIn,
listPaperAnnotations,
login as loginAlpha,
logout as logoutAlpha,
readPaperCode,
searchPapers,
} from "@companion-ai/alpha-hub/lib";
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { getExtensionCommandSpec } from "../../metadata/commands.mjs";
import { collapseExcessBlankLines, formatToolText } from "./shared.js";
type JsonRecord = Record<string, unknown>;
type AlphaSearchHit = {
rank?: number;
title?: string;
publishedAt?: string;
organizations?: string;
authors?: string;
abstract?: string;
arxivId?: string;
arxivUrl?: string;
alphaXivUrl?: string;
};
type AlphaSearchSection = {
count: number;
results: AlphaSearchHit[];
note?: string;
};
type AlphaSearchPayload = {
query?: string;
mode?: string;
results?: AlphaSearchHit[];
semantic?: AlphaSearchSection;
keyword?: AlphaSearchSection;
agentic?: AlphaSearchSection;
};
function isRecord(value: unknown): value is JsonRecord {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function cleanText(value: unknown, maxLength = 320): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const collapsed = collapseExcessBlankLines(value)
.replace(/\s*\n\s*/g, " ")
.replace(/[ \t]+/g, " ");
if (!collapsed) {
return undefined;
}
return collapsed.length > maxLength ? `${collapsed.slice(0, maxLength - 1).trimEnd()}` : collapsed;
}
function sanitizeHit(value: unknown, fallbackRank: number): AlphaSearchHit | null {
if (!isRecord(value)) {
return null;
}
const title = cleanText(value.title, 220);
if (!title) {
return null;
}
return {
rank: typeof value.rank === "number" ? value.rank : fallbackRank,
title,
publishedAt: cleanText(value.publishedAt, 48),
organizations: cleanText(value.organizations, 180),
authors: cleanText(value.authors, 220),
abstract: cleanText(value.abstract, 360),
arxivId: cleanText(value.arxivId, 32),
arxivUrl: cleanText(value.arxivUrl, 160),
alphaXivUrl: cleanText(value.alphaXivUrl, 160),
};
}
function sanitizeHits(value: unknown): AlphaSearchHit[] {
if (!Array.isArray(value)) {
return [];
}
return value
.map((entry, index) => sanitizeHit(entry, index + 1))
.filter((entry): entry is AlphaSearchHit => entry !== null);
}
function sanitizeSection(value: unknown): AlphaSearchSection {
if (!isRecord(value)) {
return { count: 0, results: [] };
}
const results = sanitizeHits(value.results);
const note = results.length === 0 ? cleanText(value.raw, 600) : undefined;
return {
count: results.length,
results,
...(note ? { note } : {}),
};
}
export function sanitizeAlphaSearchPayload(value: unknown): AlphaSearchPayload {
if (!isRecord(value)) {
return {};
}
const payload: AlphaSearchPayload = {
query: cleanText(value.query, 240),
mode: cleanText(value.mode, 32),
};
const topLevelResults = sanitizeHits(value.results);
if (topLevelResults.length > 0) {
payload.results = topLevelResults;
}
for (const key of ["semantic", "keyword", "agentic"] as const) {
if (key in value) {
payload[key] = sanitizeSection(value[key]);
}
}
return payload;
}
function pushHitLines(lines: string[], hit: AlphaSearchHit): void {
lines.push(`${hit.rank ?? "?"}. ${hit.title ?? "Untitled result"}`);
if (hit.arxivId) lines.push(` arXiv: ${hit.arxivId}`);
if (hit.publishedAt) lines.push(` published: ${hit.publishedAt}`);
if (hit.organizations) lines.push(` orgs: ${hit.organizations}`);
if (hit.authors) lines.push(` authors: ${hit.authors}`);
if (hit.abstract) lines.push(` abstract: ${hit.abstract}`);
if (hit.arxivUrl) lines.push(` arXiv URL: ${hit.arxivUrl}`);
if (hit.alphaXivUrl) lines.push(` alphaXiv URL: ${hit.alphaXivUrl}`);
}
function pushSectionLines(lines: string[], label: string, section: AlphaSearchSection): void {
lines.push(`${label} (${section.count})`);
if (section.results.length === 0) {
lines.push(section.note ? ` note: ${section.note}` : " no parsed results");
return;
}
for (const hit of section.results) {
pushHitLines(lines, hit);
}
}
export function formatAlphaSearchContext(value: unknown): string {
const payload = sanitizeAlphaSearchPayload(value);
const lines: string[] = [];
if (payload.query) lines.push(`query: ${payload.query}`);
if (payload.mode) lines.push(`mode: ${payload.mode}`);
if (payload.results) {
pushSectionLines(lines, "results", { count: payload.results.length, results: payload.results });
}
for (const [label, section] of [
["semantic", payload.semantic],
["keyword", payload.keyword],
["agentic", payload.agentic],
] as const) {
if (section) {
pushSectionLines(lines, label, section);
}
}
return lines.length > 0 ? lines.join("\n") : "No alpha search results returned.";
}
export function registerAlphaCommands(pi: ExtensionAPI): void {
pi.registerCommand("alpha-login", {
description: getExtensionCommandSpec("alpha-login")?.description ?? "Sign in to alphaXiv from inside Feynman.",
handler: async (_args, ctx) => {
if (isAlphaLoggedIn()) {
const name = getAlphaUserName();
ctx.ui.notify(name ? `alphaXiv already connected as ${name}` : "alphaXiv already connected", "info");
return;
}
await loginAlpha();
const name = getAlphaUserName();
ctx.ui.notify(name ? `alphaXiv connected as ${name}` : "alphaXiv login complete", "info");
},
});
pi.registerCommand("alpha-logout", {
description: getExtensionCommandSpec("alpha-logout")?.description ?? "Clear alphaXiv auth from inside Feynman.",
handler: async (_args, ctx) => {
logoutAlpha();
ctx.ui.notify("alphaXiv auth cleared", "info");
},
});
pi.registerCommand("alpha-status", {
description: getExtensionCommandSpec("alpha-status")?.description ?? "Show alphaXiv authentication status.",
handler: async (_args, ctx) => {
if (!isAlphaLoggedIn()) {
ctx.ui.notify("alphaXiv not connected", "warning");
return;
}
const name = getAlphaUserName();
ctx.ui.notify(name ? `alphaXiv connected as ${name}` : "alphaXiv connected", "info");
},
});
}
export function registerAlphaTools(pi: ExtensionAPI): void {
pi.registerTool({
name: "alpha_search",
label: "Alpha Search",
description: "Search papers through alphaXiv using semantic, keyword, both, agentic, or all retrieval modes.",
parameters: Type.Object({
query: Type.String({ description: "Paper search query." }),
mode: Type.Optional(
Type.String({
description: "Search mode: semantic, keyword, both, agentic, or all.",
}),
),
}),
async execute(_toolCallId, params) {
try {
const result = await searchPapers(params.query, params.mode?.trim() || "all");
const sanitized = sanitizeAlphaSearchPayload(result);
return {
content: [{ type: "text", text: formatAlphaSearchContext(sanitized) }],
details: sanitized,
};
} finally {
await disconnect();
}
},
});
pi.registerTool({
name: "alpha_get_paper",
label: "Alpha Get Paper",
description: "Fetch a paper report or full text, plus any local annotation, using alphaXiv.",
parameters: Type.Object({
paper: Type.String({
description: "arXiv ID, arXiv URL, or alphaXiv URL.",
}),
fullText: Type.Optional(
Type.Boolean({
description: "Return raw full text instead of the AI report.",
}),
),
}),
async execute(_toolCallId, params) {
try {
const result = await getPaper(params.paper, { fullText: params.fullText });
return {
content: [{ type: "text", text: formatToolText(result) }],
details: result,
};
} finally {
await disconnect();
}
},
});
pi.registerTool({
name: "alpha_ask_paper",
label: "Alpha Ask Paper",
description: "Ask a targeted question about a paper using alphaXiv's PDF analysis.",
parameters: Type.Object({
paper: Type.String({
description: "arXiv ID, arXiv URL, or alphaXiv URL.",
}),
question: Type.String({
description: "Question to ask about the paper.",
}),
}),
async execute(_toolCallId, params) {
try {
const result = await askPaper(params.paper, params.question);
return {
content: [{ type: "text", text: formatToolText(result) }],
details: result,
};
} finally {
await disconnect();
}
},
});
pi.registerTool({
name: "alpha_annotate_paper",
label: "Alpha Annotate Paper",
description: "Write or clear a persistent local annotation for a paper.",
parameters: Type.Object({
paper: Type.String({
description: "Paper ID to annotate.",
}),
note: Type.Optional(
Type.String({
description: "Annotation text. Omit when clear=true.",
}),
),
clear: Type.Optional(
Type.Boolean({
description: "Clear the existing annotation instead of writing one.",
}),
),
}),
async execute(_toolCallId, params) {
const result = params.clear
? await clearPaperAnnotation(params.paper)
: params.note
? await annotatePaper(params.paper, params.note)
: (() => {
throw new Error("Provide either note or clear=true.");
})();
return {
content: [{ type: "text", text: formatToolText(result) }],
details: result,
};
},
});
pi.registerTool({
name: "alpha_list_annotations",
label: "Alpha List Annotations",
description: "List all persistent local paper annotations.",
parameters: Type.Object({}),
async execute() {
const result = await listPaperAnnotations();
return {
content: [{ type: "text", text: formatToolText(result) }],
details: result,
};
},
});
pi.registerTool({
name: "alpha_read_code",
label: "Alpha Read Code",
description: "Read files from a paper's GitHub repository through alphaXiv.",
parameters: Type.Object({
githubUrl: Type.String({
description: "GitHub repository URL for the paper implementation.",
}),
path: Type.Optional(
Type.String({
description: "Repository path to inspect. Use / for the repo overview.",
}),
),
}),
async execute(_toolCallId, params) {
try {
const result = await readPaperCode(params.githubUrl, params.path?.trim() || "/");
return {
content: [{ type: "text", text: formatToolText(result) }],
details: result,
};
} finally {
await disconnect();
}
},
});
}

View File

@@ -1,183 +0,0 @@
import { execFile, spawn } from "node:child_process";
import { mkdir, mkdtemp, readFile, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, dirname, extname, join } from "node:path";
import { pathToFileURL } from "node:url";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
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`;
}
export 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);
});
}
export 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;
}
export 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 async function pathExists(path: string): Promise<boolean> {
try {
await stat(path);
return true;
} catch {
return false;
}
}

View File

@@ -1,14 +1,70 @@
import { mkdir, stat, writeFile } from "node:fs/promises"; import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
import { dirname, resolve as resolvePath } from "node:path"; import { join, relative, resolve as resolvePath } from "node:path";
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { getExtensionCommandSpec } from "../../metadata/commands.mjs"; import { getExtensionCommandSpec } from "../../metadata/commands.mjs";
import { renderHtmlPreview, renderPdfPreview, openWithDefaultApp, pathExists } from "./preview.js";
import { buildProjectAgentsTemplate, buildSessionLogsReadme } from "./project-scaffold.js"; import { buildProjectAgentsTemplate, buildSessionLogsReadme } from "./project-scaffold.js";
import { formatToolText } from "./shared.js";
import { searchSessionTranscripts } from "./session-search.js"; async function pathExists(path: string): Promise<boolean> {
try {
await stat(path);
return true;
} catch {
return false;
}
}
const ARTIFACT_DIRS = ["papers", "outputs", "experiments", "notes"];
const ARTIFACT_EXTS = new Set([".md", ".tex", ".pdf", ".py", ".csv", ".json", ".html", ".txt", ".log"]);
async function collectArtifacts(cwd: string): Promise<{ label: string; path: string }[]> {
const items: { label: string; path: string; mtime: number }[] = [];
for (const dir of ARTIFACT_DIRS) {
const dirPath = resolvePath(cwd, dir);
if (!(await pathExists(dirPath))) continue;
const walk = async (current: string): Promise<void> => {
let entries;
try {
entries = await readdir(current, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const full = join(current, entry.name);
if (entry.isDirectory()) {
await walk(full);
} else if (ARTIFACT_EXTS.has(entry.name.slice(entry.name.lastIndexOf(".")))) {
const rel = relative(cwd, full);
let title = "";
try {
const head = await readFile(full, "utf8").then((c) => c.slice(0, 200));
const match = head.match(/^#\s+(.+)/m);
if (match) title = match[1]!.trim();
} catch {}
const info = await stat(full).catch(() => null);
const mtime = info?.mtimeMs ?? 0;
const size = info ? formatSize(info.size) : "";
const titlePart = title ? `${title}` : "";
items.push({ label: `${rel}${titlePart} (${size})`, path: rel, mtime });
}
}
};
await walk(dirPath);
}
items.sort((a, b) => b.mtime - a.mtime);
return items;
}
function formatSize(bytes: number): string {
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
}
export function registerInitCommand(pi: ExtensionAPI): void { export function registerInitCommand(pi: ExtensionAPI): void {
pi.registerCommand("init", { pi.registerCommand("init", {
@@ -45,73 +101,23 @@ export function registerInitCommand(pi: ExtensionAPI): void {
}); });
} }
export function registerSessionSearchTool(pi: ExtensionAPI): void { export function registerOutputsCommand(pi: ExtensionAPI): void {
pi.registerTool({ pi.registerCommand("outputs", {
name: "session_search", description: "Browse all research artifacts (papers, outputs, experiments, notes).",
label: "Session Search", handler: async (_args, ctx) => {
description: "Search prior Feynman session transcripts to recover what was done, said, or written before.", const items = await collectArtifacts(ctx.cwd);
parameters: Type.Object({ if (items.length === 0) {
query: Type.String({ ctx.ui.notify("No artifacts found. Use /lit, /draft, /review, or /deepresearch to create some.", "info");
description: "Search query to look for in past sessions.", return;
}),
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,
};
},
});
}
export function registerPreviewTool(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. Rendered HTML/PDF previews are temporary and do not replace the source artifact.",
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 selected = await ctx.ui.select(`Artifacts (${items.length})`, items.map((i) => i.label));
const openedPath = if (!selected) return;
resolvePath(resolvedPath).toLowerCase().endsWith(".pdf") && target === "pdf"
? resolvedPath
: target === "pdf"
? await renderPdfPreview(resolvedPath)
: await renderHtmlPreview(resolvedPath);
await mkdir(dirname(openedPath), { recursive: true }).catch(() => {}); const match = items.find((i) => i.label === selected);
await openWithDefaultApp(openedPath); if (match) {
ctx.ui.setEditorText(`read ${match.path}`);
const result = { }
sourcePath: resolvedPath,
target,
openedPath,
temporaryPreview: openedPath !== resolvedPath,
};
return {
content: [{ type: "text", text: formatToolText(result) }],
details: result,
};
}, },
}); });
} }

View File

@@ -1,223 +0,0 @@
import { readdir, readFile, stat } from "node:fs/promises";
import { basename, join } from "node:path";
import { pathToFileURL } from "node:url";
import { getFeynmanHome } from "./shared.js";
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}`;
}
export 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),
};
}

View File

@@ -1,5 +1,4 @@
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, resolve as resolvePath } from "node:path"; import { dirname, resolve as resolvePath } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
@@ -15,30 +14,3 @@ export const FEYNMAN_VERSION = (() => {
})(); })();
export { FEYNMAN_ASCII_LOGO as FEYNMAN_AGENT_LOGO } from "../../logo.mjs"; export { FEYNMAN_ASCII_LOGO as FEYNMAN_AGENT_LOGO } from "../../logo.mjs";
export const FEYNMAN_RESEARCH_TOOLS = [
"alpha_search",
"alpha_get_paper",
"alpha_ask_paper",
"alpha_annotate_paper",
"alpha_list_annotations",
"alpha_read_code",
"session_search",
"preview_file",
];
export function collapseExcessBlankLines(text: string): string {
return text.replace(/\r\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
}
export function formatToolText(result: unknown): string {
const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
return collapseExcessBlankLines(text);
}
export function getFeynmanHome(): string {
const agentDir = process.env.FEYNMAN_CODING_AGENT_DIR ??
process.env.PI_CODING_AGENT_DIR ??
resolvePath(homedir(), ".feynman", "agent");
return dirname(agentDir);
}

View File

@@ -37,9 +37,7 @@ export function readPromptSpecs(appRoot) {
export const extensionCommandSpecs = [ export const extensionCommandSpecs = [
{ name: "help", args: "", section: "Project & Session", description: "Show grouped Feynman commands and prefill the editor with a selected command.", publicDocs: true }, { name: "help", args: "", section: "Project & Session", description: "Show grouped Feynman commands and prefill the editor with a selected command.", publicDocs: true },
{ name: "init", args: "", section: "Project & Session", description: "Bootstrap AGENTS.md and session-log folders for a research project.", publicDocs: true }, { name: "init", args: "", section: "Project & Session", description: "Bootstrap AGENTS.md and session-log folders for a research project.", publicDocs: true },
{ name: "alpha-login", args: "", section: "Setup", description: "Sign in to alphaXiv from inside Feynman.", publicDocs: true }, { name: "outputs", args: "", section: "Project & Session", description: "Browse all research artifacts (papers, outputs, experiments, notes).", publicDocs: true },
{ name: "alpha-status", args: "", section: "Setup", description: "Show alphaXiv authentication status.", publicDocs: true },
{ name: "alpha-logout", args: "", section: "Setup", description: "Clear alphaXiv auth from inside Feynman.", publicDocs: true },
]; ];
export const livePackageCommandGroups = [ export const livePackageCommandGroups = [

1
package-lock.json generated
View File

@@ -7,7 +7,6 @@
"": { "": {
"name": "@companion-ai/feynman", "name": "@companion-ai/feynman",
"version": "0.2.13", "version": "0.2.13",
"hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@companion-ai/alpha-hub": "^0.1.2", "@companion-ai/alpha-hub": "^0.1.2",

View File

@@ -51,6 +51,9 @@
], ],
"prompts": [ "prompts": [
"./prompts" "./prompts"
],
"skills": [
"./skills"
] ]
}, },
"dependencies": { "dependencies": {

View File

@@ -27,6 +27,8 @@ Ask the user where to run:
- **New git branch** — create a branch so main stays clean - **New git branch** — create a branch so main stays clean
- **Virtual environment** — create an isolated venv/conda env first - **Virtual environment** — create an isolated venv/conda env first
- **Docker** — run experiment code inside an isolated Docker container - **Docker** — run experiment code inside an isolated Docker container
- **Modal** — run on Modal's serverless GPU infrastructure. Write Modal-decorated scripts and execute with `modal run`. Best for GPU-heavy benchmarks with no persistent state between iterations. Requires `modal` CLI.
- **RunPod** — provision a GPU pod via `runpodctl` and run iterations there over SSH. Best for experiments needing persistent state, large datasets, or SSH access between iterations. Requires `runpodctl` CLI.
Do not proceed without a clear answer. Do not proceed without a clear answer.

View File

@@ -14,6 +14,8 @@ Design a replication plan for: $@
- **Local** — run in the current working directory - **Local** — run in the current working directory
- **Virtual environment** — create an isolated venv/conda env first - **Virtual environment** — create an isolated venv/conda env first
- **Docker** — run experiment code inside an isolated Docker container - **Docker** — run experiment code inside an isolated Docker container
- **Modal** — run on Modal's serverless GPU infrastructure. Write a Modal-decorated Python script and execute with `modal run <script.py>`. Best for burst GPU jobs that don't need persistent state. Requires `modal` CLI (`pip install modal && modal setup`).
- **RunPod** — provision a GPU pod on RunPod and SSH in for execution. Use `runpodctl` to create pods, transfer files, and manage lifecycle. Best for long-running experiments or when you need SSH access and persistent storage. Requires `runpodctl` CLI and `RUNPOD_API_KEY`.
- **Plan only** — produce the replication plan without executing - **Plan only** — produce the replication plan without executing
4. **Execute** — If the user chose an execution environment, implement and run the replication steps there. Save notes, scripts, raw outputs, and results to disk in a reproducible layout. Do not call the outcome replicated unless the planned checks actually passed. 4. **Execute** — If the user chose an execution environment, implement and run the replication steps there. Save notes, scripts, raw outputs, and results to disk in a reproducible layout. Do not call the outcome replicated unless the planned checks actually passed.
5. **Log** — For multi-step or resumable replication work, append concise entries to `CHANGELOG.md` after meaningful progress, failed attempts, major verification outcomes, and before stopping. Record the active objective, what changed, what was checked, and the next step. 5. **Log** — For multi-step or resumable replication work, append concise entries to `CHANGELOG.md` after meaningful progress, failed attempts, major verification outcomes, and before stopping. Record the active objective, what changed, what was checked, and the next step.

View File

@@ -0,0 +1,42 @@
---
name: alpha-research
description: Search, read, and query research papers via the `alpha` CLI (alphaXiv-backed). Use when the user asks about academic papers, wants to find research on a topic, needs to read a specific paper, ask questions about a paper, inspect a paper's code repository, or manage paper annotations.
---
# Alpha Research CLI
Use the `alpha` CLI via bash for all paper research operations.
## Commands
| Command | Description |
|---------|-------------|
| `alpha search "<query>"` | Search papers. Modes: `--mode semantic`, `--mode keyword`, `--mode agentic` |
| `alpha get <arxiv-id-or-url>` | Fetch paper content and any local annotation |
| `alpha get --full-text <arxiv-id>` | Get raw full text instead of AI report |
| `alpha ask <arxiv-id> "<question>"` | Ask a question about a paper's PDF |
| `alpha code <github-url> [path]` | Read files from a paper's GitHub repo. Use `/` for overview |
| `alpha annotate <paper-id> "<note>"` | Save a persistent annotation on a paper |
| `alpha annotate --clear <paper-id>` | Remove an annotation |
| `alpha annotate --list` | List all annotations |
## Auth
Run `alpha login` to authenticate with alphaXiv. Check status with `alpha status`.
## Examples
```bash
alpha search "transformer scaling laws"
alpha search --mode agentic "efficient attention mechanisms for long context"
alpha get 2106.09685
alpha ask 2106.09685 "What optimizer did they use?"
alpha code https://github.com/karpathy/nanoGPT src/model.py
alpha annotate 2106.09685 "Key paper on LoRA - revisit for adapter comparison"
```
## When to use
- Academic paper search, reading, Q&A → `alpha`
- Current topics (products, releases, docs) → web search tools
- Mixed topics → combine both

View File

@@ -0,0 +1,56 @@
---
name: modal-compute
description: Run GPU workloads on Modal's serverless infrastructure. Use when the user needs remote GPU compute for training, inference, benchmarks, or batch processing and Modal CLI is available.
---
# Modal Compute
Use the `modal` CLI for serverless GPU workloads. No pod lifecycle to manage — write a decorated Python script and run it.
## Setup
```bash
pip install modal
modal setup
```
## Commands
| Command | Description |
|---------|-------------|
| `modal run script.py` | Run a script on Modal (ephemeral) |
| `modal run --detach script.py` | Run detached (background) |
| `modal deploy script.py` | Deploy persistently |
| `modal serve script.py` | Serve with hot-reload (dev) |
| `modal shell --gpu a100` | Interactive shell with GPU |
| `modal app list` | List deployed apps |
## GPU types
`T4`, `L4`, `A10G`, `L40S`, `A100`, `A100-80GB`, `H100`, `H200`, `B200`
Multi-GPU: `"H100:4"` for 4x H100s.
## Script pattern
```python
import modal
app = modal.App("experiment")
image = modal.Image.debian_slim(python_version="3.11").pip_install("torch==2.8.0")
@app.function(gpu="A100", image=image, timeout=600)
def train():
import torch
# training code here
@app.local_entrypoint()
def main():
train.remote()
```
## When to use
- Stateless burst GPU jobs (training, inference, benchmarks)
- No persistent state needed between runs
- Check availability: `command -v modal`

27
skills/preview/SKILL.md Normal file
View File

@@ -0,0 +1,27 @@
---
name: preview
description: Preview Markdown, LaTeX, PDF, or code artifacts in the browser or as PDF. Use when the user wants to review a written artifact, export a report, or view a rendered document.
---
# Preview
Use the `/preview` command to render and open artifacts.
## Commands
| Command | Description |
|---------|-------------|
| `/preview` | Preview the most recent artifact in the browser |
| `/preview --file <path>` | Preview a specific file |
| `/preview-browser` | Force browser preview |
| `/preview-pdf` | Export to PDF via pandoc + LaTeX |
| `/preview-clear-cache` | Clear rendered preview cache |
## Fallback
If the preview commands are not available, use bash:
```bash
open <file.md> # macOS — opens in default app
open <file.pdf> # macOS — opens in Preview
```

View File

@@ -0,0 +1,48 @@
---
name: runpod-compute
description: Provision and manage GPU pods on RunPod for long-running experiments. Use when the user needs persistent GPU compute with SSH access, large datasets, or multi-step experiments.
---
# RunPod Compute
Use `runpodctl` CLI for persistent GPU pods with SSH access.
## Setup
```bash
brew install runpod/runpodctl/runpodctl # macOS
runpodctl config --apiKey=YOUR_KEY
```
## Commands
| Command | Description |
|---------|-------------|
| `runpodctl create pod --gpuType "NVIDIA A100 80GB PCIe" --imageName "runpod/pytorch:2.4.0-py3.11-cuda12.4.1-devel-ubuntu22.04" --name experiment` | Create a pod |
| `runpodctl get pod` | List all pods |
| `runpodctl stop pod <id>` | Stop (preserves volume) |
| `runpodctl start pod <id>` | Resume a stopped pod |
| `runpodctl remove pod <id>` | Terminate and delete |
| `runpodctl gpu list` | List available GPU types and prices |
| `runpodctl send <file>` | Transfer files to/from pods |
| `runpodctl receive <code>` | Receive transferred files |
## SSH access
```bash
ssh root@<IP> -p <PORT> -i ~/.ssh/id_ed25519
```
Get connection details from `runpodctl get pod <id>`. Pods must expose port `22/tcp`.
## GPU types
`NVIDIA GeForce RTX 4090`, `NVIDIA RTX A6000`, `NVIDIA A40`, `NVIDIA A100 80GB PCIe`, `NVIDIA H100 80GB HBM3`
## When to use
- Long-running experiments needing persistent state
- Large dataset processing
- Multi-step work with SSH access between iterations
- Always stop or remove pods after experiments
- Check availability: `command -v runpodctl`

View File

@@ -0,0 +1,26 @@
---
name: session-search
description: Search past Feynman session transcripts to recover prior work, conversations, and research context. Use when the user references something from a previous session, asks "what did we do before", or when you suspect relevant past context exists.
---
# Session Search
Use the `/search` command to search prior Feynman sessions interactively, or search session JSONL files directly via bash.
## Interactive search
```
/search <query>
```
Opens the session search UI. Supports `resume <sessionPath>` to continue a found session.
## Direct file search
Session transcripts are stored as JSONL files in `~/.feynman/sessions/`. Each line is a JSON record with `type` (session, message, model_change) and `message.content` fields.
```bash
grep -ril "scaling laws" ~/.feynman/sessions/
```
For structured search across sessions, use the interactive `/search` command.

View File

@@ -130,6 +130,7 @@ export function syncBundledAssets(appRoot: string, agentDir: string): BootstrapS
syncManagedFiles(resolve(appRoot, ".feynman", "themes"), resolve(agentDir, "themes"), state, result); syncManagedFiles(resolve(appRoot, ".feynman", "themes"), resolve(agentDir, "themes"), state, result);
syncManagedFiles(resolve(appRoot, ".feynman", "agents"), resolve(agentDir, "agents"), state, result); syncManagedFiles(resolve(appRoot, ".feynman", "agents"), resolve(agentDir, "agents"), state, result);
syncManagedFiles(resolve(appRoot, "skills"), resolve(agentDir, "skills"), state, result);
writeBootstrapState(statePath, state); writeBootstrapState(statePath, state);
return result; return result;

View File

@@ -29,6 +29,7 @@ export function resolvePiPaths(appRoot: string) {
promptTemplatePath: resolve(appRoot, "prompts"), promptTemplatePath: resolve(appRoot, "prompts"),
systemPromptPath: resolve(appRoot, ".feynman", "SYSTEM.md"), systemPromptPath: resolve(appRoot, ".feynman", "SYSTEM.md"),
piWorkspaceNodeModulesPath: resolve(appRoot, ".feynman", "npm", "node_modules"), piWorkspaceNodeModulesPath: resolve(appRoot, ".feynman", "npm", "node_modules"),
nodeModulesBinPath: resolve(appRoot, "node_modules", ".bin"),
}; };
} }
@@ -77,8 +78,12 @@ export function buildPiArgs(options: PiRuntimeOptions): string[] {
export function buildPiEnv(options: PiRuntimeOptions): NodeJS.ProcessEnv { export function buildPiEnv(options: PiRuntimeOptions): NodeJS.ProcessEnv {
const paths = resolvePiPaths(options.appRoot); const paths = resolvePiPaths(options.appRoot);
const currentPath = process.env.PATH ?? "";
const binPath = paths.nodeModulesBinPath;
return { return {
...process.env, ...process.env,
PATH: `${binPath}:${currentPath}`,
FEYNMAN_VERSION: options.feynmanVersion, FEYNMAN_VERSION: options.feynmanVersion,
FEYNMAN_SESSION_DIR: options.sessionDir, FEYNMAN_SESSION_DIR: options.sessionDir,
FEYNMAN_MEMORY_DIR: resolve(dirname(options.feynmanAgentDir), "memory"), FEYNMAN_MEMORY_DIR: resolve(dirname(options.feynmanAgentDir), "memory"),

View File

@@ -1,86 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import { formatAlphaSearchContext, sanitizeAlphaSearchPayload } from "../extensions/research-tools/alpha.js";
import { formatToolText } from "../extensions/research-tools/shared.js";
test("sanitizeAlphaSearchPayload drops raw alpha search text while keeping parsed hits", () => {
const payload = sanitizeAlphaSearchPayload({
query: "scaling laws",
mode: "all",
semantic: {
raw: "\n\n\n1. **Paper A**\n- Abstract: noisy raw block",
results: [
{
rank: 1,
title: "Paper A",
publishedAt: "2025-09-28",
organizations: "Stanford University, EPFL",
authors: "A. Author, B. Author",
abstract: "Line one.\n\n\nLine two.",
arxivId: "2509.24012",
arxivUrl: "https://arxiv.org/abs/2509.24012",
alphaXivUrl: "https://www.alphaxiv.org/overview/2509.24012",
raw: "internal raw block that should be dropped",
},
],
},
keyword: {
raw: "\n\n\nNoisy keyword fallback",
results: [],
},
});
assert.deepEqual(payload, {
query: "scaling laws",
mode: "all",
semantic: {
count: 1,
results: [
{
rank: 1,
title: "Paper A",
publishedAt: "2025-09-28",
organizations: "Stanford University, EPFL",
authors: "A. Author, B. Author",
abstract: "Line one. Line two.",
arxivId: "2509.24012",
arxivUrl: "https://arxiv.org/abs/2509.24012",
alphaXivUrl: "https://www.alphaxiv.org/overview/2509.24012",
},
],
},
keyword: {
count: 0,
results: [],
note: "Noisy keyword fallback",
},
});
});
test("formatAlphaSearchContext emits compact model-facing text without raw JSON escapes", () => {
const text = formatAlphaSearchContext({
query: "scaling laws",
mode: "semantic",
results: [
{
rank: 1,
title: "Paper A",
abstract: "First line.\n\n\nSecond line.",
arxivId: "2509.24012",
raw: "should not appear",
},
],
raw: "\n\n\nvery noisy raw payload",
});
assert.match(text, /query: scaling laws/);
assert.match(text, /1\. Paper A/);
assert.match(text, /abstract: First line\. Second line\./);
assert.ok(!text.includes("\\n"));
assert.ok(!text.includes("raw"));
});
test("formatToolText collapses excess blank lines in plain strings", () => {
assert.equal(formatToolText("alpha\n\n\n\nbeta"), "alpha\n\nbeta");
});

View File

@@ -37,7 +37,7 @@ These commands manage your model provider configuration. The `model set` command
| `feynman alpha logout` | Clear alphaXiv auth | | `feynman alpha logout` | Clear alphaXiv auth |
| `feynman alpha status` | Check alphaXiv auth status | | `feynman alpha status` | Check alphaXiv auth status |
AlphaXiv authentication enables Feynman to search and retrieve papers, access discussion threads, and pull citation metadata. You can also manage AlphaXiv auth from inside the REPL with `/alpha-login`, `/alpha-status`, and `/alpha-logout`. AlphaXiv authentication enables Feynman to search and retrieve papers, access discussion threads, and pull citation metadata. The `alpha` CLI is also available directly in the agent shell for paper search, Q&A, and code inspection.
## Package management ## Package management

View File

@@ -31,21 +31,12 @@ These are the primary commands you will use day-to-day. Each workflow dispatches
| `/jobs` | Inspect active background work: running processes, scheduled follow-ups, and active watches | | `/jobs` | Inspect active background work: running processes, scheduled follow-ups, and active watches |
| `/help` | Show grouped Feynman commands and prefill the editor with a selected command | | `/help` | Show grouped Feynman commands and prefill the editor with a selected command |
| `/init` | Bootstrap `AGENTS.md` and session-log folders for a new research project | | `/init` | Bootstrap `AGENTS.md` and session-log folders for a new research project |
| `/outputs` | Browse all research artifacts (papers, outputs, experiments, notes) |
| `/search` | Search prior session transcripts for past research and findings | | `/search` | Search prior session transcripts for past research and findings |
| `/preview` | Preview the current artifact as rendered HTML or PDF | | `/preview` | Preview the current artifact as rendered HTML or PDF |
Session management commands help you organize ongoing work. The `/log` command is particularly useful at the end of a research session to capture what was accomplished and what remains. Session management commands help you organize ongoing work. The `/log` command is particularly useful at the end of a research session to capture what was accomplished and what remains.
## Setup commands
| Command | Description |
| --- | --- |
| `/alpha-login` | Sign in to alphaXiv from inside the REPL |
| `/alpha-status` | Show alphaXiv authentication status |
| `/alpha-logout` | Clear alphaXiv auth from inside the REPL |
These provide a convenient way to manage alphaXiv authentication without leaving the REPL.
## Running workflows from the CLI ## Running workflows from the CLI
All research workflow slash commands can also be run directly from the command line: All research workflow slash commands can also be run directly from the command line:

View File

@@ -21,27 +21,32 @@ Check your authentication status:
feynman alpha status feynman alpha status
``` ```
You can also manage AlphaXiv auth from inside the REPL with `/alpha-login`, `/alpha-status`, and `/alpha-logout`.
## What it provides ## What it provides
AlphaXiv gives Feynman access to several capabilities that power the research workflows: AlphaXiv gives Feynman access to several capabilities that power the research workflows:
- **Paper search** -- Find papers by topic, author, keyword, or arXiv ID - **Paper search** -- Find papers by topic, author, keyword, or arXiv ID (`alpha search`)
- **Full-text retrieval** -- Download and parse complete PDFs for in-depth reading - **Full-text retrieval** -- Download and parse complete PDFs for in-depth reading (`alpha get`)
- **Citation metadata** -- Access citation counts, references, and citation chains - **Paper Q&A** -- Ask targeted questions about a paper's content (`alpha ask`)
- **Discussion threads** -- Read community discussions and annotations on papers - **Code inspection** -- Read files from a paper's linked GitHub repository (`alpha code`)
- **Related papers** -- Discover connected work through citation graphs and recommendations - **Annotations** -- Persistent local notes on papers across sessions (`alpha annotate`)
## How it is used ## How it is used
You do not invoke AlphaXiv directly in most cases. The researcher agent uses it automatically during workflows like deep research, literature review, and peer review. When you provide an arXiv ID (like `arxiv:2401.12345`), Feynman fetches the paper through AlphaXiv. Feynman ships an `alpha-research` skill that teaches the agent to use the `alpha` CLI for paper operations. The researcher agent uses it automatically during workflows like deep research, literature review, and peer review. When you provide an arXiv ID (like `2401.12345`), the agent fetches the paper via `alpha get`.
AlphaXiv search is especially powerful when combined with citation chaining. The researcher agent can follow references from a relevant paper to discover foundational work, then follow forward citations to find papers that built on it. This produces a much more complete picture than keyword search alone. You can also use the `alpha` CLI directly from the terminal:
```bash
alpha search "scaling laws"
alpha get 2401.12345
alpha ask 2401.12345 "What optimizer did they use?"
alpha code https://github.com/org/repo src/model.py
```
## Configuration ## Configuration
AlphaXiv configuration is managed through the CLI commands listed above. Authentication tokens are stored in `~/.feynman/auth/` and persist across sessions. No additional configuration is needed beyond logging in. Authentication tokens are stored in `~/.feynman/auth/` and persist across sessions. No additional configuration is needed beyond logging in.
## Without AlphaXiv ## Without AlphaXiv