import "dotenv/config"; import { existsSync, readFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { parseArgs } from "node:util"; import { fileURLToPath } from "node:url"; import { getUserName as getAlphaUserName, isLoggedIn as isAlphaLoggedIn, login as loginAlpha, logout as logoutAlpha, } from "@companion-ai/alpha-hub/lib"; import { SettingsManager } from "@mariozechner/pi-coding-agent"; import { syncBundledAssets } from "./bootstrap/sync.js"; import { ensureFeynmanHome, getDefaultSessionDir, getFeynmanAgentDir, getFeynmanHome } from "./config/paths.js"; import { launchPiChat } from "./pi/launch.js"; import { installPackageSources, updateConfiguredPackages } from "./pi/package-ops.js"; import { MAX_NATIVE_PACKAGE_NODE_MAJOR } from "./pi/package-presets.js"; import { CORE_PACKAGE_SOURCES, getOptionalPackagePresetSources, listOptionalPackagePresets } from "./pi/package-presets.js"; import { normalizeFeynmanSettings, normalizeThinkingLevel, parseModelSpec } from "./pi/settings.js"; import { applyFeynmanPackageManagerEnv } from "./pi/runtime.js"; import { getConfiguredServiceTier, normalizeServiceTier, setConfiguredServiceTier } from "./model/service-tier.js"; import { authenticateModelProvider, getCurrentModelSpec, loginModelProvider, logoutModelProvider, printModelList, setDefaultModelSpec, } from "./model/commands.js"; import { buildModelStatusSnapshotFromRecords, getAvailableModelRecords, getSupportedModelRecords } from "./model/catalog.js"; import { clearSearchConfig, printSearchStatus, setSearchProvider } from "./search/commands.js"; import type { PiWebSearchProvider } from "./pi/web-access.js"; import { runDoctor, runStatus } from "./setup/doctor.js"; import { setupPreviewDependencies } from "./setup/preview.js"; import { runSetup } from "./setup/setup.js"; import { ASH, printAsciiHeader, printInfo, printPanel, printSection, RESET, SAGE } from "./ui/terminal.js"; import { createModelRegistry } from "./model/registry.js"; import { cliCommandSections, formatCliWorkflowUsage, legacyFlags, readPromptSpecs, topLevelCommandNames, } from "../metadata/commands.mjs"; const TOP_LEVEL_COMMANDS = new Set(topLevelCommandNames); function printHelpLine(usage: string, description: string): void { const width = 30; const padding = Math.max(1, width - usage.length); console.log(` ${SAGE}${usage}${RESET}${" ".repeat(padding)}${ASH}${description}${RESET}`); } function printHelp(appRoot: string): void { const workflowCommands = readPromptSpecs(appRoot).filter( (command) => command.section === "Research Workflows" && command.topLevelCli, ); printAsciiHeader([ "Research-first agent shell built on Pi.", "Use `feynman setup` first if this is a new machine.", ]); printSection("Getting Started"); printInfo("feynman"); printInfo("feynman setup"); printInfo("feynman doctor"); printInfo("feynman model"); printInfo("feynman search status"); printSection("Commands"); for (const section of cliCommandSections) { for (const command of section.commands) { printHelpLine(command.usage, command.description); } } printSection("Research Workflows"); for (const command of workflowCommands) { printHelpLine(formatCliWorkflowUsage(command), command.description); } printSection("Legacy Flags"); for (const flag of legacyFlags) { printHelpLine(flag.usage, flag.description); } printSection("REPL"); printInfo("Inside the REPL, slash workflows come from the live prompt-template and extension command set."); } async function handleAlphaCommand(action: string | undefined): Promise { if (action === "login") { const result = await loginAlpha(); const name = result.userInfo && typeof result.userInfo === "object" && "name" in result.userInfo && typeof result.userInfo.name === "string" ? result.userInfo.name : getAlphaUserName(); console.log(name ? `alphaXiv login complete: ${name}` : "alphaXiv login complete"); return; } if (action === "logout") { logoutAlpha(); console.log("alphaXiv auth cleared"); return; } if (!action || action === "status") { if (isAlphaLoggedIn()) { const name = getAlphaUserName(); console.log(name ? `alphaXiv logged in as ${name}` : "alphaXiv logged in"); } else { console.log("alphaXiv not logged in"); } return; } throw new Error(`Unknown alpha command: ${action}`); } async function handleModelCommand(subcommand: string | undefined, args: string[], feynmanSettingsPath: string, feynmanAuthPath: string): Promise { if (!subcommand || subcommand === "list") { printModelList(feynmanSettingsPath, feynmanAuthPath); return; } if (subcommand === "login") { if (args[0]) { // Specific provider given - resolve OAuth vs API-key setup automatically await loginModelProvider(feynmanAuthPath, args[0], feynmanSettingsPath); } else { // No provider specified - show auth method choice await authenticateModelProvider(feynmanAuthPath, feynmanSettingsPath); } return; } if (subcommand === "logout") { await logoutModelProvider(feynmanAuthPath, args[0]); return; } if (subcommand === "set") { const spec = args[0]; if (!spec) { throw new Error("Usage: feynman model set "); } setDefaultModelSpec(feynmanSettingsPath, feynmanAuthPath, spec); return; } if (subcommand === "tier") { const requested = args[0]; if (!requested) { console.log(getConfiguredServiceTier(feynmanSettingsPath) ?? "not set"); return; } if (requested === "unset" || requested === "clear" || requested === "off") { setConfiguredServiceTier(feynmanSettingsPath, undefined); console.log("Cleared service tier override"); return; } const tier = normalizeServiceTier(requested); if (!tier) { throw new Error("Usage: feynman model tier "); } setConfiguredServiceTier(feynmanSettingsPath, tier); console.log(`Service tier set to ${tier}`); return; } throw new Error(`Unknown model command: ${subcommand}`); } async function handleUpdateCommand(workingDir: string, feynmanAgentDir: string, source?: string): Promise { try { const result = await updateConfiguredPackages(workingDir, feynmanAgentDir, source); if (result.updated.length === 0) { console.log("All packages up to date."); return; } for (const updatedSource of result.updated) { console.log(`Updated ${updatedSource}`); } for (const skippedSource of result.skipped) { console.log(`Skipped ${skippedSource} on Node ${process.versions.node} (native packages are only supported through Node ${MAX_NATIVE_PACKAGE_NODE_MAJOR}.x).`); } console.log("All packages up to date."); } catch (error) { const message = error instanceof Error ? error.message : String(error); if (message.includes("No supported package manager found")) { console.log("No package manager is available for live package updates."); console.log("If you installed the standalone app, rerun the installer to get newer bundled packages."); return; } throw error; } } async function handlePackagesCommand(subcommand: string | undefined, args: string[], workingDir: string, feynmanAgentDir: string): Promise { applyFeynmanPackageManagerEnv(feynmanAgentDir); const settingsManager = SettingsManager.create(workingDir, feynmanAgentDir); const configuredSources = new Set( settingsManager .getPackages() .map((entry) => (typeof entry === "string" ? entry : entry.source)) .filter((entry): entry is string => typeof entry === "string"), ); if (!subcommand || subcommand === "list") { printPanel("Feynman Packages", [ "Core packages are installed by default to keep first-run setup fast.", ]); printSection("Core"); for (const source of CORE_PACKAGE_SOURCES) { printInfo(source); } printSection("Optional"); for (const preset of listOptionalPackagePresets()) { const installed = preset.sources.every((source) => configuredSources.has(source)); printInfo(`${preset.name}${installed ? " (installed)" : ""} ${preset.description}`); } printInfo("Install with: feynman packages install "); return; } if (subcommand !== "install") { throw new Error(`Unknown packages command: ${subcommand}`); } const target = args[0]; if (!target) { throw new Error("Usage: feynman packages install "); } const sources = getOptionalPackagePresetSources(target); if (!sources) { throw new Error(`Unknown package preset: ${target}`); } const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const isStandaloneBundle = !existsSync(resolve(appRoot, ".feynman", "runtime-workspace.tgz")) && existsSync(resolve(appRoot, ".feynman", "npm")); if (target === "generative-ui" && process.platform === "darwin" && isStandaloneBundle) { console.log("The generative-ui preset is currently unavailable in the standalone macOS bundle."); console.log("Its native glimpseui dependency fails to compile reliably in that environment."); console.log("If you need generative-ui, install Feynman through npm instead of the standalone bundle."); return; } const pendingSources = sources.filter((source) => !configuredSources.has(source)); for (const source of sources) { if (configuredSources.has(source)) { console.log(`${source} already installed`); } } if (pendingSources.length === 0) { console.log("Optional packages installed."); return; } try { const result = await installPackageSources(workingDir, feynmanAgentDir, pendingSources, { persist: true }); for (const skippedSource of result.skipped) { console.log(`Skipped ${skippedSource} on Node ${process.versions.node} (native packages are only supported through Node ${MAX_NATIVE_PACKAGE_NODE_MAJOR}.x).`); } await settingsManager.flush(); console.log("Optional packages installed."); } catch (error) { const message = error instanceof Error ? error.message : String(error); if (message.includes("No supported package manager found")) { console.log("No package manager is available for optional package installs."); console.log("Install npm, pnpm, or bun, or rerun the standalone installer for bundled package updates."); return; } throw error; } } function handleSearchCommand(subcommand: string | undefined, args: string[]): void { if (!subcommand || subcommand === "status") { printSearchStatus(); return; } if (subcommand === "set") { const provider = args[0] as PiWebSearchProvider | undefined; const validProviders: PiWebSearchProvider[] = ["auto", "perplexity", "exa", "gemini"]; if (!provider || !validProviders.includes(provider)) { throw new Error("Usage: feynman search set [api-key]"); } setSearchProvider(provider, args[1]); return; } if (subcommand === "clear") { clearSearchConfig(); return; } throw new Error(`Unknown search command: ${subcommand}`); } function loadPackageVersion(appRoot: string): { version?: string } { try { return JSON.parse(readFileSync(resolve(appRoot, "package.json"), "utf8")) as { version?: string }; } catch { return {}; } } export function resolveInitialPrompt( command: string | undefined, rest: string[], oneShotPrompt: string | undefined, workflowCommands: Set, ): string | undefined { if (oneShotPrompt) { return oneShotPrompt; } if (!command) { return undefined; } if (command === "chat") { return rest.length > 0 ? rest.join(" ") : undefined; } if (workflowCommands.has(command)) { return [`/${command}`, ...rest].join(" ").trim(); } if (!TOP_LEVEL_COMMANDS.has(command)) { return [command, ...rest].join(" "); } return undefined; } export function shouldRunInteractiveSetup( explicitModelSpec: string | undefined, currentModelSpec: string | undefined, isInteractiveTerminal: boolean, authPath: string, ): boolean { if (explicitModelSpec || !isInteractiveTerminal) { return false; } const status = buildModelStatusSnapshotFromRecords( getSupportedModelRecords(authPath), getAvailableModelRecords(authPath), currentModelSpec, ); return !status.currentValid; } export async function main(): Promise { const here = dirname(fileURLToPath(import.meta.url)); const appRoot = resolve(here, ".."); const feynmanVersion = loadPackageVersion(appRoot).version; const bundledSettingsPath = resolve(appRoot, ".feynman", "settings.json"); const feynmanHome = getFeynmanHome(); const feynmanAgentDir = getFeynmanAgentDir(feynmanHome); ensureFeynmanHome(feynmanHome); syncBundledAssets(appRoot, feynmanAgentDir); const { values, positionals } = parseArgs({ args: process.argv.slice(2), allowPositionals: true, options: { cwd: { type: "string" }, doctor: { type: "boolean" }, help: { type: "boolean" }, version: { type: "boolean" }, "alpha-login": { type: "boolean" }, "alpha-logout": { type: "boolean" }, "alpha-status": { type: "boolean" }, mode: { type: "string" }, model: { type: "string" }, "new-session": { type: "boolean" }, prompt: { type: "string" }, "service-tier": { type: "string" }, "session-dir": { type: "string" }, "setup-preview": { type: "boolean" }, thinking: { type: "string" }, }, }); if (values.help) { printHelp(appRoot); return; } if (values.version) { if (feynmanVersion) { console.log(feynmanVersion); return; } throw new Error("Unable to determine the installed Feynman version."); } const workingDir = resolve(values.cwd ?? process.cwd()); 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"; normalizeFeynmanSettings(feynmanSettingsPath, bundledSettingsPath, thinkingLevel, feynmanAuthPath); if (values.doctor) { runDoctor({ settingsPath: feynmanSettingsPath, authPath: feynmanAuthPath, sessionDir, workingDir, appRoot, }); return; } if (values["setup-preview"]) { const result = setupPreviewDependencies(); console.log(result.message); return; } if (values["alpha-login"]) { await handleAlphaCommand("login"); return; } if (values["alpha-logout"]) { await handleAlphaCommand("logout"); return; } if (values["alpha-status"]) { await handleAlphaCommand("status"); return; } const [command, ...rest] = positionals; if (command === "help") { printHelp(appRoot); return; } if (command === "setup") { await runSetup({ settingsPath: feynmanSettingsPath, bundledSettingsPath, authPath: feynmanAuthPath, workingDir, sessionDir, appRoot, defaultThinkingLevel: thinkingLevel, }); return; } if (command === "doctor") { runDoctor({ settingsPath: feynmanSettingsPath, authPath: feynmanAuthPath, sessionDir, workingDir, appRoot, }); return; } if (command === "status") { runStatus({ settingsPath: feynmanSettingsPath, authPath: feynmanAuthPath, sessionDir, workingDir, appRoot, }); return; } if (command === "model") { await handleModelCommand(rest[0], rest.slice(1), feynmanSettingsPath, feynmanAuthPath); return; } if (command === "search") { handleSearchCommand(rest[0], rest.slice(1)); return; } if (command === "packages") { await handlePackagesCommand(rest[0], rest.slice(1), workingDir, feynmanAgentDir); return; } if (command === "update") { await handleUpdateCommand(workingDir, feynmanAgentDir, rest[0]); return; } if (command === "alpha") { await handleAlphaCommand(rest[0]); return; } const explicitModelSpec = values.model ?? process.env.FEYNMAN_MODEL; const explicitServiceTier = normalizeServiceTier(values["service-tier"] ?? process.env.FEYNMAN_SERVICE_TIER); const mode = values.mode; if (mode !== undefined && mode !== "text" && mode !== "json" && mode !== "rpc") { throw new Error("Unknown mode. Use text, json, or rpc."); } if ((values["service-tier"] ?? process.env.FEYNMAN_SERVICE_TIER) && !explicitServiceTier) { throw new Error("Unknown service tier. Use auto, default, flex, priority, or standard_only."); } if (explicitServiceTier) { process.env.FEYNMAN_SERVICE_TIER = explicitServiceTier; } if (explicitModelSpec) { const modelRegistry = createModelRegistry(feynmanAuthPath); const explicitModel = parseModelSpec(explicitModelSpec, modelRegistry); if (!explicitModel) { throw new Error(`Unknown model: ${explicitModelSpec}`); } } const currentModelSpec = getCurrentModelSpec(feynmanSettingsPath); if (shouldRunInteractiveSetup( explicitModelSpec, currentModelSpec, Boolean(process.stdin.isTTY && process.stdout.isTTY), feynmanAuthPath, )) { await runSetup({ settingsPath: feynmanSettingsPath, bundledSettingsPath, authPath: feynmanAuthPath, workingDir, sessionDir, appRoot, defaultThinkingLevel: thinkingLevel, }); if (!getCurrentModelSpec(feynmanSettingsPath)) { return; } normalizeFeynmanSettings(feynmanSettingsPath, bundledSettingsPath, thinkingLevel, feynmanAuthPath); } await launchPiChat({ appRoot, workingDir, sessionDir, feynmanAgentDir, feynmanVersion, mode, thinkingLevel, explicitModelSpec, oneShotPrompt: values.prompt, initialPrompt: resolveInitialPrompt(command, rest, values.prompt, new Set(readPromptSpecs(appRoot).filter((s) => s.topLevelCli).map((s) => s.name))), }); }