import { dirname, resolve } from "node:path"; import { homedir } from "node:os"; import { fileURLToPath } from "node:url"; import { createInterface } from "node:readline/promises"; import { POLICIES, type ModelRosterConfig, type Policy } from "./types.js"; import { DEFAULT_CREDITS_PER_USD, DEFAULT_PRICE_CATALOG, defaultRosterConfig } from "./config/defaults.js"; import { MAX_ROSTER_TIERS } from "./models/roster.js"; import { access } from "node:fs/promises"; import { join } from "node:path"; import { FREE_UNAVAILABLE_MARKER } from "./security/profile-marker.js"; import { createProfiles, writeProfileLaunchers, type PrivateProfileDefaults } from "./security/profiles.js"; export interface InstallOptions extends PrivateProfileDefaults { models?: ModelRosterConfig; binDir?: string } export interface InstallSummary { privateDir: string; freeDir: string; binDir: string; binDirOnPath: boolean; privateModels: string[]; freeReady: boolean; } export const DEFAULT_PER_TASK_POLICY: Policy = "balanced"; const MODEL_LIST = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._:+-]+(?:,[A-Za-z0-9._-]+\/[A-Za-z0-9._:+-]+)*$/; function parseModelList(value: string, flag: string): string[] { if (!MODEL_LIST.test(value)) throw new Error(`${flag} must be a comma-separated list of provider/model ids`); const models = value.split(","); if (models.length > MAX_ROSTER_TIERS) throw new Error(`${flag} accepts at most ${MAX_ROSTER_TIERS} models`); if (new Set(models).size !== models.length) throw new Error(`${flag} must not repeat a model`); return models; } function parsePositive(value: string | undefined, flag: string): number { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`${flag} must be a positive number`); return parsed; } const FLAGS = ["--weekly-credit-budget", "--daily-credit-budget", "--per-task-policy", "--models", "--non-critical-models", "--bin-dir"] as const; const MISSING_BUDGET = `UltraPi needs a spending limit before it will run a task on its own. --weekly-credit-budget hard ceiling for a rolling week, in your provider's credit units --daily-credit-budget hard ceiling for one day; must be at or under the weekly one --per-task-policy

how much one task may spend: ${POLICIES.join(" | ")} (default: ${DEFAULT_PER_TASK_POLICY}) There is no default budget on purpose: a spend cap guessed on your behalf is worse than being asked once. A complete non-interactive install, with your own models: ultrapi --weekly-credit-budget 20 --daily-credit-budget 5 \\ --models anthropic/claude-haiku-4-5,anthropic/claude-sonnet-5,anthropic/claude-opus-5 \\ --non-critical-models anthropic/claude-haiku-4-5 Run it on a terminal instead and it will ask.`; function parsePartial(args: readonly string[]): Partial { const values = new Map(); for (let index = 0; index < args.length; index += 1) { const argument = args[index]!; const [flag, inline] = argument.split("=", 2); if (!(FLAGS as readonly string[]).includes(flag!)) throw new Error(`Unknown installer option ${flag}`); const value = inline ?? args[++index]; if (!value || value.startsWith("--")) throw new Error(`${flag} requires a value`); values.set(flag!, value); } const policy = values.get("--per-task-policy"); if (policy && !POLICIES.includes(policy as Policy)) throw new Error(`--per-task-policy must be one of: ${POLICIES.join(", ")}`); const tiers = values.has("--models") ? parseModelList(values.get("--models")!, "--models") : undefined; const nonCritical = values.has("--non-critical-models") ? parseModelList(values.get("--non-critical-models")!, "--non-critical-models") : undefined; if (nonCritical && !tiers) throw new Error("--non-critical-models requires --models"); for (const entry of nonCritical ?? []) if (!tiers!.includes(entry)) throw new Error(`--non-critical-models lists ${entry}, which is not in --models`); if (tiers && nonCritical && tiers.every((tier) => nonCritical.includes(tier))) throw new Error("--non-critical-models cannot cover every model in --models"); return { weeklyCreditBudget: values.has("--weekly-credit-budget") ? parsePositive(values.get("--weekly-credit-budget"), "--weekly-credit-budget") : undefined, dailyCreditBudget: values.has("--daily-credit-budget") ? parsePositive(values.get("--daily-credit-budget"), "--daily-credit-budget") : undefined, perTaskPolicy: policy as Policy | undefined, ...(tiers ? { models: { tiers, ...(nonCritical ? { nonCritical } : {}) } } : {}), ...(values.has("--bin-dir") ? { binDir: resolve(values.get("--bin-dir")!) } : {}), }; } /** * The two budgets stay required: a spend ceiling chosen for someone is the one default that * can cost them money. The policy does not gate spend on its own, so it takes the same value * a fresh config would have had anyway. */ export function parseInstallArgs(args: readonly string[]): InstallOptions { const parsed = parsePartial(args); if (parsed.weeklyCreditBudget === undefined || parsed.dailyCreditBudget === undefined) throw new Error(MISSING_BUDGET); return { ...parsed, perTaskPolicy: parsed.perTaskPolicy ?? DEFAULT_PER_TASK_POLICY } as InstallOptions; } /** * Prompts explain the value before asking for it, and offer a starting number the user can * accept by pressing Enter. That is still a decision they saw and made, which a silent default * would not be. */ export interface InstallerPrompt { interactive: boolean; question: (text: string) => Promise; write: (text: string) => void; close: () => void; } function terminalPrompt(): InstallerPrompt { const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY); if (!interactive) return { interactive, question: async () => "", write: () => {}, close: () => {} }; const prompt = createInterface({ input: process.stdin, output: process.stdout }); return { interactive, question: (text) => prompt.question(text), write: (text) => process.stdout.write(text), close: () => prompt.close() }; } export async function installerOptions(args: readonly string[], io: InstallerPrompt = terminalPrompt()): Promise { const parsed = parsePartial(args); if ((parsed.weeklyCreditBudget !== undefined && parsed.dailyCreditBudget !== undefined) || !io.interactive) { io.close(); return parseInstallArgs(args); } const prompt = io; const ask = async (question: string, fallback: string): Promise => (await prompt.question(question)).trim() || fallback; try { prompt.write("\nUltraPi install\n===============\nUltraPi refuses to run a task on its own until it knows what it may spend.\nPress Enter to accept the value in brackets.\n\n"); const weekly = parsed.weeklyCreditBudget ?? await ask("Weekly credit ceiling, in your provider's units [20]: ", "20"); const daily = parsed.dailyCreditBudget ?? await ask("Daily credit ceiling [5]: ", "5"); const policy = parsed.perTaskPolicy ?? await ask(`How much may a single task spend — ${POLICIES.join("/")} [${DEFAULT_PER_TASK_POLICY}]: `, DEFAULT_PER_TASK_POLICY); prompt.write(`\nWhich models may UltraPi use, cheapest first? Any provider works.\nPress Enter for the built-in example (${defaultRosterConfig("private").tiers.join(", ")}).\n`); const tiers = parsed.models?.tiers.join(",") ?? await ask("Models, comma separated []: ", ""); const nonCritical = parsed.models?.nonCritical?.join(",") ?? (tiers ? await ask("Of those, any that may scout but must not decide []: ", "") : ""); return parseInstallArgs([ "--weekly-credit-budget", String(weekly), "--daily-credit-budget", String(daily), "--per-task-policy", String(policy), ...(tiers ? ["--models", tiers] : []), ...(tiers && nonCritical ? ["--non-critical-models", nonCritical] : []), ...(parsed.binDir ? ["--bin-dir", parsed.binDir] : []), ]); } finally { prompt.close(); } } function onPath(directory: string, pathVariable = process.env.PATH ?? ""): boolean { return pathVariable.split(":").some((entry) => entry && resolve(entry) === resolve(directory)); } export async function installProfiles(agentDir = process.env.PI_CODING_AGENT_DIR ?? resolve(process.env.HOME ?? ".", ".pi", "agent"), options?: InstallOptions, home = homedir()): Promise { if (!options) throw new Error("Private profile budget defaults are required"); const extensionDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const roster = options.models ?? defaultRosterConfig("private"); const profiles = await createProfiles(agentDir, extensionDir, home, options, roster); const binDir = options.binDir ?? resolve(home, ".local", "bin"); await writeProfileLaunchers(binDir, profiles); const freeReady = await access(join(profiles.freeDir, FREE_UNAVAILABLE_MARKER)).then(() => false, () => true); return { privateDir: profiles.privateDir, freeDir: profiles.freeDir, binDir, binDirOnPath: onPath(binDir), privateModels: [...roster.tiers], freeReady }; } /** * What the user has to know to get to a first successful task, without opening the docs: where * the launcher is, whether their shell can find it, which models the profile will accept, and * the exact first command. */ export function installNextSteps(summary: InstallSummary): string { const lines = [ "", "UltraPi is installed.", ` private profile ${summary.privateDir}`, ` models ${summary.privateModels.join(", ")}`, ` free profile ${summary.freeReady ? summary.freeDir : `${summary.freeDir} (no critical free model found; the pi-free launcher fails closed)`}`, ` launchers ${join(summary.binDir, "pi-private")}, ${join(summary.binDir, "pi-free")}`, "", ]; if (!summary.binDirOnPath) { lines.push( `${summary.binDir} is not on your PATH, so \`pi-private\` will not be found yet. Add it:`, "", ` export PATH="${summary.binDir}:$PATH"`, "", "Put that line in your shell profile to make it stick, or run the launcher by full path.", "", ); } const uncatalogued = summary.privateModels.filter((model) => !DEFAULT_PRICE_CATALOG.models[model]); if (uncatalogued.length) { // The installer runs outside Pi and has no model registry, so it cannot price these here — it // can only say where the price will come from and how to check it. lines.push( `${uncatalogued.length} of your models are not in the shipped price catalogue:`, ...uncatalogued.map((model) => ` ${model}`), "", `Their credit price is derived from the cost the Pi model registry reports, at ${DEFAULT_CREDITS_PER_USD} credits per USD.`, "Run /ultra-config doctor to see the derived price per call, and how many calls one task buys", "at your policy. An expensive roster buys fewer turns; raise --per-task-policy if it is too few.", "", ); } lines.push( "Next:", ` 1. ${summary.binDirOnPath ? "pi-private" : join(summary.binDir, "pi-private")} # start Pi on the private profile and sign in there`, " 2. Open a project, then run:", " /ultra Investigate the failing checkout test and fix the proven root cause. Acceptance: npm test", "", "If a model is refused, it is not in this profile's roster. /ultra-config doctor says which models are declared.", "", ); return lines.join("\n"); } export async function runInstallCli(args: readonly string[]): Promise { const summary = await installProfiles(undefined, await installerOptions(args)); process.stdout.write(installNextSteps(summary)); } if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await runInstallCli(process.argv.slice(2));