Overhaul Feynman harness: streamline agents, prompts, and extensions

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

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

View File

@@ -11,33 +11,26 @@ import {
login as loginAlpha,
logout as logoutAlpha,
} 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 { editConfig, printConfig, printConfigPath, printConfigValue, setConfigValue } from "./config/commands.js";
import { getConfiguredSessionDir, loadFeynmanConfig } from "./config/feynman-config.js";
import { ensureFeynmanHome, getFeynmanAgentDir, getFeynmanHome } from "./config/paths.js";
import { buildFeynmanSystemPrompt } from "./feynman-prompt.js";
import { ensureFeynmanHome, getDefaultSessionDir, getFeynmanAgentDir, getFeynmanHome } from "./config/paths.js";
import { launchPiChat } from "./pi/launch.js";
import { normalizeFeynmanSettings, normalizeThinkingLevel, parseModelSpec } from "./pi/settings.js";
import {
loginModelProvider,
logoutModelProvider,
printModelList,
printModelProviders,
printModelRecommendation,
printModelStatus,
setDefaultModelSpec,
} 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 { setupPreviewDependencies } from "./setup/preview.js";
import { runSetup } from "./setup/setup.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([
"ablate",
"audit",
"autoresearch",
"compare",
@@ -46,11 +39,7 @@ const RESEARCH_WORKFLOW_COMMANDS = new Set([
"jobs",
"lit",
"log",
"memo",
"reading",
"related",
"replicate",
"rebuttal",
"review",
"watch",
]);
@@ -64,40 +53,32 @@ function printHelp(): void {
printSection("Getting Started");
printInfo("feynman");
printInfo("feynman setup");
printInfo("feynman setup quick");
printInfo("feynman doctor");
printInfo("feynman model");
printInfo("feynman search");
printInfo("feynman search status");
printSection("Commands");
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 quick Configure only missing items");
printInfo("feynman setup Run the guided setup");
printInfo("feynman doctor Diagnose config, auth, Pi runtime, and preview deps");
printInfo("feynman status Show the current setup summary");
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 logout [id] Logout from a Pi OAuth model provider");
printInfo("feynman model set <spec> Set the default model");
printInfo("feynman search status Show web research provider status");
printInfo("feynman search set <id> Set web research provider");
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 update [package] Update installed packages (or a specific one)");
printInfo("feynman search status Show Pi web-access status and config path");
printInfo("feynman alpha login|logout|status");
printSection("Research Workflows");
printInfo("feynman lit <topic> Start the literature-review workflow");
printInfo("feynman review <artifact> Start the peer-review workflow");
printInfo("feynman audit <item> Start the paper/code audit 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 watch <topic> Start the recurring research watch workflow");
printInfo("feynman deepresearch <topic> Start a thorough source-heavy investigation");
printInfo("feynman lit <topic> Start the literature-review workflow");
printInfo("feynman review <artifact> Start the peer-review workflow");
printInfo("feynman audit <item> Start the paper/code audit workflow");
printInfo("feynman replicate <target> Start the replication 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");
printSection("Legacy Flags");
printInfo('--prompt "<text>" Run one prompt and exit');
@@ -148,71 +129,19 @@ async function handleAlphaCommand(action: string | undefined): Promise<void> {
throw new Error(`Unknown alpha command: ${action}`);
}
function handleConfigCommand(subcommand: string | undefined, args: string[]): void {
if (!subcommand || subcommand === "show") {
printConfig();
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);
async function handleModelCommand(subcommand: string | undefined, args: string[], feynmanSettingsPath: string, feynmanAuthPath: string): Promise<void> {
if (!subcommand || subcommand === "list") {
printModelList(feynmanSettingsPath, feynmanAuthPath);
return;
}
if (subcommand === "login") {
await loginModelProvider(authPath, args[0]);
await loginModelProvider(feynmanAuthPath, args[0]);
return;
}
if (subcommand === "logout") {
await logoutModelProvider(authPath, args[0]);
await logoutModelProvider(feynmanAuthPath, args[0]);
return;
}
@@ -221,33 +150,42 @@ async function handleModelCommand(subcommand: string | undefined, args: string[]
if (!spec) {
throw new Error("Usage: feynman model set <provider/model>");
}
setDefaultModelSpec(settingsPath, authPath, spec);
setDefaultModelSpec(feynmanSettingsPath, feynmanAuthPath, spec);
return;
}
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") {
printSearchStatus();
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}`);
}
@@ -317,9 +255,8 @@ export async function main(): Promise<void> {
return;
}
const config = loadFeynmanConfig();
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 feynmanAuthPath = resolve(feynmanAgentDir, "auth.json");
const thinkingLevel = normalizeThinkingLevel(values.thinking ?? process.env.FEYNMAN_THINKING) ?? "medium";
@@ -366,7 +303,6 @@ export async function main(): Promise<void> {
if (command === "setup") {
await runSetup({
section: rest[0],
settingsPath: feynmanSettingsPath,
bundledSettingsPath,
authPath: feynmanAuthPath,
@@ -400,18 +336,18 @@ export async function main(): Promise<void> {
return;
}
if (command === "config") {
handleConfigCommand(rest[0], rest.slice(1));
return;
}
if (command === "model") {
await handleModelCommand(rest[0], rest.slice(1), feynmanSettingsPath, feynmanAuthPath);
return;
}
if (command === "search") {
handleSearchCommand(rest[0], rest.slice(1));
handleSearchCommand(rest[0]);
return;
}
if (command === "update") {
await handleUpdateCommand(workingDir, feynmanAgentDir, rest[0]);
return;
}
@@ -439,6 +375,5 @@ export async function main(): Promise<void> {
explicitModelSpec,
oneShotPrompt: values.prompt,
initialPrompt: resolveInitialPrompt(command, rest, values.prompt),
systemPrompt: buildFeynmanSystemPrompt(),
});
}

View File

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

View File

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

View File

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

View File

@@ -1,63 +0,0 @@
export function buildFeynmanSystemPrompt(): string {
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.
Operating rules:
- Evidence over fluency.
- Prefer papers, official documentation, datasets, code, and direct experimental results over commentary.
- Separate observations from inferences.
- State uncertainty explicitly.
- 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.
- 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.
- 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.
- 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.
- Feynman ships project subagents for research work. Prefer the \`researcher\`, \`verifier\`, \`reviewer\`, and \`writer\` 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/citation pass.
- 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.
- 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.
- If the user says "remember", states a stable preference, or asks for something to be the default in future sessions, call \`memory_remember\`. Do not just say you will remember it.
- Session recall is package-backed. Use \`session_search\` when the user references prior work, asks what has been done before, or when you suspect relevant past context exists.
- Feynman is intended to support always-on research work. Use the scheduling package when recurring or deferred work is appropriate instead of telling the user to remember manually.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- Default artifact locations:
- outputs/ for reviews, reading lists, and summaries
- experiments/ for runnable experiment code and result logs
- notes/ for scratch notes and intermediate synthesis
- papers/ for polished paper-style drafts and writeups
- Default deliverables should include: summary, strongest evidence, disagreements or gaps, open questions, recommended next steps, and links to the source material.
Default workflow:
1. Clarify the research objective if needed.
2. Search for relevant primary sources.
3. Inspect the most relevant papers or materials directly.
4. Synthesize consensus, disagreements, and missing evidence.
5. Design and run experiments when they would resolve uncertainty.
6. Write the requested output artifact.
Style:
- Concise, skeptical, and explicit.
- Avoid fake certainty.
- Do not present unverified claims as facts.
- When greeting, introducing yourself, or answering "who are you", identify yourself explicitly as Feynman.`;
}

View File

@@ -6,21 +6,11 @@ import { promptChoice, promptText } from "../setup/prompts.js";
import { printInfo, printSection, printSuccess, printWarning } from "../ui/terminal.js";
import {
buildModelStatusSnapshotFromRecords,
chooseRecommendedModel,
getAvailableModelRecords,
getSupportedModelRecords,
type ModelStatusSnapshot,
} 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 {
return buildModelStatusSnapshotFromRecords(
getSupportedModelRecords(authPath),
@@ -94,50 +84,6 @@ export function getCurrentModelSpec(settingsPath: string): string | 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 {
const status = collectModelStatus(settingsPath, authPath);
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> {
const provider = providerId ? resolveOAuthProvider(authPath, providerId) : await selectOAuthProvider(authPath, "login");
if (!provider) {

View File

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

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

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

View File

@@ -1,49 +1,13 @@
import {
DEFAULT_WEB_SEARCH_PROVIDER,
WEB_SEARCH_PROVIDERS,
configureWebSearchProvider,
getWebSearchStatus,
loadFeynmanConfig,
saveFeynmanConfig,
type WebSearchProviderId,
} from "../config/feynman-config.js";
import { printInfo, printSuccess } from "../ui/terminal.js";
import { getPiWebAccessStatus } from "../pi/web-access.js";
import { printInfo } from "../ui/terminal.js";
export function printSearchStatus(): void {
const status = getWebSearchStatus(loadFeynmanConfig().webSearch ?? {});
printInfo(`Provider: ${status.selected.label}`);
printInfo(`Runtime route: ${status.selected.runtimeProvider}`);
const status = getPiWebAccessStatus();
printInfo("Managed by: pi-web-access");
printInfo(`Search route: ${status.routeLabel}`);
printInfo(`Request route: ${status.requestProvider}`);
printInfo(`Perplexity API configured: ${status.perplexityConfigured ? "yes" : "no"}`);
printInfo(`Gemini API configured: ${status.geminiApiConfigured ? "yes" : "no"}`);
printInfo(`Browser mode: ${status.browserHint}${status.chromeProfile ? ` (${status.chromeProfile})` : ""}`);
}
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}`);
printInfo(`Browser profile: ${status.chromeProfile ?? "default Chromium profile"}`);
printInfo(`Config path: ${status.configPath}`);
}

View File

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

View File

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

View File

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