8 Commits

Author SHA1 Message Date
Advait Paliwal
ec4cbfb57e Update Pi runtime packages 2026-04-17 13:45:16 -07:00
Advait Paliwal
1cd1a147f2 Remove runtime hygiene extension bloat 2026-04-17 11:47:18 -07:00
Advait Paliwal
92914acff7 Add Pi event guards for workflow state 2026-04-17 11:13:57 -07:00
Advait Paliwal
f0bbb25910 Use Pi runtime hooks for research context hygiene 2026-04-17 10:38:42 -07:00
Advait Paliwal
9841342866 Fix workflow continuation and provider setup gaps 2026-04-17 09:47:38 -07:00
Advait Paliwal
d30506c82a Link bundled runtime dependencies for core packages 2026-04-16 15:56:53 -07:00
Advait Paliwal
c3f7f6ec08 Add LM Studio setup and blocked research artifacts 2026-04-16 15:39:01 -07:00
Advait Paliwal
d2570188f9 Add first-class LM Studio setup 2026-04-16 15:34:32 -07:00
34 changed files with 1210 additions and 669 deletions

View File

@@ -15,6 +15,8 @@ Operating rules:
- Never answer a latest/current question from arXiv or alpha-backed paper search alone.
- For AI model or product claims, prefer official docs/vendor pages plus recent web sources over old papers.
- Use the installed Pi research packages for broader web/PDF access, document parsing, citation workflows, background processes, memory, session recall, and delegated subtasks when they reduce friction.
- You are running inside the Feynman/Pi runtime with filesystem tools, package tools, and configured extensions. Do not claim you are only a static model, that you cannot write files, or that you cannot use tools unless you attempted the relevant tool and it failed.
- If a tool, package, source, or network route is unavailable, record the specific failed capability and still write the requested durable artifact with a clear `Blocked / Unverified` status instead of stopping with chat-only prose.
- Feynman ships project subagents for research work. Prefer the `researcher`, `writer`, `verifier`, 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`.
- 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 pass.
@@ -44,6 +46,7 @@ Operating rules:
- When citing papers from alpha-backed tools, prefer direct arXiv or alphaXiv links and include the arXiv ID.
- 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.
- If a workflow requests a durable artifact, verify the file exists on disk before the final response. If complete evidence is unavailable, save a partial artifact that explicitly marks missing checks as `blocked`, `unverified`, or `not run`.
- 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.
- Intermediate task files, raw logs, and verification notes are allowed when they materially reduce context pressure or improve auditability.

View File

@@ -25,7 +25,7 @@ curl -fsSL https://feynman.is/install | bash
irm https://feynman.is/install.ps1 | iex
```
The one-line installer fetches the latest tagged release. To pin a version, pass it explicitly, for example `curl -fsSL https://feynman.is/install | bash -s -- 0.2.21`.
The one-line installer fetches the latest tagged release. To pin a version, pass it explicitly, for example `curl -fsSL https://feynman.is/install | bash -s -- 0.2.29`.
The installer downloads a standalone native bundle with its own Node.js runtime.
@@ -33,7 +33,7 @@ To upgrade the standalone app later, rerun the installer. `feynman update` only
To uninstall the standalone app, remove the launcher and runtime bundle, then optionally remove `~/.feynman` if you also want to delete settings, sessions, and installed package state. If you also want to delete alphaXiv login state, remove `~/.ahub`. See the installation guide for platform-specific paths.
Local models are supported through the custom-provider flow. For Ollama, run `feynman setup`, choose `Custom provider (baseUrl + API key)`, use `openai-completions`, and point it at `http://localhost:11434/v1`.
Local models are supported through the setup flow. For LM Studio, run `feynman setup`, choose `LM Studio`, and keep the default `http://localhost:1234/v1` unless you changed the server port. For LiteLLM, choose `LiteLLM Proxy` and keep the default `http://localhost:4000/v1`. For Ollama or vLLM, choose `Custom provider (baseUrl + API key)`, use `openai-completions`, and point it at the local `/v1` endpoint.
### Skills Only

1105
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@companion-ai/feynman",
"version": "0.2.21",
"version": "0.2.29",
"description": "Research-first CLI agent built on Pi and alphaXiv",
"license": "MIT",
"type": "module",
@@ -61,16 +61,16 @@
"dependencies": {
"@clack/prompts": "^1.2.0",
"@companion-ai/alpha-hub": "^0.1.3",
"@mariozechner/pi-ai": "^0.66.1",
"@mariozechner/pi-coding-agent": "^0.66.1",
"@sinclair/typebox": "^0.34.48",
"dotenv": "^17.3.1"
"@mariozechner/pi-ai": "^0.67.6",
"@mariozechner/pi-coding-agent": "^0.67.6",
"@sinclair/typebox": "^0.34.49",
"dotenv": "^17.4.2"
},
"overrides": {
"basic-ftp": "5.2.2",
"basic-ftp": "5.3.0",
"@modelcontextprotocol/sdk": {
"@hono/node-server": "1.19.13",
"hono": "4.12.12"
"@hono/node-server": "1.19.14",
"hono": "4.12.14"
},
"express": {
"router": {
@@ -80,16 +80,17 @@
"proxy-agent": {
"pac-proxy-agent": {
"get-uri": {
"basic-ftp": "5.2.2"
"basic-ftp": "5.3.0"
}
}
},
"protobufjs": "7.5.5",
"minimatch": {
"brace-expansion": "5.0.5"
}
},
"devDependencies": {
"@types/node": "^25.5.0",
"@types/node": "^25.6.0",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
},

View File

@@ -9,7 +9,7 @@ Audit the paper and codebase for: $@
Derive a short slug from the audit target (lowercase, hyphens, no filler words, ≤5 words). Use this slug for all files in this run.
Requirements:
- Before starting, outline the audit plan: which paper, which repo, which claims to check. Write the plan to `outputs/.plans/<slug>.md`. Present the plan to the user. If this is an unattended or one-shot run, continue automatically. If the user is actively interacting, give them a brief chance to request changes before proceeding.
- Before starting, outline the audit plan: which paper, which repo, which claims to check. Write the plan to `outputs/.plans/<slug>.md`. Briefly summarize the plan to the user and continue immediately. Do not ask for confirmation or wait for a proceed response unless the user explicitly requested plan review.
- Use the `researcher` subagent for evidence gathering and the `verifier` subagent to verify sources and add inline citations when the audit is non-trivial.
- Compare claimed methods, defaults, metrics, and data handling against the actual code.
- Call out missing code, mismatches, ambiguous defaults, and reproduction risks.

View File

@@ -9,7 +9,7 @@ Compare sources for: $@
Derive a short slug from the comparison topic (lowercase, hyphens, no filler words, ≤5 words). Use this slug for all files in this run.
Requirements:
- Before starting, outline the comparison plan: which sources to compare, which dimensions to evaluate, expected output structure. Write the plan to `outputs/.plans/<slug>.md`. Present the plan to the user. If this is an unattended or one-shot run, continue automatically. If the user is actively interacting, give them a brief chance to request changes before proceeding.
- Before starting, outline the comparison plan: which sources to compare, which dimensions to evaluate, expected output structure. Write the plan to `outputs/.plans/<slug>.md`. Briefly summarize the plan to the user and continue immediately. Do not ask for confirmation or wait for a proceed response unless the user explicitly requested plan review.
- Use the `researcher` subagent to gather source material when the comparison set is broad, and the `verifier` subagent to verify sources and add inline citations to the final matrix.
- Build a comparison matrix covering: source, key claim, evidence type, caveats, confidence.
- Generate charts with `pi-charts` when the comparison involves quantitative metrics. Use Mermaid for method or architecture comparisons.

View File

@@ -6,6 +6,8 @@ topLevelCli: true
---
Run a deep research workflow for: $@
This is an execution request, not a request to explain or implement the workflow instructions. Carry out the workflow with tools and durable files. Do not answer by describing the protocol, converting it into programming steps, or saying how someone could implement it.
You are the Lead Researcher. You plan, delegate, evaluate, verify, write, and cite. Internal orchestration is invisible to the user unless they ask.
## 1. Plan
@@ -51,7 +53,9 @@ If `CHANGELOG.md` exists, read the most recent relevant entries before finalizin
Also save the plan with `memory_remember` (type: `fact`, key: `deepresearch.<slug>.plan`) so it survives context truncation.
Present the plan to the user. If this is an unattended or one-shot run, continue automatically. If the user is actively interacting in the terminal, give them a brief chance to request plan changes before proceeding.
Briefly summarize the plan to the user and continue immediately. Do not ask for confirmation or wait for a proceed response unless the user explicitly requested plan review.
Do not stop after planning. If live search, subagents, web access, alphaXiv, or any other capability is unavailable, continue in degraded mode and write a durable blocked/partial report that records exactly which capabilities failed.
## 2. Scale decision
@@ -105,6 +109,13 @@ When the work spans multiple rounds, also append a concise chronological entry t
Most topics need 1-2 rounds. Stop when additional rounds would not materially change conclusions.
If no researcher files can be produced because tools, subagents, or network access failed, create `outputs/.drafts/<slug>-draft.md` yourself as a blocked report with:
- what was requested,
- which capabilities failed,
- what evidence was and was not gathered,
- a proposed source-gathering plan,
- no invented sources or results.
## 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:
@@ -190,6 +201,7 @@ Before you stop, verify on disk that all of these exist:
- `outputs/<slug>.provenance.md` or `papers/<slug>.provenance.md` provenance sidecar
Do not stop at `<slug>-brief.md` alone. If the cited brief exists but the promoted final output or provenance sidecar does not, create them before responding.
If full verification could not be completed, still create the final deliverable and provenance sidecar with `Verification: BLOCKED` or `PASS WITH NOTES` and list the missing checks. Never end with only an explanation in chat.
## Background execution

View File

@@ -9,7 +9,7 @@ Write a paper-style draft for: $@
Derive a short slug from the topic (lowercase, hyphens, no filler words, ≤5 words). Use this slug for all files in this run.
Requirements:
- Before writing, outline the draft structure: proposed title, sections, key claims to make, source material to draw from, and a verification log for the critical claims, figures, and calculations. Write the outline to `outputs/.plans/<slug>.md`. Present the outline to the user. If this is an unattended or one-shot run, continue automatically. If the user is actively interacting, give them a brief chance to request changes before proceeding.
- Before writing, outline the draft structure: proposed title, sections, key claims to make, source material to draw from, and a verification log for the critical claims, figures, and calculations. Write the outline to `outputs/.plans/<slug>.md`. Briefly summarize the outline to the user and continue immediately. Do not ask for confirmation or wait for a proceed response unless the user explicitly requested outline review.
- Use the `writer` subagent when the draft should be produced from already-collected notes, then use the `verifier` subagent to add inline citations and verify sources.
- Include at minimum: title, abstract, problem statement, related work, method or synthesis, evidence or experiments, limitations, conclusion.
- Use clean Markdown with LaTeX where equations materially help.

View File

@@ -10,7 +10,7 @@ Derive a short slug from the topic (lowercase, hyphens, no filler words, ≤5 wo
## Workflow
1. **Plan** — Outline the scope: key questions, source types to search (papers, web, repos), time period, expected sections, and a small task ledger plus verification log. Write the plan to `outputs/.plans/<slug>.md`. Present the plan to the user. If this is an unattended or one-shot run, continue automatically. If the user is actively interacting, give them a brief chance to request changes before proceeding.
1. **Plan** — Outline the scope: key questions, source types to search (papers, web, repos), time period, expected sections, and a small task ledger plus verification log. Write the plan to `outputs/.plans/<slug>.md`. Briefly summarize the plan to the user and continue immediately. Do not ask for confirmation or wait for a proceed response unless the user explicitly requested plan review.
2. **Gather** — Use the `researcher` subagent when the sweep is wide enough to benefit from delegated paper triage before synthesis. For narrow topics, search directly. Researcher outputs go to `<slug>-research-*.md`. Do not silently skip assigned questions; mark them `done`, `blocked`, or `superseded`.
3. **Synthesize** — Separate consensus, disagreements, and open questions. When useful, propose concrete next experiments or follow-up reading. Generate charts with `pi-charts` for quantitative comparisons across papers and Mermaid diagrams for taxonomies or method pipelines. Before finishing the draft, sweep every strong claim against the verification log and downgrade anything that is inferred or single-source critical.
4. **Cite** — Spawn the `verifier` agent to add inline citations and verify every source URL in the draft.

View File

@@ -9,7 +9,7 @@ Review this AI research artifact: $@
Derive a short slug from the artifact name (lowercase, hyphens, no filler words, ≤5 words). Use this slug for all files in this run.
Requirements:
- Before starting, outline what will be reviewed, the review criteria (novelty, empirical rigor, baselines, reproducibility, etc.), and any verification-specific checks needed for claims, figures, and reported metrics. Present the plan to the user. If this is an unattended or one-shot run, continue automatically. If the user is actively interacting, give them a brief chance to request changes before proceeding.
- Before starting, outline what will be reviewed, the review criteria (novelty, empirical rigor, baselines, reproducibility, etc.), and any verification-specific checks needed for claims, figures, and reported metrics. Briefly summarize the plan to the user and continue immediately. Do not ask for confirmation or wait for a proceed response unless the user explicitly requested plan review.
- Spawn a `researcher` subagent to gather evidence on the artifact — inspect the paper, code, cited work, and any linked experimental artifacts. Save to `<slug>-research.md`.
- Spawn a `reviewer` subagent with `<slug>-research.md` to produce the final peer review with inline annotations.
- For small or simple artifacts where evidence gathering is overkill, run the `reviewer` subagent directly instead.

View File

@@ -101,7 +101,7 @@ print(f"[summarize] chunks={len(chunks)} chunk_size={chunk_size} overlap={overla
### 3b. Confirm before spawning
If this is an unattended or one-shot run, continue automatically. Otherwise tell the user: "Source is ~<chars> chars -> <N> chunks -> <N> researcher subagents. This may take several minutes. Proceed?" Wait for confirmation before launching Tier 3.
Briefly summarize: "Source is ~<chars> chars -> <N> chunks -> <N> researcher subagents. This may take several minutes." Then continue automatically. Do not ask for confirmation or wait for a proceed response unless the user explicitly requested review before launching.
### 3c. Dispatch researcher subagents

View File

@@ -9,7 +9,7 @@ Create a research watch for: $@
Derive a short slug from the watch topic (lowercase, hyphens, no filler words, ≤5 words). Use this slug for all files in this run.
Requirements:
- Before starting, outline the watch plan: what to monitor, what signals matter, what counts as a meaningful change, and the check frequency. Write the plan to `outputs/.plans/<slug>.md`. Present the plan to the user. If this is an unattended or one-shot run, continue automatically. If the user is actively interacting, give them a brief chance to request changes before proceeding.
- Before starting, outline the watch plan: what to monitor, what signals matter, what counts as a meaningful change, and the check frequency. Write the plan to `outputs/.plans/<slug>.md`. Briefly summarize the plan to the user and continue immediately. Do not ask for confirmation or wait for a proceed response unless the user explicitly requested plan review.
- Start with a baseline sweep of the topic.
- Use `schedule_prompt` to create the recurring or delayed follow-up instead of merely promising to check later.
- Save exactly one baseline artifact to `outputs/<slug>-baseline.md`.

View File

@@ -110,7 +110,7 @@ This usually means the release exists, but not all platform bundles were uploade
Workarounds:
- try again after the release finishes publishing
- pass the latest published version explicitly, e.g.:
& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.21
& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.29
"@
}

View File

@@ -261,7 +261,7 @@ This usually means the release exists, but not all platform bundles were uploade
Workarounds:
- try again after the release finishes publishing
- pass the latest published version explicitly, e.g.:
curl -fsSL https://feynman.is/install | bash -s -- 0.2.21
curl -fsSL https://feynman.is/install | bash -s -- 0.2.29
EOF
exit 1
fi

View File

@@ -1,2 +1,3 @@
export const PI_SUBAGENTS_PATCH_TARGETS: string[];
export function patchPiSubagentsSource(relativePath: string, source: string): string;
export function stripPiSubagentBuiltinModelSource(source: string): string;

View File

@@ -66,6 +66,24 @@ function replaceAll(source, from, to) {
return source.split(from).join(to);
}
export function stripPiSubagentBuiltinModelSource(source) {
if (!source.startsWith("---\n")) {
return source;
}
const endIndex = source.indexOf("\n---", 4);
if (endIndex === -1) {
return source;
}
const frontmatter = source.slice(4, endIndex);
const nextFrontmatter = frontmatter
.split("\n")
.filter((line) => !/^\s*model\s*:/.test(line))
.join("\n");
return `---\n${nextFrontmatter}${source.slice(endIndex)}`;
}
export function patchPiSubagentsSource(relativePath, source) {
let patched = source;

View File

@@ -1,5 +1,5 @@
import { spawnSync } from "node:child_process";
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import { homedir } from "node:os";
import { delimiter, dirname, resolve } from "node:path";
@@ -9,7 +9,7 @@ import { patchAlphaHubAuthSource } from "./lib/alpha-hub-auth-patch.mjs";
import { patchPiExtensionLoaderSource } from "./lib/pi-extension-loader-patch.mjs";
import { patchPiGoogleLegacySchemaSource } from "./lib/pi-google-legacy-schema-patch.mjs";
import { PI_WEB_ACCESS_PATCH_TARGETS, patchPiWebAccessSource } from "./lib/pi-web-access-patch.mjs";
import { PI_SUBAGENTS_PATCH_TARGETS, patchPiSubagentsSource } from "./lib/pi-subagents-patch.mjs";
import { PI_SUBAGENTS_PATCH_TARGETS, patchPiSubagentsSource, stripPiSubagentBuiltinModelSource } from "./lib/pi-subagents-patch.mjs";
const here = dirname(fileURLToPath(import.meta.url));
const appRoot = resolve(here, "..");
@@ -286,15 +286,30 @@ function linkPointsTo(linkPath, targetPath) {
}
}
function ensureBundledPackageLinks(packageSpecs) {
if (!workspaceMatchesRuntime(packageSpecs)) return;
function listWorkspacePackageNames(root) {
if (!existsSync(root)) return [];
const names = [];
for (const entry of readdirSync(root, { withFileTypes: true })) {
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
if (entry.name.startsWith(".")) continue;
if (entry.name.startsWith("@")) {
const scopeRoot = resolve(root, entry.name);
for (const scopedEntry of readdirSync(scopeRoot, { withFileTypes: true })) {
if (!scopedEntry.isDirectory() && !scopedEntry.isSymbolicLink()) continue;
names.push(`${entry.name}/${scopedEntry.name}`);
}
continue;
}
names.push(entry.name);
}
return names;
}
for (const spec of packageSpecs) {
const packageName = parsePackageName(spec);
function linkBundledPackage(packageName) {
const sourcePath = resolve(workspaceRoot, packageName);
const targetPath = resolve(globalNodeModulesRoot, packageName);
if (!existsSync(sourcePath)) continue;
if (linkPointsTo(targetPath, sourcePath)) continue;
if (!existsSync(sourcePath)) return false;
if (linkPointsTo(targetPath, sourcePath)) return false;
try {
if (lstatSync(targetPath).isSymbolicLink()) {
rmSync(targetPath, { force: true });
@@ -302,12 +317,22 @@ function ensureBundledPackageLinks(packageSpecs) {
rmSync(targetPath, { recursive: true, force: true });
}
} catch {}
if (existsSync(targetPath)) continue;
if (existsSync(targetPath)) return false;
ensureParentDir(targetPath);
try {
symlinkSync(sourcePath, targetPath, process.platform === "win32" ? "junction" : "dir");
} catch {}
return true;
} catch {
return false;
}
}
function ensureBundledPackageLinks(packageSpecs) {
if (!workspaceMatchesRuntime(packageSpecs)) return;
for (const packageName of listWorkspacePackageNames(workspaceRoot)) {
linkBundledPackage(packageName);
}
}
@@ -454,6 +479,19 @@ if (existsSync(piSubagentsRoot)) {
writeFileSync(entryPath, patched, "utf8");
}
}
const builtinAgentsRoot = resolve(piSubagentsRoot, "agents");
if (existsSync(builtinAgentsRoot)) {
for (const entry of readdirSync(builtinAgentsRoot, { withFileTypes: true })) {
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
const entryPath = resolve(builtinAgentsRoot, entry.name);
const source = readFileSync(entryPath, "utf8");
const patched = stripPiSubagentBuiltinModelSource(source);
if (patched !== source) {
writeFileSync(entryPath, patched, "utf8");
}
}
}
}
if (packageJsonPath && existsSync(packageJsonPath)) {

View File

@@ -1,26 +1,44 @@
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { createHash } from "node:crypto";
import { resolve } from "node:path";
import { spawnSync } from "node:child_process";
import { stripPiSubagentBuiltinModelSource } from "./lib/pi-subagents-patch.mjs";
const appRoot = resolve(import.meta.dirname, "..");
const settingsPath = resolve(appRoot, ".feynman", "settings.json");
const packageJsonPath = resolve(appRoot, "package.json");
const packageLockPath = resolve(appRoot, "package-lock.json");
const feynmanDir = resolve(appRoot, ".feynman");
const workspaceDir = resolve(appRoot, ".feynman", "npm");
const workspaceNodeModulesDir = resolve(workspaceDir, "node_modules");
const manifestPath = resolve(workspaceDir, ".runtime-manifest.json");
const workspacePackageJsonPath = resolve(workspaceDir, "package.json");
const workspaceArchivePath = resolve(feynmanDir, "runtime-workspace.tgz");
const PRUNE_VERSION = 3;
const PRUNE_VERSION = 4;
const PINNED_RUNTIME_PACKAGES = [
"@mariozechner/pi-agent-core",
"@mariozechner/pi-ai",
"@mariozechner/pi-coding-agent",
"@mariozechner/pi-tui",
];
function readPackageSpecs() {
const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
if (!Array.isArray(settings.packages)) {
return [];
const packageSpecs = Array.isArray(settings.packages)
? settings.packages
.filter((value) => typeof value === "string" && value.startsWith("npm:"))
.map((value) => value.slice(4))
: [];
for (const packageName of PINNED_RUNTIME_PACKAGES) {
const version = readLockedPackageVersion(packageName);
if (version) {
packageSpecs.push(`${packageName}@${version}`);
}
}
return settings.packages
.filter((value) => typeof value === "string" && value.startsWith("npm:"))
.map((value) => value.slice(4));
return Array.from(new Set(packageSpecs));
}
function parsePackageName(spec) {
@@ -28,10 +46,41 @@ function parsePackageName(spec) {
return match?.[1] ?? spec;
}
function readLockedPackageVersion(packageName) {
if (!existsSync(packageLockPath)) {
return undefined;
}
try {
const lockfile = JSON.parse(readFileSync(packageLockPath, "utf8"));
const entry = lockfile.packages?.[`node_modules/${packageName}`];
return typeof entry?.version === "string" ? entry.version : undefined;
} catch {
return undefined;
}
}
function arraysMatch(left, right) {
return left.length === right.length && left.every((value, index) => value === right[index]);
}
function hashFile(path) {
if (!existsSync(path)) {
return null;
}
return createHash("sha256").update(readFileSync(path)).digest("hex");
}
function getRuntimeInputHash() {
const hash = createHash("sha256");
for (const path of [packageJsonPath, packageLockPath, settingsPath]) {
hash.update(path);
hash.update("\0");
hash.update(hashFile(path) ?? "missing");
hash.update("\0");
}
return hash.digest("hex");
}
function workspaceIsCurrent(packageSpecs) {
if (!existsSync(manifestPath) || !existsSync(workspaceNodeModulesDir)) {
return false;
@@ -42,6 +91,9 @@ function workspaceIsCurrent(packageSpecs) {
if (!Array.isArray(manifest.packageSpecs) || !arraysMatch(manifest.packageSpecs, packageSpecs)) {
return false;
}
if (manifest.runtimeInputHash !== getRuntimeInputHash()) {
return false;
}
if (
manifest.nodeAbi !== process.versions.modules ||
manifest.platform !== process.platform ||
@@ -72,6 +124,17 @@ function writeWorkspacePackageJson() {
);
}
function childNpmInstallEnv() {
return {
...process.env,
// `npm pack --dry-run` exports dry-run config to lifecycle scripts. The
// vendored runtime workspace must still install real node_modules so the
// publish artifact can be validated without poisoning the archive.
npm_config_dry_run: "false",
NPM_CONFIG_DRY_RUN: "false",
};
}
function prepareWorkspace(packageSpecs) {
rmSync(workspaceDir, { recursive: true, force: true });
mkdirSync(workspaceDir, { recursive: true });
@@ -84,9 +147,9 @@ function prepareWorkspace(packageSpecs) {
const result = spawnSync(
process.env.npm_execpath ? process.execPath : "npm",
process.env.npm_execpath
? [process.env.npm_execpath, "install", "--prefer-offline", "--no-audit", "--no-fund", "--loglevel", "error", "--prefix", workspaceDir, ...packageSpecs]
: ["install", "--prefer-offline", "--no-audit", "--no-fund", "--loglevel", "error", "--prefix", workspaceDir, ...packageSpecs],
{ stdio: "inherit" },
? [process.env.npm_execpath, "install", "--prefer-offline", "--no-audit", "--no-fund", "--no-dry-run", "--legacy-peer-deps", "--loglevel", "error", "--prefix", workspaceDir, ...packageSpecs]
: ["install", "--prefer-offline", "--no-audit", "--no-fund", "--no-dry-run", "--legacy-peer-deps", "--loglevel", "error", "--prefix", workspaceDir, ...packageSpecs],
{ stdio: "inherit", env: childNpmInstallEnv() },
);
if (result.status !== 0) {
process.exit(result.status ?? 1);
@@ -99,6 +162,7 @@ function writeManifest(packageSpecs) {
JSON.stringify(
{
packageSpecs,
runtimeInputHash: getRuntimeInputHash(),
generatedAt: new Date().toISOString(),
nodeAbi: process.versions.modules,
nodeVersion: process.version,
@@ -122,6 +186,25 @@ function pruneWorkspace() {
}
}
function stripBundledPiSubagentModelPins() {
const agentsRoot = resolve(workspaceNodeModulesDir, "pi-subagents", "agents");
if (!existsSync(agentsRoot)) {
return false;
}
let changed = false;
for (const entry of readdirSync(agentsRoot, { withFileTypes: true })) {
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
const entryPath = resolve(agentsRoot, entry.name);
const source = readFileSync(entryPath, "utf8");
const patched = stripPiSubagentBuiltinModelSource(source);
if (patched === source) continue;
writeFileSync(entryPath, patched, "utf8");
changed = true;
}
return changed;
}
function archiveIsCurrent() {
if (!existsSync(workspaceArchivePath) || !existsSync(manifestPath)) {
return false;
@@ -145,6 +228,10 @@ const packageSpecs = readPackageSpecs();
if (workspaceIsCurrent(packageSpecs)) {
console.log("[feynman] vendored runtime workspace already up to date");
if (stripBundledPiSubagentModelPins()) {
writeManifest(packageSpecs);
console.log("[feynman] stripped bundled pi-subagents model pins");
}
if (archiveIsCurrent()) {
process.exit(0);
}
@@ -157,6 +244,7 @@ if (workspaceIsCurrent(packageSpecs)) {
console.log("[feynman] preparing vendored runtime workspace...");
prepareWorkspace(packageSpecs);
pruneWorkspace();
stripBundledPiSubagentModelPins();
writeManifest(packageSpecs);
createWorkspaceArchive();
console.log("[feynman] vendored runtime workspace ready");

View File

@@ -558,6 +558,7 @@ export async function main(): Promise<void> {
normalizeFeynmanSettings(feynmanSettingsPath, bundledSettingsPath, thinkingLevel, feynmanAuthPath);
}
const workflowCommandNames = new Set(readPromptSpecs(appRoot).filter((s) => s.topLevelCli).map((s) => s.name));
await launchPiChat({
appRoot,
workingDir,
@@ -568,6 +569,6 @@ export async function main(): Promise<void> {
thinkingLevel,
explicitModelSpec,
oneShotPrompt: values.prompt,
initialPrompt: resolveInitialPrompt(command, rest, values.prompt, new Set(readPromptSpecs(appRoot).filter((s) => s.topLevelCli).map((s) => s.name))),
initialPrompt: resolveInitialPrompt(command, rest, values.prompt, workflowCommandNames),
});
}

View File

@@ -48,6 +48,7 @@ const PROVIDER_LABELS: Record<string, string> = {
huggingface: "Hugging Face",
"amazon-bedrock": "Amazon Bedrock",
"azure-openai-responses": "Azure OpenAI Responses",
litellm: "LiteLLM Proxy",
};
const RESEARCH_MODEL_PREFERENCES = [

View File

@@ -83,6 +83,8 @@ const API_KEY_PROVIDERS: ApiKeyProviderInfo[] = [
{ id: "openai", label: "OpenAI Platform API", envVar: "OPENAI_API_KEY" },
{ id: "anthropic", label: "Anthropic API", envVar: "ANTHROPIC_API_KEY" },
{ id: "google", label: "Google Gemini API", envVar: "GEMINI_API_KEY" },
{ id: "lm-studio", label: "LM Studio (local OpenAI-compatible server)" },
{ id: "litellm", label: "LiteLLM Proxy (OpenAI-compatible gateway)" },
{ id: "__custom__", label: "Custom provider (local/self-hosted/proxy)" },
{ id: "amazon-bedrock", label: "Amazon Bedrock (AWS credential chain)" },
{ id: "openrouter", label: "OpenRouter", envVar: "OPENROUTER_API_KEY" },
@@ -126,13 +128,24 @@ export function resolveModelProviderForCommand(
return undefined;
}
function apiKeyProviderHint(provider: ApiKeyProviderInfo): string {
if (provider.id === "__custom__") {
return "Ollama, vLLM, LM Studio, proxies";
}
if (provider.id === "lm-studio") {
return "http://localhost:1234/v1";
}
if (provider.id === "litellm") {
return "http://localhost:4000/v1";
}
return provider.envVar ?? provider.id;
}
async function selectApiKeyProvider(): Promise<ApiKeyProviderInfo | undefined> {
const options: PromptSelectOption<ApiKeyProviderInfo | "cancel">[] = API_KEY_PROVIDERS.map((provider) => ({
value: provider,
label: provider.label,
hint: provider.id === "__custom__"
? "Ollama, vLLM, LM Studio, proxies"
: provider.envVar ?? provider.id,
hint: apiKeyProviderHint(provider),
}));
options.push({ value: "cancel", label: "Cancel" });
@@ -362,6 +375,103 @@ async function promptCustomProviderSetup(): Promise<CustomProviderSetup | undefi
return { providerId, modelIds, baseUrl, api, apiKeyConfig, authHeader };
}
async function promptLmStudioProviderSetup(): Promise<CustomProviderSetup | undefined> {
printSection("LM Studio");
printInfo("Start the LM Studio local server first, then load a model.");
const baseUrlRaw = await promptText("Base URL", "http://localhost:1234/v1");
const { baseUrl } = normalizeCustomProviderBaseUrl("openai-completions", baseUrlRaw);
if (!baseUrl) {
printWarning("Base URL is required.");
return undefined;
}
const detectedModelIds = await bestEffortFetchOpenAiModelIds(baseUrl, "lm-studio", false);
let modelIdsDefault = "local-model";
if (detectedModelIds && detectedModelIds.length > 0) {
const sample = detectedModelIds.slice(0, 10).join(", ");
printInfo(`Detected LM Studio models: ${sample}${detectedModelIds.length > 10 ? ", ..." : ""}`);
modelIdsDefault = detectedModelIds[0]!;
} else {
printInfo("No models detected from /models. Enter the exact model id shown in LM Studio.");
}
const modelIdsRaw = await promptText("Model id(s) (comma-separated)", modelIdsDefault);
const modelIds = normalizeModelIds(modelIdsRaw);
if (modelIds.length === 0) {
printWarning("At least one model id is required.");
return undefined;
}
return {
providerId: "lm-studio",
modelIds,
baseUrl,
api: "openai-completions",
apiKeyConfig: "lm-studio",
authHeader: false,
};
}
async function promptLiteLlmProviderSetup(): Promise<CustomProviderSetup | undefined> {
printSection("LiteLLM Proxy");
printInfo("Start the LiteLLM proxy first. Feynman uses the OpenAI-compatible chat-completions API.");
const baseUrlRaw = await promptText("Base URL", "http://localhost:4000/v1");
const { baseUrl } = normalizeCustomProviderBaseUrl("openai-completions", baseUrlRaw);
if (!baseUrl) {
printWarning("Base URL is required.");
return undefined;
}
const keyChoices = [
"Yes (use LITELLM_MASTER_KEY and send Authorization: Bearer <key>)",
"No (proxy runs without authentication)",
"Cancel",
];
const keySelection = await promptChoice("Is the proxy protected by a master key?", keyChoices, 0);
if (keySelection >= 2) {
return undefined;
}
const hasKey = keySelection === 0;
const apiKeyConfig = hasKey ? "LITELLM_MASTER_KEY" : "local";
const authHeader = hasKey;
if (hasKey) {
printInfo("Set LITELLM_MASTER_KEY in your shell or .env before using Feynman.");
}
const resolvedKey = hasKey ? await resolveApiKeyConfig(apiKeyConfig) : apiKeyConfig;
const detectedModelIds = resolvedKey
? await bestEffortFetchOpenAiModelIds(baseUrl, resolvedKey, authHeader)
: undefined;
let modelIdsDefault = "gpt-4";
if (detectedModelIds && detectedModelIds.length > 0) {
const sample = detectedModelIds.slice(0, 10).join(", ");
printInfo(`Detected LiteLLM models: ${sample}${detectedModelIds.length > 10 ? ", ..." : ""}`);
modelIdsDefault = detectedModelIds[0]!;
} else {
printInfo("No models detected from /models. Enter the model id(s) from your LiteLLM config.");
}
const modelIdsRaw = await promptText("Model id(s) (comma-separated)", modelIdsDefault);
const modelIds = normalizeModelIds(modelIdsRaw);
if (modelIds.length === 0) {
printWarning("At least one model id is required.");
return undefined;
}
return {
providerId: "litellm",
modelIds,
baseUrl,
api: "openai-completions",
apiKeyConfig,
authHeader,
};
}
async function verifyCustomProvider(setup: CustomProviderSetup, authPath: string): Promise<void> {
const registry = createModelRegistry(authPath);
const modelsError = registry.getError();
@@ -548,6 +658,56 @@ async function configureApiKeyProvider(authPath: string, providerId?: string): P
return configureBedrockProvider(authPath);
}
if (provider.id === "lm-studio") {
const setup = await promptLmStudioProviderSetup();
if (!setup) {
printInfo("LM Studio setup cancelled.");
return false;
}
const modelsJsonPath = getModelsJsonPath(authPath);
const result = upsertProviderConfig(modelsJsonPath, setup.providerId, {
baseUrl: setup.baseUrl,
apiKey: setup.apiKeyConfig,
api: setup.api,
authHeader: setup.authHeader,
models: setup.modelIds.map((id) => ({ id })),
});
if (!result.ok) {
printWarning(result.error);
return false;
}
printSuccess("Saved LM Studio provider.");
await verifyCustomProvider(setup, authPath);
return true;
}
if (provider.id === "litellm") {
const setup = await promptLiteLlmProviderSetup();
if (!setup) {
printInfo("LiteLLM setup cancelled.");
return false;
}
const modelsJsonPath = getModelsJsonPath(authPath);
const result = upsertProviderConfig(modelsJsonPath, setup.providerId, {
baseUrl: setup.baseUrl,
apiKey: setup.apiKeyConfig,
api: setup.api,
authHeader: setup.authHeader,
models: setup.modelIds.map((id) => ({ id })),
});
if (!result.ok) {
printWarning(result.error);
return false;
}
printSuccess("Saved LiteLLM provider.");
await verifyCustomProvider(setup, authPath);
return true;
}
if (provider.id === "__custom__") {
const setup = await promptCustomProviderSetup();
if (!setup) {

View File

@@ -1,5 +1,5 @@
import { spawn } from "node:child_process";
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join, resolve } from "node:path";
@@ -169,6 +169,15 @@ function resolvePackageManagerCommand(settingsManager: SettingsManager): { comma
return { command: executable, args };
}
function childPackageManagerEnv(): NodeJS.ProcessEnv {
return {
...process.env,
PATH: getPathWithCurrentNode(process.env.PATH),
npm_config_dry_run: "false",
NPM_CONFIG_DRY_RUN: "false",
};
}
async function runPackageManagerInstall(
settingsManager: SettingsManager,
workingDir: string,
@@ -207,10 +216,7 @@ async function runPackageManagerInstall(
const child = spawn(packageManagerCommand.command, args, {
cwd: scope === "user" ? agentDir : workingDir,
stdio: ["ignore", "pipe", "pipe"],
env: {
...process.env,
PATH: getPathWithCurrentNode(process.env.PATH),
},
env: childPackageManagerEnv(),
});
child.stdout?.on("data", (chunk) => relayFilteredOutput(chunk, process.stdout));
@@ -427,6 +433,28 @@ function packageNameToPath(root: string, packageName: string): string {
return resolve(root, packageName);
}
function listBundledWorkspacePackageNames(root: string): string[] {
if (!existsSync(root)) {
return [];
}
const names: string[] = [];
for (const entry of readdirSync(root, { withFileTypes: true })) {
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
if (entry.name.startsWith(".")) continue;
if (entry.name.startsWith("@")) {
const scopeRoot = resolve(root, entry.name);
for (const scopedEntry of readdirSync(scopeRoot, { withFileTypes: true })) {
if (!scopedEntry.isDirectory() && !scopedEntry.isSymbolicLink()) continue;
names.push(`${entry.name}/${scopedEntry.name}`);
}
continue;
}
names.push(entry.name);
}
return names;
}
function packageDependencyExists(packagePath: string, globalNodeModulesRoot: string, dependency: string): boolean {
return existsSync(packageNameToPath(resolve(packagePath, "node_modules"), dependency)) ||
existsSync(packageNameToPath(globalNodeModulesRoot, dependency));
@@ -464,6 +492,23 @@ function replaceBrokenPackageWithBundledCopy(targetPath: string, bundledPackageP
return true;
}
function seedBundledPackage(globalNodeModulesRoot: string, bundledNodeModulesRoot: string, packageName: string): boolean {
const bundledPackagePath = resolve(bundledNodeModulesRoot, packageName);
if (!existsSync(bundledPackagePath)) {
return false;
}
const targetPath = resolve(globalNodeModulesRoot, packageName);
if (replaceBrokenPackageWithBundledCopy(targetPath, bundledPackagePath, globalNodeModulesRoot)) {
return true;
}
if (!existsSync(targetPath)) {
linkDirectory(targetPath, bundledPackagePath);
return true;
}
return false;
}
export function seedBundledWorkspacePackages(
agentDir: string,
appRoot: string,
@@ -476,6 +521,10 @@ export function seedBundledWorkspacePackages(
const globalNodeModulesRoot = resolve(getFeynmanNpmPrefixPath(agentDir), "lib", "node_modules");
const seeded: string[] = [];
const bundledPackageNames = listBundledWorkspacePackageNames(bundledNodeModulesRoot);
for (const packageName of bundledPackageNames) {
seedBundledPackage(globalNodeModulesRoot, bundledNodeModulesRoot, packageName);
}
for (const source of sources) {
if (shouldSkipNativeSource(source)) continue;
@@ -483,16 +532,8 @@ export function seedBundledWorkspacePackages(
const parsed = parseNpmSource(source);
if (!parsed) continue;
const bundledPackagePath = resolve(bundledNodeModulesRoot, parsed.name);
if (!existsSync(bundledPackagePath)) continue;
const targetPath = resolve(globalNodeModulesRoot, parsed.name);
if (replaceBrokenPackageWithBundledCopy(targetPath, bundledPackagePath, globalNodeModulesRoot)) {
seeded.push(source);
continue;
}
if (!existsSync(targetPath)) {
linkDirectory(targetPath, bundledPackagePath);
if (pathsMatchSymlinkTarget(targetPath, resolve(bundledNodeModulesRoot, parsed.name))) {
seeded.push(source);
}
}

View File

@@ -57,3 +57,45 @@ test("research writing prompts forbid fabricated results and unproven figures",
assert.match(draftPrompt, /placeholder or proposed experimental plan/i);
assert.match(draftPrompt, /source-backed quantitative data/i);
});
test("deepresearch workflow requires durable artifacts even when blocked", () => {
const systemPrompt = readFileSync(join(repoRoot, ".feynman", "SYSTEM.md"), "utf8");
const deepResearchPrompt = readFileSync(join(repoRoot, "prompts", "deepresearch.md"), "utf8");
assert.match(systemPrompt, /Do not claim you are only a static model/i);
assert.match(systemPrompt, /write the requested durable artifact/i);
assert.match(deepResearchPrompt, /Do not stop after planning/i);
assert.match(deepResearchPrompt, /not a request to explain or implement/i);
assert.match(deepResearchPrompt, /Do not answer by describing the protocol/i);
assert.match(deepResearchPrompt, /degraded mode/i);
assert.match(deepResearchPrompt, /Verification: BLOCKED/i);
assert.match(deepResearchPrompt, /Never end with only an explanation in chat/i);
});
test("workflow prompts do not introduce implicit confirmation gates", () => {
const workflowPrompts = [
"audit.md",
"compare.md",
"deepresearch.md",
"draft.md",
"lit.md",
"review.md",
"summarize.md",
"watch.md",
];
const bannedConfirmationGates = [
/Do you want to proceed/i,
/Wait for confirmation/i,
/wait for user confirmation/i,
/give them a brief chance/i,
/request changes before proceeding/i,
];
for (const fileName of workflowPrompts) {
const content = readFileSync(join(repoRoot, "prompts", fileName), "utf8");
assert.match(content, /continue (immediately|automatically)/i, `${fileName} should keep running after planning`);
for (const pattern of bannedConfirmationGates) {
assert.doesNotMatch(content, pattern, `${fileName} contains confirmation gate ${pattern}`);
}
}
});

View File

@@ -79,6 +79,24 @@ test("resolveModelProviderForCommand falls back to API-key providers when OAuth
assert.equal(resolved?.id, "google");
});
test("resolveModelProviderForCommand supports LM Studio as a first-class local provider", () => {
const authPath = createAuthPath({});
const resolved = resolveModelProviderForCommand(authPath, "lm-studio");
assert.equal(resolved?.kind, "api-key");
assert.equal(resolved?.id, "lm-studio");
});
test("resolveModelProviderForCommand supports LiteLLM as a first-class proxy provider", () => {
const authPath = createAuthPath({});
const resolved = resolveModelProviderForCommand(authPath, "litellm");
assert.equal(resolved?.kind, "api-key");
assert.equal(resolved?.id, "litellm");
});
test("resolveModelProviderForCommand prefers OAuth when a provider supports both auth modes", () => {
const authPath = createAuthPath({});

View File

@@ -30,3 +30,45 @@ test("upsertProviderConfig creates models.json and merges provider config", () =
assert.equal(parsed.providers.custom.authHeader, true);
assert.deepEqual(parsed.providers.custom.models, [{ id: "llama3.1:8b" }]);
});
test("upsertProviderConfig writes LiteLLM proxy config with master key", () => {
const dir = mkdtempSync(join(tmpdir(), "feynman-litellm-"));
const modelsPath = join(dir, "models.json");
const result = upsertProviderConfig(modelsPath, "litellm", {
baseUrl: "http://localhost:4000/v1",
apiKey: "LITELLM_MASTER_KEY",
api: "openai-completions",
authHeader: true,
models: [{ id: "gpt-4o" }],
});
assert.deepEqual(result, { ok: true });
const parsed = JSON.parse(readFileSync(modelsPath, "utf8")) as any;
assert.equal(parsed.providers.litellm.baseUrl, "http://localhost:4000/v1");
assert.equal(parsed.providers.litellm.apiKey, "LITELLM_MASTER_KEY");
assert.equal(parsed.providers.litellm.api, "openai-completions");
assert.equal(parsed.providers.litellm.authHeader, true);
assert.deepEqual(parsed.providers.litellm.models, [{ id: "gpt-4o" }]);
});
test("upsertProviderConfig writes LiteLLM proxy config without master key", () => {
const dir = mkdtempSync(join(tmpdir(), "feynman-litellm-"));
const modelsPath = join(dir, "models.json");
const result = upsertProviderConfig(modelsPath, "litellm", {
baseUrl: "http://localhost:4000/v1",
apiKey: "local",
api: "openai-completions",
authHeader: false,
models: [{ id: "llama3" }],
});
assert.deepEqual(result, { ok: true });
const parsed = JSON.parse(readFileSync(modelsPath, "utf8")) as any;
assert.equal(parsed.providers.litellm.baseUrl, "http://localhost:4000/v1");
assert.equal(parsed.providers.litellm.apiKey, "local");
assert.equal(parsed.providers.litellm.api, "openai-completions");
assert.equal(parsed.providers.litellm.authHeader, false);
assert.deepEqual(parsed.providers.litellm.models, [{ id: "llama3" }]);
});

View File

@@ -101,6 +101,7 @@ test("seedBundledWorkspacePackages repairs broken existing bundled packages", ()
assert.deepEqual(seeded, ["npm:pi-markdown-preview"]);
assert.equal(lstatSync(existingPackageDir).isSymbolicLink(), true);
assert.equal(lstatSync(resolve(homeRoot, "npm-global", "lib", "node_modules", "puppeteer-core")).isSymbolicLink(), true);
assert.equal(
readFileSync(resolve(existingPackageDir, "package.json"), "utf8").includes('"version": "1.0.0"'),
true,
@@ -187,6 +188,46 @@ test("installPackageSources skips native packages on unsupported Node majors bef
}
});
test("installPackageSources disables inherited npm dry-run config for child installs", async () => {
const root = mkdtempSync(join(tmpdir(), "feynman-package-ops-"));
const workingDir = resolve(root, "project");
const agentDir = resolve(root, "agent");
const markerPath = resolve(root, "install-env-ok.txt");
mkdirSync(workingDir, { recursive: true });
const scriptPath = writeFakeNpmScript(root, [
`import { writeFileSync } from "node:fs";`,
`if (process.env.npm_config_dry_run !== "false" || process.env.NPM_CONFIG_DRY_RUN !== "false") process.exit(42);`,
`writeFileSync(${JSON.stringify(markerPath)}, "ok\\n", "utf8");`,
"process.exit(0);",
].join("\n"));
writeSettings(agentDir, {
npmCommand: [process.execPath, scriptPath],
});
const originalLower = process.env.npm_config_dry_run;
const originalUpper = process.env.NPM_CONFIG_DRY_RUN;
process.env.npm_config_dry_run = "true";
process.env.NPM_CONFIG_DRY_RUN = "true";
try {
const result = await installPackageSources(workingDir, agentDir, ["npm:test-package"]);
assert.deepEqual(result.installed, ["npm:test-package"]);
assert.equal(existsSync(markerPath), true);
} finally {
if (originalLower === undefined) {
delete process.env.npm_config_dry_run;
} else {
process.env.npm_config_dry_run = originalLower;
}
if (originalUpper === undefined) {
delete process.env.NPM_CONFIG_DRY_RUN;
} else {
process.env.NPM_CONFIG_DRY_RUN = originalUpper;
}
}
});
test("updateConfiguredPackages batches multiple npm updates into a single install per scope", async () => {
const root = mkdtempSync(join(tmpdir(), "feynman-package-ops-"));
const workingDir = resolve(root, "project");
@@ -202,6 +243,10 @@ test("updateConfiguredPackages batches multiple npm updates into a single instal
` console.log(resolve(${JSON.stringify(root)}, "npm-global", "lib", "node_modules"));`,
` process.exit(0);`,
`}`,
`if (args.length >= 4 && args[0] === "view" && args[2] === "version" && args[3] === "--json") {`,
` console.log(JSON.stringify("2.0.0"));`,
` process.exit(0);`,
`}`,
`appendFileSync(${JSON.stringify(logPath)}, JSON.stringify(args) + "\\n", "utf8");`,
"process.exit(0);",
].join("\n"));
@@ -217,7 +262,7 @@ test("updateConfiguredPackages batches multiple npm updates into a single instal
globalThis.fetch = (async () => ({
ok: true,
json: async () => ({ version: "2.0.0" }),
})) as typeof fetch;
})) as unknown as typeof fetch;
try {
const result = await updateConfiguredPackages(workingDir, agentDir);
@@ -249,6 +294,10 @@ test("updateConfiguredPackages skips native package updates on unsupported Node
` console.log(resolve(${JSON.stringify(root)}, "npm-global", "lib", "node_modules"));`,
` process.exit(0);`,
`}`,
`if (args.length >= 4 && args[0] === "view" && args[2] === "version" && args[3] === "--json") {`,
` console.log(JSON.stringify("2.0.0"));`,
` process.exit(0);`,
`}`,
`appendFileSync(${JSON.stringify(logPath)}, JSON.stringify(args) + "\\n", "utf8");`,
"process.exit(0);",
].join("\n"));
@@ -265,7 +314,7 @@ test("updateConfiguredPackages skips native package updates on unsupported Node
globalThis.fetch = (async () => ({
ok: true,
json: async () => ({ version: "2.0.0" }),
})) as typeof fetch;
})) as unknown as typeof fetch;
Object.defineProperty(process.versions, "node", { value: "25.0.0", configurable: true });
try {

View File

@@ -1,7 +1,7 @@
import test from "node:test";
import assert from "node:assert/strict";
import { patchPiSubagentsSource } from "../scripts/lib/pi-subagents-patch.mjs";
import { patchPiSubagentsSource, stripPiSubagentBuiltinModelSource } from "../scripts/lib/pi-subagents-patch.mjs";
const CASES = [
{
@@ -140,3 +140,22 @@ test("patchPiSubagentsSource rewrites modern agents.ts discovery paths", () => {
assert.ok(!patched.includes('loadChainsFromDir(userDirNew, "user")'));
assert.ok(!patched.includes('fs.existsSync(userDirNew) ? userDirNew : userDirOld'));
});
test("stripPiSubagentBuiltinModelSource removes built-in model pins", () => {
const input = [
"---",
"name: researcher",
"description: Web researcher",
"model: anthropic/claude-sonnet-4-6",
"tools: read, web_search",
"---",
"",
"Body",
].join("\n");
const patched = stripPiSubagentBuiltinModelSource(input);
assert.ok(!patched.includes("model: anthropic/claude-sonnet-4-6"));
assert.match(patched, /name: researcher/);
assert.match(patched, /tools: read, web_search/);
});

View File

@@ -1544,9 +1544,9 @@
}
},
"node_modules/@hono/node-server": {
"version": "1.19.13",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz",
"integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==",
"version": "1.19.14",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz",
"integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==",
"license": "MIT",
"engines": {
"node": ">=18.14.1"
@@ -7998,9 +7998,9 @@
}
},
"node_modules/hono": {
"version": "4.12.12",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz",
"integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==",
"version": "4.12.14",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz",
"integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"

View File

@@ -36,8 +36,8 @@
},
"overrides": {
"@modelcontextprotocol/sdk": {
"@hono/node-server": "1.19.13",
"hono": "4.12.12"
"@hono/node-server": "1.19.14",
"hono": "4.12.14"
},
"router": {
"path-to-regexp": "8.4.2"

View File

@@ -261,7 +261,7 @@ This usually means the release exists, but not all platform bundles were uploade
Workarounds:
- try again after the release finishes publishing
- pass the latest published version explicitly, e.g.:
curl -fsSL https://feynman.is/install | bash -s -- 0.2.21
curl -fsSL https://feynman.is/install | bash -s -- 0.2.29
EOF
exit 1
fi

View File

@@ -110,7 +110,7 @@ This usually means the release exists, but not all platform bundles were uploade
Workarounds:
- try again after the release finishes publishing
- pass the latest published version explicitly, e.g.:
& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.21
& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.29
"@
}

View File

@@ -117,13 +117,13 @@ These installers download the bundled `skills/` and `prompts/` trees plus the re
The one-line installer already targets the latest tagged release. To pin an exact version, pass it explicitly:
```bash
curl -fsSL https://feynman.is/install | bash -s -- 0.2.21
curl -fsSL https://feynman.is/install | bash -s -- 0.2.29
```
On Windows:
```powershell
& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.21
& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.29
```
## Post-install setup

View File

@@ -52,9 +52,41 @@ Amazon Bedrock (AWS credential chain)
Feynman verifies the same AWS credential chain Pi uses at runtime, including `AWS_PROFILE`, `~/.aws` credentials/config, SSO, ECS/IRSA, and EC2 instance roles. Once that check passes, Bedrock models become available in `feynman model list` without needing a traditional API key.
### Local models: Ollama, LM Studio, vLLM
### Local models: LM Studio, LiteLLM, Ollama, vLLM
If you want to use a model running locally, choose the API-key flow and then select:
If you want to use LM Studio, start the LM Studio local server, load a model, choose the API-key flow, and then select:
```text
LM Studio (local OpenAI-compatible server)
```
The default settings are:
```text
Base URL: http://localhost:1234/v1
Authorization header: No
API key: lm-studio
```
Feynman attempts to read LM Studio's `/models` endpoint and prefill the loaded model id.
For LiteLLM, start the proxy, choose the API-key flow, and then select:
```text
LiteLLM Proxy (OpenAI-compatible gateway)
```
The default settings are:
```text
Base URL: http://localhost:4000/v1
API mode: openai-completions
Master key: optional, read from LITELLM_MASTER_KEY
```
Feynman attempts to read LiteLLM's `/models` endpoint and prefill model ids from the proxy config.
For Ollama, vLLM, or another OpenAI-compatible local server, choose:
```text
Custom provider (baseUrl + API key)
@@ -70,7 +102,7 @@ Model ids: llama3.1:8b
API key: local
```
That same custom-provider flow also works for other OpenAI-compatible local servers such as LM Studio or vLLM. After saving the provider, run:
After saving the provider, run:
```bash
feynman model list

View File

@@ -22,7 +22,9 @@ These are installed by default with every Feynman installation. They provide the
| `pi-mermaid` | Render Mermaid diagrams in the terminal UI |
| `@aliou/pi-processes` | Manage long-running experiments, background tasks, and log tailing |
| `pi-zotero` | Integration with Zotero for citation library management |
| `@kaiserlich-dev/pi-session-search` | Indexed session recall with summarize and resume UI. Powers session lookup |
| `pi-schedule-prompt` | Schedule recurring and deferred research jobs. Powers the `/watch` workflow |
| `@samfp/pi-memory` | Pi-managed preference and correction memory across sessions |
| `@tmustier/pi-ralph-wiggum` | Long-running agent loops for iterative development. Powers `/autoresearch` |
These packages are updated together when you run `feynman update`. You do not need to install them individually.
@@ -34,8 +36,6 @@ Install on demand with `feynman packages install <preset>`. These extend Feynman
| Package | Preset | Purpose |
| --- | --- | --- |
| `pi-generative-ui` | `generative-ui` | Interactive HTML-style widgets for rich output |
| `@kaiserlich-dev/pi-session-search` | `session-search` | Indexed session recall with summarize and resume UI. Powers `/search` |
| `@samfp/pi-memory` | `memory` | Automatic preference and correction memory across sessions |
## Installing and managing packages
@@ -48,17 +48,9 @@ feynman packages list
Install a specific optional preset:
```bash
feynman packages install session-search
feynman packages install memory
feynman packages install generative-ui
```
Install all optional packages at once:
```bash
feynman packages install all-extras
```
## Updating packages
Update all installed packages to their latest versions: