feat: add cron agent binding

This commit is contained in:
Peter Steinberger
2026-01-12 10:23:45 +00:00
parent a3938d62f6
commit 115591c5b6
14 changed files with 383 additions and 83 deletions

View File

@@ -74,6 +74,39 @@ describe("cron cli", () => {
expect(params?.payload?.thinking).toBe("low");
});
it("sends agent id on cron add", async () => {
callGatewayFromCli.mockClear();
const { registerCronCli } = await import("./cron-cli.js");
const program = new Command();
program.exitOverride();
registerCronCli(program);
await program.parseAsync(
[
"cron",
"add",
"--name",
"Agent pinned",
"--cron",
"* * * * *",
"--session",
"isolated",
"--message",
"hi",
"--agent",
"ops",
],
{ from: "user" },
);
const addCall = callGatewayFromCli.mock.calls.find(
(call) => call[0] === "cron.add",
);
const params = addCall?.[2] as { agentId?: string };
expect(params?.agentId).toBe("ops");
});
it("omits empty model and thinking on cron edit", async () => {
callGatewayFromCli.mockClear();
@@ -142,6 +175,36 @@ describe("cron cli", () => {
expect(patch?.patch?.payload?.thinking).toBe("high");
});
it("sets and clears agent id on cron edit", async () => {
callGatewayFromCli.mockClear();
const { registerCronCli } = await import("./cron-cli.js");
const program = new Command();
program.exitOverride();
registerCronCli(program);
await program.parseAsync(
["cron", "edit", "job-1", "--agent", " Ops ", "--message", "hello"],
{ from: "user" },
);
const updateCall = callGatewayFromCli.mock.calls.find(
(call) => call[0] === "cron.update",
);
const patch = updateCall?.[2] as { patch?: { agentId?: unknown } };
expect(patch?.patch?.agentId).toBe("Ops");
callGatewayFromCli.mockClear();
await program.parseAsync(["cron", "edit", "job-2", "--clear-agent"], {
from: "user",
});
const clearCall = callGatewayFromCli.mock.calls.find(
(call) => call[0] === "cron.update",
);
const clearPatch = clearCall?.[2] as { patch?: { agentId?: unknown } };
expect(clearPatch?.patch?.agentId).toBeNull();
});
it("does not include model/thinking when no payload change is requested", async () => {
callGatewayFromCli.mockClear();

View File

@@ -2,6 +2,7 @@ import type { Command } from "commander";
import type { CronJob, CronSchedule } from "../cron/types.js";
import { danger } from "../globals.js";
import { PROVIDER_IDS } from "../providers/registry.js";
import { normalizeAgentId } from "../routing/session-key.js";
import { defaultRuntime } from "../runtime.js";
import { formatDocsLink } from "../terminal/links.js";
import { colorize, isRich, theme } from "../terminal/theme.js";
@@ -72,6 +73,7 @@ const CRON_NEXT_PAD = 10;
const CRON_LAST_PAD = 10;
const CRON_STATUS_PAD = 9;
const CRON_TARGET_PAD = 9;
const CRON_AGENT_PAD = 10;
const pad = (value: string, width: number) => value.padEnd(width);
@@ -139,6 +141,7 @@ function printCronList(jobs: CronJob[], runtime = defaultRuntime) {
pad("Last", CRON_LAST_PAD),
pad("Status", CRON_STATUS_PAD),
pad("Target", CRON_TARGET_PAD),
pad("Agent", CRON_AGENT_PAD),
].join(" ");
runtime.log(rich ? theme.heading(header) : header);
@@ -162,6 +165,10 @@ function printCronList(jobs: CronJob[], runtime = defaultRuntime) {
const statusRaw = formatStatus(job);
const statusLabel = pad(statusRaw, CRON_STATUS_PAD);
const targetLabel = pad(job.sessionTarget, CRON_TARGET_PAD);
const agentLabel = pad(
truncate(job.agentId ?? "default", CRON_AGENT_PAD),
CRON_AGENT_PAD,
);
const coloredStatus = (() => {
if (statusRaw === "ok") return colorize(rich, theme.success, statusLabel);
@@ -178,6 +185,9 @@ function printCronList(jobs: CronJob[], runtime = defaultRuntime) {
job.sessionTarget === "isolated"
? colorize(rich, theme.accentBright, targetLabel)
: colorize(rich, theme.accent, targetLabel);
const coloredAgent = job.agentId
? colorize(rich, theme.info, agentLabel)
: colorize(rich, theme.muted, agentLabel);
const line = [
colorize(rich, theme.accent, idLabel),
@@ -187,6 +197,7 @@ function printCronList(jobs: CronJob[], runtime = defaultRuntime) {
colorize(rich, theme.muted, lastLabel),
coloredStatus,
coloredTarget,
coloredAgent,
].join(" ");
runtime.log(line.trimEnd());
@@ -283,6 +294,7 @@ export function registerCronCli(program: Command) {
.requiredOption("--name <name>", "Job name")
.option("--description <text>", "Optional description")
.option("--disabled", "Create job disabled", false)
.option("--agent <id>", "Agent id for this job")
.option("--session <target>", "Session target (main|isolated)", "main")
.option(
"--wake <mode>",
@@ -375,6 +387,11 @@ export function registerCronCli(program: Command) {
throw new Error("--wake must be now or next-heartbeat");
}
const agentId =
typeof opts.agent === "string" && opts.agent.trim()
? normalizeAgentId(opts.agent)
: undefined;
const payload = (() => {
const systemEvent =
typeof opts.systemEvent === "string"
@@ -451,6 +468,7 @@ export function registerCronCli(program: Command) {
name,
description,
enabled: !opts.disabled,
agentId,
schedule,
sessionTarget,
wakeMode,
@@ -561,6 +579,8 @@ export function registerCronCli(program: Command) {
.option("--enable", "Enable job", false)
.option("--disable", "Disable job", false)
.option("--session <target>", "Session target (main|isolated)")
.option("--agent <id>", "Set agent id")
.option("--clear-agent", "Unset agent and use default", false)
.option("--wake <mode>", "Wake mode (now|next-heartbeat)")
.option("--at <when>", "Set one-shot time (ISO) or duration like 20m")
.option("--every <duration>", "Set interval duration like 10m")
@@ -613,6 +633,15 @@ export function registerCronCli(program: Command) {
if (typeof opts.session === "string")
patch.sessionTarget = opts.session;
if (typeof opts.wake === "string") patch.wakeMode = opts.wake;
if (opts.agent && opts.clearAgent) {
throw new Error("Use --agent or --clear-agent, not both");
}
if (typeof opts.agent === "string" && opts.agent.trim()) {
patch.agentId = normalizeAgentId(opts.agent);
}
if (opts.clearAgent) {
patch.agentId = null;
}
const scheduleChosen = [opts.at, opts.every, opts.cron].filter(
Boolean,