abd5b13d52
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
89 lines
3.7 KiB
TypeScript
89 lines
3.7 KiB
TypeScript
/**
|
|
* `penguin run` — send a single Task in one shot.
|
|
*
|
|
* penguin run -m <msg> [--model-id <id> --provider <group>] [--workspace <path>]
|
|
* [--project-id <id>] [--agent-id <id>]
|
|
* [--approve <allow-all|deny-all|read-only|always-ask>]
|
|
*
|
|
* 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 <message>", t.run.message)
|
|
.option("--model-id <id>", t.common.modelId)
|
|
.option("--provider <group>", t.common.provider)
|
|
.option("--project-id <id>", t.common.projectId)
|
|
.option("--agent-id <id>", t.common.agentId)
|
|
.option("--workspace <path>", t.common.workspace)
|
|
.option("--approve <mode>", 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");
|
|
});
|
|
}
|