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:
@@ -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.
|
||||||
}
|
|
||||||
@@ -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
38
.pi/agents/citation.md
Normal 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.
|
||||||
@@ -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 2–4 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.
|
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
42
README.md
42
README.md
@@ -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
211
extensions/research-tools/alpha.ts
Normal file
211
extensions/research-tools/alpha.ts
Normal 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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
328
extensions/research-tools/header.ts
Normal file
328
extensions/research-tools/header.ts
Normal 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() {},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
70
extensions/research-tools/help.ts
Normal file
70
extensions/research-tools/help.ts
Normal 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");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
233
extensions/research-tools/preview.ts
Normal file
233
extensions/research-tools/preview.ts
Normal 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
|
||||||
|
`;
|
||||||
|
}
|
||||||
115
extensions/research-tools/project.ts
Normal file
115
extensions/research-tools/project.ts
Normal 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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
223
extensions/research-tools/session-search.ts
Normal file
223
extensions/research-tools/session-search.ts
Normal 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),
|
||||||
|
};
|
||||||
|
}
|
||||||
46
extensions/research-tools/shared.ts
Normal file
46
extensions/research-tools/shared.ts
Normal 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
1282
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
159
src/cli.ts
159
src/cli.ts
@@ -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(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`);
|
|
||||||
}
|
|
||||||
@@ -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}`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
109
src/pi/web-access.ts
Normal 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}`,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -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}`);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"}`);
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
|
||||||
|
|||||||
@@ -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");
|
|
||||||
});
|
|
||||||
@@ -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");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
54
tests/pi-web-access.test.ts
Normal file
54
tests/pi-web-access.test.ts
Normal 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")));
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user