feat(cli): expand cron commands

This commit is contained in:
Peter Steinberger
2025-12-13 12:09:20 +00:00
parent c02613e15f
commit 5f159c43c5
2 changed files with 89 additions and 8 deletions

View File

@@ -76,8 +76,7 @@ Each job is a JSON object with stable keys (unknown keys ignored for forward com
- `{"kind":"systemEvent","text":string}` (enqueue as `System:`)
- `{"kind":"agentTurn","message":string,"deliver"?:boolean,"channel"?: "last"|"whatsapp"|"telegram","to"?:string,"timeoutSeconds"?:number}`
- `isolation` (optional; only meaningful for isolated jobs)
- `{"postToMain"?: boolean, "postToMainPrefix"?: string}`
- Note: `postToMain` is deprecated (no-op). Isolated jobs always post a summary; only the prefix is configurable.
- `{"postToMainPrefix"?: string}`
- `runtime` (optional)
- `{"maxAttempts"?:number,"retryBackoffMs"?:number}` (best-effort retries; defaults off)
- `state` (runtime-maintained)
@@ -177,6 +176,7 @@ When due:
- Optionally deliver output (`payload.deliver === true`) to the configured channel/to.
- Isolated jobs always enqueue a summary system event to the main session when they finish (derived from the last agent text output).
- Prefix defaults to `Cron`, and can be customized via `isolation.postToMainPrefix`.
- If `deliver` is omitted/false, nothing is sent to external providers; you still get the main-session summary and can inspect the full isolated transcript in `cron:<jobId>`.
### “Run in parallel to main”
@@ -215,6 +215,8 @@ Path rules:
Retention:
- Best-effort pruning when the file grows beyond ~2MB; keep the newest ~2000 lines.
Each log line includes (at minimum) job id, status/error, timing, and a `summary` string (systemEvent text for main jobs, and the last agent text output for isolated jobs).
## Gateway API
New methods (names can be bikeshed; `cron.*` is suggested):
@@ -269,6 +271,7 @@ Add a `cron` command group (all commands should also support `--json` where sens
- `clawdis cron rm <id>`
- `clawdis cron enable <id>` / `clawdis cron disable <id>`
- `clawdis cron run <id> [--force]` (debug)
- `clawdis cron runs [--id <id>] [--limit <n>]` (run history)
- `clawdis cron status` (scheduler enabled + next wake)
Additionally:

View File

@@ -163,7 +163,6 @@ export function registerCronCli(program: Command) {
"Do not fail the job if delivery fails",
false,
)
.option("--post-to-main", "Deprecated (no-op)", false)
.option(
"--post-prefix <prefix>",
"Prefix for summary system event",
@@ -261,14 +260,15 @@ export function registerCronCli(program: Command) {
};
})();
if (sessionTarget === "main" && payload.kind !== "systemEvent") {
throw new Error("Main jobs require --system-event (systemEvent).");
}
if (sessionTarget === "isolated" && payload.kind !== "agentTurn") {
throw new Error(
"Isolated jobs require --message (agentTurn payload).",
);
throw new Error("Isolated jobs require --message (agentTurn).");
}
const isolation =
payload.kind === "agentTurn"
sessionTarget === "isolated"
? {
postToMainPrefix:
typeof opts.postPrefix === "string" &&
@@ -318,6 +318,71 @@ export function registerCronCli(program: Command) {
}),
);
addGatewayClientOptions(
cron
.command("enable")
.description("Enable a cron job")
.argument("<id>", "Job id")
.action(async (id, opts) => {
try {
const res = await callGatewayFromCli("cron.update", opts, {
id,
patch: { enabled: true },
});
defaultRuntime.log(JSON.stringify(res, null, 2));
await warnIfCronSchedulerDisabled(opts);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
}),
);
addGatewayClientOptions(
cron
.command("disable")
.description("Disable a cron job")
.argument("<id>", "Job id")
.action(async (id, opts) => {
try {
const res = await callGatewayFromCli("cron.update", opts, {
id,
patch: { enabled: false },
});
defaultRuntime.log(JSON.stringify(res, null, 2));
await warnIfCronSchedulerDisabled(opts);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
}),
);
addGatewayClientOptions(
cron
.command("runs")
.description("Show cron run history (JSONL-backed)")
.option("--id <id>", "Job id (required when store is jobs.json)")
.option("--limit <n>", "Max entries (default 50)", "50")
.action(async (opts) => {
try {
const limitRaw = Number.parseInt(String(opts.limit ?? "50"), 10);
const limit =
Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : 50;
const id =
typeof opts.id === "string" && opts.id.trim() ? opts.id : undefined;
const res = await callGatewayFromCli("cron.runs", opts, {
id,
limit,
});
defaultRuntime.log(JSON.stringify(res, null, 2));
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
}),
);
addGatewayClientOptions(
cron
.command("edit")
@@ -347,10 +412,23 @@ export function registerCronCli(program: Command) {
"Do not fail job if delivery fails",
false,
)
.option("--post-to-main", "Deprecated (no-op)", false)
.option("--post-prefix <prefix>", "Prefix for summary system event")
.action(async (id, opts) => {
try {
if (opts.session === "main" && opts.message) {
throw new Error(
"Main jobs cannot use --message; use --system-event or --session isolated.",
);
}
if (opts.session === "isolated" && opts.systemEvent) {
throw new Error(
"Isolated jobs cannot use --system-event; use --message or --session main.",
);
}
if (opts.session === "main" && typeof opts.postPrefix === "string") {
throw new Error("--post-prefix only applies to isolated jobs.");
}
const patch: Record<string, unknown> = {};
if (typeof opts.name === "string") patch.name = opts.name;
if (opts.enable && opts.disable)