/** * `penguin run` — send a single Task in one shot. * * penguin run -m [--model-id --provider ] [--workspace ] * [--project-id ] [--agent-id ] * [--approve ] * * Uses the current directory when Workspace is unspecified; uses the Project's default model * when model is unspecified. A model reference is always an explicit `(provider, model_id)` * pair, so `--model-id` and `--provider` must be given together — giving only one of them is * an error, never a lookup. Defaults to interactive per-call approval; `--approve` * selects the permission mode. * Docs: /docs/cli § "penguin run". */ import type { Command } from "commander"; import { createAgent, userText, VERSION } from "@prismshadow/penguin-core"; import { StreamRenderer, sessionMetaTools } from "../render.js"; import { runTask } from "../task-loop.js"; import { denyActivePrompt, resolveApprovalMode } from "../approval.js"; import type { Messages } from "../i18n.js"; export function registerRunCommand(program: Command, t: Messages): void { program .command("run") .description(t.run.desc) .requiredOption("-m, --message ", t.run.message) .option("--model-id ", t.common.modelId) .option("--provider ", t.common.provider) .option("--project-id ", t.common.projectId) .option("--agent-id ", t.common.agentId) .option("--workspace ", t.common.workspace) .option("--approve ", t.common.approve) .action(async (opts) => { // The model reference is a pair: commander can only require each option on its own, // so the "both or neither" rule is enforced here. Giving neither is the normal case // and falls back to the Project's default model. if (Boolean(opts.modelId) !== Boolean(opts.provider)) { process.stderr.write(`${t.error(t.modelRefIncomplete())}\n`); process.exitCode = 1; return; } const mode = resolveApprovalMode(opts.approve, t); const agent = await createAgent({ ...(opts.agentId ? { agentId: opts.agentId } : {}), ...(opts.projectId ? { projectId: opts.projectId } : {}), }); const session = await agent.createSession({ workspaceDir: opts.workspace ?? process.cwd(), ...(opts.modelId ? { modelId: opts.modelId } : {}), ...(opts.provider ? { provider: opts.provider } : {}), }); const out = process.stdout; out.write( `${t.header("run", VERSION, agent.state.agentId, session.workspaceDir, session.modelId)}\n`, ); const controller = new AbortController(); const onSigint = () => { // Single SIGINT handler: Ctrl-C during approval collapses to "deny this tool" (see // approval.ts); at all other times it interrupts the whole turn. if (denyActivePrompt()) return; controller.abort(); }; process.on("SIGINT", onSigint); const renderer = new StreamRenderer(out, t); // The assembled tool schemas decide each tool's call-line preview path (see render.ts). renderer.useToolSchemas(sessionMetaTools(session)); try { const result = await runTask(session, [userText(opts.message)], { mode, signal: controller.signal, renderer, t, }); // Task ended with an abort (LLM failure/reconnect exhausted/user interrupt): non-zero // exit code, for scripts/CI to check. if (result.aborted) process.exitCode = 1; } finally { process.off("SIGINT", onSigint); session.dispose(); // Tear down managed long-running command sessions to avoid leaking background processes } out.write("\n"); }); }