Finalize remaining repo updates

This commit is contained in:
Advait Paliwal
2026-03-24 14:30:09 -07:00
parent 771b39cbba
commit 21b8bcd4c4
13 changed files with 369 additions and 87 deletions

View File

@@ -1,44 +1,59 @@
# Feynman
<p align="center">
<a href="https://feynman.is">
<img src="assets/hero.png" alt="Feynman CLI" width="800" />
</a>
</p>
<p align="center">The open source AI research agent.</p>
<p align="center">
<a href="https://feynman.is/docs"><img alt="Docs" src="https://img.shields.io/badge/docs-feynman.is-0d9668?style=flat-square" /></a>
<a href="https://www.npmjs.com/package/@companion-ai/feynman"><img alt="npm" src="https://img.shields.io/npm/v/@companion-ai/feynman?style=flat-square" /></a>
<a href="https://github.com/getcompanion-ai/feynman/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/getcompanion-ai/feynman?style=flat-square" /></a>
</p>
The open source AI research agent
---
### Installation
```bash
curl -fsSL https://feynman.is/install | bash
```
```powershell
# Windows
irm https://feynman.is/install.ps1 | iex
```
Or install the npm fallback:
```bash
# npm fallback
npm install -g @companion-ai/feynman
```
```bash
feynman setup
feynman
Then run `feynman setup` to configure your model and get started.
---
### What you type → what happens
```
$ feynman "what do we know about scaling laws"
→ Searches papers and web, produces a cited research brief
$ feynman deepresearch "mechanistic interpretability"
→ Multi-agent investigation with parallel researchers, synthesis, verification
$ feynman lit "RLHF alternatives"
→ Literature review with consensus, disagreements, open questions
$ feynman audit 2401.12345
→ Compares paper claims against the public codebase
$ feynman replicate "chain-of-thought improves math"
→ Asks where to run, then builds a replication plan
```
Feynman works directly inside your folder or repo. For long-running work, keep the stable repo contract in `AGENTS.md`, the current task brief in `outputs/.plans/`, and the chronological lab notebook in `CHANGELOG.md`.
---
## What you type → what happens
| Prompt | Result |
| --- | --- |
| `feynman "what do we know about scaling laws"` | Searches papers and web, produces a cited research brief |
| `feynman deepresearch "mechanistic interpretability"` | Multi-agent investigation with parallel researchers, synthesis, verification |
| `feynman lit "RLHF alternatives"` | Literature review with consensus, disagreements, open questions |
| `feynman audit 2401.12345` | Compares paper claims against the public codebase |
| `feynman replicate "chain-of-thought improves math"` | Asks where to run, then builds a replication plan |
| `feynman "summarize this PDF" --prompt paper.pdf` | One-shot mode, no REPL |
---
## Workflows
### Workflows
Ask naturally or use slash commands as shortcuts.
@@ -56,9 +71,9 @@ Ask naturally or use slash commands as shortcuts.
---
## Agents
### Agents
Four bundled research agents, dispatched automatically or via subagent commands.
Four bundled research agents, dispatched automatically.
- **Researcher** — gather evidence across papers, web, repos, docs
- **Reviewer** — simulated peer review with severity-graded feedback
@@ -67,42 +82,23 @@ Four bundled research agents, dispatched automatically or via subagent commands.
---
## Tools
### Tools
- **[AlphaXiv](https://www.alphaxiv.org/)** — paper search, Q&A, code reading, persistent annotations
- **Docker** — isolated container execution for safe experiments on your machine
- **Web search** — Gemini or Perplexity, zero-config default via signed-in Chromium
- **Session search** — optional indexed recall across prior research sessions
- **Web search** — Gemini or Perplexity, zero-config default
- **Session search** — indexed recall across prior research sessions
- **Preview** — browser and PDF export of generated artifacts
---
## CLI
### How it works
```bash
feynman # REPL
feynman setup # guided setup
feynman doctor # diagnose everything
feynman status # current config summary
feynman model login [provider] # model auth
feynman model set <provider/model> # set default model
feynman alpha login # alphaXiv auth
feynman packages list # core vs optional packages
feynman packages install memory # opt into heavier packages on demand
feynman search status # web search config
```
Built on [Pi](https://github.com/badlogic/pi-mono) for the agent runtime, [alphaXiv](https://www.alphaxiv.org/) for paper search and analysis, and [Docker](https://www.docker.com/) for isolated local execution. Every output is source-grounded — claims link to papers, docs, or repos with direct URLs.
---
## How it works
Built on [Pi](https://github.com/badlogic/pi-mono) for the agent runtime, [alphaXiv](https://www.alphaxiv.org/) for paper search and analysis, and [Docker](https://www.docker.com/) for isolated local execution
Every output is source-grounded — claims link to papers, docs, or repos with direct URLs
---
## Contributing
### Contributing
```bash
git clone https://github.com/getcompanion-ai/feynman.git

BIN
assets/hero-raw.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 KiB

BIN
assets/hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

View File

@@ -16,7 +16,174 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { getExtensionCommandSpec } from "../../metadata/commands.mjs";
import { formatToolText } from "./shared.js";
import { collapseExcessBlankLines, formatToolText } from "./shared.js";
type JsonRecord = Record<string, unknown>;
type AlphaSearchHit = {
rank?: number;
title?: string;
publishedAt?: string;
organizations?: string;
authors?: string;
abstract?: string;
arxivId?: string;
arxivUrl?: string;
alphaXivUrl?: string;
};
type AlphaSearchSection = {
count: number;
results: AlphaSearchHit[];
note?: string;
};
type AlphaSearchPayload = {
query?: string;
mode?: string;
results?: AlphaSearchHit[];
semantic?: AlphaSearchSection;
keyword?: AlphaSearchSection;
agentic?: AlphaSearchSection;
};
function isRecord(value: unknown): value is JsonRecord {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function cleanText(value: unknown, maxLength = 320): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const collapsed = collapseExcessBlankLines(value)
.replace(/\s*\n\s*/g, " ")
.replace(/[ \t]+/g, " ");
if (!collapsed) {
return undefined;
}
return collapsed.length > maxLength ? `${collapsed.slice(0, maxLength - 1).trimEnd()}` : collapsed;
}
function sanitizeHit(value: unknown, fallbackRank: number): AlphaSearchHit | null {
if (!isRecord(value)) {
return null;
}
const title = cleanText(value.title, 220);
if (!title) {
return null;
}
return {
rank: typeof value.rank === "number" ? value.rank : fallbackRank,
title,
publishedAt: cleanText(value.publishedAt, 48),
organizations: cleanText(value.organizations, 180),
authors: cleanText(value.authors, 220),
abstract: cleanText(value.abstract, 360),
arxivId: cleanText(value.arxivId, 32),
arxivUrl: cleanText(value.arxivUrl, 160),
alphaXivUrl: cleanText(value.alphaXivUrl, 160),
};
}
function sanitizeHits(value: unknown): AlphaSearchHit[] {
if (!Array.isArray(value)) {
return [];
}
return value
.map((entry, index) => sanitizeHit(entry, index + 1))
.filter((entry): entry is AlphaSearchHit => entry !== null);
}
function sanitizeSection(value: unknown): AlphaSearchSection {
if (!isRecord(value)) {
return { count: 0, results: [] };
}
const results = sanitizeHits(value.results);
const note = results.length === 0 ? cleanText(value.raw, 600) : undefined;
return {
count: results.length,
results,
...(note ? { note } : {}),
};
}
export function sanitizeAlphaSearchPayload(value: unknown): AlphaSearchPayload {
if (!isRecord(value)) {
return {};
}
const payload: AlphaSearchPayload = {
query: cleanText(value.query, 240),
mode: cleanText(value.mode, 32),
};
const topLevelResults = sanitizeHits(value.results);
if (topLevelResults.length > 0) {
payload.results = topLevelResults;
}
for (const key of ["semantic", "keyword", "agentic"] as const) {
if (key in value) {
payload[key] = sanitizeSection(value[key]);
}
}
return payload;
}
function pushHitLines(lines: string[], hit: AlphaSearchHit): void {
lines.push(`${hit.rank ?? "?"}. ${hit.title ?? "Untitled result"}`);
if (hit.arxivId) lines.push(` arXiv: ${hit.arxivId}`);
if (hit.publishedAt) lines.push(` published: ${hit.publishedAt}`);
if (hit.organizations) lines.push(` orgs: ${hit.organizations}`);
if (hit.authors) lines.push(` authors: ${hit.authors}`);
if (hit.abstract) lines.push(` abstract: ${hit.abstract}`);
if (hit.arxivUrl) lines.push(` arXiv URL: ${hit.arxivUrl}`);
if (hit.alphaXivUrl) lines.push(` alphaXiv URL: ${hit.alphaXivUrl}`);
}
function pushSectionLines(lines: string[], label: string, section: AlphaSearchSection): void {
lines.push(`${label} (${section.count})`);
if (section.results.length === 0) {
lines.push(section.note ? ` note: ${section.note}` : " no parsed results");
return;
}
for (const hit of section.results) {
pushHitLines(lines, hit);
}
}
export function formatAlphaSearchContext(value: unknown): string {
const payload = sanitizeAlphaSearchPayload(value);
const lines: string[] = [];
if (payload.query) lines.push(`query: ${payload.query}`);
if (payload.mode) lines.push(`mode: ${payload.mode}`);
if (payload.results) {
pushSectionLines(lines, "results", { count: payload.results.length, results: payload.results });
}
for (const [label, section] of [
["semantic", payload.semantic],
["keyword", payload.keyword],
["agentic", payload.agentic],
] as const) {
if (section) {
pushSectionLines(lines, label, section);
}
}
return lines.length > 0 ? lines.join("\n") : "No alpha search results returned.";
}
export function registerAlphaCommands(pi: ExtensionAPI): void {
pi.registerCommand("alpha-login", {
@@ -72,9 +239,10 @@ export function registerAlphaTools(pi: ExtensionAPI): void {
async execute(_toolCallId, params) {
try {
const result = await searchPapers(params.query, params.mode?.trim() || "all");
const sanitized = sanitizeAlphaSearchPayload(result);
return {
content: [{ type: "text", text: formatToolText(result) }],
details: result,
content: [{ type: "text", text: formatAlphaSearchContext(sanitized) }],
details: sanitized,
};
} finally {
await disconnect();

View File

@@ -27,8 +27,13 @@ export const FEYNMAN_RESEARCH_TOOLS = [
"preview_file",
];
export function collapseExcessBlankLines(text: string): string {
return text.replace(/\r\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
}
export function formatToolText(result: unknown): string {
return typeof result === "string" ? result : JSON.stringify(result, null, 2);
const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
return collapseExcessBlankLines(text);
}
export function getFeynmanHome(): string {

View File

@@ -23,13 +23,13 @@ export const OPTIONAL_PACKAGE_PRESETS = {
},
} as const;
export type OptionalPackagePresetName = keyof typeof OPTIONAL_PACKAGE_PRESETS;
const LEGACY_DEFAULT_PACKAGE_SOURCES = [
...CORE_PACKAGE_SOURCES,
"npm:pi-generative-ui",
] as const;
export type OptionalPackagePresetName = keyof typeof OPTIONAL_PACKAGE_PRESETS;
function arraysMatchAsSets(left: readonly string[], right: readonly string[]): boolean {
if (left.length !== right.length) {
return false;

86
tests/alpha-tools.test.ts Normal file
View File

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

File diff suppressed because one or more lines are too long

View File

@@ -7,8 +7,8 @@ export default defineConfig({
markdown: {
shikiConfig: {
themes: {
light: 'github-light',
dark: 'github-dark',
light: 'everforest-light',
dark: 'everforest-dark',
},
},
},

BIN
website/public/hero-raw.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 KiB

BIN
website/public/hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

View File

@@ -6,7 +6,6 @@ import AsciiLogo from '../components/AsciiLogo.astro';
<Base title="Feynman — The open source AI research agent" active="home">
<section class="text-center pt-24 pb-20 px-6">
<div class="max-w-2xl mx-auto">
<AsciiLogo size="hero" class="mb-4" />
<h1 class="text-5xl sm:text-6xl font-bold tracking-tight mb-6" style="text-wrap: balance">The open source AI research agent</h1>
<p class="text-lg text-text-muted mb-10 leading-relaxed" style="text-wrap: pretty">Investigate topics, write papers, run experiments, review research, audit codebases &mdash; every output cited and source-grounded</p>
<button id="copy-btn" class="group inline-flex items-center justify-between gap-3 bg-surface rounded-lg px-5 py-3 mb-8 font-mono text-sm hover:border-accent/40 hover:text-accent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent rounded-lg border border-border max-w-full" aria-label="Copy install command">
@@ -20,6 +19,9 @@ import AsciiLogo from '../components/AsciiLogo.astro';
<a href="https://github.com/getcompanion-ai/feynman" target="_blank" rel="noopener" class="px-6 py-2.5 rounded-lg border border-border text-text-muted font-semibold text-sm hover:border-text-dim hover:text-text-primary transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg">GitHub</a>
</div>
</div>
<div class="max-w-4xl mx-auto mt-16">
<img src="/hero-raw.png" alt="Feynman CLI" class="w-full" />
</div>
</section>
<section class="py-20 px-6">

View File

@@ -3,31 +3,31 @@
@tailwind utilities;
:root {
--color-bg: #f0f5f1;
--color-surface: #e4ece6;
--color-surface-2: #d8e3db;
--color-border: #c2d1c6;
--color-text: #1a2e22;
--color-text-muted: #3d5c4a;
--color-text-dim: #6b8f7a;
--color-accent: #0d9668;
--color-accent-hover: #077a54;
--color-accent-subtle: #c6e4d4;
--color-teal: #0e8a7d;
--color-bg: #f3ead3;
--color-surface: #eae4ca;
--color-surface-2: #e0dbc2;
--color-border: #c9c4b0;
--color-text: #3a464c;
--color-text-muted: #5c6a72;
--color-text-dim: #859289;
--color-accent: #6e8b53;
--color-accent-hover: #5a7342;
--color-accent-subtle: #d5e3bf;
--color-teal: #5da09a;
}
.dark {
--color-bg: #050a08;
--color-surface: #0c1410;
--color-surface-2: #131f1a;
--color-border: #1b2f26;
--color-text: #f0f5f2;
--color-text-muted: #8aaa9a;
--color-text-dim: #4d7565;
--color-accent: #34d399;
--color-accent-hover: #10b981;
--color-accent-subtle: #064e3b;
--color-teal: #2dd4bf;
--color-bg: #2d353b;
--color-surface: #343f44;
--color-surface-2: #3a464c;
--color-border: #5c6a72;
--color-text: #d3c6aa;
--color-text-muted: #9da9a0;
--color-text-dim: #859289;
--color-accent: #a7c080;
--color-accent-hover: #93ad6c;
--color-accent-subtle: #425047;
--color-teal: #7fbbb3;
}
html {
@@ -87,7 +87,7 @@ body {
.prose code {
font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
font-size: 0.875rem;
background-color: var(--color-surface);
background-color: var(--color-surface-2);
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
color: var(--color-text);
@@ -95,7 +95,6 @@ body {
.prose pre {
position: relative;
background-color: var(--color-surface) !important;
border-radius: 0.5rem;
padding: 1rem 1.25rem;
overflow-x: auto;
@@ -103,13 +102,13 @@ body {
font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
font-size: 0.875rem;
line-height: 1.7;
border: 1px solid var(--color-border);
}
.prose pre code {
background: none !important;
border: none;
padding: 0;
color: var(--color-text);
}
.copy-code {
@@ -137,9 +136,6 @@ pre:hover .copy-code {
color: var(--color-accent);
}
.prose pre code span {
color: inherit !important;
}
.prose table {
width: 100%;
@@ -201,6 +197,34 @@ pre:hover .copy-code {
margin-bottom: 1rem;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--color-border) transparent;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-text-dim);
}
::selection {
background: var(--color-accent-subtle);
color: var(--color-text);
}
.agent-entry {
background-color: var(--color-surface);
border-radius: 0.75rem;