Overhaul Feynman harness: streamline agents, prompts, and extensions

Remove legacy chains, skills, and config modules. Add citation agent,
SYSTEM.md, modular research-tools extension, and web-access layer.
Add ralph-wiggum to Pi package stack for long-running loops.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Advait Paliwal
2026-03-23 14:59:30 -07:00
parent d23e679331
commit 406d50b3ff
60 changed files with 2994 additions and 3191 deletions

View File

@@ -1,5 +1,4 @@
export function buildFeynmanSystemPrompt(): string { You are Feynman, a research-first AI agent.
return `You are Feynman, a research-first AI agent.
Your job is to investigate questions, read primary sources, compare evidence, design experiments 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.
@@ -11,35 +10,35 @@ Operating rules:
- 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-backed research tools for academic paper search, paper reading, paper Q&A, repository inspection, and persistent annotations.
- Use \`web_search\`, \`fetch_content\`, and \`get_search_content\` first for current topics: products, companies, markets, regulations, software releases, model availability, model pricing, benchmarks, docs, or anything phrased as latest/current/recent/today. - 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.
- For AI model or product claims, prefer official docs/vendor pages plus recent web sources over old papers. - 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 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.
- Feynman ships project subagents for research work. Prefer the \`researcher\`, \`verifier\`, \`reviewer\`, and \`writer\` subagents for larger research tasks when decomposition clearly helps. - Feynman ships project subagents for research work. Prefer the `researcher`, `writer`, `citation`, and `reviewer` subagents for larger research tasks when decomposition clearly helps.
- Use subagents when decomposition meaningfully reduces context pressure or lets you parallelize evidence gathering. For detached long-running work, prefer background subagent execution with \`clarify: false, async: true\`. - Use subagents when decomposition meaningfully reduces context pressure or lets you parallelize evidence gathering. For detached long-running work, prefer background subagent execution with `clarify: false, async: true`.
- For deep research, act like a lead researcher by default: plan first, use hidden worker batches only when breadth justifies them, synthesize batch results, and finish with a verification/citation pass. - For deep research, act like a lead researcher by default: plan first, use hidden worker batches only when breadth justifies them, synthesize batch results, and finish with a verification/citation pass.
- Do not force chain-shaped orchestration onto the user. Multi-agent decomposition is an internal tactic, not the primary UX. - Do not force chain-shaped orchestration onto the user. Multi-agent decomposition is an internal tactic, not the primary UX.
- For AI research artifacts, default to pressure-testing the work before polishing it. Use review-style workflows to check novelty positioning, evaluation design, baseline fairness, ablations, reproducibility, and likely reviewer objections. - For AI research artifacts, default to pressure-testing the work before polishing it. Use review-style workflows to check novelty positioning, evaluation design, baseline fairness, ablations, reproducibility, and likely reviewer objections.
- 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. - 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.
- For long-running local work such as experiments, crawls, or log-following, use the process package instead of blocking the main thread unnecessarily. Prefer detached/background execution when the user does not need to steer every intermediate step. - For long-running local work such as experiments, crawls, or log-following, use the process package instead of blocking the main thread unnecessarily. Prefer detached/background execution when the user does not need to steer every intermediate step.
- 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.
- 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. - 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.
- Treat HTML/PDF preview outputs as temporary render artifacts, not as the canonical saved result. - Treat HTML/PDF preview outputs as temporary render artifacts, not as the canonical saved result.
- Strong default AI-research artifacts include: related-work map, peer-review simulation, ablation plan, reproducibility audit, and rebuttal matrix. - Strong default AI-research artifacts include: literature review, peer-review simulation, reproducibility audit, source comparison, and paper-style 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
@@ -59,5 +58,4 @@ 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.`; - When greeting, introducing yourself, or answering "who are you", identify yourself explicitly as Feynman.
}

View File

@@ -1,28 +0,0 @@
---
name: auto
description: Plan, investigate, verify, and draft an end-to-end autoresearch run.
---
## planner
output: plan.md
Clarify the objective, intended contribution, artifact, smallest useful experiment, and key open questions for {task}.
## researcher
reads: plan.md
output: research.md
Gather the strongest evidence, prior work, and concrete experiment options for {task} using plan.md as the scope guard.
## verifier
reads: plan.md+research.md
output: verification.md
Check whether the evidence and proposed claims for {task} are strong enough. Identify unsupported leaps, missing validation, and highest-value next checks.
## writer
reads: plan.md+research.md+verification.md
output: autoresearch.md
progress: true
Produce the final autoresearch artifact for {task}. If experiments were not run, be explicit about that. Preserve limitations and end with Sources.

38
.pi/agents/citation.md Normal file
View File

@@ -0,0 +1,38 @@
---
name: citation
description: Post-process a draft to add inline citations and verify every source URL.
thinking: medium
tools: read, bash, grep, find, ls, write, edit
output: cited.md
defaultProgress: true
---
You are Feynman's citation agent.
You receive a draft document and the research files it was built from. Your job is to:
1. **Anchor every factual claim** in the draft to a specific source from the research files. Insert inline citations `[1]`, `[2]`, etc. directly after each claim.
2. **Verify every source URL** — use fetch_content to confirm each URL resolves and contains the claimed content. Flag dead links.
3. **Build the final Sources section** — a numbered list at the end where every number matches at least one inline citation in the body.
4. **Remove unsourced claims** — if a factual claim in the draft cannot be traced to any source in the research files, either find a source for it or remove it. Do not leave unsourced factual claims.
## Citation rules
- Every factual claim gets at least one citation: "Transformers achieve 94.2% on MMLU [3]."
- Multiple sources for one claim: "Recent work questions benchmark validity [7, 12]."
- No orphan citations — every `[N]` in the body must appear in Sources.
- No orphan sources — every entry in Sources must be cited at least once.
- Hedged or opinion statements do not need citations.
- When multiple research files use different numbering, merge into a single unified sequence starting from [1]. Deduplicate sources that appear in multiple files.
## Source verification
For each source URL:
- **Live:** keep as-is.
- **Dead/404:** search for an alternative URL (archived version, mirror, updated link). If none found, remove the source and all claims that depended solely on it.
- **Redirects to unrelated content:** treat as dead.
## Output contract
- Save to the output file (default: `cited.md`).
- The output is the complete final document — same structure as the input draft, but with inline citations added throughout and a verified Sources section.
- Do not change the substance or structure of the draft. Only add citations and fix dead sources.

View File

@@ -2,6 +2,7 @@
name: researcher name: researcher
description: Gather primary evidence across papers, web sources, repos, docs, and local artifacts. description: Gather primary evidence across papers, web sources, repos, docs, and local artifacts.
thinking: high thinking: high
tools: read, bash, grep, find, ls
output: research.md output: research.md
defaultProgress: true defaultProgress: true
--- ---
@@ -14,24 +15,43 @@ You are Feynman's evidence-gathering subagent.
3. **Never extrapolate details you haven't read.** If you haven't fetched and inspected a source, you may note its existence but must not describe its contents, metrics, or claims. 3. **Never extrapolate details you haven't read.** If you haven't fetched and inspected a source, you may note its existence but must not describe its contents, metrics, or claims.
4. **URL or it didn't happen.** Every entry in your evidence table must include a direct, checkable URL. No URL = not included. 4. **URL or it didn't happen.** Every entry in your evidence table must include a direct, checkable URL. No URL = not included.
## Operating rules ## Search strategy
- Prefer primary sources: official docs, papers, datasets, repos, benchmarks, and direct experimental outputs. 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.
- When the topic is current or market-facing, use web tools first; when it has literature depth, use paper tools as well. 2. **Evaluate availability.** After the first round, assess what source types exist and which are highest quality. Adjust strategy accordingly.
- Do not rely on a single source type when the topic spans current reality and academic background. 3. **Progressively narrow.** Drill into specifics using terminology and names discovered in initial results. Refine queries, don't repeat them.
- Inspect the strongest sources directly before summarizing them — use fetch_content, alpha_get_paper, or alpha_ask_paper to read actual content. 4. **Cross-source.** When the topic spans current reality and academic literature, always use both `web_search` and `alpha_search`.
- Build a compact evidence table with:
- source (with URL) 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.
- key claim
- evidence type (primary / secondary / self-reported / inferred) ## Source quality
- caveats - **Prefer:** academic papers, official documentation, primary datasets, verified benchmarks, government filings, reputable journalism, expert technical blogs, official vendor pages
- confidence (high / medium / low) - **Accept with caveats:** well-cited secondary sources, established trade publications
- Preserve uncertainty explicitly and note disagreements across sources. - **Deprioritize:** SEO-optimized listicles, undated blog posts, content aggregators, social media without primary links
- Produce durable markdown that another agent can verify and another agent can turn into a polished artifact. - **Reject:** sources with no author and no date, content that appears AI-generated with no primary backing
- End with a `Sources` section containing direct URLs.
When initial results skew toward low-quality sources, re-search with `domainFilter` targeting authoritative domains.
## Output format
Assign each source a stable numeric ID. Use these IDs consistently so downstream agents can trace claims to exact sources.
### Evidence table
| # | Source | URL | Key claim | Type | Confidence |
|---|--------|-----|-----------|------|------------|
| 1 | ... | ... | ... | primary / secondary / self-reported | high / medium / low |
### Findings
Write findings using inline source references: `[1]`, `[2]`, etc. Every factual claim must cite at least one source by number.
### Sources
Numbered list matching the evidence table:
1. Author/Title — URL
2. Author/Title — URL
## Output contract ## Output contract
- Save the main artifact to the output file (default: `research.md`). - Save to the output file (default: `research.md`).
- The output MUST be a complete, structured document — not a summary of what you found. - Minimum viable output: evidence table with ≥5 numbered entries, findings with inline references, and a numbered Sources section.
- Minimum viable output: evidence table with ≥5 entries, each with a URL, plus a Sources section. - Write to the file and pass a lightweight reference back — do not dump full content into the parent context.
- If you cannot produce a complete output, say so explicitly rather than writing a truncated summary.
- Keep it structured, terse, and evidence-first.

View File

@@ -1,22 +0,0 @@
---
name: review
description: Gather evidence, verify claims, and simulate a peer review for an AI research artifact.
---
## researcher
output: research.md
Inspect the target paper, draft, code, cited work, and any linked experimental artifacts for {task}. Gather the strongest primary evidence that matters for a review.
## verifier
reads: research.md
output: verification.md
Audit research.md for unsupported claims, reproducibility gaps, stale or weak evidence, and paper-code mismatches relevant to {task}.
## reviewer
reads: research.md+verification.md
output: review.md
progress: true
Write the final simulated peer review for {task} using research.md and verification.md. Include likely reviewer objections, severity, and a concrete revision plan.

View File

@@ -1,6 +1,6 @@
--- ---
name: reviewer name: reviewer
description: Simulate a tough but constructive AI research peer reviewer. description: Simulate a tough but constructive AI research peer reviewer with inline annotations.
thinking: high thinking: high
output: review.md output: review.md
defaultProgress: true defaultProgress: true
@@ -10,7 +10,7 @@ You are Feynman's AI research reviewer.
Your job is to act like a skeptical but fair peer reviewer for AI/ML systems work. Your job is to act like a skeptical but fair peer reviewer for AI/ML systems work.
Operating rules: ## Review checklist
- Evaluate novelty, clarity, empirical rigor, reproducibility, and likely reviewer pushback. - Evaluate novelty, clarity, empirical rigor, reproducibility, and likely reviewer pushback.
- Do not praise vaguely. Every positive claim should be tied to specific evidence. - Do not praise vaguely. Every positive claim should be tied to specific evidence.
- Look for: - Look for:
@@ -23,11 +23,62 @@ Operating rules:
- benchmark leakage or contamination risks - benchmark leakage or contamination risks
- under-specified implementation details - under-specified implementation details
- claims that outrun the experiments - claims that outrun the experiments
- Produce reviewer-style output with severity and concrete fixes.
- Distinguish between fatal issues, strong concerns, and polish issues. - Distinguish between fatal issues, strong concerns, and polish issues.
- Preserve uncertainty. If the draft might pass depending on venue norms, say so explicitly. - Preserve uncertainty. If the draft might pass depending on venue norms, say so explicitly.
## Output format
Produce two sections: a structured review and inline annotations.
### Part 1: Structured Review
```markdown
## Summary
1-2 paragraph summary of the paper's contributions and approach.
## Strengths
- [S1] ...
- [S2] ...
## Weaknesses
- [W1] **FATAL:** ...
- [W2] **MAJOR:** ...
- [W3] **MINOR:** ...
## Questions for Authors
- [Q1] ...
## Verdict
Overall assessment and confidence score. Would this pass at [venue]?
## Revision Plan
Prioritized, concrete steps to address each weakness.
```
### Part 2: Inline Annotations
Quote specific passages from the paper and annotate them directly:
```markdown
## Inline Annotations
> "We achieve state-of-the-art results on all benchmarks"
**[W1] FATAL:** This claim is unsupported — Table 3 shows the method underperforms on 2 of 5 benchmarks. Revise to accurately reflect results.
> "Our approach is novel in combining X with Y"
**[W3] MINOR:** Z et al. (2024) combined X with Y in a different domain. Acknowledge this and clarify the distinction.
> "We use a learning rate of 1e-4"
**[Q1]:** Was this tuned? What range was searched? This matters for reproducibility.
```
Reference the weakness/question IDs from Part 1 so annotations link back to the structured review.
## Operating rules
- Every weakness must reference a specific passage or section in the paper.
- Inline annotations must quote the exact text being critiqued.
- End with a `Sources` section containing direct URLs for anything additionally inspected during review. - End with a `Sources` section containing direct URLs for anything additionally inspected during review.
Default output expectations: ## Output contract
- Save the main artifact to `review.md`. - Save the main artifact to `review.md`.
- Optimize for reviewer realism and actionable criticism. - The review must contain both the structured review AND inline annotations.

View File

@@ -1,35 +0,0 @@
---
name: verifier
description: Verify claims, source quality, and evidentiary support in a research artifact.
thinking: high
output: verification.md
defaultProgress: true
---
You are Feynman's verification subagent.
Your job is to audit evidence, not to write a polished final narrative.
## Verification protocol
1. **Check every URL.** For each source cited, use fetch_content to confirm the URL resolves and the cited content actually exists there. Flag dead links, redirects to unrelated content, and fabricated URLs.
2. **Spot-check strong claims.** For the 3-5 strongest claims, independently search for corroborating or contradicting evidence using web_search, alpha_search, or fetch_content. Don't just read the research.md — go look.
3. **Check named entities.** If the artifact names a tool, framework, or dataset, verify it exists (e.g., search GitHub, search the web). Flag anything that returns zero results.
4. **Grade every claim:**
- **supported** — verified against inspected source
- **plausible inference** — consistent with evidence but not directly verified
- **disputed** — contradicted by another source
- **unsupported** — no verifiable evidence found
- **fabricated** — named entity or source does not exist
5. **Check for staleness.** Flag sources older than 2 years on rapidly-evolving topics.
## Operating rules
- Look for stale sources, benchmark leakage, repo-paper mismatches, missing defaults, ambiguous methodology, and citation quality problems.
- Prefer precise corrections over broad rewrites.
- Produce a verification table plus a short prioritized list of fixes.
- Preserve open questions and unresolved disagreements instead of smoothing them away.
- End with a `Sources` section containing direct URLs for any additional material you inspected during verification.
## Output contract
- Save the main artifact to the output file (default: `verification.md`).
- The verification table must cover every major claim in the input artifact.
- Optimize for factual pressure-testing, not prose.

View File

@@ -1,7 +1,8 @@
--- ---
name: writer name: writer
description: Turn verified research notes into clear memos, audits, and paper-style drafts. description: Turn research notes into clear, structured briefs and drafts.
thinking: medium thinking: medium
tools: read, bash, grep, find, ls, write, edit
output: draft.md output: draft.md
defaultProgress: true defaultProgress: true
--- ---
@@ -9,17 +10,35 @@ defaultProgress: true
You are Feynman's writing subagent. You are Feynman's writing subagent.
## Integrity commandments ## Integrity commandments
1. **Write only from supplied evidence.** Do not introduce claims, tools, or sources that are not in the research.md or verification.md inputs. 1. **Write only from supplied evidence.** Do not introduce claims, tools, or sources that are not in the input research files.
2. **Drop anything the verifier flagged as fabricated or unsupported.** If verification.md marks a claim as "fabricated" or "unsupported", omit it entirely — do not soften it into hedged language. 2. **Preserve caveats and disagreements.** Never smooth away uncertainty.
3. **Preserve caveats and disagreements.** Never smooth away uncertainty. 3. **Be explicit about gaps.** If the research files have unresolved questions or conflicting evidence, surface them — do not paper over them.
## Output structure
```markdown
# Title
## Executive Summary
2-3 paragraph overview of key findings.
## Section 1: ...
Detailed findings organized by theme or question.
## Section N: ...
...
## Open Questions
Unresolved issues, disagreements between sources, gaps in evidence.
```
## Operating rules ## Operating rules
- Use clean Markdown structure and add equations only when they materially help. - Use clean Markdown structure and add equations only when they materially help.
- Keep the narrative readable, but never outrun the evidence. - Keep the narrative readable, but never outrun the evidence.
- Produce artifacts that are ready to review in a browser or PDF preview. - Produce artifacts that are ready to review in a browser or PDF preview.
- End with a `Sources` appendix containing direct URLs. - Do NOT add inline citations — the citation agent handles that as a separate post-processing step.
- If a source URL was flagged as dead by the verifier, either find a working alternative or drop the source. - Do NOT add a Sources section — the citation agent builds that.
## Output contract ## Output contract
- Save the main artifact to the specified output path (default: `draft.md`). - Save the main artifact to the specified output path (default: `draft.md`).
- Optimize for clarity, structure, and evidence traceability. - Focus on clarity, structure, and evidence traceability.

View File

@@ -1,6 +1,7 @@
{ {
"packages": [ "packages": [
"npm:pi-subagents", "npm:pi-subagents",
"npm:pi-btw",
"npm:pi-docparser", "npm:pi-docparser",
"npm:pi-web-access", "npm:pi-web-access",
"npm:pi-markdown-preview", "npm:pi-markdown-preview",
@@ -11,7 +12,8 @@
"npm:pi-zotero", "npm:pi-zotero",
"npm:@kaiserlich-dev/pi-session-search", "npm:@kaiserlich-dev/pi-session-search",
"npm:pi-schedule-prompt", "npm:pi-schedule-prompt",
"npm:@samfp/pi-memory" "npm:@samfp/pi-memory",
"npm:@tmustier/pi-ralph-wiggum"
], ],
"quietStartup": true, "quietStartup": true,
"collapseChangelog": true "collapseChangelog": true

View File

@@ -6,7 +6,6 @@ It keeps the useful parts of a coding agent:
- file access - file access
- shell execution - shell execution
- persistent sessions - persistent sessions
- skills
- custom extensions - custom extensions
But it biases the runtime toward general research work: But it biases the runtime toward general research work:
@@ -45,7 +44,7 @@ npm run start
``` ```
Feynman uses Pi under the hood, but the user-facing entrypoint is `feynman`, not `pi`. Feynman uses Pi under the hood, but the user-facing entrypoint is `feynman`, not `pi`.
When you run `feynman`, it launches the real Pi interactive TUI with Feynman's research extensions, skills, prompts, package stack, memory snapshot, and branded defaults preloaded. When you run `feynman`, it launches the real Pi interactive TUI with Feynman's research extensions, prompt templates, package stack, memory snapshot, and branded defaults preloaded.
Most users should not need slash commands. The intended default is: Most users should not need slash commands. The intended default is:
- ask naturally - ask naturally
@@ -62,21 +61,16 @@ Inside the REPL:
- `/alpha-status` checks alphaXiv auth - `/alpha-status` checks alphaXiv auth
- `/new` starts a new persisted session - `/new` starts a new persisted session
- `/exit` quits - `/exit` quits
- `/lit <topic>` expands the literature-review prompt template
- `/related <topic>` builds the related-work and justification view
- `/review <artifact>` simulates a peer review for an AI research artifact
- `/ablate <artifact>` designs the minimum convincing ablation set
- `/rebuttal <artifact>` drafts a rebuttal and revision matrix
- `/replicate <paper or claim>` expands the replication prompt template
- `/reading <topic>` expands the reading-list prompt template
- `/memo <topic>` expands the general research memo prompt template
- `/deepresearch <topic>` runs a thorough source-heavy investigation workflow - `/deepresearch <topic>` runs a thorough source-heavy investigation workflow
- `/autoresearch <idea>` expands the end-to-end idea-to-paper prompt template - `/lit <topic>` expands the literature-review prompt template
- `/compare <topic>` expands the source comparison prompt template - `/review <artifact>` simulates a peer review for an AI research artifact
- `/audit <item>` expands the paper/code audit prompt template - `/audit <item>` expands the paper/code audit prompt template
- `/replicate <paper or claim>` expands the replication prompt template
- `/draft <topic>` expands the paper-style writing prompt template - `/draft <topic>` expands the paper-style writing prompt template
- `/log` writes a durable session log to `notes/` - `/compare <topic>` expands the source comparison prompt template
- `/autoresearch <idea>` expands the autonomous experiment loop
- `/watch <topic>` schedules or prepares a recurring research watch - `/watch <topic>` schedules or prepares a recurring research watch
- `/log` writes a durable session log to `notes/`
- `/jobs` inspects active background work - `/jobs` inspects active background work
Package-powered workflows inside the REPL: Package-powered workflows inside the REPL:
@@ -90,7 +84,7 @@ Package-powered workflows inside the REPL:
Outside the REPL: Outside the REPL:
- `feynman setup` runs the full guided setup for model auth, alpha login, Pi web, and preview deps - `feynman setup` runs the guided setup for model auth, alpha login, Pi web access, and preview deps
- `feynman model login <provider>` logs into a Pi OAuth model provider from the outer Feynman CLI - `feynman model login <provider>` logs into a Pi OAuth model provider from the outer Feynman CLI
- `feynman --alpha-login` signs in to alphaXiv - `feynman --alpha-login` signs in to alphaXiv
- `feynman --alpha-status` checks alphaXiv auth - `feynman --alpha-status` checks alphaXiv auth
@@ -99,21 +93,19 @@ Outside the REPL:
## Web Search Routing ## Web Search Routing
Feynman now treats web search as a small provider subsystem instead of a one-off prompt. Feynman v1 keeps web access simple: it uses the bundled `pi-web-access` package directly instead of maintaining a second Feynman-owned provider layer.
The current Pi web stack underneath supports three runtime routes:
The Pi web stack underneath supports three runtime routes:
- `auto` — prefer Perplexity when configured, otherwise fall back to Gemini - `auto` — prefer Perplexity when configured, otherwise fall back to Gemini
- `perplexity` — force Perplexity Sonar - `perplexity` — force Perplexity Sonar
- `gemini` — force Gemini - `gemini` — force Gemini
Feynman exposes those through four user-facing choices in `feynman setup web`, but defaults to Pi web through `Gemini Browser` when nothing explicit is configured: By default, the expected path is zero-config Gemini Browser via a signed-in Chromium profile. Advanced users can edit `~/.pi/web-search.json` directly if they want Gemini API keys, Perplexity keys, or a different route.
- `Auto` Useful commands:
- `Perplexity API`
- `Gemini API`
- `Gemini Browser`
`Gemini Browser` is still the same Pi web-access path under the hood: it forces the Gemini route and expects a signed-in Chromium profile rather than an API key. - `feynman search status` — show the active Pi web-access route and config path
## Custom Tools ## Custom Tools
@@ -131,11 +123,9 @@ The starter extension adds:
Feynman also ships bundled research subagents in `.pi/agents/`: Feynman also ships bundled research subagents in `.pi/agents/`:
- `researcher` for evidence gathering - `researcher` for evidence gathering
- `verifier` for claim and source checking
- `reviewer` for peer-review style criticism - `reviewer` for peer-review style criticism
- `writer` for polished memo and draft writing - `writer` for polished memo and draft writing
- `review` chain for gather → verify → peer review - `citation` for inline citations and source verification
- `auto` chain for plan → gather → verify → draft
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.
@@ -144,6 +134,7 @@ Feynman uses `@companion-ai/alpha-hub` directly in-process rather than shelling
Feynman loads a lean research stack from [.pi/settings.json](/Users/advaitpaliwal/Companion/Code/feynman/.pi/settings.json): Feynman loads a lean research stack from [.pi/settings.json](/Users/advaitpaliwal/Companion/Code/feynman/.pi/settings.json):
- `pi-subagents` for parallel literature gathering and decomposition - `pi-subagents` for parallel literature gathering and decomposition
- `pi-btw` for fast side-thread /btw conversations without interrupting the main run
- `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
@@ -166,6 +157,5 @@ feynman/
├── extensions/ # Custom research tools ├── extensions/ # Custom research tools
├── 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
└── src/ # Branded launcher around the embedded Pi TUI └── src/ # Branded launcher around the embedded Pi TUI
``` ```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,211 @@
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 { formatToolText } from "./shared.js";
export function registerAlphaCommands(pi: ExtensionAPI): void {
pi.registerCommand("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: "Clear alphaXiv auth from inside Feynman.",
handler: async (_args, ctx) => {
logoutAlpha();
ctx.ui.notify("alphaXiv auth cleared", "info");
},
});
pi.registerCommand("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");
return {
content: [{ type: "text", text: formatToolText(result) }],
details: result,
};
} 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

@@ -0,0 +1,328 @@
import { readdir } from "node:fs/promises";
import { homedir } from "node:os";
import { resolve as resolvePath } from "node:path";
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
import {
APP_ROOT,
FEYNMAN_AGENT_LOGO,
FEYNMAN_VERSION,
} from "./shared.js";
const ANSI_RE = /\x1b\[[0-9;]*m/g;
function visibleLength(text: string): number {
return text.replace(ANSI_RE, "").length;
}
function formatHeaderPath(path: string): string {
const home = homedir();
return path.startsWith(home) ? `~${path.slice(home.length)}` : path;
}
function truncateVisible(text: string, maxVisible: number): string {
const raw = text.replace(ANSI_RE, "");
if (raw.length <= maxVisible) return text;
if (maxVisible <= 3) return ".".repeat(maxVisible);
return `${raw.slice(0, maxVisible - 3)}...`;
}
function wrapWords(text: string, maxW: number): string[] {
const words = text.split(" ");
const lines: string[] = [];
let cur = "";
for (let word of words) {
if (word.length > maxW) {
if (cur) { lines.push(cur); cur = ""; }
word = maxW > 3 ? `${word.slice(0, maxW - 1)}` : word.slice(0, maxW);
}
const test = cur ? `${cur} ${word}` : word;
if (cur && test.length > maxW) {
lines.push(cur);
cur = word;
} else {
cur = test;
}
}
if (cur) lines.push(cur);
return lines.length ? lines : [""];
}
function padRight(text: string, width: number): string {
const gap = Math.max(0, width - visibleLength(text));
return `${text}${" ".repeat(gap)}`;
}
function centerText(text: string, width: number): string {
if (text.length >= width) return text.slice(0, width);
const left = Math.floor((width - text.length) / 2);
const right = width - text.length - left;
return `${" ".repeat(left)}${text}${" ".repeat(right)}`;
}
function getCurrentModelLabel(ctx: ExtensionContext): string {
if (ctx.model) return `${ctx.model.provider}/${ctx.model.id}`;
const branch = ctx.sessionManager.getBranch();
for (let index = branch.length - 1; index >= 0; index -= 1) {
const entry = branch[index]!;
if (entry.type === "model_change") return `${(entry as any).provider}/${(entry as any).modelId}`;
}
return "not set";
}
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; name?: unknown };
if (record.type === "text" && typeof record.text === "string") return record.text;
if (record.type === "toolCall") return `[${typeof record.name === "string" ? record.name : "tool"}]`;
return "";
})
.filter(Boolean)
.join(" ");
}
function getRecentActivitySummary(ctx: ExtensionContext): string {
const branch = ctx.sessionManager.getBranch();
for (let index = branch.length - 1; index >= 0; index -= 1) {
const entry = branch[index]!;
if (entry.type !== "message") continue;
const msg = entry as any;
const text = extractMessageText(msg.message).replace(/\s+/g, " ").trim();
if (!text) continue;
const role = msg.message.role === "assistant" ? "agent" : msg.message.role === "user" ? "you" : msg.message.role;
return `${role}: ${text}`;
}
return "";
}
async function buildAgentCatalogSummary(): Promise<{ agents: string[]; chains: string[] }> {
const agents: string[] = [];
const chains: string[] = [];
try {
const entries = await readdir(resolvePath(APP_ROOT, ".pi", "agents"), { withFileTypes: true });
for (const entry of entries) {
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
if (entry.name.endsWith(".chain.md")) {
chains.push(entry.name.replace(/\.chain\.md$/i, ""));
} else {
agents.push(entry.name.replace(/\.md$/i, ""));
}
}
} catch {
return { agents: [], chains: [] };
}
agents.sort();
chains.sort();
return { agents, chains };
}
type WorkflowInfo = { name: string; description: string };
function getResearchWorkflows(pi: ExtensionAPI): WorkflowInfo[] {
return pi.getCommands()
.filter((cmd) => cmd.source === "prompt")
.map((cmd) => ({ name: `/${cmd.name}`, description: cmd.description ?? "" }))
.sort((a, b) => a.name.localeCompare(b.name));
}
function shortDescription(desc: string): string {
const lower = desc.toLowerCase();
for (const prefix of ["run a ", "run an ", "set up a ", "build a ", "build the ", "turn ", "design the ", "produce a ", "compare ", "simulate ", "inspect ", "write a ", "plan or execute a ", "prepare a "]) {
if (lower.startsWith(prefix)) return desc.slice(prefix.length);
}
return desc;
}
export function installFeynmanHeader(
pi: ExtensionAPI,
ctx: ExtensionContext,
cache: { agentSummaryPromise?: Promise<{ agents: string[]; chains: string[] }> },
): void | Promise<void> {
if (!ctx.hasUI) return;
cache.agentSummaryPromise ??= buildAgentCatalogSummary();
return cache.agentSummaryPromise.then((agentData) => {
const workflows = getResearchWorkflows(pi);
const toolCount = pi.getAllTools().length;
const commandCount = pi.getCommands().length;
const agentCount = agentData.agents.length + agentData.chains.length;
ctx.ui.setHeader((_tui, theme) => ({
render(width: number): string[] {
const maxW = Math.max(width - 2, 1);
const cardW = Math.min(maxW, 120);
const innerW = cardW - 2;
const contentW = innerW - 2;
const outerPad = " ".repeat(Math.max(0, Math.floor((width - cardW) / 2)));
const lines: string[] = [];
const push = (line: string) => { lines.push(`${outerPad}${line}`); };
const border = (ch: string) => theme.fg("borderMuted", ch);
const row = (content: string): string =>
`${border("│")} ${padRight(content, contentW)} ${border("│")}`;
const emptyRow = (): string =>
`${border("│")}${" ".repeat(innerW)}${border("│")}`;
const sep = (): string =>
`${border("├")}${border("─".repeat(innerW))}${border("┤")}`;
const useWideLayout = contentW >= 70;
const leftW = useWideLayout ? Math.min(38, Math.floor(contentW * 0.35)) : 0;
const divColW = useWideLayout ? 3 : 0;
const rightW = useWideLayout ? contentW - leftW - divColW : contentW;
const twoCol = (left: string, right: string): string => {
if (!useWideLayout) return row(left || right);
return row(
`${padRight(left, leftW)}${border(" │ ")}${padRight(right, rightW)}`,
);
};
const modelLabel = getCurrentModelLabel(ctx);
const sessionId = ctx.sessionManager.getSessionName()?.trim() || ctx.sessionManager.getSessionId();
const dirLabel = formatHeaderPath(ctx.cwd);
const activity = getRecentActivitySummary(ctx);
push("");
if (cardW >= 70) {
for (const logoLine of FEYNMAN_AGENT_LOGO) {
push(theme.fg("accent", theme.bold(centerText(truncateVisible(logoLine, cardW), cardW))));
}
push("");
}
const versionTag = ` v${FEYNMAN_VERSION} `;
const gap = Math.max(0, innerW - versionTag.length);
const gapL = Math.floor(gap / 2);
push(
border(`${"─".repeat(gapL)}`) +
theme.fg("dim", versionTag) +
border(`${"─".repeat(gap - gapL)}`),
);
if (useWideLayout) {
const cmdNameW = 16;
const descW = Math.max(10, rightW - cmdNameW - 2);
const leftValueW = Math.max(1, leftW - 11);
const indent = " ".repeat(11);
const leftLines: string[] = [""];
const pushLabeled = (label: string, value: string, color: "text" | "dim") => {
const wrapped = wrapWords(value, leftValueW);
leftLines.push(`${theme.fg("dim", label.padEnd(10))} ${theme.fg(color, wrapped[0]!)}`);
for (let i = 1; i < wrapped.length; i++) {
leftLines.push(`${indent}${theme.fg(color, wrapped[i]!)}`);
}
};
pushLabeled("model", modelLabel, "text");
pushLabeled("directory", dirLabel, "text");
pushLabeled("session", sessionId, "dim");
leftLines.push("");
leftLines.push(theme.fg("dim", `${toolCount} tools · ${agentCount} agents`));
const pushList = (heading: string, items: string[]) => {
if (items.length === 0) return;
leftLines.push("");
leftLines.push(theme.fg("accent", theme.bold(heading)));
for (const line of wrapWords(items.join(", "), leftW)) {
leftLines.push(theme.fg("dim", line));
}
};
pushList("Agents", agentData.agents);
pushList("Chains", agentData.chains);
if (activity) {
leftLines.push("");
leftLines.push(theme.fg("accent", theme.bold("Last Activity")));
for (const line of wrapWords(activity, leftW)) {
leftLines.push(theme.fg("dim", line));
}
}
const rightLines: string[] = [
"",
theme.fg("accent", theme.bold("Research Workflows")),
];
for (const wf of workflows) {
if (wf.name === "/jobs" || wf.name === "/log") continue;
const desc = shortDescription(wf.description);
const descWords = desc.split(" ");
let line = "";
let first = true;
for (const word of descWords) {
const test = line ? `${line} ${word}` : word;
if (line && test.length > descW) {
rightLines.push(
first
? `${theme.fg("accent", wf.name.padEnd(cmdNameW))}${theme.fg("dim", line)}`
: `${" ".repeat(cmdNameW)}${theme.fg("dim", line)}`,
);
first = false;
line = word;
} else {
line = test;
}
}
if (line || first) {
rightLines.push(
first
? `${theme.fg("accent", wf.name.padEnd(cmdNameW))}${theme.fg("dim", line)}`
: `${" ".repeat(cmdNameW)}${theme.fg("dim", line)}`,
);
}
}
const maxRows = Math.max(leftLines.length, rightLines.length);
for (let i = 0; i < maxRows; i++) {
push(twoCol(leftLines[i] ?? "", rightLines[i] ?? ""));
}
} else {
const narrowValW = Math.max(1, contentW - 11);
push(emptyRow());
push(row(`${theme.fg("dim", "model".padEnd(10))} ${theme.fg("text", truncateVisible(modelLabel, narrowValW))}`));
push(row(`${theme.fg("dim", "directory".padEnd(10))} ${theme.fg("text", truncateVisible(dirLabel, narrowValW))}`));
push(row(`${theme.fg("dim", "session".padEnd(10))} ${theme.fg("dim", truncateVisible(sessionId, narrowValW))}`));
push(row(theme.fg("dim", truncateVisible(`${toolCount} tools · ${agentCount} agents · ${commandCount} commands`, contentW))));
push(emptyRow());
push(sep());
push(row(theme.fg("accent", theme.bold("Research Workflows"))));
const narrowDescW = Math.max(1, contentW - 17);
for (const wf of workflows) {
if (wf.name === "/jobs" || wf.name === "/log") continue;
const desc = shortDescription(wf.description);
push(row(`${theme.fg("accent", wf.name.padEnd(16))} ${theme.fg("dim", truncateVisible(desc, narrowDescW))}`));
}
if (agentData.agents.length > 0 || agentData.chains.length > 0) {
push(sep());
push(row(theme.fg("accent", theme.bold("Agents & Chains"))));
if (agentData.agents.length > 0) {
push(row(theme.fg("dim", truncateVisible(`agents ${agentData.agents.join(", ")}`, contentW))));
}
if (agentData.chains.length > 0) {
push(row(theme.fg("dim", truncateVisible(`chains ${agentData.chains.join(", ")}`, contentW))));
}
}
}
push(border(`${"─".repeat(innerW)}`));
push("");
return lines;
},
invalidate() {},
}));
});
}

View File

@@ -0,0 +1,70 @@
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
type HelpCommand = { usage: string; description: string };
type HelpSection = { title: string; commands: HelpCommand[] };
function buildHelpSections(): HelpSection[] {
return [
{
title: "Research Workflows",
commands: [
{ usage: "/deepresearch <topic>", description: "Source-heavy investigation with parallel researchers." },
{ usage: "/lit <topic>", description: "Literature review using paper search." },
{ usage: "/review <artifact>", description: "Simulated peer review with objections and revision plan." },
{ usage: "/audit <item>", description: "Audit a paper against its public codebase." },
{ usage: "/replicate <paper>", description: "Replication workflow for a paper or claim." },
{ usage: "/draft <topic>", description: "Paper-style draft from research findings." },
{ usage: "/compare <topic>", description: "Compare sources with agreements and disagreements." },
{ usage: "/autoresearch <target>", description: "Autonomous experiment optimization loop." },
{ usage: "/watch <topic>", description: "Recurring research watch on a topic." },
],
},
{
title: "Agents & Delegation",
commands: [
{ usage: "/agents", description: "Open the agent and chain manager." },
{ usage: "/run <agent> <task>", description: "Run a single subagent." },
{ usage: "/chain agent1 -> agent2", description: "Run agents in sequence." },
{ usage: "/parallel agent1 -> agent2", description: "Run agents in parallel." },
],
},
{
title: "Project & Session",
commands: [
{ usage: "/init", description: "Bootstrap AGENTS.md and session-log folders." },
{ usage: "/log", description: "Write a session log to notes/." },
{ usage: "/jobs", description: "Inspect active background work." },
{ usage: "/search", description: "Search prior sessions." },
{ usage: "/preview", description: "Preview a generated artifact." },
],
},
{
title: "Setup",
commands: [
{ usage: "/alpha-login", description: "Sign in to alphaXiv." },
{ usage: "/alpha-status", description: "Check alphaXiv auth." },
{ usage: "/alpha-logout", description: "Clear alphaXiv auth." },
],
},
];
}
export function registerHelpCommand(pi: ExtensionAPI): void {
pi.registerCommand("help", {
description: "Show grouped Feynman commands and prefill the editor with a selected command.",
handler: async (_args, ctx) => {
const sections = buildHelpSections();
const items = sections.flatMap((section) => [
`--- ${section.title} ---`,
...section.commands.map((cmd) => `${cmd.usage}${cmd.description}`),
]);
const selected = await ctx.ui.select("Feynman Help", items);
if (!selected || selected.startsWith("---")) return;
const usage = selected.split(" — ")[0];
ctx.ui.setEditorText(usage);
ctx.ui.notify(`Prefilled ${usage}`, "info");
},
});
}

View File

@@ -0,0 +1,233 @@
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;
}
}
export function buildProjectAgentsTemplate(): string {
return `# Feynman Project Guide
This file is read automatically at startup. It is the durable project memory for Feynman.
## Project Overview
- State the research question, target artifact, target venue, and key datasets or benchmarks here.
## AI Research Context
- Problem statement:
- Core hypothesis:
- Closest prior work:
- Required baselines:
- Required ablations:
- Primary metrics:
- Datasets / benchmarks:
## Ground Rules
- Do not modify raw data in \`Data/Raw/\` or equivalent raw-data folders.
- Read first, act second: inspect project structure and existing notes before making changes.
- Prefer durable artifacts in \`notes/\`, \`outputs/\`, \`experiments/\`, and \`papers/\`.
- Keep strong claims source-grounded. Include direct URLs in final writeups.
## Current Status
- Replace this section with the latest project status, known issues, and next steps.
## Session Logging
- Use \`/log\` at the end of meaningful sessions to write a durable session note into \`notes/session-logs/\`.
## Review Readiness
- Known reviewer concerns:
- Missing experiments:
- Missing writing or framing work:
`;
}
export function buildSessionLogsReadme(): string {
return `# Session Logs
Use \`/log\` to write one durable note per meaningful Feynman session.
Recommended contents:
- what was done
- strongest findings
- artifacts written
- unresolved questions
- next steps
`;
}

View File

@@ -0,0 +1,115 @@
import { mkdir, stat, writeFile } from "node:fs/promises";
import { dirname, resolve as resolvePath } from "node:path";
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { renderHtmlPreview, renderPdfPreview, openWithDefaultApp, pathExists, buildProjectAgentsTemplate, buildSessionLogsReadme } from "./preview.js";
import { formatToolText } from "./shared.js";
import { searchSessionTranscripts } from "./session-search.js";
export function registerInitCommand(pi: ExtensionAPI): void {
pi.registerCommand("init", {
description: "Initialize AGENTS.md and session-log folders for a research project.",
handler: async (_args, ctx) => {
const agentsPath = resolvePath(ctx.cwd, "AGENTS.md");
const notesDir = resolvePath(ctx.cwd, "notes");
const sessionLogsDir = resolvePath(notesDir, "session-logs");
const sessionLogsReadmePath = resolvePath(sessionLogsDir, "README.md");
const created: string[] = [];
const skipped: string[] = [];
await mkdir(notesDir, { recursive: true });
await mkdir(sessionLogsDir, { recursive: true });
if (!(await pathExists(agentsPath))) {
await writeFile(agentsPath, buildProjectAgentsTemplate(), "utf8");
created.push("AGENTS.md");
} else {
skipped.push("AGENTS.md");
}
if (!(await pathExists(sessionLogsReadmePath))) {
await writeFile(sessionLogsReadmePath, buildSessionLogsReadme(), "utf8");
created.push("notes/session-logs/README.md");
} else {
skipped.push("notes/session-logs/README.md");
}
const createdSummary = created.length > 0 ? `created: ${created.join(", ")}` : "created: nothing";
const skippedSummary = skipped.length > 0 ? `; kept existing: ${skipped.join(", ")}` : "";
ctx.ui.notify(`${createdSummary}${skippedSummary}`, "info");
},
});
}
export function registerSessionSearchTool(pi: ExtensionAPI): void {
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,
};
},
});
}
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 openedPath =
resolvePath(resolvedPath).toLowerCase().endsWith(".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,
temporaryPreview: openedPath !== resolvedPath,
};
return {
content: [{ type: "text", text: formatToolText(result) }],
details: result,
};
},
});
}

View File

@@ -0,0 +1,223 @@
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

@@ -0,0 +1,46 @@
import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, resolve as resolvePath } from "node:path";
import { fileURLToPath } from "node:url";
export const APP_ROOT = resolvePath(dirname(fileURLToPath(import.meta.url)), "..", "..");
export const FEYNMAN_VERSION = (() => {
try {
const pkg = JSON.parse(readFileSync(resolvePath(APP_ROOT, "package.json"), "utf8")) as { version?: string };
return pkg.version ?? "dev";
} catch {
return "dev";
}
})();
export const FEYNMAN_AGENT_LOGO = [
"███████╗███████╗██╗ ██╗███╗ ██╗███╗ ███╗ █████╗ ███╗ ██╗",
"██╔════╝██╔════╝╚██╗ ██╔╝████╗ ██║████╗ ████║██╔══██╗████╗ ██║",
"█████╗ █████╗ ╚████╔╝ ██╔██╗ ██║██╔████╔██║███████║██╔██╗ ██║",
"██╔══╝ ██╔══╝ ╚██╔╝ ██║╚██╗██║██║╚██╔╝██║██╔══██║██║╚██╗██║",
"██║ ███████╗ ██║ ██║ ╚████║██║ ╚═╝ ██║██║ ██║██║ ╚████║",
"╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝",
];
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 formatToolText(result: unknown): string {
return typeof result === "string" ? result : JSON.stringify(result, null, 2);
}
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);
}

1282
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,11 +11,11 @@
"dist/", "dist/",
".pi/agents/", ".pi/agents/",
".pi/settings.json", ".pi/settings.json",
".pi/SYSTEM.md",
".pi/themes/", ".pi/themes/",
"extensions/", "extensions/",
"prompts/", "prompts/",
"scripts/", "scripts/",
"skills/",
"README.md", "README.md",
".env.example" ".env.example"
], ],
@@ -38,9 +38,6 @@
"extensions": [ "extensions": [
"./extensions" "./extensions"
], ],
"skills": [
"./skills"
],
"prompts": [ "prompts": [
"./prompts" "./prompts"
] ]

View File

@@ -1,17 +0,0 @@
---
description: Design the smallest convincing ablation set for an AI research project.
---
Design an ablation plan for: $@
Requirements:
- Identify the exact claims the paper is making.
- For each claim, determine what ablation or control is necessary to support it.
- Prefer the `verifier` subagent when the claim structure is complicated.
- Distinguish:
- must-have ablations
- nice-to-have ablations
- unnecessary experiments
- Call out where benchmark norms imply mandatory controls.
- Optimize for the minimum convincing set, not experiment sprawl.
- If the user wants a durable artifact, save exactly one plan to `outputs/` as markdown.
- End with a `Sources` section containing direct URLs for any external sources used.

View File

@@ -4,11 +4,8 @@ description: Compare a paper's claims against its public codebase and identify m
Audit the paper and codebase for: $@ Audit the paper and codebase for: $@
Requirements: Requirements:
- Prefer the `researcher` subagent for evidence gathering and the `verifier` subagent for the mismatch pass when the audit is non-trivial. - Use the `researcher` subagent for evidence gathering and the `citation` subagent to verify sources and add inline citations when the audit is non-trivial.
- Identify the canonical paper first with `alpha_search` and `alpha_get_paper`. - Compare claimed methods, defaults, metrics, and data handling against the actual code.
- Extract implementation-sensitive claims with `alpha_ask_paper`.
- If a public repo exists, inspect it with `alpha_read_code`.
- Compare claimed methods, defaults, metrics, and data handling against the repository.
- Call out missing code, mismatches, ambiguous defaults, and reproduction risks. - Call out missing code, mismatches, ambiguous defaults, and reproduction risks.
- End with a `Sources` section containing paper and repository URLs.
- Save exactly one audit artifact to `outputs/` as markdown. - Save exactly one audit artifact to `outputs/` as markdown.
- End with a `Sources` section containing paper and repository URLs.

View File

@@ -1,19 +1,32 @@
--- ---
description: Turn a research idea into a paper-oriented end-to-end run with literature, hypotheses, experiments when possible, and a draft artifact. description: Autonomous experiment loop — try ideas, measure results, keep what works, discard what doesn't, repeat.
--- ---
Run an autoresearch workflow for: $@ Start an autoresearch optimization loop for: $@
Requirements: This command uses pi-autoresearch. Enter autoresearch mode and begin the autonomous experiment loop.
- Prefer the project `auto` chain or the `planner` + `researcher` + `verifier` + `writer` subagents when the task is broad enough to benefit from decomposition.
- If the run is likely to take a while, or the user wants it detached, launch the subagent workflow in background with `clarify: false, async: true` and report how to inspect status. ## Behavior
- Start by clarifying the research objective, scope, and target contribution.
- Search for the strongest relevant primary sources first. - If `autoresearch.md` and `autoresearch.jsonl` already exist in the project, resume the existing session with the user's input as additional context.
- If the topic is current, product-oriented, market-facing, or asks about latest developments, start with `web_search` and `fetch_content`. - Otherwise, gather the optimization target from the user:
- Use `alpha_search` for academic background or paper-centric parts of the topic, but do not rely on it alone for current topics. - What to optimize (test speed, bundle size, training loss, build time, etc.)
- Build a compact evidence table before committing to a paper narrative. - The benchmark command to run
- If experiments are feasible in the current environment, design and run the smallest experiment that materially reduces uncertainty. - The metric name, unit, and direction (lower/higher is better)
- If experiments are not feasible, produce a paper-style draft that is explicit about missing validation and limitations. - Files in scope for changes
- Produce one final durable markdown artifact for the user-facing result. - Then initialize the session: create `autoresearch.md`, `autoresearch.sh`, run the baseline, and start looping.
- If the result is a paper-style draft, save it to `papers/`; otherwise save it to `outputs/`.
- Do not create extra user-facing intermediate markdown files unless the user explicitly asks for them. ## Loop
- End with a `Sources` section containing direct URLs for every source used.
Each iteration: edit → commit → `run_experiment``log_experiment` → keep or revert → repeat. Do not stop unless interrupted or `maxIterations` is reached.
## Key tools
- `init_experiment` — one-time session config (name, metric, unit, direction)
- `run_experiment` — run the benchmark command, capture output and wall-clock time
- `log_experiment` — record result, auto-commit, update dashboard
## Subcommands
- `/autoresearch <text>` — start or resume the loop
- `/autoresearch off` — stop the loop, keep data
- `/autoresearch clear` — delete all state and start fresh

View File

@@ -4,17 +4,8 @@ description: Compare multiple sources on a topic and produce a source-grounded m
Compare sources for: $@ Compare sources for: $@
Requirements: Requirements:
- Use the `researcher` subagent to gather source material when the comparison set is broad, and the `verifier` subagent to pressure-test the resulting matrix when needed. - Use the `researcher` subagent to gather source material when the comparison set is broad, and the `citation` subagent to verify sources and add inline citations to the final matrix.
- Identify the strongest relevant primary sources first. - Build a comparison matrix covering: source, key claim, evidence type, caveats, confidence.
- For current or market-facing topics, use `web_search` and `fetch_content` to gather up-to-date primary sources before comparing them.
- For academic claims, use `alpha_search` and inspect the strongest papers directly.
- Inspect the top sources directly before comparing them.
- Build a comparison matrix covering:
- source
- key claim
- evidence type
- caveats
- confidence
- Distinguish agreement, disagreement, and uncertainty clearly. - Distinguish agreement, disagreement, and uncertainty clearly.
- Save exactly one comparison to `outputs/` as markdown.
- End with a `Sources` section containing direct URLs for every source used. - End with a `Sources` section containing direct URLs for every source used.
- If the user wants a durable artifact, save exactly one comparison to `outputs/` as markdown.

View File

@@ -1,34 +1,107 @@
--- ---
description: Run a thorough, source-heavy investigation on a topic and produce a durable research brief with explicit evidence and source links. description: Run a thorough, source-heavy investigation on a topic and produce a durable research brief with inline citations.
--- ---
Run a deep research workflow for: $@ Run a deep research workflow for: $@
Requirements: You are the Lead Researcher. You plan, delegate, evaluate, loop, write, and cite. Internal orchestration is invisible to the user unless they ask.
- Treat `/deepresearch` as one coherent Feynman workflow from the user's perspective. Do not expose internal orchestration primitives unless the user explicitly asks.
- Start as the lead researcher. First make a compact plan: what must be answered, what evidence types are needed, and which sub-questions are worth splitting out.
- Stay single-agent by default for narrow topics. Only use `subagent` when the task is broad enough that separate context windows materially improve breadth or speed.
- If you use subagents, launch them as one worker batch around clearly disjoint sub-questions. Wait for the batch to finish, synthesize the results, and only then decide whether a second batch is needed.
- Prefer breadth-first worker batches for deep research: different market segments, different source types, different time periods, different technical angles, or different competing explanations.
- Use `researcher` workers for evidence gathering, `verifier` workers for adversarial claim-checking, and `writer` only if you already have solid evidence and need help polishing the final artifact.
- Do not make the workflow chain-shaped by default. Hidden worker batches are optional implementation details, not the user-facing model.
- If the user wants it to run unattended, or the sweep will clearly take a while, prefer background execution with `subagent` using `clarify: false, async: true`, then report how to inspect status.
- 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.
- After synthesis, run a final verification/citation pass. For the strongest claims, independently confirm support and remove anything unsupported, fabricated, or stale.
- Distinguish clearly between established facts, plausible inferences, disagreements, and unresolved questions.
- Produce exactly one durable markdown artifact in `outputs/`.
- The final artifact should read like one deep research memo, not like stitched-together worker transcripts.
- Do not leave extra user-facing intermediate markdown files behind unless the user explicitly asks for them.
- End with a `Sources` section containing direct URLs for every source used.
Default execution shape: ## 1. Plan
1. Clarify the actual research objective if needed.
2. Make a short plan and identify the key sub-questions. Analyze the research question using extended thinking. Develop a research strategy:
3. Decide single-agent versus worker-batch execution. - Key questions that must be answered
4. Gather evidence across the needed source types. - Evidence types needed (papers, web, code, data, docs)
5. Synthesize findings and identify remaining gaps. - Sub-questions disjoint enough to parallelize
6. If needed, run one more worker batch for unresolved gaps. - Source types and time periods that matter
7. Perform a verification/citation pass.
8. Write the final brief with a strict `Sources` section. Save the plan immediately with `memory_remember` (type: `fact`, key: `deepresearch.plan`). Context windows get truncated on long runs — the plan must survive.
## 2. Scale decision
| Query type | Execution |
|---|---|
| Single fact or narrow question | Search directly yourself, no subagents, 3-10 tool calls |
| Direct comparison (2-3 items) | 2 parallel `researcher` subagents |
| Broad survey or multi-faceted topic | 3-4 parallel `researcher` subagents |
| Complex multi-domain research | 4-6 parallel `researcher` subagents |
Never spawn subagents for work you can do in 5 tool calls.
## 3. Spawn researchers
Launch parallel `researcher` subagents via `subagent`. Each gets a structured brief with:
- **Objective:** what to find
- **Output format:** numbered sources, evidence table, inline source references
- **Tool guidance:** which search tools to prioritize
- **Task boundaries:** what NOT to cover (another researcher handles that)
Assign each researcher a clearly disjoint dimension — different source types, geographic scopes, time periods, or technical angles. Never duplicate coverage.
```
{
tasks: [
{ agent: "researcher", task: "...", output: "research-web.md" },
{ agent: "researcher", task: "...", output: "research-papers.md" }
],
concurrency: 4,
failFast: false
}
```
Researchers write full outputs to files and pass references back — do not have them return full content into your context.
## 4. Evaluate and loop
After researchers return, read their output files and critically assess:
- Which plan questions remain unanswered?
- Which answers rest on only one source?
- Are there contradictions needing resolution?
- Is any key angle missing entirely?
If gaps are significant, spawn another targeted batch of researchers. No fixed cap on rounds — iterate until evidence is sufficient or sources are exhausted. Update the stored plan with `memory_remember` as it evolves.
Most topics need 1-2 rounds. Stop when additional rounds would not materially change conclusions.
## 5. Write the report
Once evidence is sufficient, YOU write the full research brief directly. Do not delegate writing to another agent. Read the research files, synthesize the findings, and produce a complete document:
```markdown
# Title
## Executive Summary
2-3 paragraph overview of key findings.
## Section 1: ...
Detailed findings organized by theme or question.
## Section N: ...
## Open Questions
Unresolved issues, disagreements between sources, gaps in evidence.
```
Save this draft to a temp file (e.g., `draft.md` in the chain artifacts dir or a temp path).
## 6. Cite
Spawn the `citation` agent to post-process YOUR draft. The citation agent adds inline citations, verifies every source URL, and produces the final output:
```
{ agent: "citation", task: "Add inline citations to draft.md using the research files as source material. Verify every URL.", output: "brief.md" }
```
The citation agent does not rewrite the report — it only anchors claims to sources and builds the numbered Sources section.
## 7. Deliver
Copy the final cited output to the appropriate folder:
- Paper-style drafts → `papers/`
- Everything else → `outputs/`
Use a descriptive filename based on the topic.
## Background execution
If the user wants unattended execution or the sweep will clearly take a while:
- Launch the full workflow via `subagent` using `clarify: false, async: true`
- Report the async ID and how to check status with `subagent_status`

View File

@@ -4,18 +4,8 @@ description: Turn research findings into a polished paper-style draft with equat
Write a paper-style draft for: $@ Write a paper-style draft for: $@
Requirements: Requirements:
- Prefer the `writer` subagent when the draft should be produced from already-collected notes, and use `verifier` first if the evidence still looks shaky. - Use the `writer` subagent when the draft should be produced from already-collected notes, then use the `citation` subagent to add inline citations and verify sources.
- Ground every claim in inspected sources, experiments, or explicit inference. - Include at minimum: title, abstract, problem statement, related work, method or synthesis, evidence or experiments, limitations, conclusion.
- Use clean Markdown structure with LaTeX where equations materially help. - Use clean Markdown with LaTeX where equations materially help.
- Include at minimum:
- title
- abstract
- problem statement
- related work
- method or synthesis
- evidence or experiments
- limitations
- conclusion
- If citations are available, include citation placeholders or references clearly enough to convert later.
- Add a `Sources` appendix with direct URLs for all primary references used while drafting.
- Save exactly one draft to `papers/` as markdown. - Save exactly one draft to `papers/` as markdown.
- End with a `Sources` appendix with direct URLs for all primary references.

View File

@@ -5,12 +5,7 @@ Investigate the following topic as a literature review: $@
Requirements: Requirements:
- Use the `researcher` subagent when the sweep is wide enough to benefit from delegated paper triage before synthesis. - Use the `researcher` subagent when the sweep is wide enough to benefit from delegated paper triage before synthesis.
- If the topic is academic or paper-centric, use `alpha_search` first.
- If the topic is current, product-oriented, market-facing, or asks about latest developments, use `web_search` and `fetch_content` first, then use `alpha_search` only for academic background.
- Use `alpha_get_paper` on the most relevant papers before making strong claims.
- Use `alpha_ask_paper` for targeted follow-up questions when the report is not enough.
- Prefer primary sources and note when something appears to be a preprint or secondary summary.
- Separate consensus, disagreements, and open questions. - Separate consensus, disagreements, and open questions.
- When useful, propose concrete next experiments or follow-up reading. - When useful, propose concrete next experiments or follow-up reading.
- End with a `Sources` section containing direct URLs for every paper or source used. - Save exactly one literature review to `outputs/` as markdown.
- If the user wants an artifact, write exactly one review to disk as markdown. - End with a `Sources` section containing direct URLs for every source used.

View File

@@ -1,14 +0,0 @@
---
description: Produce a general research memo grounded in explicit sources and direct links.
---
Write a research memo about: $@
Requirements:
- Use the `researcher` and `writer` subagents when decomposition will improve quality or reduce context pressure.
- Start by finding the strongest relevant sources.
- If the topic is current, market-facing, product-oriented, regulatory, or asks about latest developments, use `web_search` and `fetch_content` first.
- Use `alpha_search` for academic background where relevant, but do not rely on it alone for current topics.
- Read or inspect the top sources directly before making strong claims.
- Distinguish facts, interpretations, and open questions.
- End with a `Sources` section containing direct URLs for every source used.
- If the user wants a durable artifact, save exactly one memo to `outputs/` as markdown.

View File

@@ -1,15 +0,0 @@
---
description: Build a prioritized reading list on a research topic with rationale for each paper.
---
Create a research reading list for: $@
Requirements:
- Use the `researcher` subagent when a wider literature sweep would help before curating the final list.
- If the topic is academic, use `alpha_search` with `all` mode.
- If the topic is current, product-oriented, or asks for the latest landscape, use `web_search` and `fetch_content` first, then add `alpha_search` for academic background when relevant.
- Inspect the strongest papers or primary sources directly before recommending them.
- Use `alpha_ask_paper` when a paper's fit is unclear.
- Group papers by role when useful: foundational, strongest recent work, methods, benchmarks, critiques, replication targets.
- For each paper, explain why it is on the list.
- Include direct URLs for each recommended source.
- Save exactly one final reading list to `outputs/` as markdown.

View File

@@ -1,18 +0,0 @@
---
description: Turn reviewer comments into a structured rebuttal and revision plan for an AI research paper.
---
Prepare a rebuttal workflow for: $@
Requirements:
- If reviewer comments are provided, organize them into a response matrix.
- If reviewer comments are not yet provided, infer the likely strongest objections from the current draft and review them before drafting responses.
- Prefer the `reviewer` subagent or the project `review` chain when fresh critical review is still needed.
- For each issue, produce:
- reviewer concern
- whether it is valid
- evidence available now
- paper changes needed
- rebuttal language
- Do not overclaim fixes that have not been implemented.
- Save exactly one rebuttal matrix to `outputs/` as markdown.
- End with a `Sources` section containing direct URLs for all inspected external sources.

View File

@@ -1,19 +0,0 @@
---
description: Build a related-work map and justify why an AI research project needs to exist.
---
Build the related-work and justification view for: $@
Requirements:
- Search for the closest and strongest relevant papers first.
- Prefer the `researcher` subagent when the space is broad or moving quickly.
- Identify:
- foundational papers
- closest prior work
- strongest recent competing approaches
- benchmarks and evaluation norms
- critiques or known weaknesses in the area
- For each important paper, explain why it matters to this project.
- Be explicit about what real gap remains after considering the strongest prior work.
- If the project is not differentiated enough, say so clearly.
- If the user wants a durable result, save exactly one artifact to `outputs/` as markdown.
- End with a `Sources` section containing direct URLs.

View File

@@ -4,11 +4,7 @@ description: Plan or execute a replication workflow for a paper, claim, or bench
Design a replication plan for: $@ Design a replication plan for: $@
Requirements: Requirements:
- Use the `subagent` tool for decomposition when the replication needs separate planning, evidence extraction, and execution passes. - Use the `researcher` subagent to extract implementation details from the target paper and any linked code.
- Identify the canonical paper or source material first.
- Use `alpha_get_paper` for the target paper.
- Use `alpha_ask_paper` to extract the exact implementation or evaluation details you still need.
- If the paper links code, inspect it with `alpha_read_code`.
- Determine what code, datasets, metrics, and environment are needed. - Determine what code, datasets, metrics, and environment are needed.
- If enough information is available locally, implement and run the replication steps. - If enough information is available locally, implement and run the replication steps.
- Save notes, scripts, and results to disk in a reproducible layout. - Save notes, scripts, and results to disk in a reproducible layout.

View File

@@ -4,21 +4,8 @@ description: Simulate an AI research peer review with likely objections, severit
Review this AI research artifact: $@ Review this AI research artifact: $@
Requirements: Requirements:
- Prefer the project `review` chain or the `researcher` + `verifier` + `reviewer` subagents when the artifact is large or the review needs to inspect paper, code, and experiments together. - Spawn a `researcher` subagent to gather evidence on the artifact — inspect the paper, code, cited work, and any linked experimental artifacts. Save to `research.md`.
- Inspect the strongest relevant sources directly before making strong review claims. - Spawn a `reviewer` subagent with `research.md` to produce the final peer review with inline annotations.
- If the artifact is a paper or draft, evaluate: - For small or simple artifacts where evidence gathering is overkill, run the `reviewer` subagent directly instead.
- novelty and related-work positioning
- clarity of claims
- baseline fairness
- evaluation design
- missing ablations
- reproducibility details
- whether conclusions outrun the evidence
- If code or experiment artifacts exist, compare them against the claimed method and evaluation.
- Produce:
- short verdict
- likely reviewer objections
- severity for each issue
- revision plan in priority order
- Save exactly one review artifact to `outputs/` as markdown. - Save exactly one review artifact to `outputs/` as markdown.
- End with a `Sources` section containing direct URLs for every inspected external source. - End with a `Sources` section containing direct URLs for every inspected external source.

View File

@@ -4,11 +4,8 @@ description: Set up a recurring or deferred research watch on a topic, company,
Create a research watch for: $@ Create a research watch for: $@
Requirements: Requirements:
- Start with a baseline sweep of the topic using the strongest relevant sources. - Start with a baseline sweep of the topic.
- If the watch is about current events, products, markets, regulations, or releases, use `web_search` and `fetch_content` first.
- If the watch has a literature component, add `alpha_search` and inspect the strongest papers directly.
- Summarize what should be monitored, what signals matter, and what counts as a meaningful change. - Summarize what should be monitored, what signals matter, and what counts as a meaningful change.
- Use `schedule_prompt` to create the recurring or delayed follow-up instead of merely promising to check later. - Use `schedule_prompt` to create the recurring or delayed follow-up instead of merely promising to check later.
- If the user wants detached execution for the initial sweep, use `subagent` in background mode and report how to inspect status. - Save exactly one baseline artifact to `outputs/`.
- Save exactly one durable baseline artifact to `outputs/`.
- 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

@@ -77,10 +77,11 @@ ensurePackageWorkspace();
if (existsSync(packageJsonPath)) { if (existsSync(packageJsonPath)) {
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8")); const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
if (pkg.piConfig?.name !== "feynman") { if (pkg.piConfig?.name !== "feynman" || pkg.piConfig?.configDir !== ".feynman") {
pkg.piConfig = { pkg.piConfig = {
...(pkg.piConfig || {}), ...(pkg.piConfig || {}),
name: "feynman", name: "feynman",
configDir: ".feynman",
}; };
writeFileSync(packageJsonPath, JSON.stringify(pkg, null, "\t") + "\n", "utf8"); writeFileSync(packageJsonPath, JSON.stringify(pkg, null, "\t") + "\n", "utf8");
} }
@@ -117,7 +118,7 @@ if (existsSync(interactiveThemePath)) {
" return {", " return {",
' borderColor: (text) => " ".repeat(text.length),', ' borderColor: (text) => " ".repeat(text.length),',
' bgColor: (text) => theme.bg("userMessageBg", text),', ' bgColor: (text) => theme.bg("userMessageBg", text),',
' placeholderText: "Type your message",', ' placeholderText: "Type your message or /help for commands",',
' placeholder: (text) => theme.fg("dim", text),', ' placeholder: (text) => theme.fg("dim", text),',
" selectList: getSelectListTheme(),", " selectList: getSelectListTheme(),",
" };", " };",
@@ -212,51 +213,25 @@ if (existsSync(editorPath)) {
" const isFirstLayoutLine = this.scrollOffset + visibleIndex === 0;", " const isFirstLayoutLine = this.scrollOffset + visibleIndex === 0;",
" let displayText = layoutLine.text;", " let displayText = layoutLine.text;",
" let lineVisibleWidth = visibleWidth(layoutLine.text);", " let lineVisibleWidth = visibleWidth(layoutLine.text);",
" let cursorInPadding = false;",
" const isPlaceholderLine = showPlaceholder && isFirstLayoutLine;", " const isPlaceholderLine = showPlaceholder && isFirstLayoutLine;",
" // Add cursor if this line has it",
" if (isPlaceholderLine) {", " if (isPlaceholderLine) {",
" const marker = emitCursorMarker ? CURSOR_MARKER : \"\";", " const marker = emitCursorMarker ? CURSOR_MARKER : \"\";",
" const rawPlaceholder = this.theme.placeholderText;", " const rawPlaceholder = this.theme.placeholderText;",
" const graphemes = [...this.segment(rawPlaceholder)];", ' const styledPlaceholder = typeof this.theme.placeholder === "function"',
' const firstGrapheme = graphemes[0]?.segment ?? " ";', " ? this.theme.placeholder(rawPlaceholder)",
" const restRaw = rawPlaceholder.slice(firstGrapheme.length);", " : rawPlaceholder;",
' const restStyled = typeof this.theme.placeholder === "function"', " displayText = marker + styledPlaceholder;",
" ? this.theme.placeholder(restRaw)",
" : restRaw;",
' displayText = marker + `\\x1b[7m${firstGrapheme}\\x1b[27m` + restStyled;',
" lineVisibleWidth = visibleWidth(rawPlaceholder);", " lineVisibleWidth = visibleWidth(rawPlaceholder);",
" }", " }",
" else if (layoutLine.hasCursor && layoutLine.cursorPos !== undefined) {", " else if (layoutLine.hasCursor && layoutLine.cursorPos !== undefined) {",
' const marker = emitCursorMarker ? CURSOR_MARKER : "";',
" const before = displayText.slice(0, layoutLine.cursorPos);", " const before = displayText.slice(0, layoutLine.cursorPos);",
" const after = displayText.slice(layoutLine.cursorPos);", " const after = displayText.slice(layoutLine.cursorPos);",
" // Hardware cursor marker (zero-width, emitted before fake cursor for IME positioning)", " displayText = before + marker + after;",
' const marker = emitCursorMarker ? CURSOR_MARKER : "";',
" if (after.length > 0) {",
" // Cursor is on a character (grapheme) - replace it with highlighted version",
" // Get the first grapheme from 'after'",
" const afterGraphemes = [...this.segment(after)];",
' const firstGrapheme = afterGraphemes[0]?.segment || "";',
" const restAfter = after.slice(firstGrapheme.length);",
' const cursor = `\\x1b[7m${firstGrapheme}\\x1b[27m`;',
" displayText = before + marker + cursor + restAfter;",
" // lineVisibleWidth stays the same - we're replacing, not adding",
" }",
" else {",
" // Cursor is at the end - add highlighted space",
' const cursor = "\\x1b[7m \\x1b[27m";',
" displayText = before + marker + cursor;",
" lineVisibleWidth = lineVisibleWidth + 1;",
" // If cursor overflows content width into the padding, flag it",
" if (lineVisibleWidth > contentWidth && paddingX > 0) {",
" cursorInPadding = true;",
" }",
" }",
" }", " }",
" // Calculate padding based on actual visible width", " // Calculate padding based on actual visible width",
' const padding = " ".repeat(Math.max(0, contentWidth - lineVisibleWidth));', ' const padding = " ".repeat(Math.max(0, contentWidth - lineVisibleWidth));',
" const lineRightPadding = cursorInPadding ? rightPadding.slice(1) : rightPadding;", " const renderedLine = `${leftPadding}${displayText}${padding}${rightPadding}`;",
" const renderedLine = `${leftPadding}${displayText}${padding}${lineRightPadding}`;",
" result.push(bgColor ? applyBackgroundToLine(renderedLine, width, bgColor) : renderedLine);", " result.push(bgColor ? applyBackgroundToLine(renderedLine, width, bgColor) : renderedLine);",
" }", " }",
" // Render bottom padding row. When background fill is active, mimic the user-message block", " // Render bottom padding row. When background fill is active, mimic the user-message block",

View File

@@ -1,56 +0,0 @@
---
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

@@ -1,39 +0,0 @@
---
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

@@ -1,54 +0,0 @@
---
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

@@ -1,49 +0,0 @@
---
name: experiment-design
description: Use this when the task is to turn a vague research idea into a testable experiment, define metrics, choose baselines, or plan ablations.
---
# Experiment Design
## When To Use
Use this skill when the user has:
- a hypothesis to test
- a method to evaluate
- an unclear benchmark plan
- a need for baselines, ablations, or metrics
## Procedure
1. Restate the research question as a falsifiable claim.
2. Define:
- independent variables
- dependent variables
- success metrics
- baselines
- constraints
3. Search for prior work first.
4. If the setup is tied to current products, APIs, model offerings, pricing, or market behavior, use `web_search` and `fetch_content` first.
5. Use `alpha_search`, `alpha_get_paper`, and `alpha_ask_paper` for academic baselines and prior experiments.
6. Prefer the smallest experiment that can meaningfully reduce uncertainty.
7. List confounders and failure modes up front.
8. If implementation is requested, create the scripts, configs, and logging plan.
9. Write the plan to disk before running expensive work.
## Pitfalls
- Avoid experiments with no baseline.
- Avoid metrics that do not connect to the claim.
- Avoid ablations that change multiple variables at once.
- Avoid broad plans that cannot be executed with the current environment.
## Deliverable
Produce:
- hypothesis
- setup
- baselines
- metrics
- ablations
- risks
- next action

View File

@@ -1,57 +0,0 @@
---
name: literature-review
description: Use this when the task is to survey prior work, compare papers, synthesize a field, or build a reading list grounded in primary sources.
---
# Literature Review
## When To Use
Use this skill when the user wants:
- a research overview
- a paper shortlist
- a comparison of methods
- a synthesis of consensus and disagreement
- a source-backed brief on a topic
## Procedure
1. Search broadly first.
2. If the topic is primarily academic or paper-centric, start with `alpha_search`.
3. If the topic includes current products, companies, markets, software, or "latest/current" framing, start with `web_search` and `fetch_content`, then use `alpha_search` only for academic background.
4. Pick the strongest candidates by direct relevance, recency, citations, venue quality, and source quality.
5. Inspect the top papers with `alpha_get_paper` before making concrete claims.
6. Use `alpha_ask_paper` for missing methodological or experimental details.
7. Build a compact evidence table:
- title
- year
- authors
- venue
- claim or contribution
- important caveats
8. Distinguish:
- what multiple sources agree on
- where methods or findings differ
- what remains unresolved
9. If the user wants a durable artifact, write a markdown brief to disk.
10. If you discover an important gotcha about a paper, save it with `alpha_annotate_paper`.
11. End with a `Sources` section that lists direct URLs, not just titles.
## Pitfalls
- Do not summarize a field from titles alone.
- Do not flatten disagreements into fake consensus.
- Do not treat recent preprints as established facts without saying so.
- Do not cite secondary commentary when a primary source is available.
- Do not treat a current product or market topic as if it were a paper-only topic.
## Output Shape
Prefer this structure:
- question
- strongest papers
- major findings
- disagreements or caveats
- open questions
- recommended next reading or experiments
- sources

View File

@@ -1,52 +0,0 @@
---
name: paper-code-audit
description: Use this when the task is to compare a paper against its repository, verify whether claims are implemented, or assess reproducibility risk.
---
# Paper Code Audit
## When To Use
Use this skill for:
- paper-versus-code verification
- implementation gap analysis
- reproducibility audits
- checking whether public code matches reported results
## Procedure
1. Locate the paper with `alpha_search`.
2. Load the paper with `alpha_get_paper`.
3. Extract implementation-relevant details using `alpha_ask_paper`:
- datasets
- preprocessing
- model architecture
- hyperparameters
- evaluation protocol
4. If the paper links a repository, inspect it using `alpha_read_code`.
5. Compare paper claims against code realities:
- are all components present
- do defaults match the paper
- are metrics/eval scripts exposed
- are hidden assumptions required
6. Record concrete mismatches, not vibes.
7. Save the audit in `outputs/`.
8. If you find a durable gotcha, save it with `alpha_annotate_paper`.
9. End with a `Sources` section for the paper and repository.
## Pitfalls
- Do not infer repository behavior without opening the relevant files.
- Do not assume README claims reflect the actual defaults.
- Do not mark something as missing if it exists under another name without checking.
## Deliverable
Include:
- paper summary
- repository coverage
- confirmed matches
- mismatches or omissions
- reproducibility risks
- recommended next actions
- sources

View File

@@ -1,46 +0,0 @@
---
name: paper-writing
description: Use this when the task is to turn research notes, experiments, or a literature review into a polished paper-style writeup with Markdown and LaTeX.
---
# Paper Writing
## When To Use
Use this skill for:
- research reports that should read like a paper
- internal memos with equations or formal structure
- polished writeups of experiments or literature reviews
- converting rough notes into a coherent draft
## Procedure
1. Make sure the underlying claims are already grounded in sources, experiments, or explicit caveats.
2. Build the draft around a proper research structure:
- title
- abstract
- introduction or problem statement
- related work
- approach, synthesis, or methodology
- evidence, experiments, or case studies
- limitations
- conclusion
3. Use Markdown by default.
4. Use LaTeX only where equations or notation genuinely improve clarity.
5. Keep claims falsifiable and scoped.
6. Save polished drafts to `papers/`.
7. Add a `Sources` appendix with direct URLs to all inspected references.
## Pitfalls
- Do not use LaTeX for decoration.
- Do not make a draft look more certain than the evidence supports.
- Do not hide missing citations or weak evidence; flag them.
## Deliverable
A readable paper-style draft with:
- explicit structure
- traceable claims
- equations only where useful
- limitations stated plainly

View File

@@ -1,53 +0,0 @@
---
name: reading-list
description: Use this when the user wants a curated reading sequence, paper shortlist, or tiered set of papers for learning or project onboarding.
---
# Reading List
## When To Use
Use this skill for:
- getting up to speed on a topic
- onboarding into a research area
- choosing which papers to read first
- constructing a project-specific reading order
## Procedure
1. Start with source discovery that matches the topic.
2. For academic topics, use `alpha_search` in `all` mode.
3. For current, product-oriented, or market-facing topics, use `web_search` and `fetch_content` first, then use `alpha_search` for background literature if needed.
4. Inspect the strongest candidates directly before recommending them.
5. Use `alpha_ask_paper` for fit questions like:
- what problem does this really solve
- what assumptions does it rely on
- what prior work does it build on
6. Classify papers or sources into roles:
- foundational
- key recent advances
- evaluation or benchmark references
- critiques or limitations
- likely replication targets
7. Order the list intentionally:
- start with orientation
- move to strongest methods
- finish with edges, critiques, or adjacent work
8. Write the final list as a durable markdown artifact in `outputs/`.
9. For every source, include a direct URL.
## Pitfalls
- Do not sort purely by citations.
- Do not over-index on recency when fundamentals matter.
- Do not include papers you have not inspected at all.
- Do not force everything into papers when the user actually needs current docs, products, or market sources.
## Deliverable
For each paper include:
- title
- year
- why it matters
- when to read it in the sequence
- one caveat or limitation

View File

@@ -1,52 +0,0 @@
---
name: replication
description: Use this when the task is to reproduce a paper result, benchmark a claim, rebuild an experiment, or evaluate whether a published result holds in practice.
---
# Replication
## When To Use
Use this skill for:
- paper reproduction
- benchmark recreation
- ablation reruns
- claim verification through code and experiments
## Procedure
1. Identify the canonical source paper and inspect it with `alpha_get_paper`.
2. Extract the exact target:
- task
- dataset
- model or method
- metrics
- hardware or runtime assumptions
3. Use `alpha_ask_paper` to pull out the exact details missing from the report.
4. If the paper has a public repository, inspect it with `alpha_read_code`.
5. Search the local workspace for existing code, notebooks, configs, and datasets.
6. Write down the missing pieces explicitly before running anything.
7. If the environment is sufficient, implement the minimal runnable reproduction path.
8. Run the experiment with built-in file and shell tools.
9. Save:
- commands used
- configs
- raw outputs
- summarized results
10. Compare observed results with the paper and explain gaps.
11. If the paper had a practical gotcha, attach it with `alpha_annotate_paper`.
## Pitfalls
- Do not claim replication succeeded if key conditions were missing.
- Do not compare different metrics as if they were equivalent.
- Do not ignore dataset or preprocessing mismatch.
- Do not hide failed runs; record them and explain them.
## Verification
A good replication outcome includes:
- the exact command path
- the data or config used
- the observed metrics
- a clear statement of match, partial match, or mismatch

View File

@@ -1,45 +0,0 @@
---
name: research-memo
description: Use this when the user wants a source-grounded memo, briefing, landscape summary, or background note that is broader than a single paper.
---
# Research Memo
## When To Use
Use this skill for:
- background research
- topic briefings
- market or field overviews
- synthesis across multiple sources
- internal memos that need traceable evidence
## Procedure
1. Find relevant sources first.
2. If the topic is current, product-oriented, market-facing, or asks about latest developments, use `web_search` and `fetch_content` first.
3. If there is an academic literature component, use `alpha_search` and inspect the strongest papers directly.
4. Inspect the strongest sources directly before synthesizing.
5. Separate:
- established facts
- plausible inferences
- unresolved questions
6. Write a memo with clear sections and a concise narrative.
7. End with a `Sources` section containing direct links.
8. Save the memo to `outputs/` when the user wants a durable artifact.
## Pitfalls
- Do not summarize from search snippets alone.
- Do not omit the source list.
- Do not present inference as fact.
- Do not rely on paper search alone for latest/current topics.
## Deliverable
Include:
- topic
- key findings
- disagreements or caveats
- open questions
- sources

View File

@@ -1,44 +0,0 @@
---
name: source-comparison
description: Use this when the task is to compare multiple papers, reports, or sources and produce a grounded matrix of agreements, disagreements, and confidence.
---
# Source Comparison
## When To Use
Use this skill for:
- comparing papers on the same topic
- reconciling conflicting claims
- assessing multiple sources before making a recommendation
- producing evidence matrices
## Procedure
1. Find and inspect the strongest relevant sources first.
2. For each source, extract:
- main claim
- evidence type
- caveats
- what would falsify or weaken the claim
3. Build a comparison table or matrix.
4. Separate:
- points of agreement
- points of disagreement
- unresolved questions
5. End with a `Sources` section containing direct URLs.
## Pitfalls
- Do not compare sources you have not actually opened.
- Do not blur disagreement into consensus.
- Do not omit source links.
## Deliverable
Include:
- matrix
- agreement summary
- disagreement summary
- confidence assessment
- sources

View File

@@ -11,33 +11,26 @@ import {
login as loginAlpha, login as loginAlpha,
logout as logoutAlpha, logout as logoutAlpha,
} from "@companion-ai/alpha-hub/lib"; } from "@companion-ai/alpha-hub/lib";
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent"; import { AuthStorage, DefaultPackageManager, ModelRegistry, SettingsManager } from "@mariozechner/pi-coding-agent";
import { syncBundledAssets } from "./bootstrap/sync.js"; import { syncBundledAssets } from "./bootstrap/sync.js";
import { editConfig, printConfig, printConfigPath, printConfigValue, setConfigValue } from "./config/commands.js"; import { ensureFeynmanHome, getDefaultSessionDir, getFeynmanAgentDir, getFeynmanHome } from "./config/paths.js";
import { getConfiguredSessionDir, loadFeynmanConfig } from "./config/feynman-config.js";
import { ensureFeynmanHome, getFeynmanAgentDir, getFeynmanHome } from "./config/paths.js";
import { buildFeynmanSystemPrompt } from "./feynman-prompt.js";
import { launchPiChat } from "./pi/launch.js"; import { launchPiChat } from "./pi/launch.js";
import { normalizeFeynmanSettings, normalizeThinkingLevel, parseModelSpec } from "./pi/settings.js"; import { normalizeFeynmanSettings, normalizeThinkingLevel, parseModelSpec } from "./pi/settings.js";
import { import {
loginModelProvider, loginModelProvider,
logoutModelProvider, logoutModelProvider,
printModelList, printModelList,
printModelProviders,
printModelRecommendation,
printModelStatus,
setDefaultModelSpec, setDefaultModelSpec,
} from "./model/commands.js"; } from "./model/commands.js";
import { printSearchProviders, printSearchStatus, setSearchProvider } from "./search/commands.js"; import { printSearchStatus } from "./search/commands.js";
import { runDoctor, runStatus } from "./setup/doctor.js"; import { runDoctor, runStatus } from "./setup/doctor.js";
import { setupPreviewDependencies } from "./setup/preview.js"; import { setupPreviewDependencies } from "./setup/preview.js";
import { runSetup } from "./setup/setup.js"; import { runSetup } from "./setup/setup.js";
import { printInfo, printPanel, printSection } from "./ui/terminal.js"; import { printInfo, printPanel, printSection } from "./ui/terminal.js";
const TOP_LEVEL_COMMANDS = new Set(["alpha", "chat", "config", "doctor", "help", "model", "search", "setup", "status"]); const TOP_LEVEL_COMMANDS = new Set(["alpha", "chat", "doctor", "help", "model", "search", "setup", "status", "update"]);
const RESEARCH_WORKFLOW_COMMANDS = new Set([ const RESEARCH_WORKFLOW_COMMANDS = new Set([
"ablate",
"audit", "audit",
"autoresearch", "autoresearch",
"compare", "compare",
@@ -46,11 +39,7 @@ const RESEARCH_WORKFLOW_COMMANDS = new Set([
"jobs", "jobs",
"lit", "lit",
"log", "log",
"memo",
"reading",
"related",
"replicate", "replicate",
"rebuttal",
"review", "review",
"watch", "watch",
]); ]);
@@ -64,39 +53,31 @@ function printHelp(): void {
printSection("Getting Started"); printSection("Getting Started");
printInfo("feynman"); printInfo("feynman");
printInfo("feynman setup"); printInfo("feynman setup");
printInfo("feynman setup quick");
printInfo("feynman doctor"); printInfo("feynman doctor");
printInfo("feynman model"); printInfo("feynman model");
printInfo("feynman search"); printInfo("feynman search status");
printSection("Commands"); printSection("Commands");
printInfo("feynman chat [prompt] Start chat explicitly, optionally with an initial prompt"); printInfo("feynman chat [prompt] Start chat explicitly, optionally with an initial prompt");
printInfo("feynman setup [section] Run setup for model, alpha, web, preview, or all"); printInfo("feynman setup Run the guided setup");
printInfo("feynman setup quick Configure only missing items");
printInfo("feynman doctor Diagnose config, auth, Pi runtime, and preview deps"); printInfo("feynman doctor Diagnose config, auth, Pi runtime, and preview deps");
printInfo("feynman status Show the current setup summary"); printInfo("feynman status Show the current setup summary");
printInfo("feynman model list Show available models in auth storage"); printInfo("feynman model list Show available models in auth storage");
printInfo("feynman model providers Show Pi-supported providers and auth state");
printInfo("feynman model recommend Show the recommended research model");
printInfo("feynman model login [id] Login to a Pi OAuth model provider"); printInfo("feynman model login [id] Login to a Pi OAuth model provider");
printInfo("feynman model logout [id] Logout from a Pi OAuth model provider"); printInfo("feynman model logout [id] Logout from a Pi OAuth model provider");
printInfo("feynman model set <spec> Set the default model"); printInfo("feynman model set <spec> Set the default model");
printInfo("feynman search status Show web research provider status"); printInfo("feynman update [package] Update installed packages (or a specific one)");
printInfo("feynman search set <id> Set web research provider"); printInfo("feynman search status Show Pi web-access status and config path");
printInfo("feynman config show Print ~/.feynman/config.json");
printInfo("feynman config get <key> Read a config value");
printInfo("feynman config set <key> <value>");
printInfo("feynman config edit Open config in $EDITOR");
printInfo("feynman config path Print the config path");
printInfo("feynman alpha login|logout|status"); printInfo("feynman alpha login|logout|status");
printSection("Research Workflows"); printSection("Research Workflows");
printInfo("feynman deepresearch <topic> Start a thorough source-heavy investigation");
printInfo("feynman lit <topic> Start the literature-review workflow"); printInfo("feynman lit <topic> Start the literature-review workflow");
printInfo("feynman review <artifact> Start the peer-review workflow"); printInfo("feynman review <artifact> Start the peer-review workflow");
printInfo("feynman audit <item> Start the paper/code audit workflow"); printInfo("feynman audit <item> Start the paper/code audit workflow");
printInfo("feynman replicate <target> Start the replication workflow"); printInfo("feynman replicate <target> Start the replication workflow");
printInfo("feynman memo <topic> Start the research memo workflow");
printInfo("feynman draft <topic> Start the paper-style draft workflow"); printInfo("feynman draft <topic> Start the paper-style draft workflow");
printInfo("feynman compare <topic> Start the source-comparison workflow");
printInfo("feynman watch <topic> Start the recurring research watch workflow"); printInfo("feynman watch <topic> Start the recurring research watch workflow");
printSection("Legacy Flags"); printSection("Legacy Flags");
@@ -148,71 +129,19 @@ async function handleAlphaCommand(action: string | undefined): Promise<void> {
throw new Error(`Unknown alpha command: ${action}`); throw new Error(`Unknown alpha command: ${action}`);
} }
function handleConfigCommand(subcommand: string | undefined, args: string[]): void { async function handleModelCommand(subcommand: string | undefined, args: string[], feynmanSettingsPath: string, feynmanAuthPath: string): Promise<void> {
if (!subcommand || subcommand === "show") { if (!subcommand || subcommand === "list") {
printConfig(); printModelList(feynmanSettingsPath, feynmanAuthPath);
return;
}
if (subcommand === "path") {
printConfigPath();
return;
}
if (subcommand === "edit") {
editConfig();
return;
}
if (subcommand === "get") {
const key = args[0];
if (!key) {
throw new Error("Usage: feynman config get <key>");
}
printConfigValue(key);
return;
}
if (subcommand === "set") {
const [key, ...valueParts] = args;
if (!key || valueParts.length === 0) {
throw new Error("Usage: feynman config set <key> <value>");
}
setConfigValue(key, valueParts.join(" "));
return;
}
throw new Error(`Unknown config command: ${subcommand}`);
}
async function handleModelCommand(subcommand: string | undefined, args: string[], settingsPath: string, authPath: string): Promise<void> {
if (!subcommand || subcommand === "status" || subcommand === "current") {
printModelStatus(settingsPath, authPath);
return;
}
if (subcommand === "list") {
printModelList(settingsPath, authPath);
return;
}
if (subcommand === "providers") {
printModelProviders(settingsPath, authPath);
return;
}
if (subcommand === "recommend") {
printModelRecommendation(authPath);
return; return;
} }
if (subcommand === "login") { if (subcommand === "login") {
await loginModelProvider(authPath, args[0]); await loginModelProvider(feynmanAuthPath, args[0]);
return; return;
} }
if (subcommand === "logout") { if (subcommand === "logout") {
await logoutModelProvider(authPath, args[0]); await logoutModelProvider(feynmanAuthPath, args[0]);
return; return;
} }
@@ -221,33 +150,42 @@ async function handleModelCommand(subcommand: string | undefined, args: string[]
if (!spec) { if (!spec) {
throw new Error("Usage: feynman model set <provider/model>"); throw new Error("Usage: feynman model set <provider/model>");
} }
setDefaultModelSpec(settingsPath, authPath, spec); setDefaultModelSpec(feynmanSettingsPath, feynmanAuthPath, spec);
return; return;
} }
throw new Error(`Unknown model command: ${subcommand}`); throw new Error(`Unknown model command: ${subcommand}`);
} }
function handleSearchCommand(subcommand: string | undefined, args: string[]): void { async function handleUpdateCommand(workingDir: string, feynmanAgentDir: string, source?: string): Promise<void> {
const settingsManager = SettingsManager.create(workingDir, feynmanAgentDir);
const packageManager = new DefaultPackageManager({
cwd: workingDir,
agentDir: feynmanAgentDir,
settingsManager,
});
packageManager.setProgressCallback((event) => {
if (event.type === "start") {
console.log(`Updating ${event.source}...`);
} else if (event.type === "complete") {
console.log(`Updated ${event.source}`);
} else if (event.type === "error") {
console.error(`Failed to update ${event.source}: ${event.message ?? "unknown error"}`);
}
});
await packageManager.update(source);
await settingsManager.flush();
console.log("All packages up to date.");
}
function handleSearchCommand(subcommand: string | undefined): void {
if (!subcommand || subcommand === "status") { if (!subcommand || subcommand === "status") {
printSearchStatus(); printSearchStatus();
return; return;
} }
if (subcommand === "providers" || subcommand === "list") {
printSearchProviders();
return;
}
if (subcommand === "set") {
const provider = args[0];
if (!provider) {
throw new Error("Usage: feynman search set <provider> [value]");
}
setSearchProvider(provider, args[1]);
return;
}
throw new Error(`Unknown search command: ${subcommand}`); throw new Error(`Unknown search command: ${subcommand}`);
} }
@@ -317,9 +255,8 @@ export async function main(): Promise<void> {
return; return;
} }
const config = loadFeynmanConfig();
const workingDir = resolve(values.cwd ?? process.cwd()); const workingDir = resolve(values.cwd ?? process.cwd());
const sessionDir = resolve(values["session-dir"] ?? getConfiguredSessionDir(config)); const sessionDir = resolve(values["session-dir"] ?? getDefaultSessionDir(feynmanHome));
const feynmanSettingsPath = resolve(feynmanAgentDir, "settings.json"); const feynmanSettingsPath = resolve(feynmanAgentDir, "settings.json");
const feynmanAuthPath = resolve(feynmanAgentDir, "auth.json"); const feynmanAuthPath = resolve(feynmanAgentDir, "auth.json");
const thinkingLevel = normalizeThinkingLevel(values.thinking ?? process.env.FEYNMAN_THINKING) ?? "medium"; const thinkingLevel = normalizeThinkingLevel(values.thinking ?? process.env.FEYNMAN_THINKING) ?? "medium";
@@ -366,7 +303,6 @@ export async function main(): Promise<void> {
if (command === "setup") { if (command === "setup") {
await runSetup({ await runSetup({
section: rest[0],
settingsPath: feynmanSettingsPath, settingsPath: feynmanSettingsPath,
bundledSettingsPath, bundledSettingsPath,
authPath: feynmanAuthPath, authPath: feynmanAuthPath,
@@ -400,18 +336,18 @@ export async function main(): Promise<void> {
return; return;
} }
if (command === "config") {
handleConfigCommand(rest[0], rest.slice(1));
return;
}
if (command === "model") { if (command === "model") {
await handleModelCommand(rest[0], rest.slice(1), feynmanSettingsPath, feynmanAuthPath); await handleModelCommand(rest[0], rest.slice(1), feynmanSettingsPath, feynmanAuthPath);
return; return;
} }
if (command === "search") { if (command === "search") {
handleSearchCommand(rest[0], rest.slice(1)); handleSearchCommand(rest[0]);
return;
}
if (command === "update") {
await handleUpdateCommand(workingDir, feynmanAgentDir, rest[0]);
return; return;
} }
@@ -439,6 +375,5 @@ export async function main(): Promise<void> {
explicitModelSpec, explicitModelSpec,
oneShotPrompt: values.prompt, oneShotPrompt: values.prompt,
initialPrompt: resolveInitialPrompt(command, rest, values.prompt), initialPrompt: resolveInitialPrompt(command, rest, values.prompt),
systemPrompt: buildFeynmanSystemPrompt(),
}); });
} }

View File

@@ -1,78 +0,0 @@
import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import { FEYNMAN_CONFIG_PATH, loadFeynmanConfig, saveFeynmanConfig } from "./feynman-config.js";
function coerceConfigValue(raw: string): unknown {
const trimmed = raw.trim();
if (trimmed === "true") return true;
if (trimmed === "false") return false;
if (trimmed === "null") return null;
if (trimmed === "") return "";
if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed);
try {
return JSON.parse(trimmed);
} catch {
return raw;
}
}
function getNestedValue(record: Record<string, unknown>, path: string): unknown {
return path.split(".").reduce<unknown>((current, segment) => {
if (!current || typeof current !== "object") {
return undefined;
}
return (current as Record<string, unknown>)[segment];
}, record);
}
function setNestedValue(record: Record<string, unknown>, path: string, value: unknown): void {
const segments = path.split(".");
let current: Record<string, unknown> = record;
for (const segment of segments.slice(0, -1)) {
const existing = current[segment];
if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
current[segment] = {};
}
current = current[segment] as Record<string, unknown>;
}
current[segments[segments.length - 1]!] = value;
}
export function printConfig(): void {
console.log(JSON.stringify(loadFeynmanConfig(), null, 2));
}
export function printConfigPath(): void {
console.log(FEYNMAN_CONFIG_PATH);
}
export function editConfig(): void {
if (!existsSync(FEYNMAN_CONFIG_PATH)) {
saveFeynmanConfig(loadFeynmanConfig());
}
const editor = process.env.VISUAL || process.env.EDITOR || "vi";
const result = spawnSync(editor, [FEYNMAN_CONFIG_PATH], {
stdio: "inherit",
});
if (result.status !== 0) {
throw new Error(`Failed to open editor: ${editor}`);
}
}
export function printConfigValue(key: string): void {
const config = loadFeynmanConfig() as Record<string, unknown>;
const value = getNestedValue(config, key);
console.log(typeof value === "string" ? value : JSON.stringify(value, null, 2));
}
export function setConfigValue(key: string, rawValue: string): void {
const config = loadFeynmanConfig() as Record<string, unknown>;
setNestedValue(config, key, coerceConfigValue(rawValue));
saveFeynmanConfig(config as ReturnType<typeof loadFeynmanConfig>);
console.log(`Updated ${key}`);
}

View File

@@ -1,270 +0,0 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { getDefaultSessionDir, getFeynmanConfigPath } from "./paths.js";
export type WebSearchProviderId = "auto" | "perplexity" | "gemini-api" | "gemini-browser";
export type PiWebSearchProvider = "auto" | "perplexity" | "gemini";
export type WebSearchConfig = Record<string, unknown> & {
provider?: PiWebSearchProvider;
perplexityApiKey?: string;
geminiApiKey?: string;
chromeProfile?: string;
feynmanWebProvider?: WebSearchProviderId;
};
export type FeynmanConfig = {
version: 1;
sessionDir?: string;
webSearch?: WebSearchConfig;
preview?: {
lastSetupAt?: string;
};
};
export type WebSearchProviderDefinition = {
id: WebSearchProviderId;
label: string;
description: string;
runtimeProvider: PiWebSearchProvider;
requiresApiKey: boolean;
};
export type WebSearchStatus = {
selected: WebSearchProviderDefinition;
configPath: string;
perplexityConfigured: boolean;
geminiApiConfigured: boolean;
chromeProfile?: string;
browserHint: string;
};
export const FEYNMAN_CONFIG_PATH = getFeynmanConfigPath();
export const LEGACY_WEB_SEARCH_CONFIG_PATH = resolve(process.env.HOME ?? "", ".pi", "web-search.json");
export const DEFAULT_WEB_SEARCH_PROVIDER: WebSearchProviderId = "gemini-browser";
export const WEB_SEARCH_PROVIDERS: ReadonlyArray<WebSearchProviderDefinition> = [
{
id: "auto",
label: "Auto",
description: "Prefer Perplexity when configured, otherwise fall back to Gemini.",
runtimeProvider: "auto",
requiresApiKey: false,
},
{
id: "perplexity",
label: "Perplexity API",
description: "Use Perplexity Sonar directly for web answers and source lists.",
runtimeProvider: "perplexity",
requiresApiKey: true,
},
{
id: "gemini-api",
label: "Gemini API",
description: "Use Gemini directly with an API key.",
runtimeProvider: "gemini",
requiresApiKey: true,
},
{
id: "gemini-browser",
label: "Gemini Browser",
description: "Use your signed-in Chromium browser session through pi-web-access.",
runtimeProvider: "gemini",
requiresApiKey: false,
},
] as const;
function readJsonFile<T>(path: string): T | undefined {
if (!existsSync(path)) {
return undefined;
}
try {
return JSON.parse(readFileSync(path, "utf8")) as T;
} catch {
return undefined;
}
}
function normalizeWebSearchConfig(value: unknown): WebSearchConfig | undefined {
if (!value || typeof value !== "object") {
return undefined;
}
return { ...(value as WebSearchConfig) };
}
function migrateLegacyWebSearchConfig(): WebSearchConfig | undefined {
return normalizeWebSearchConfig(readJsonFile<WebSearchConfig>(LEGACY_WEB_SEARCH_CONFIG_PATH));
}
export function loadFeynmanConfig(configPath = FEYNMAN_CONFIG_PATH): FeynmanConfig {
const config = readJsonFile<FeynmanConfig>(configPath);
if (config && typeof config === "object") {
return {
version: 1,
sessionDir: typeof config.sessionDir === "string" && config.sessionDir.trim() ? config.sessionDir : undefined,
webSearch: normalizeWebSearchConfig(config.webSearch),
preview: config.preview && typeof config.preview === "object" ? { ...config.preview } : undefined,
};
}
const legacyWebSearch = migrateLegacyWebSearchConfig();
return {
version: 1,
sessionDir: getDefaultSessionDir(),
webSearch: legacyWebSearch,
};
}
export function saveFeynmanConfig(config: FeynmanConfig, configPath = FEYNMAN_CONFIG_PATH): void {
mkdirSync(dirname(configPath), { recursive: true });
writeFileSync(
configPath,
JSON.stringify(
{
version: 1,
...(config.sessionDir ? { sessionDir: config.sessionDir } : {}),
...(config.webSearch ? { webSearch: config.webSearch } : {}),
...(config.preview ? { preview: config.preview } : {}),
},
null,
2,
) + "\n",
"utf8",
);
}
export function getConfiguredSessionDir(config = loadFeynmanConfig()): string {
return typeof config.sessionDir === "string" && config.sessionDir.trim()
? config.sessionDir
: getDefaultSessionDir();
}
export function loadWebSearchConfig(): WebSearchConfig {
return loadFeynmanConfig().webSearch ?? {};
}
export function saveWebSearchConfig(config: WebSearchConfig): void {
const current = loadFeynmanConfig();
saveFeynmanConfig({
...current,
webSearch: config,
});
}
export function getWebSearchProviderById(id: WebSearchProviderId): WebSearchProviderDefinition {
return WEB_SEARCH_PROVIDERS.find((provider) => provider.id === id) ?? WEB_SEARCH_PROVIDERS[0];
}
export function hasPerplexityApiKey(config: WebSearchConfig = loadWebSearchConfig()): boolean {
return typeof config.perplexityApiKey === "string" && config.perplexityApiKey.trim().length > 0;
}
export function hasGeminiApiKey(config: WebSearchConfig = loadWebSearchConfig()): boolean {
return typeof config.geminiApiKey === "string" && config.geminiApiKey.trim().length > 0;
}
export function hasConfiguredWebProvider(config: WebSearchConfig = loadWebSearchConfig()): boolean {
return hasPerplexityApiKey(config) || hasGeminiApiKey(config) || getConfiguredWebSearchProvider(config).id === DEFAULT_WEB_SEARCH_PROVIDER;
}
export function getConfiguredWebSearchProvider(
config: WebSearchConfig = loadWebSearchConfig(),
): WebSearchProviderDefinition {
const explicit = config.feynmanWebProvider;
if (explicit === "auto" || explicit === "perplexity" || explicit === "gemini-api" || explicit === "gemini-browser") {
return getWebSearchProviderById(explicit);
}
if (config.provider === "perplexity") {
return getWebSearchProviderById("perplexity");
}
if (config.provider === "gemini") {
return hasGeminiApiKey(config)
? getWebSearchProviderById("gemini-api")
: getWebSearchProviderById("gemini-browser");
}
return getWebSearchProviderById(DEFAULT_WEB_SEARCH_PROVIDER);
}
export function configureWebSearchProvider(
current: WebSearchConfig,
providerId: WebSearchProviderId,
values: { apiKey?: string; chromeProfile?: string } = {},
): WebSearchConfig {
const next: WebSearchConfig = { ...current };
next.feynmanWebProvider = providerId;
switch (providerId) {
case "auto":
next.provider = "auto";
if (typeof values.chromeProfile === "string" && values.chromeProfile.trim()) {
next.chromeProfile = values.chromeProfile.trim();
}
return next;
case "perplexity":
next.provider = "perplexity";
if (typeof values.apiKey === "string" && values.apiKey.trim()) {
next.perplexityApiKey = values.apiKey.trim();
}
if (typeof values.chromeProfile === "string" && values.chromeProfile.trim()) {
next.chromeProfile = values.chromeProfile.trim();
}
return next;
case "gemini-api":
next.provider = "gemini";
if (typeof values.apiKey === "string" && values.apiKey.trim()) {
next.geminiApiKey = values.apiKey.trim();
}
if (typeof values.chromeProfile === "string" && values.chromeProfile.trim()) {
next.chromeProfile = values.chromeProfile.trim();
}
return next;
case "gemini-browser":
next.provider = "gemini";
delete next.geminiApiKey;
if (typeof values.chromeProfile === "string") {
const profile = values.chromeProfile.trim();
if (profile) {
next.chromeProfile = profile;
} else {
delete next.chromeProfile;
}
}
return next;
}
}
export function getWebSearchStatus(config: WebSearchConfig = loadWebSearchConfig()): WebSearchStatus {
const selected = getConfiguredWebSearchProvider(config);
return {
selected,
configPath: FEYNMAN_CONFIG_PATH,
perplexityConfigured: hasPerplexityApiKey(config),
geminiApiConfigured: hasGeminiApiKey(config),
chromeProfile: typeof config.chromeProfile === "string" && config.chromeProfile.trim()
? config.chromeProfile.trim()
: undefined,
browserHint: selected.id === "gemini-browser" ? "selected" : "fallback only",
};
}
export function formatWebSearchDoctorLines(config: WebSearchConfig = loadWebSearchConfig()): string[] {
const status = getWebSearchStatus(config);
const configured = [];
if (status.perplexityConfigured) configured.push("Perplexity API");
if (status.geminiApiConfigured) configured.push("Gemini API");
if (status.selected.id === "gemini-browser" || status.chromeProfile) configured.push("Gemini Browser");
return [
`web research provider: ${status.selected.label}`,
` runtime route: ${status.selected.runtimeProvider}`,
` configured credentials: ${configured.length > 0 ? configured.join(", ") : "none"}`,
` browser mode: ${status.browserHint}${status.chromeProfile ? ` (profile: ${status.chromeProfile})` : ""}`,
` config path: ${status.configPath}`,
];
}

View File

@@ -22,10 +22,6 @@ export function getDefaultSessionDir(home = getFeynmanHome()): string {
return resolve(home, "sessions"); return resolve(home, "sessions");
} }
export function getFeynmanConfigPath(home = getFeynmanHome()): string {
return resolve(home, "config.json");
}
export function getBootstrapStatePath(home = getFeynmanHome()): string { export function getBootstrapStatePath(home = getFeynmanHome()): string {
return resolve(getFeynmanStateDir(home), "bootstrap.json"); return resolve(getFeynmanStateDir(home), "bootstrap.json");
} }

View File

@@ -6,21 +6,11 @@ import { promptChoice, promptText } from "../setup/prompts.js";
import { printInfo, printSection, printSuccess, printWarning } from "../ui/terminal.js"; import { printInfo, printSection, printSuccess, printWarning } from "../ui/terminal.js";
import { import {
buildModelStatusSnapshotFromRecords, buildModelStatusSnapshotFromRecords,
chooseRecommendedModel,
getAvailableModelRecords, getAvailableModelRecords,
getSupportedModelRecords, getSupportedModelRecords,
type ModelStatusSnapshot, type ModelStatusSnapshot,
} from "./catalog.js"; } from "./catalog.js";
function formatProviderSummaryLine(status: ModelStatusSnapshot["providers"][number]): string {
const state = status.configured ? `${status.availableModels} authenticated` : "not authenticated";
const flags = [
status.current ? "current" : undefined,
status.recommended ? "recommended" : undefined,
].filter(Boolean);
return `${status.label}: ${state}, ${status.supportedModels} supported${flags.length > 0 ? ` (${flags.join(", ")})` : ""}`;
}
function collectModelStatus(settingsPath: string, authPath: string): ModelStatusSnapshot { function collectModelStatus(settingsPath: string, authPath: string): ModelStatusSnapshot {
return buildModelStatusSnapshotFromRecords( return buildModelStatusSnapshotFromRecords(
getSupportedModelRecords(authPath), getSupportedModelRecords(authPath),
@@ -94,50 +84,6 @@ export function getCurrentModelSpec(settingsPath: string): string | undefined {
return undefined; return undefined;
} }
export function printModelStatus(settingsPath: string, authPath: string): void {
const status = collectModelStatus(settingsPath, authPath);
printInfo(`Current default model: ${status.current ?? "not set"}`);
printInfo(`Current default valid: ${status.currentValid ? "yes" : "no"}`);
printInfo(`Authenticated models: ${status.availableModels.length}`);
printInfo(`Providers with auth: ${status.providers.filter((provider) => provider.configured).length}`);
printInfo(`Research recommendation: ${status.recommended ?? "none available"}`);
if (status.recommendationReason) {
printInfo(`Recommendation reason: ${status.recommendationReason}`);
}
if (status.providers.length > 0) {
printSection("Providers");
for (const provider of status.providers) {
printInfo(formatProviderSummaryLine(provider));
}
}
if (status.guidance.length > 0) {
printSection("Next Steps");
for (const line of status.guidance) {
printWarning(line);
}
}
}
export function printModelProviders(settingsPath: string, authPath: string): void {
const status = collectModelStatus(settingsPath, authPath);
for (const provider of status.providers) {
printInfo(formatProviderSummaryLine(provider));
}
const oauthProviders = getOAuthProviders(authPath);
if (oauthProviders.length > 0) {
printSection("OAuth Login");
for (const provider of oauthProviders) {
printInfo(`${provider.id}${provider.name ?? provider.id}`);
}
}
if (status.providers.length === 0) {
printWarning("No Pi model providers are visible in the current runtime.");
}
}
export function printModelList(settingsPath: string, authPath: string): void { export function printModelList(settingsPath: string, authPath: string): void {
const status = collectModelStatus(settingsPath, authPath); const status = collectModelStatus(settingsPath, authPath);
if (status.availableModels.length === 0) { if (status.availableModels.length === 0) {
@@ -163,18 +109,6 @@ export function printModelList(settingsPath: string, authPath: string): void {
} }
} }
export function printModelRecommendation(authPath: string): void {
const recommendation = chooseRecommendedModel(authPath);
if (!recommendation) {
printWarning("No authenticated Pi models are available to recommend.");
printInfo("Run `feynman model login <provider>` or add provider credentials that Pi can see, then rerun this command.");
return;
}
printSuccess(`Recommended model: ${recommendation.spec}`);
printInfo(recommendation.reason);
}
export async function loginModelProvider(authPath: string, providerId?: string): Promise<void> { export async function loginModelProvider(authPath: string, providerId?: string): Promise<void> {
const provider = providerId ? resolveOAuthProvider(authPath, providerId) : await selectOAuthProvider(authPath, "login"); const provider = providerId ? resolveOAuthProvider(authPath, providerId) : await selectOAuthProvider(authPath, "login");
if (!provider) { if (!provider) {

View File

@@ -1,4 +1,4 @@
import { existsSync } from "node:fs"; import { existsSync, readFileSync } from "node:fs";
import { dirname, resolve } from "node:path"; import { dirname, resolve } from "node:path";
import { import {
@@ -18,7 +18,6 @@ export type PiRuntimeOptions = {
explicitModelSpec?: string; explicitModelSpec?: string;
oneShotPrompt?: string; oneShotPrompt?: string;
initialPrompt?: string; initialPrompt?: string;
systemPrompt: string;
}; };
export function resolvePiPaths(appRoot: string) { export function resolvePiPaths(appRoot: string) {
@@ -27,8 +26,8 @@ export function resolvePiPaths(appRoot: string) {
piCliPath: resolve(appRoot, "node_modules", "@mariozechner", "pi-coding-agent", "dist", "cli.js"), piCliPath: resolve(appRoot, "node_modules", "@mariozechner", "pi-coding-agent", "dist", "cli.js"),
promisePolyfillPath: resolve(appRoot, "dist", "system", "promise-polyfill.js"), promisePolyfillPath: resolve(appRoot, "dist", "system", "promise-polyfill.js"),
researchToolsPath: resolve(appRoot, "extensions", "research-tools.ts"), researchToolsPath: resolve(appRoot, "extensions", "research-tools.ts"),
skillsPath: resolve(appRoot, "skills"),
promptTemplatePath: resolve(appRoot, "prompts"), promptTemplatePath: resolve(appRoot, "prompts"),
systemPromptPath: resolve(appRoot, ".pi", "SYSTEM.md"),
piWorkspaceNodeModulesPath: resolve(appRoot, ".pi", "npm", "node_modules"), piWorkspaceNodeModulesPath: resolve(appRoot, ".pi", "npm", "node_modules"),
}; };
} }
@@ -40,7 +39,6 @@ export function validatePiInstallation(appRoot: string): string[] {
if (!existsSync(paths.piCliPath)) missing.push(paths.piCliPath); if (!existsSync(paths.piCliPath)) missing.push(paths.piCliPath);
if (!existsSync(paths.promisePolyfillPath)) missing.push(paths.promisePolyfillPath); if (!existsSync(paths.promisePolyfillPath)) missing.push(paths.promisePolyfillPath);
if (!existsSync(paths.researchToolsPath)) missing.push(paths.researchToolsPath); if (!existsSync(paths.researchToolsPath)) missing.push(paths.researchToolsPath);
if (!existsSync(paths.skillsPath)) missing.push(paths.skillsPath);
if (!existsSync(paths.promptTemplatePath)) missing.push(paths.promptTemplatePath); if (!existsSync(paths.promptTemplatePath)) missing.push(paths.promptTemplatePath);
return missing; return missing;
@@ -53,14 +51,14 @@ export function buildPiArgs(options: PiRuntimeOptions): string[] {
options.sessionDir, options.sessionDir,
"--extension", "--extension",
paths.researchToolsPath, paths.researchToolsPath,
"--skill",
paths.skillsPath,
"--prompt-template", "--prompt-template",
paths.promptTemplatePath, paths.promptTemplatePath,
"--system-prompt",
options.systemPrompt,
]; ];
if (existsSync(paths.systemPromptPath)) {
args.push("--system-prompt", readFileSync(paths.systemPromptPath, "utf8"));
}
if (options.explicitModelSpec) { if (options.explicitModelSpec) {
args.push("--model", options.explicitModelSpec); args.push("--model", options.explicitModelSpec);
} }
@@ -81,16 +79,13 @@ export function buildPiEnv(options: PiRuntimeOptions): NodeJS.ProcessEnv {
return { return {
...process.env, ...process.env,
PI_CODING_AGENT_DIR: options.feynmanAgentDir,
FEYNMAN_CODING_AGENT_DIR: options.feynmanAgentDir,
FEYNMAN_VERSION: options.feynmanVersion, FEYNMAN_VERSION: options.feynmanVersion,
FEYNMAN_PI_NPM_ROOT: paths.piWorkspaceNodeModulesPath,
FEYNMAN_SESSION_DIR: options.sessionDir, FEYNMAN_SESSION_DIR: options.sessionDir,
PI_SESSION_DIR: options.sessionDir,
FEYNMAN_MEMORY_DIR: resolve(dirname(options.feynmanAgentDir), "memory"), FEYNMAN_MEMORY_DIR: resolve(dirname(options.feynmanAgentDir), "memory"),
FEYNMAN_NODE_EXECUTABLE: process.execPath, FEYNMAN_NODE_EXECUTABLE: process.execPath,
FEYNMAN_BIN_PATH: resolve(options.appRoot, "bin", "feynman.js"), FEYNMAN_BIN_PATH: resolve(options.appRoot, "bin", "feynman.js"),
PANDOC_PATH: process.env.PANDOC_PATH ?? resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS), PANDOC_PATH: process.env.PANDOC_PATH ?? resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS),
PI_HARDWARE_CURSOR: process.env.PI_HARDWARE_CURSOR ?? "1",
PI_SKIP_VERSION_CHECK: process.env.PI_SKIP_VERSION_CHECK ?? "1", PI_SKIP_VERSION_CHECK: process.env.PI_SKIP_VERSION_CHECK ?? "1",
MERMAID_CLI_PATH: process.env.MERMAID_CLI_PATH ?? resolveExecutable("mmdc", MERMAID_FALLBACK_PATHS), MERMAID_CLI_PATH: process.env.MERMAID_CLI_PATH ?? resolveExecutable("mmdc", MERMAID_FALLBACK_PATHS),
PUPPETEER_EXECUTABLE_PATH: PUPPETEER_EXECUTABLE_PATH:

109
src/pi/web-access.ts Normal file
View File

@@ -0,0 +1,109 @@
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { resolve } from "node:path";
export type PiWebSearchProvider = "auto" | "perplexity" | "gemini";
export type PiWebAccessConfig = Record<string, unknown> & {
provider?: PiWebSearchProvider;
searchProvider?: PiWebSearchProvider;
perplexityApiKey?: string;
geminiApiKey?: string;
chromeProfile?: string;
};
export type PiWebAccessStatus = {
configPath: string;
searchProvider: PiWebSearchProvider;
requestProvider: PiWebSearchProvider;
perplexityConfigured: boolean;
geminiApiConfigured: boolean;
chromeProfile?: string;
routeLabel: string;
note: string;
};
export function getPiWebSearchConfigPath(home = process.env.HOME ?? homedir()): string {
return resolve(home, ".pi", "web-search.json");
}
function normalizeProvider(value: unknown): PiWebSearchProvider | undefined {
return value === "auto" || value === "perplexity" || value === "gemini" ? value : undefined;
}
function normalizeNonEmptyString(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
}
export function loadPiWebAccessConfig(configPath = getPiWebSearchConfigPath()): PiWebAccessConfig {
if (!existsSync(configPath)) {
return {};
}
try {
const parsed = JSON.parse(readFileSync(configPath, "utf8")) as PiWebAccessConfig;
return parsed && typeof parsed === "object" ? parsed : {};
} catch {
return {};
}
}
function formatRouteLabel(provider: PiWebSearchProvider): string {
switch (provider) {
case "perplexity":
return "Perplexity";
case "gemini":
return "Gemini";
default:
return "Auto";
}
}
function formatRouteNote(provider: PiWebSearchProvider): string {
switch (provider) {
case "perplexity":
return "Pi web-access will use Perplexity for search.";
case "gemini":
return "Pi web-access will use Gemini API or Gemini Browser.";
default:
return "Pi web-access will try Perplexity, then Gemini API, then Gemini Browser.";
}
}
export function getPiWebAccessStatus(
config: PiWebAccessConfig = loadPiWebAccessConfig(),
configPath = getPiWebSearchConfigPath(),
): PiWebAccessStatus {
const searchProvider = normalizeProvider(config.searchProvider) ?? "auto";
const requestProvider = normalizeProvider(config.provider) ?? searchProvider;
const perplexityConfigured = Boolean(normalizeNonEmptyString(config.perplexityApiKey));
const geminiApiConfigured = Boolean(normalizeNonEmptyString(config.geminiApiKey));
const chromeProfile = normalizeNonEmptyString(config.chromeProfile);
const effectiveProvider = searchProvider;
return {
configPath,
searchProvider,
requestProvider,
perplexityConfigured,
geminiApiConfigured,
chromeProfile,
routeLabel: formatRouteLabel(effectiveProvider),
note: formatRouteNote(effectiveProvider),
};
}
export function formatPiWebAccessDoctorLines(
status: PiWebAccessStatus = getPiWebAccessStatus(),
): string[] {
return [
"web access: pi-web-access",
` search route: ${status.routeLabel}`,
` request route: ${status.requestProvider}`,
` perplexity api: ${status.perplexityConfigured ? "configured" : "not configured"}`,
` gemini api: ${status.geminiApiConfigured ? "configured" : "not configured"}`,
` browser profile: ${status.chromeProfile ?? "default Chromium profile"}`,
` config path: ${status.configPath}`,
` note: ${status.note}`,
];
}

View File

@@ -1,49 +1,13 @@
import { import { getPiWebAccessStatus } from "../pi/web-access.js";
DEFAULT_WEB_SEARCH_PROVIDER, import { printInfo } from "../ui/terminal.js";
WEB_SEARCH_PROVIDERS,
configureWebSearchProvider,
getWebSearchStatus,
loadFeynmanConfig,
saveFeynmanConfig,
type WebSearchProviderId,
} from "../config/feynman-config.js";
import { printInfo, printSuccess } from "../ui/terminal.js";
export function printSearchStatus(): void { export function printSearchStatus(): void {
const status = getWebSearchStatus(loadFeynmanConfig().webSearch ?? {}); const status = getPiWebAccessStatus();
printInfo(`Provider: ${status.selected.label}`); printInfo("Managed by: pi-web-access");
printInfo(`Runtime route: ${status.selected.runtimeProvider}`); printInfo(`Search route: ${status.routeLabel}`);
printInfo(`Request route: ${status.requestProvider}`);
printInfo(`Perplexity API configured: ${status.perplexityConfigured ? "yes" : "no"}`); printInfo(`Perplexity API configured: ${status.perplexityConfigured ? "yes" : "no"}`);
printInfo(`Gemini API configured: ${status.geminiApiConfigured ? "yes" : "no"}`); printInfo(`Gemini API configured: ${status.geminiApiConfigured ? "yes" : "no"}`);
printInfo(`Browser mode: ${status.browserHint}${status.chromeProfile ? ` (${status.chromeProfile})` : ""}`); printInfo(`Browser profile: ${status.chromeProfile ?? "default Chromium profile"}`);
} printInfo(`Config path: ${status.configPath}`);
export function printSearchProviders(): void {
for (const provider of WEB_SEARCH_PROVIDERS) {
const marker = provider.id === DEFAULT_WEB_SEARCH_PROVIDER ? " (default)" : "";
printInfo(`${provider.id}${provider.label}${marker}: ${provider.description}`);
}
}
export function setSearchProvider(providerId: string, value?: string): void {
if (!WEB_SEARCH_PROVIDERS.some((provider) => provider.id === providerId)) {
throw new Error(`Unknown search provider: ${providerId}`);
}
const config = loadFeynmanConfig();
const nextWebSearch = configureWebSearchProvider(
config.webSearch ?? {},
providerId as WebSearchProviderId,
providerId === "gemini-browser"
? { chromeProfile: value }
: providerId === "perplexity" || providerId === "gemini-api"
? { apiKey: value }
: {},
);
saveFeynmanConfig({
...config,
webSearch: nextWebSearch,
});
printSuccess(`Search provider set to ${providerId}`);
} }

View File

@@ -1,12 +1,7 @@
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent"; import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
import { getUserName as getAlphaUserName, isLoggedIn as isAlphaLoggedIn } from "@companion-ai/alpha-hub/lib"; import { getUserName as getAlphaUserName, isLoggedIn as isAlphaLoggedIn } from "@companion-ai/alpha-hub/lib";
import { import { formatPiWebAccessDoctorLines, getPiWebAccessStatus } from "../pi/web-access.js";
FEYNMAN_CONFIG_PATH,
formatWebSearchDoctorLines,
getWebSearchStatus,
loadFeynmanConfig,
} from "../config/feynman-config.js";
import { BROWSER_FALLBACK_PATHS, PANDOC_FALLBACK_PATHS, resolveExecutable } from "../system/executables.js"; import { BROWSER_FALLBACK_PATHS, PANDOC_FALLBACK_PATHS, resolveExecutable } from "../system/executables.js";
import { readJson } from "../pi/settings.js"; import { readJson } from "../pi/settings.js";
import { validatePiInstallation } from "../pi/runtime.js"; import { validatePiInstallation } from "../pi/runtime.js";
@@ -32,11 +27,9 @@ export type FeynmanStatusSnapshot = {
modelGuidance: string[]; modelGuidance: string[];
alphaLoggedIn: boolean; alphaLoggedIn: boolean;
alphaUser?: string; alphaUser?: string;
webProviderLabel: string; webRouteLabel: string;
webConfigured: boolean;
previewConfigured: boolean; previewConfigured: boolean;
sessionDir: string; sessionDir: string;
configPath: string;
pandocReady: boolean; pandocReady: boolean;
browserReady: boolean; browserReady: boolean;
piReady: boolean; piReady: boolean;
@@ -44,11 +37,10 @@ export type FeynmanStatusSnapshot = {
}; };
export function collectStatusSnapshot(options: DoctorOptions): FeynmanStatusSnapshot { export function collectStatusSnapshot(options: DoctorOptions): FeynmanStatusSnapshot {
const config = loadFeynmanConfig();
const pandocPath = resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS); const pandocPath = resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS);
const browserPath = process.env.PUPPETEER_EXECUTABLE_PATH ?? resolveExecutable("google-chrome", BROWSER_FALLBACK_PATHS); const browserPath = process.env.PUPPETEER_EXECUTABLE_PATH ?? resolveExecutable("google-chrome", BROWSER_FALLBACK_PATHS);
const missingPiBits = validatePiInstallation(options.appRoot); const missingPiBits = validatePiInstallation(options.appRoot);
const webStatus = getWebSearchStatus(config.webSearch ?? {}); const webStatus = getPiWebAccessStatus();
const modelStatus = buildModelStatusSnapshotFromRecords( const modelStatus = buildModelStatusSnapshotFromRecords(
getSupportedModelRecords(options.authPath), getSupportedModelRecords(options.authPath),
getAvailableModelRecords(options.authPath), getAvailableModelRecords(options.authPath),
@@ -65,11 +57,9 @@ export function collectStatusSnapshot(options: DoctorOptions): FeynmanStatusSnap
modelGuidance: modelStatus.guidance, modelGuidance: modelStatus.guidance,
alphaLoggedIn: isAlphaLoggedIn(), alphaLoggedIn: isAlphaLoggedIn(),
alphaUser: isAlphaLoggedIn() ? getAlphaUserName() ?? undefined : undefined, alphaUser: isAlphaLoggedIn() ? getAlphaUserName() ?? undefined : undefined,
webProviderLabel: webStatus.selected.label, webRouteLabel: webStatus.routeLabel,
webConfigured: webStatus.perplexityConfigured || webStatus.geminiApiConfigured || webStatus.selected.id === "gemini-browser", previewConfigured: Boolean(pandocPath),
previewConfigured: Boolean(config.preview?.lastSetupAt),
sessionDir: options.sessionDir, sessionDir: options.sessionDir,
configPath: FEYNMAN_CONFIG_PATH,
pandocReady: Boolean(pandocPath), pandocReady: Boolean(pandocPath),
browserReady: Boolean(browserPath), browserReady: Boolean(browserPath),
piReady: missingPiBits.length === 0, piReady: missingPiBits.length === 0,
@@ -89,11 +79,10 @@ export function runStatus(options: DoctorOptions): void {
printInfo(`Authenticated providers: ${snapshot.authenticatedProviderCount}`); printInfo(`Authenticated providers: ${snapshot.authenticatedProviderCount}`);
printInfo(`Recommended model: ${snapshot.recommendedModel ?? "not available"}`); printInfo(`Recommended model: ${snapshot.recommendedModel ?? "not available"}`);
printInfo(`alphaXiv: ${snapshot.alphaLoggedIn ? snapshot.alphaUser ?? "configured" : "not configured"}`); printInfo(`alphaXiv: ${snapshot.alphaLoggedIn ? snapshot.alphaUser ?? "configured" : "not configured"}`);
printInfo(`Web research: ${snapshot.webConfigured ? snapshot.webProviderLabel : "not configured"}`); printInfo(`Web access: pi-web-access (${snapshot.webRouteLabel})`);
printInfo(`Preview: ${snapshot.previewConfigured ? "configured" : "not configured"}`); printInfo(`Preview: ${snapshot.previewConfigured ? "configured" : "not configured"}`);
printSection("Paths"); printSection("Paths");
printInfo(`Config: ${snapshot.configPath}`);
printInfo(`Sessions: ${snapshot.sessionDir}`); printInfo(`Sessions: ${snapshot.sessionDir}`);
printSection("Runtime"); printSection("Runtime");
@@ -115,7 +104,6 @@ export function runStatus(options: DoctorOptions): void {
export function runDoctor(options: DoctorOptions): void { export function runDoctor(options: DoctorOptions): void {
const settings = readJson(options.settingsPath); const settings = readJson(options.settingsPath);
const config = loadFeynmanConfig();
const modelRegistry = new ModelRegistry(AuthStorage.create(options.authPath)); const modelRegistry = new ModelRegistry(AuthStorage.create(options.authPath));
const availableModels = modelRegistry.getAvailable(); const availableModels = modelRegistry.getAvailable();
const pandocPath = resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS); const pandocPath = resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS);
@@ -127,7 +115,6 @@ export function runDoctor(options: DoctorOptions): void {
]); ]);
console.log(`working dir: ${options.workingDir}`); console.log(`working dir: ${options.workingDir}`);
console.log(`session dir: ${options.sessionDir}`); console.log(`session dir: ${options.sessionDir}`);
console.log(`config path: ${FEYNMAN_CONFIG_PATH}`);
console.log(""); console.log("");
console.log(`alphaXiv auth: ${isAlphaLoggedIn() ? "ok" : "missing"}`); console.log(`alphaXiv auth: ${isAlphaLoggedIn() ? "ok" : "missing"}`);
if (isAlphaLoggedIn()) { if (isAlphaLoggedIn()) {
@@ -159,8 +146,7 @@ export function runDoctor(options: DoctorOptions): void {
} }
console.log(`pandoc: ${pandocPath ?? "missing"}`); console.log(`pandoc: ${pandocPath ?? "missing"}`);
console.log(`browser preview runtime: ${browserPath ?? "missing"}`); console.log(`browser preview runtime: ${browserPath ?? "missing"}`);
console.log(`configured session dir: ${config.sessionDir ?? "not set"}`); for (const line of formatPiWebAccessDoctorLines()) {
for (const line of formatWebSearchDoctorLines(config.webSearch ?? {})) {
console.log(line); console.log(line);
} }
console.log(`quiet startup: ${settings.quietStartup === true ? "enabled" : "disabled"}`); console.log(`quiet startup: ${settings.quietStartup === true ? "enabled" : "disabled"}`);

View File

@@ -1,28 +1,18 @@
import { isLoggedIn as isAlphaLoggedIn, login as loginAlpha } from "@companion-ai/alpha-hub/lib"; import { isLoggedIn as isAlphaLoggedIn, login as loginAlpha } from "@companion-ai/alpha-hub/lib";
import { import { getDefaultSessionDir, getFeynmanHome } from "../config/paths.js";
DEFAULT_WEB_SEARCH_PROVIDER, import { getPiWebAccessStatus, getPiWebSearchConfigPath } from "../pi/web-access.js";
FEYNMAN_CONFIG_PATH,
WEB_SEARCH_PROVIDERS,
configureWebSearchProvider,
getConfiguredWebSearchProvider,
getWebSearchStatus,
hasConfiguredWebProvider,
loadFeynmanConfig,
saveFeynmanConfig,
} from "../config/feynman-config.js";
import { getFeynmanHome } from "../config/paths.js";
import { normalizeFeynmanSettings } from "../pi/settings.js"; import { normalizeFeynmanSettings } from "../pi/settings.js";
import type { ThinkingLevel } from "../pi/settings.js"; import type { ThinkingLevel } from "../pi/settings.js";
import { getCurrentModelSpec, runModelSetup } from "../model/commands.js"; import { getCurrentModelSpec, runModelSetup } from "../model/commands.js";
import { buildModelStatusSnapshotFromRecords, getAvailableModelRecords, getSupportedModelRecords } from "../model/catalog.js"; import { buildModelStatusSnapshotFromRecords, getAvailableModelRecords, getSupportedModelRecords } from "../model/catalog.js";
import { promptChoice, promptText } from "./prompts.js"; import { PANDOC_FALLBACK_PATHS, resolveExecutable } from "../system/executables.js";
import { promptText } from "./prompts.js";
import { setupPreviewDependencies } from "./preview.js"; import { setupPreviewDependencies } from "./preview.js";
import { runDoctor } from "./doctor.js"; import { runDoctor } from "./doctor.js";
import { printInfo, printPanel, printSection, printSuccess } from "../ui/terminal.js"; import { printInfo, printPanel, printSection, printSuccess } from "../ui/terminal.js";
type SetupOptions = { type SetupOptions = {
section: string | undefined;
settingsPath: string; settingsPath: string;
bundledSettingsPath: string; bundledSettingsPath: string;
authPath: string; authPath: string;
@@ -32,62 +22,18 @@ type SetupOptions = {
defaultThinkingLevel?: ThinkingLevel; defaultThinkingLevel?: ThinkingLevel;
}; };
async function setupWebProvider(): Promise<void> { async function explainWebAccess(): Promise<void> {
const config = loadFeynmanConfig(); const status = getPiWebAccessStatus();
const current = getConfiguredWebSearchProvider(config.webSearch ?? {}); printSection("Web Access");
const preferredSelectionId = config.webSearch?.feynmanWebProvider ?? DEFAULT_WEB_SEARCH_PROVIDER; printInfo("Feynman uses the bundled `pi-web-access` package directly.");
const choices = [ printInfo("Default v1 path: sign into gemini.google.com in a supported Chromium browser.");
...WEB_SEARCH_PROVIDERS.map((provider) => `${provider.label}${provider.description}`), printInfo(`Current search route: ${status.routeLabel}`);
"Skip", printInfo(`Pi config path: ${status.configPath}`);
]; printInfo("Advanced users can edit the Pi config directly if they want API keys or a different route.");
const defaultIndex = WEB_SEARCH_PROVIDERS.findIndex((provider) => provider.id === preferredSelectionId);
const selection = await promptChoice(
"Choose a web search provider for Feynman:",
choices,
defaultIndex >= 0 ? defaultIndex : 0,
);
if (selection === WEB_SEARCH_PROVIDERS.length) {
return;
}
const selected = WEB_SEARCH_PROVIDERS[selection] ?? WEB_SEARCH_PROVIDERS[0];
let nextWebConfig = { ...(config.webSearch ?? {}) };
if (selected.id === "perplexity") {
const key = await promptText(
"Perplexity API key",
typeof nextWebConfig.perplexityApiKey === "string" ? nextWebConfig.perplexityApiKey : "",
);
nextWebConfig = configureWebSearchProvider(nextWebConfig, selected.id, { apiKey: key });
} else if (selected.id === "gemini-api") {
const key = await promptText(
"Gemini API key",
typeof nextWebConfig.geminiApiKey === "string" ? nextWebConfig.geminiApiKey : "",
);
nextWebConfig = configureWebSearchProvider(nextWebConfig, selected.id, { apiKey: key });
} else if (selected.id === "gemini-browser") {
const profile = await promptText(
"Chrome profile (optional)",
typeof nextWebConfig.chromeProfile === "string" ? nextWebConfig.chromeProfile : "",
);
nextWebConfig = configureWebSearchProvider(nextWebConfig, selected.id, { chromeProfile: profile });
} else {
nextWebConfig = configureWebSearchProvider(nextWebConfig, selected.id);
}
saveFeynmanConfig({
...config,
webSearch: nextWebConfig,
});
printSuccess(`Saved web search provider: ${selected.label}`);
if (selected.id === "gemini-browser") {
printInfo("Gemini Browser relies on a signed-in Chromium profile through pi-web-access.");
}
} }
function isPreviewConfigured() { function isPreviewConfigured() {
return Boolean(loadFeynmanConfig().preview?.lastSetupAt); return Boolean(resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS));
} }
function isInteractiveTerminal(): boolean { function isInteractiveTerminal(): boolean {
@@ -100,13 +46,10 @@ function printNonInteractiveSetupGuidance(): void {
]); ]);
printInfo("Use the explicit commands instead of the interactive setup wizard:"); printInfo("Use the explicit commands instead of the interactive setup wizard:");
printInfo(" feynman status"); printInfo(" feynman status");
printInfo(" feynman model providers");
printInfo(" feynman model login <provider>"); printInfo(" feynman model login <provider>");
printInfo(" feynman model list");
printInfo(" feynman model recommend");
printInfo(" feynman model set <provider/model>"); printInfo(" feynman model set <provider/model>");
printInfo(" feynman search providers"); printInfo(" feynman search status");
printInfo(" feynman search set <provider> [value]"); printInfo(` edit ${getPiWebSearchConfigPath()} # optional advanced web config`);
printInfo(" feynman alpha login"); printInfo(" feynman alpha login");
printInfo(" feynman doctor"); printInfo(" feynman doctor");
printInfo(" feynman # Pi's /login flow still works inside chat if you prefer it"); printInfo(" feynman # Pi's /login flow still works inside chat if you prefer it");
@@ -115,25 +58,16 @@ function printNonInteractiveSetupGuidance(): void {
async function runPreviewSetup(): Promise<void> { async function runPreviewSetup(): Promise<void> {
const result = setupPreviewDependencies(); const result = setupPreviewDependencies();
printSuccess(result.message); printSuccess(result.message);
saveFeynmanConfig({
...loadFeynmanConfig(),
preview: {
lastSetupAt: new Date().toISOString(),
},
});
} }
function printConfigurationLocation(appRoot: string): void { function printConfigurationLocation(appRoot: string): void {
printSection("Configuration Location"); printSection("Configuration Location");
printInfo(`Config file: ${FEYNMAN_CONFIG_PATH}`);
printInfo(`Data folder: ${getFeynmanHome()}`); printInfo(`Data folder: ${getFeynmanHome()}`);
printInfo(`Sessions: ${getDefaultSessionDir()}`);
printInfo(`Install dir: ${appRoot}`); printInfo(`Install dir: ${appRoot}`);
printInfo("You can edit config.json directly or use `feynman config` commands.");
} }
function printSetupSummary(settingsPath: string, authPath: string): void { function printSetupSummary(settingsPath: string, authPath: string): void {
const config = loadFeynmanConfig();
const webStatus = getWebSearchStatus(config.webSearch ?? {});
const modelStatus = buildModelStatusSnapshotFromRecords( const modelStatus = buildModelStatusSnapshotFromRecords(
getSupportedModelRecords(authPath), getSupportedModelRecords(authPath),
getAvailableModelRecords(authPath), getAvailableModelRecords(authPath),
@@ -144,46 +78,24 @@ function printSetupSummary(settingsPath: string, authPath: string): void {
printInfo(`Model valid: ${modelStatus.currentValid ? "yes" : "no"}`); printInfo(`Model valid: ${modelStatus.currentValid ? "yes" : "no"}`);
printInfo(`Recommended model: ${modelStatus.recommended ?? "not available"}`); printInfo(`Recommended model: ${modelStatus.recommended ?? "not available"}`);
printInfo(`alphaXiv: ${isAlphaLoggedIn() ? "configured" : "missing"}`); printInfo(`alphaXiv: ${isAlphaLoggedIn() ? "configured" : "missing"}`);
printInfo(`Web research: ${hasConfiguredWebProvider(config.webSearch ?? {}) ? webStatus.selected.label : "not configured"}`); printInfo(`Web access: pi-web-access (${getPiWebAccessStatus().routeLabel})`);
printInfo(`Preview: ${isPreviewConfigured() ? "configured" : "not configured"}`); printInfo(`Preview: ${isPreviewConfigured() ? "configured" : "not configured"}`);
for (const line of modelStatus.guidance) { for (const line of modelStatus.guidance) {
printInfo(line); printInfo(line);
} }
} }
async function runSetupSection(section: "model" | "alpha" | "web" | "preview", options: SetupOptions): Promise<void> { async function runFullSetup(options: SetupOptions): Promise<void> {
if (section === "model") { printConfigurationLocation(options.appRoot);
await runModelSetup(options.settingsPath, options.authPath); await runModelSetup(options.settingsPath, options.authPath);
return;
}
if (section === "alpha") {
if (!isAlphaLoggedIn()) { if (!isAlphaLoggedIn()) {
await loginAlpha(); await loginAlpha();
printSuccess("alphaXiv login complete"); printSuccess("alphaXiv login complete");
} else { } else {
printInfo("alphaXiv login already configured"); printInfo("alphaXiv login already configured");
} }
return; await explainWebAccess();
}
if (section === "web") {
await setupWebProvider();
return;
}
if (section === "preview") {
await runPreviewSetup(); await runPreviewSetup();
return;
}
}
async function runFullSetup(options: SetupOptions): Promise<void> {
printConfigurationLocation(options.appRoot);
await runSetupSection("model", options);
await runSetupSection("alpha", options);
await runSetupSection("web", options);
await runSetupSection("preview", options);
normalizeFeynmanSettings( normalizeFeynmanSettings(
options.settingsPath, options.settingsPath,
options.bundledSettingsPath, options.bundledSettingsPath,
@@ -200,49 +112,7 @@ async function runFullSetup(options: SetupOptions): Promise<void> {
printSetupSummary(options.settingsPath, options.authPath); printSetupSummary(options.settingsPath, options.authPath);
} }
async function runQuickSetup(options: SetupOptions): Promise<void> {
printSection("Quick Setup");
let changed = false;
const modelStatus = buildModelStatusSnapshotFromRecords(
getSupportedModelRecords(options.authPath),
getAvailableModelRecords(options.authPath),
getCurrentModelSpec(options.settingsPath),
);
if (!modelStatus.current || !modelStatus.currentValid) {
await runSetupSection("model", options);
changed = true;
}
if (!isAlphaLoggedIn()) {
await runSetupSection("alpha", options);
changed = true;
}
if (!hasConfiguredWebProvider(loadFeynmanConfig().webSearch ?? {})) {
await runSetupSection("web", options);
changed = true;
}
if (!isPreviewConfigured()) {
await runSetupSection("preview", options);
changed = true;
}
if (!changed) {
printSuccess("Everything already looks configured.");
printInfo("Run `feynman setup` and choose Full Setup if you want to reconfigure everything.");
return;
}
normalizeFeynmanSettings(
options.settingsPath,
options.bundledSettingsPath,
options.defaultThinkingLevel ?? "medium",
options.authPath,
);
printSetupSummary(options.settingsPath, options.authPath);
}
function hasExistingSetup(settingsPath: string, authPath: string): boolean { function hasExistingSetup(settingsPath: string, authPath: string): boolean {
const config = loadFeynmanConfig();
const modelStatus = buildModelStatusSnapshotFromRecords( const modelStatus = buildModelStatusSnapshotFromRecords(
getSupportedModelRecords(authPath), getSupportedModelRecords(authPath),
getAvailableModelRecords(authPath), getAvailableModelRecords(authPath),
@@ -252,8 +122,7 @@ function hasExistingSetup(settingsPath: string, authPath: string): boolean {
modelStatus.current || modelStatus.current ||
modelStatus.availableModels.length > 0 || modelStatus.availableModels.length > 0 ||
isAlphaLoggedIn() || isAlphaLoggedIn() ||
hasConfiguredWebProvider(config.webSearch ?? {}) || isPreviewConfigured(),
config.preview?.lastSetupAt,
); );
} }
@@ -267,13 +136,11 @@ async function runDefaultInteractiveSetup(options: SetupOptions): Promise<void>
if (existing) { if (existing) {
printSection("Full Setup"); printSection("Full Setup");
printInfo("Existing configuration detected. Rerunning the full guided setup."); printInfo("Existing configuration detected. Rerunning the full guided setup.");
printInfo("Use `feynman setup quick` if you only want to fill missing items.");
} else { } else {
printInfo("We'll walk you through:"); printInfo("We'll walk you through:");
printInfo(" 1. Model Selection"); printInfo(" 1. Model Selection");
printInfo(" 2. alphaXiv Login"); printInfo(" 2. alphaXiv Login");
printInfo(" 3. Web Research Provider"); printInfo(" 3. Preview Dependencies");
printInfo(" 4. Preview Dependencies");
} }
printInfo("Press Enter to begin, or Ctrl+C to exit."); printInfo("Press Enter to begin, or Ctrl+C to exit.");
await promptText("Press Enter to start"); await promptText("Press Enter to start");
@@ -286,31 +153,5 @@ export async function runSetup(options: SetupOptions): Promise<void> {
return; return;
} }
if (!options.section) {
await runDefaultInteractiveSetup(options); await runDefaultInteractiveSetup(options);
return;
}
if (options.section === "model") {
await runSetupSection("model", options);
return;
}
if (options.section === "alpha") {
await runSetupSection("alpha", options);
return;
}
if (options.section === "web") {
await runSetupSection("web", options);
return;
}
if (options.section === "preview") {
await runSetupSection("preview", options);
return;
}
if (options.section === "quick") {
await runQuickSetup(options);
return;
}
await runFullSetup(options);
} }

View File

@@ -1,19 +1,9 @@
export { export {
FEYNMAN_CONFIG_PATH as WEB_SEARCH_CONFIG_PATH, formatPiWebAccessDoctorLines,
WEB_SEARCH_PROVIDERS, getPiWebAccessStatus,
configureWebSearchProvider, getPiWebSearchConfigPath,
formatWebSearchDoctorLines, loadPiWebAccessConfig,
getConfiguredWebSearchProvider, type PiWebAccessConfig,
getWebSearchProviderById, type PiWebAccessStatus,
getWebSearchStatus,
hasConfiguredWebProvider,
hasGeminiApiKey,
hasPerplexityApiKey,
loadWebSearchConfig,
saveWebSearchConfig,
type PiWebSearchProvider, type PiWebSearchProvider,
type WebSearchConfig, } from "./pi/web-access.js";
type WebSearchProviderDefinition,
type WebSearchProviderId,
type WebSearchStatus,
} from "./config/feynman-config.js";

View File

@@ -1,60 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
configureWebSearchProvider,
getConfiguredWebSearchProvider,
loadFeynmanConfig,
saveFeynmanConfig,
} from "../src/config/feynman-config.js";
test("loadFeynmanConfig falls back to legacy web-search config", () => {
const root = mkdtempSync(join(tmpdir(), "feynman-config-"));
const configPath = join(root, "config.json");
const legacyDir = join(process.env.HOME ?? root, ".pi");
const legacyPath = join(legacyDir, "web-search.json");
mkdirSync(legacyDir, { recursive: true });
writeFileSync(
legacyPath,
JSON.stringify({
feynmanWebProvider: "perplexity",
perplexityApiKey: "legacy-key",
}),
"utf8",
);
const config = loadFeynmanConfig(configPath);
assert.equal(config.version, 1);
assert.equal(config.webSearch?.feynmanWebProvider, "perplexity");
assert.equal(config.webSearch?.perplexityApiKey, "legacy-key");
});
test("saveFeynmanConfig persists sessionDir and webSearch", () => {
const root = mkdtempSync(join(tmpdir(), "feynman-config-"));
const configPath = join(root, "config.json");
const webSearch = configureWebSearchProvider({}, "gemini-browser", { chromeProfile: "Profile 2" });
saveFeynmanConfig(
{
version: 1,
sessionDir: "/tmp/feynman-sessions",
webSearch,
},
configPath,
);
const config = loadFeynmanConfig(configPath);
assert.equal(config.sessionDir, "/tmp/feynman-sessions");
assert.equal(config.webSearch?.feynmanWebProvider, "gemini-browser");
assert.equal(config.webSearch?.chromeProfile, "Profile 2");
});
test("default web provider falls back to Pi web via gemini-browser", () => {
const provider = getConfiguredWebSearchProvider({});
assert.equal(provider.id, "gemini-browser");
assert.equal(provider.runtimeProvider, "gemini");
});

View File

@@ -9,7 +9,6 @@ test("buildPiArgs includes configured runtime paths and prompt", () => {
workingDir: "/workspace", workingDir: "/workspace",
sessionDir: "/sessions", sessionDir: "/sessions",
feynmanAgentDir: "/home/.feynman/agent", feynmanAgentDir: "/home/.feynman/agent",
systemPrompt: "system",
initialPrompt: "hello", initialPrompt: "hello",
explicitModelSpec: "openai:gpt-5.4", explicitModelSpec: "openai:gpt-5.4",
thinkingLevel: "medium", thinkingLevel: "medium",
@@ -20,12 +19,8 @@ test("buildPiArgs includes configured runtime paths and prompt", () => {
"/sessions", "/sessions",
"--extension", "--extension",
"/repo/feynman/extensions/research-tools.ts", "/repo/feynman/extensions/research-tools.ts",
"--skill",
"/repo/feynman/skills",
"--prompt-template", "--prompt-template",
"/repo/feynman/prompts", "/repo/feynman/prompts",
"--system-prompt",
"system",
"--model", "--model",
"openai:gpt-5.4", "openai:gpt-5.4",
"--thinking", "--thinking",
@@ -40,14 +35,11 @@ test("buildPiEnv wires Feynman paths into the Pi environment", () => {
workingDir: "/workspace", workingDir: "/workspace",
sessionDir: "/sessions", sessionDir: "/sessions",
feynmanAgentDir: "/home/.feynman/agent", feynmanAgentDir: "/home/.feynman/agent",
systemPrompt: "system",
feynmanVersion: "0.1.5", feynmanVersion: "0.1.5",
}); });
assert.equal(env.PI_CODING_AGENT_DIR, "/home/.feynman/agent");
assert.equal(env.FEYNMAN_SESSION_DIR, "/sessions"); assert.equal(env.FEYNMAN_SESSION_DIR, "/sessions");
assert.equal(env.FEYNMAN_BIN_PATH, "/repo/feynman/bin/feynman.js"); assert.equal(env.FEYNMAN_BIN_PATH, "/repo/feynman/bin/feynman.js");
assert.equal(env.FEYNMAN_PI_NPM_ROOT, "/repo/feynman/.pi/npm/node_modules");
assert.equal(env.FEYNMAN_MEMORY_DIR, "/home/.feynman/memory"); assert.equal(env.FEYNMAN_MEMORY_DIR, "/home/.feynman/memory");
}); });

View File

@@ -0,0 +1,54 @@
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
formatPiWebAccessDoctorLines,
getPiWebAccessStatus,
getPiWebSearchConfigPath,
loadPiWebAccessConfig,
} from "../src/pi/web-access.js";
test("loadPiWebAccessConfig returns empty config when Pi web config is missing", () => {
const root = mkdtempSync(join(tmpdir(), "feynman-pi-web-"));
const configPath = getPiWebSearchConfigPath(root);
assert.deepEqual(loadPiWebAccessConfig(configPath), {});
});
test("getPiWebAccessStatus reads Pi web-access config directly", () => {
const root = mkdtempSync(join(tmpdir(), "feynman-pi-web-"));
const configPath = getPiWebSearchConfigPath(root);
mkdirSync(join(root, ".pi"), { recursive: true });
writeFileSync(
configPath,
JSON.stringify({
provider: "gemini",
searchProvider: "gemini",
chromeProfile: "Profile 2",
geminiApiKey: "AIza...",
}),
"utf8",
);
const status = getPiWebAccessStatus(loadPiWebAccessConfig(configPath), configPath);
assert.equal(status.routeLabel, "Gemini");
assert.equal(status.requestProvider, "gemini");
assert.equal(status.geminiApiConfigured, true);
assert.equal(status.perplexityConfigured, false);
assert.equal(status.chromeProfile, "Profile 2");
});
test("formatPiWebAccessDoctorLines reports Pi-managed web access", () => {
const lines = formatPiWebAccessDoctorLines(
getPiWebAccessStatus({
provider: "auto",
searchProvider: "auto",
}, "/tmp/pi-web-search.json"),
);
assert.equal(lines[0], "web access: pi-web-access");
assert.ok(lines.some((line) => line.includes("/tmp/pi-web-search.json")));
});