import { CliUsageError, csv, printData, rejectArgs, runCliAction, runtimeRequest, summaryLines, takeBooleanOption, takeFlag, takeIntegerOption, takeOption, type RuntimeApiDeps, } from "./runtime-api"; import { readFileSync } from "node:fs"; interface WebSearchModelOption { value: string; model: string; backend: "openai" | "anthropic"; authSlot?: boolean; } const USAGE = `Usage: ocx agent [status] [--json] ocx agent injection [--model ] [--effort ] [--prompt ] [--guidance ] [--json] ocx agent effort [--main ] [--subagent ] [--json] ocx agent subagents [model,model...] [--json] ocx agent authority [--file ] ocx agent roles [--file ] [--json] ocx agent fallback [model,model...] [--poll-ms <5000-600000>] [--json] ocx agent sidecar [--list] [--model ] [--backend web: vision:] [--reasoning ] [--max-descriptions ] [--json] ocx agent request-user-input [on|off] [--json]`; function clearable(value: string | undefined): string | null | undefined { return value === "-" ? null : value; } async function status(argv: string[], deps: RuntimeApiDeps): Promise { const args = [...argv]; const wantsJson = takeFlag(args, "--json"); rejectArgs(args, USAGE); const [v2, injection, caps, subagents, roles, fallback, sidecars] = await Promise.all([ runtimeRequest("/api/v2", {}, deps), runtimeRequest("/api/injection-model", {}, deps), runtimeRequest("/api/effort-caps", {}, deps), runtimeRequest("/api/subagent-models", {}, deps), runtimeRequest("/api/subagent-roles", {}, deps), runtimeRequest("/api/subagent-model-fallback", {}, deps), runtimeRequest("/api/sidecar-settings", {}, deps), ]); const result = { v2, injection, caps, subagents, roles, fallback, sidecars }; printData(result, wantsJson, summaryLines(result)); } async function injection(argv: string[], deps: RuntimeApiDeps): Promise { const args = [...argv]; const action = (args.shift() ?? "status").toLowerCase(); const wantsJson = takeFlag(args, "--json"); if (action === "status") { rejectArgs(args, USAGE); const result = await runtimeRequest("/api/injection-model", {}, deps); printData(result, wantsJson, summaryLines(result)); return; } if (action !== "set") throw new CliUsageError(`unknown injection action ${action}`, USAGE); const model = clearable(takeOption(args, "--model")); const effort = clearable(takeOption(args, "--effort")); const prompt = clearable(takeOption(args, "--prompt")); const guidance = takeBooleanOption(args, "--guidance"); rejectArgs(args, USAGE); const body: Record = {}; if (model !== undefined) body.model = model; if (effort !== undefined) body.effort = effort; if (prompt !== undefined) body.prompt = prompt; if (guidance !== undefined) body.multiAgentGuidanceEnabled = guidance; if (Object.keys(body).length === 0) throw new CliUsageError("at least one injection option is required", USAGE); const result = await runtimeRequest("/api/injection-model", { method: "PUT", body: JSON.stringify(body) }, deps); printData(result, wantsJson, ["Agent injection settings updated."]); } async function effort(argv: string[], deps: RuntimeApiDeps): Promise { const args = [...argv]; const action = (args.shift() ?? "status").toLowerCase(); const wantsJson = takeFlag(args, "--json"); if (action === "status") { rejectArgs(args, USAGE); const result = await runtimeRequest("/api/effort-caps", {}, deps); printData(result, wantsJson, summaryLines(result)); return; } if (action !== "set") throw new CliUsageError(`unknown effort action ${action}`, USAGE); const main = clearable(takeOption(args, "--main")); const subagent = clearable(takeOption(args, "--subagent")); rejectArgs(args, USAGE); const body: Record = {}; if (main !== undefined) body.effortCap = main; if (subagent !== undefined) body.subagentEffortCap = subagent; if (Object.keys(body).length === 0) throw new CliUsageError("--main and/or --subagent is required", USAGE); const result = await runtimeRequest("/api/effort-caps", { method: "PUT", body: JSON.stringify(body) }, deps); printData(result, wantsJson, ["Agent effort caps updated."]); } async function subagents(argv: string[], deps: RuntimeApiDeps): Promise { const args = [...argv]; const action = (args.shift() ?? "status").toLowerCase(); const wantsJson = takeFlag(args, "--json"); if (action === "status") { rejectArgs(args, USAGE); const result = await runtimeRequest("/api/subagent-models", {}, deps); printData(result, wantsJson, summaryLines(result)); return; } let models: string[]; if (action === "clear") models = []; else if (action === "set") { const raw = args.shift(); if (!raw) throw new CliUsageError("comma-separated subagent models are required", USAGE); models = csv(raw) ?? []; } else throw new CliUsageError(`unknown subagents action ${action}`, USAGE); rejectArgs(args, USAGE); if (models.length > 5) throw new CliUsageError("at most 5 subagent models are allowed", USAGE); const result = await runtimeRequest("/api/subagent-models", { method: "PUT", body: JSON.stringify({ models }) }, deps); printData(result, wantsJson, [`Subagent roster: ${models.join(", ") || "cleared"}`]); } async function readRolesDocument(deps: RuntimeApiDeps, file: string | undefined): Promise { const raw = file ? readFileSync(file, "utf8") : await readStdinDocument(deps); let parsed: unknown; try { parsed = JSON.parse(raw); } catch { throw new CliUsageError("roles JSON is invalid", USAGE); } if (Array.isArray(parsed)) return { roles: parsed }; return parsed; } async function readStdinDocument(deps: RuntimeApiDeps, label = "roles set"): Promise { const input = deps.stdinImpl ?? process.stdin; if (input.isTTY) throw new CliUsageError(`${label} requires --file or JSON on stdin`, USAGE); const chunks: Buffer[] = []; for await (const chunk of input) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))); } const text = Buffer.concat(chunks).toString("utf8").trim(); if (!text) throw new CliUsageError("roles JSON was empty", USAGE); return text; } async function authority(argv: string[], deps: RuntimeApiDeps): Promise { const args = [...argv]; const file = takeOption(args, "--file"); rejectArgs(args, USAGE); const raw = file ? readFileSync(file, "utf8") : await readStdinDocument(deps, "authority"); let body: unknown; try { body = JSON.parse(raw); } catch { throw new CliUsageError("authority JSON is invalid", USAGE); } const resolved = await runtimeRequest("/api/subagent-model-authority", { method: "POST", body: JSON.stringify(body), }, deps); printData(resolved, true, []); } async function roles(argv: string[], deps: RuntimeApiDeps): Promise { const args = [...argv]; const action = (args.shift() ?? "status").toLowerCase(); const wantsJson = takeFlag(args, "--json"); if (action === "status") { rejectArgs(args, USAGE); const result = await runtimeRequest("/api/subagent-roles", {}, deps); printData(result, wantsJson, summaryLines(result)); return; } if (action === "set") { const file = takeOption(args, "--file"); rejectArgs(args, USAGE); const body = await readRolesDocument(deps, file); if (!body || typeof body !== "object" || Array.isArray(body) || !("roles" in body)) { throw new CliUsageError("roles JSON must be { roles: [...] } or an array", USAGE); } const result = await runtimeRequest("/api/subagent-roles", { method: "PUT", body: JSON.stringify(body) }, deps); printData(result, wantsJson, ["Agent roles updated."]); return; } if (action !== "remove") throw new CliUsageError(`unknown roles action ${action}`, USAGE); const id = args.shift(); if (!id) throw new CliUsageError("role id is required", USAGE); rejectArgs(args, USAGE); const result = await runtimeRequest("/api/subagent-roles", { method: "PUT", body: JSON.stringify({ remove: id }), }, deps); printData(result, wantsJson, [`Removed role ${id}.`]); } async function fallback(argv: string[], deps: RuntimeApiDeps): Promise { const args = [...argv]; const action = (args.shift() ?? "status").toLowerCase(); const wantsJson = takeFlag(args, "--json"); if (action === "status") { rejectArgs(args, USAGE); const result = await runtimeRequest("/api/subagent-model-fallback", {}, deps); printData(result, wantsJson, summaryLines(result)); return; } const body: Record = {}; if (action === "clear") body.models = []; else if (action === "set") { const raw = args[0] && !args[0].startsWith("--") ? args.shift() : undefined; if (raw) body.models = csv(raw) ?? []; } else throw new CliUsageError(`unknown fallback action ${action}`, USAGE); const pollMs = takeIntegerOption(args, "--poll-ms", { min: 5_000 }); if (pollMs !== undefined) { if (pollMs > 600_000) throw new CliUsageError("--poll-ms must be <= 600000", USAGE); body.pollMs = pollMs; } rejectArgs(args, USAGE); if (Object.keys(body).length === 0) throw new CliUsageError("models and/or --poll-ms is required", USAGE); const result = await runtimeRequest("/api/subagent-model-fallback", { method: "PUT", body: JSON.stringify(body) }, deps); printData(result, wantsJson, ["Subagent fallback settings updated."]); } async function sidecar(argv: string[], deps: RuntimeApiDeps): Promise { const args = [...argv]; const section = (args.shift() ?? "status").toLowerCase(); const wantsJson = takeFlag(args, "--json"); if (section === "status") { rejectArgs(args, USAGE); const result = await runtimeRequest("/api/sidecar-settings", {}, deps); printData(result, wantsJson, summaryLines(result)); return; } if (section !== "web" && section !== "vision") throw new CliUsageError("sidecar must be web, vision, or status", USAGE); // --list must be consumed BEFORE rejectArgs sees it. It prints the server's // candidate set — the exact list the GUI picker shows (#2188): the server // computes it once and every surface consumes it, so the CLI cannot drift. const wantsList = takeFlag(args, "--list"); if (wantsList) { rejectArgs(args, USAGE); const settings = await runtimeRequest("/api/sidecar-settings", {}, deps) as { webSearchModels?: WebSearchModelOption[]; visionModels?: Array<{ value: string; backend?: string; baseline?: boolean }>; }; if (section === "web") { const options = settings.webSearchModels ?? []; printData(options, wantsJson, options.length === 0 ? ["no runnable web-search sidecar models (log in to ChatGPT or Anthropic)"] : options.map(option => `${option.value} [${option.backend}]${option.authSlot ? " (auth slot)" : ""}`)); } else { const options = settings.visionModels ?? []; printData(options, wantsJson, options.length === 0 ? ["no eligible vision describers"] : options.map(option => `${option.value}${option.backend ? ` [${option.backend}]` : ""}${option.baseline ? " (baseline)" : ""}`)); } return; } const model = takeOption(args, "--model"); const backend = takeOption(args, "--backend"); const reasoning = takeOption(args, "--reasoning"); const maxDescriptionsPerTurn = takeIntegerOption(args, "--max-descriptions", { min: 1 }); rejectArgs(args, USAGE); const settings: Record = {}; if (model !== undefined) settings.model = model === "-" ? "" : model; if (backend !== undefined) settings.backend = backend === "-" ? null : backend; if (reasoning !== undefined) settings.reasoning = reasoning; if (maxDescriptionsPerTurn !== undefined) settings.maxDescriptionsPerTurn = maxDescriptionsPerTurn; if (Object.keys(settings).length === 0) throw new CliUsageError("at least one sidecar option is required", USAGE); if (section === "web" && model !== undefined && model !== "-") { const offered = await runtimeRequest("/api/sidecar-settings", {}, deps) as { webSearchModels?: WebSearchModelOption[]; }; const requestedBackend = backend === "-" ? "openai" : backend; const option = offered.webSearchModels?.find(candidate => (candidate.value === model || candidate.model === model) && (requestedBackend === undefined || candidate.backend === requestedBackend)); if (option) { settings.model = option.model; if (backend !== "-") settings.backend = option.backend; } } const body = section === "web" ? { webSearch: settings } : { vision: settings }; const result = await runtimeRequest("/api/sidecar-settings", { method: "PUT", body: JSON.stringify(body) }, deps); printData(result, wantsJson, [`${section} sidecar settings updated.`]); } export async function handleAgentCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise { return runCliAction(async () => { const [sub = "status", ...rest] = argv; if (sub === "status") await status(rest, deps); else if (sub === "injection" || sub === "guidance") await injection(rest, deps); else if (sub === "effort") await effort(rest, deps); else if (sub === "subagents" || sub === "roster") await subagents(rest, deps); else if (sub === "authority") await authority(rest, deps); else if (sub === "roles") await roles(rest, deps); else if (sub === "fallback") await fallback(rest, deps); else if (sub === "sidecar") await sidecar(rest, deps); // Lives here rather than as a top-level verb because it is an agent-behavior feature flag: // it controls whether default mode may ask the operator a question mid-task. else if (sub === "request-user-input") { const { requestUserInputAction } = await import("./inspect"); await requestUserInputAction(rest, deps); } else throw new CliUsageError(`unknown agent command ${sub}`, USAGE); }); } export const AGENT_USAGE = USAGE;