openskynet / src /cli /cron-cli /register.cron-add.ts
Darochin's picture
Mirror OpenSkyNet workspace snapshot from Git HEAD
fc93158 verified
import type { Command } from "commander";
import type { CronJob } from "../../cron/types.js";
import { sanitizeAgentId } from "../../routing/session-key.js";
import { defaultRuntime } from "../../runtime.js";
import type { GatewayRpcOpts } from "../gateway-rpc.js";
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
import { parsePositiveIntOrUndefined } from "../program/helpers.js";
import {
getCronChannelOptions,
handleCronCliError,
parseAt,
parseCronStaggerMs,
parseDurationMs,
printCronJson,
printCronList,
warnIfCronSchedulerDisabled,
} from "./shared.js";
export function registerCronStatusCommand(cron: Command) {
addGatewayClientOptions(
cron
.command("status")
.description("Show cron scheduler status")
.option("--json", "Output JSON", false)
.action(async (opts) => {
try {
const res = await callGatewayFromCli("cron.status", opts, {});
printCronJson(res);
} catch (err) {
handleCronCliError(err);
}
}),
);
}
export function registerCronListCommand(cron: Command) {
addGatewayClientOptions(
cron
.command("list")
.description("List cron jobs")
.option("--all", "Include disabled jobs", false)
.option("--json", "Output JSON", false)
.action(async (opts) => {
try {
const res = await callGatewayFromCli("cron.list", opts, {
includeDisabled: Boolean(opts.all),
});
if (opts.json) {
printCronJson(res);
return;
}
const jobs = (res as { jobs?: CronJob[] } | null)?.jobs ?? [];
printCronList(jobs, defaultRuntime);
} catch (err) {
handleCronCliError(err);
}
}),
);
}
export function registerCronAddCommand(cron: Command) {
addGatewayClientOptions(
cron
.command("add")
.alias("create")
.description("Add a cron job")
.requiredOption("--name <name>", "Job name")
.option("--description <text>", "Optional description")
.option("--disabled", "Create job disabled", false)
.option("--delete-after-run", "Delete one-shot job after it succeeds", false)
.option("--keep-after-run", "Keep one-shot job after it succeeds", false)
.option("--agent <id>", "Agent id for this job")
.option("--session <target>", "Session target (main|isolated)")
.option("--session-key <key>", "Session key for job routing (e.g. agent:my-agent:my-session)")
.option("--wake <mode>", "Wake mode (now|next-heartbeat)", "now")
.option("--at <when>", "Run once at time (ISO) or +duration (e.g. 20m)")
.option("--every <duration>", "Run every duration (e.g. 10m, 1h)")
.option("--cron <expr>", "Cron expression (5-field or 6-field with seconds)")
.option("--tz <iana>", "Timezone for cron expressions (IANA)", "")
.option("--stagger <duration>", "Cron stagger window (e.g. 30s, 5m)")
.option("--exact", "Disable cron staggering (set stagger to 0)", false)
.option("--system-event <text>", "System event payload (main session)")
.option("--message <text>", "Agent message payload")
.option(
"--thinking <level>",
"Thinking level for agent jobs (off|minimal|low|medium|high|xhigh)",
)
.option("--model <model>", "Model override for agent jobs (provider/model or alias)")
.option("--timeout-seconds <n>", "Timeout seconds for agent jobs")
.option("--light-context", "Use lightweight bootstrap context for agent jobs", false)
.option("--announce", "Announce summary to a chat (subagent-style)", false)
.option("--deliver", "Deprecated (use --announce). Announces a summary to a chat.")
.option("--no-deliver", "Disable announce delivery and skip main-session summary")
.option("--channel <channel>", `Delivery channel (${getCronChannelOptions()})`, "last")
.option(
"--to <dest>",
"Delivery destination (E.164, Telegram chatId, or Discord channel/user)",
)
.option("--account <id>", "Channel account id for delivery (multi-account setups)")
.option("--best-effort-deliver", "Do not fail the job if delivery fails", false)
.option("--json", "Output JSON", false)
.action(async (opts: GatewayRpcOpts & Record<string, unknown>, cmd?: Command) => {
try {
const staggerRaw = typeof opts.stagger === "string" ? opts.stagger.trim() : "";
const useExact = Boolean(opts.exact);
if (staggerRaw && useExact) {
throw new Error("Choose either --stagger or --exact, not both");
}
const schedule = (() => {
const at = typeof opts.at === "string" ? opts.at : "";
const every = typeof opts.every === "string" ? opts.every : "";
const cronExpr = typeof opts.cron === "string" ? opts.cron : "";
const chosen = [Boolean(at), Boolean(every), Boolean(cronExpr)].filter(Boolean).length;
if (chosen !== 1) {
throw new Error("Choose exactly one schedule: --at, --every, or --cron");
}
if ((useExact || staggerRaw) && !cronExpr) {
throw new Error("--stagger/--exact are only valid with --cron");
}
if (at) {
const atIso = parseAt(at);
if (!atIso) {
throw new Error("Invalid --at; use ISO time or duration like 20m");
}
return { kind: "at" as const, at: atIso };
}
if (every) {
const everyMs = parseDurationMs(every);
if (!everyMs) {
throw new Error("Invalid --every; use e.g. 10m, 1h, 1d");
}
return { kind: "every" as const, everyMs };
}
const staggerMs = parseCronStaggerMs({ staggerRaw, useExact });
return {
kind: "cron" as const,
expr: cronExpr,
tz: typeof opts.tz === "string" && opts.tz.trim() ? opts.tz.trim() : undefined,
staggerMs,
};
})();
const wakeModeRaw = typeof opts.wake === "string" ? opts.wake : "now";
const wakeMode = wakeModeRaw.trim() || "now";
if (wakeMode !== "now" && wakeMode !== "next-heartbeat") {
throw new Error("--wake must be now or next-heartbeat");
}
const agentId =
typeof opts.agent === "string" && opts.agent.trim()
? sanitizeAgentId(opts.agent.trim())
: undefined;
const hasAnnounce = Boolean(opts.announce) || opts.deliver === true;
const hasNoDeliver = opts.deliver === false;
const deliveryFlagCount = [hasAnnounce, hasNoDeliver].filter(Boolean).length;
if (deliveryFlagCount > 1) {
throw new Error("Choose at most one of --announce or --no-deliver");
}
const payload = (() => {
const systemEvent = typeof opts.systemEvent === "string" ? opts.systemEvent.trim() : "";
const message = typeof opts.message === "string" ? opts.message.trim() : "";
const chosen = [Boolean(systemEvent), Boolean(message)].filter(Boolean).length;
if (chosen !== 1) {
throw new Error("Choose exactly one payload: --system-event or --message");
}
if (systemEvent) {
return { kind: "systemEvent" as const, text: systemEvent };
}
const timeoutSeconds = parsePositiveIntOrUndefined(opts.timeoutSeconds);
return {
kind: "agentTurn" as const,
message,
model:
typeof opts.model === "string" && opts.model.trim() ? opts.model.trim() : undefined,
thinking:
typeof opts.thinking === "string" && opts.thinking.trim()
? opts.thinking.trim()
: undefined,
timeoutSeconds:
timeoutSeconds && Number.isFinite(timeoutSeconds) ? timeoutSeconds : undefined,
lightContext: opts.lightContext === true ? true : undefined,
};
})();
const optionSource =
typeof cmd?.getOptionValueSource === "function"
? (name: string) => cmd.getOptionValueSource(name)
: () => undefined;
const sessionSource = optionSource("session");
const sessionTargetRaw = typeof opts.session === "string" ? opts.session.trim() : "";
const inferredSessionTarget = payload.kind === "agentTurn" ? "isolated" : "main";
const sessionTarget =
sessionSource === "cli" ? sessionTargetRaw || "" : inferredSessionTarget;
if (sessionTarget !== "main" && sessionTarget !== "isolated") {
throw new Error("--session must be main or isolated");
}
if (opts.deleteAfterRun && opts.keepAfterRun) {
throw new Error("Choose --delete-after-run or --keep-after-run, not both");
}
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).");
}
if (
(opts.announce || typeof opts.deliver === "boolean") &&
(sessionTarget !== "isolated" || payload.kind !== "agentTurn")
) {
throw new Error("--announce/--no-deliver require --session isolated.");
}
const accountId =
typeof opts.account === "string" && opts.account.trim()
? opts.account.trim()
: undefined;
if (accountId && (sessionTarget !== "isolated" || payload.kind !== "agentTurn")) {
throw new Error("--account requires an isolated agentTurn job with delivery.");
}
const deliveryMode =
sessionTarget === "isolated" && payload.kind === "agentTurn"
? hasAnnounce
? "announce"
: hasNoDeliver
? "none"
: "announce"
: undefined;
const nameRaw = typeof opts.name === "string" ? opts.name : "";
const name = nameRaw.trim();
if (!name) {
throw new Error("--name is required");
}
const description =
typeof opts.description === "string" && opts.description.trim()
? opts.description.trim()
: undefined;
const sessionKey =
typeof opts.sessionKey === "string" && opts.sessionKey.trim()
? opts.sessionKey.trim()
: undefined;
const params = {
name,
description,
enabled: !opts.disabled,
deleteAfterRun: opts.deleteAfterRun ? true : opts.keepAfterRun ? false : undefined,
agentId,
sessionKey,
schedule,
sessionTarget,
wakeMode,
payload,
delivery: deliveryMode
? {
mode: deliveryMode,
channel:
typeof opts.channel === "string" && opts.channel.trim()
? opts.channel.trim()
: undefined,
to: typeof opts.to === "string" && opts.to.trim() ? opts.to.trim() : undefined,
accountId,
bestEffort: opts.bestEffortDeliver ? true : undefined,
}
: undefined,
};
const res = await callGatewayFromCli("cron.add", opts, params);
printCronJson(res);
await warnIfCronSchedulerDisabled(opts);
} catch (err) {
handleCronCliError(err);
}
}),
);
}