Download src/cli/cron-cli/register.cron-add.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 18.8 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/cli/cron-cli/register.cron-add.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/cli/cron-cli/register.cron-add.ts
-
curl -L -o register.cron-add.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/cli/cron-cli/register.cron-add.ts
18.8 kB
| // Cron status/list/add command registration and create-payload normalization. | |
| import { | |
| normalizeLowercaseStringOrEmpty, | |
| normalizeOptionalString, | |
| readNonBlankString, | |
| } from "@openclaw/normalization-core/string-coerce"; | |
| import type { Command } from "commander"; | |
| import { theme } from "../../../packages/terminal-core/src/theme.js"; | |
| import type { CronJob } from "../../cron/types.js"; | |
| import { normalizeHttpWebhookUrl } from "../../cron/webhook-url.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 { CronCliError } from "./cron-cli-error.js"; | |
| import { listCronJobsFromGateway } from "./list-jobs.js"; | |
| import { createCronOutputCommand } from "./output-mode.js"; | |
| import { registerCronMutationOptions } from "./register.cron-options.js"; | |
| import { resolveCronCreateScheduleFromArgs } from "./schedule-options.js"; | |
| import { | |
| coerceCronDeliveryPreviews, | |
| enrichCronJsonWithStatus, | |
| handleCronCliError, | |
| parseCronCommandArgv, | |
| parseCronCommandEnv, | |
| parseCronIntegerOption, | |
| parseCronNoOutputTimeoutOption, | |
| parseCronStringList, | |
| printCronJson, | |
| printCronList, | |
| warnIfCronSchedulerDisabled, | |
| } from "./shared.js"; | |
| import { normalizeCronSessionTargetOption, parseCronThreadIdOption } from "./thread-id-shared.js"; | |
| import { readCronPayloadScript, readCronTriggerScript } from "./trigger-options.js"; | |
| export function registerCronStatusCommand(cron: Command) { | |
| addGatewayClientOptions( | |
| createCronOutputCommand(cron, "status") | |
| .description("Show automations scheduler status") | |
| .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 automations") | |
| .option("--all", "Include disabled jobs", false) | |
| .option("--agent <id>", "Filter by agent id") | |
| .option("--json", "Output JSON", false) | |
| .action(async (opts) => { | |
| try { | |
| const listParams: { includeDisabled: boolean; agentId?: string } = { | |
| includeDisabled: Boolean(opts.all), | |
| }; | |
| const agentId = normalizeOptionalString(opts.agent); | |
| if (typeof opts.agent === "string" && !agentId) { | |
| throw new CronCliError("--agent must not be blank"); | |
| } | |
| if (agentId) { | |
| listParams.agentId = sanitizeAgentId(agentId); | |
| } | |
| const res = await listCronJobsFromGateway(opts, listParams); | |
| if (opts.json) { | |
| printCronJson(enrichCronJsonWithStatus(res)); | |
| return; | |
| } | |
| const jobs = (res as { jobs?: CronJob[] } | null)?.jobs ?? []; | |
| const deliveryPreviews = coerceCronDeliveryPreviews(res); | |
| printCronList(jobs, defaultRuntime, { deliveryPreviews }); | |
| } catch (err) { | |
| handleCronCliError(err); | |
| } | |
| }), | |
| ); | |
| } | |
| export function registerCronAddCommand(cron: Command) { | |
| addGatewayClientOptions( | |
| registerCronMutationOptions( | |
| createCronOutputCommand(cron, "add") | |
| .description("Add an automation") | |
| .argument("[scheduleOrName]", "Schedule string, or job name when using --at/--every/--cron") | |
| .argument("[message]", "Agent message when using a positional schedule"), | |
| "add", | |
| ) | |
| .option("--declaration-key <key>", "Idempotent declaration identity key") | |
| .option("--disabled", "Create job disabled", false) | |
| .action( | |
| async ( | |
| nameArg: string | undefined, | |
| messageArg: string | undefined, | |
| opts: GatewayRpcOpts & Record<string, unknown>, | |
| cmd: Command, | |
| ) => { | |
| try { | |
| const hasScheduleFlag = | |
| typeof opts.at === "string" || | |
| typeof opts.cron === "string" || | |
| typeof opts.every === "string" || | |
| typeof opts.onExit === "string" || | |
| typeof opts.streamCommand === "string"; | |
| const positionalSchedule = hasScheduleFlag ? undefined : nameArg; | |
| const schedule = resolveCronCreateScheduleFromArgs({ ...opts, positionalSchedule }); | |
| const wakeMode = normalizeOptionalString(opts.wake) ?? "now"; | |
| if (wakeMode !== "now" && wakeMode !== "next-heartbeat") { | |
| throw new CronCliError("--wake must be now or next-heartbeat"); | |
| } | |
| const rawAgentId = normalizeOptionalString(opts.agent); | |
| const agentId = rawAgentId ? sanitizeAgentId(rawAgentId) : undefined; | |
| const hasAnnounce = Boolean(opts.announce) || opts.deliver === true; | |
| const hasNoDeliver = opts.deliver === false; | |
| const webhookUrl = | |
| typeof opts.webhook === "string" ? normalizeHttpWebhookUrl(opts.webhook) : null; | |
| if (typeof opts.webhook === "string" && !webhookUrl) { | |
| throw new CronCliError("--webhook must be a valid http(s) URL"); | |
| } | |
| const hasWebhook = Boolean(webhookUrl); | |
| const deliveryFlagCount = [hasAnnounce, hasNoDeliver, hasWebhook].filter( | |
| Boolean, | |
| ).length; | |
| if (deliveryFlagCount > 1) { | |
| throw new CronCliError( | |
| "Choose at most one of --announce, --no-deliver, or --webhook", | |
| ); | |
| } | |
| const resolvedPayload = await (async () => { | |
| // Main-session jobs use system events; isolated/current/session jobs use messages. | |
| const systemEvent = normalizeOptionalString(opts.systemEvent) ?? ""; | |
| const optionMessage = normalizeOptionalString(opts.message); | |
| const positionalMessage = normalizeOptionalString(messageArg); | |
| const commandShell = normalizeOptionalString(opts.command); | |
| const commandArgv = parseCronCommandArgv(opts.commandArgv); | |
| // File arguments identify exact local paths; trimming can select another file. | |
| const scriptPath = readNonBlankString(opts.script); | |
| if (typeof opts.script === "string" && !scriptPath) { | |
| throw new CronCliError("--script must not be blank"); | |
| } | |
| const toolsAllow = parseCronStringList(opts.tools); | |
| if (optionMessage && positionalMessage && optionMessage !== positionalMessage) { | |
| throw new CronCliError( | |
| "Pass the automation message either positionally or with --message, not both.", | |
| ); | |
| } | |
| const message = optionMessage ?? positionalMessage ?? ""; | |
| if (commandShell && commandArgv) { | |
| throw new CronCliError( | |
| "Pass command payload either with --command or --command-argv, not both.", | |
| ); | |
| } | |
| const chosen = [ | |
| Boolean(systemEvent), | |
| Boolean(message), | |
| Boolean(commandShell) || Boolean(commandArgv), | |
| Boolean(scriptPath), | |
| ].filter(Boolean).length; | |
| if (chosen !== 1) { | |
| throw new CronCliError( | |
| "Choose exactly one payload: --system-event, --message, --command, or --script", | |
| ); | |
| } | |
| if (systemEvent) { | |
| return { | |
| kind: "systemEvent" as const, | |
| text: systemEvent, | |
| ...(toolsAllow ? { toolsAllow } : {}), | |
| }; | |
| } | |
| if (scriptPath) { | |
| if (opts.timeoutSeconds !== undefined) { | |
| throw new CronCliError( | |
| "Use --script-timeout-seconds for script jobs, not --timeout-seconds.", | |
| ); | |
| } | |
| const scriptTimeoutSeconds = parseCronIntegerOption( | |
| opts.scriptTimeoutSeconds, | |
| "--script-timeout-seconds", | |
| ); | |
| const scriptToolBudget = parseCronIntegerOption( | |
| opts.scriptToolBudget, | |
| "--script-tool-budget", | |
| ); | |
| return { | |
| kind: "script" as const, | |
| timeoutSeconds: scriptTimeoutSeconds, | |
| toolBudget: scriptToolBudget, | |
| toolsAllow, | |
| script: await readCronPayloadScript(scriptPath), | |
| }; | |
| } | |
| const timeoutSeconds = parseCronIntegerOption( | |
| opts.timeoutSeconds, | |
| "--timeout-seconds", | |
| "non-negative", | |
| ); | |
| if (commandShell || commandArgv) { | |
| const noOutputTimeoutSeconds = parseCronNoOutputTimeoutOption(opts); | |
| const outputMaxBytes = parseCronIntegerOption( | |
| opts.outputMaxBytes, | |
| "--output-max-bytes", | |
| ); | |
| return { | |
| kind: "command" as const, | |
| argv: commandArgv ?? ["sh", "-lc", commandShell ?? ""], | |
| cwd: normalizeOptionalString(opts.commandCwd), | |
| env: parseCronCommandEnv(opts.commandEnv), | |
| input: typeof opts.commandInput === "string" ? opts.commandInput : undefined, | |
| timeoutSeconds, | |
| noOutputTimeoutSeconds, | |
| outputMaxBytes, | |
| ...(toolsAllow ? { toolsAllow } : {}), | |
| }; | |
| } | |
| return { | |
| kind: "agentTurn" as const, | |
| message, | |
| model: normalizeOptionalString(opts.model), | |
| fallbacks: parseCronStringList(opts.fallbacks), | |
| thinking: normalizeOptionalString(opts.thinking), | |
| timeoutSeconds, | |
| lightContext: opts.lightContext === true ? true : undefined, | |
| toolsAllow, | |
| }; | |
| })(); | |
| const sessionSource = cmd.getOptionValueSource("session"); | |
| const sessionTargetRaw = normalizeOptionalString(opts.session) ?? ""; | |
| const inferredSessionTarget = | |
| resolvedPayload.kind === "agentTurn" || | |
| resolvedPayload.kind === "command" || | |
| resolvedPayload.kind === "script" | |
| ? "isolated" | |
| : "main"; | |
| const sessionTarget = | |
| sessionSource === "cli" | |
| ? normalizeCronSessionTargetOption(sessionTargetRaw) || "" | |
| : inferredSessionTarget; | |
| const isCustomSessionTarget = | |
| normalizeLowercaseStringOrEmpty(sessionTarget).startsWith("session:") && | |
| Boolean(normalizeOptionalString(sessionTarget.slice(8))); | |
| const isIsolatedLikeSessionTarget = | |
| sessionTarget === "isolated" || sessionTarget === "current" || isCustomSessionTarget; | |
| if (sessionTarget !== "main" && !isIsolatedLikeSessionTarget) { | |
| throw new CronCliError("--session must be main, isolated, current, or session:<id>"); | |
| } | |
| if (opts.deleteAfterRun && opts.keepAfterRun) { | |
| throw new CronCliError("Choose --delete-after-run or --keep-after-run, not both"); | |
| } | |
| if ( | |
| sessionTarget === "main" && | |
| resolvedPayload.kind !== "systemEvent" && | |
| resolvedPayload.kind !== "script" | |
| ) { | |
| throw new CronCliError("Main jobs require --system-event or --script."); | |
| } | |
| if ( | |
| resolvedPayload.kind === "script" && | |
| sessionTarget !== "main" && | |
| sessionTarget !== "isolated" | |
| ) { | |
| throw new CronCliError("Script jobs require --session main or --session isolated."); | |
| } | |
| if ( | |
| isIsolatedLikeSessionTarget && | |
| resolvedPayload.kind !== "agentTurn" && | |
| resolvedPayload.kind !== "command" && | |
| resolvedPayload.kind !== "script" | |
| ) { | |
| throw new CronCliError("Isolated jobs require --message, --command, or --script."); | |
| } | |
| if ( | |
| (opts.announce || typeof opts.deliver === "boolean") && | |
| (!isIsolatedLikeSessionTarget || | |
| (resolvedPayload.kind !== "agentTurn" && | |
| resolvedPayload.kind !== "command" && | |
| resolvedPayload.kind !== "script")) | |
| ) { | |
| throw new CronCliError( | |
| "--announce/--no-deliver require a non-main agentTurn, command, or script session target.", | |
| ); | |
| } | |
| const accountId = normalizeOptionalString(opts.account); | |
| const threadId = parseCronThreadIdOption(opts.threadId); | |
| const hasThreadId = typeof threadId === "number"; | |
| const hasChatDeliveryTarget = | |
| cmd.getOptionValueSource("channel") === "cli" || | |
| typeof opts.to === "string" || | |
| Boolean(accountId) || | |
| hasThreadId; | |
| if ( | |
| hasChatDeliveryTarget && | |
| (!isIsolatedLikeSessionTarget || | |
| (resolvedPayload.kind !== "agentTurn" && | |
| resolvedPayload.kind !== "command" && | |
| resolvedPayload.kind !== "script")) | |
| ) { | |
| throw new CronCliError( | |
| "--channel, --to, --account, and --thread-id require a non-main agentTurn, command, or script job with delivery.", | |
| ); | |
| } | |
| if (hasWebhook && hasChatDeliveryTarget) { | |
| throw new CronCliError("--webhook cannot be combined with chat delivery options."); | |
| } | |
| const deliveryMode = hasWebhook | |
| ? "webhook" | |
| : isIsolatedLikeSessionTarget && | |
| (resolvedPayload.kind === "agentTurn" || | |
| resolvedPayload.kind === "command" || | |
| resolvedPayload.kind === "script") | |
| ? hasAnnounce | |
| ? "announce" | |
| : hasNoDeliver | |
| ? "none" | |
| : "announce" | |
| : undefined; | |
| const optionName = normalizeOptionalString(opts.name); | |
| const positionalName = hasScheduleFlag ? normalizeOptionalString(nameArg) : undefined; | |
| if (optionName && positionalName && optionName !== positionalName) { | |
| throw new CronCliError( | |
| "Pass the automation name either positionally or with --name, not both.", | |
| ); | |
| } | |
| const name = optionName ?? positionalName ?? ""; | |
| if (!name) { | |
| throw new CronCliError("Cron job name is required. Pass a name or --name <name>."); | |
| } | |
| const description = normalizeOptionalString(opts.description); | |
| const declarationKey = normalizeOptionalString(opts.declarationKey); | |
| if (typeof opts.declarationKey === "string" && !declarationKey) { | |
| throw new CronCliError("--declaration-key must not be blank"); | |
| } | |
| const displayName = normalizeOptionalString(opts.displayName); | |
| if (typeof opts.displayName === "string" && !displayName) { | |
| throw new CronCliError("--display-name must not be blank"); | |
| } | |
| const pacingMin = normalizeOptionalString(opts.pacingMin); | |
| const pacingMax = normalizeOptionalString(opts.pacingMax); | |
| if (typeof opts.pacingMin === "string" && !pacingMin) { | |
| throw new CronCliError("--pacing-min must not be blank"); | |
| } | |
| if (typeof opts.pacingMax === "string" && !pacingMax) { | |
| throw new CronCliError("--pacing-max must not be blank"); | |
| } | |
| const sessionKey = normalizeOptionalString(opts.sessionKey); | |
| const triggerScriptPath = readNonBlankString(opts.triggerScript); | |
| if ((opts.triggerOnce || opts.triggerScript !== undefined) && !triggerScriptPath) { | |
| throw new CronCliError( | |
| `--trigger-${opts.triggerOnce ? "once requires --trigger-script" : "script must not be blank"}`, | |
| ); | |
| } | |
| const trigger = triggerScriptPath && { | |
| script: await readCronTriggerScript(triggerScriptPath), | |
| ...(opts.triggerOnce ? { once: true } : {}), | |
| }; | |
| if ( | |
| (resolvedPayload.kind === "agentTurn" || resolvedPayload.kind === "script") && | |
| !agentId | |
| ) { | |
| defaultRuntime.error( | |
| theme.warn( | |
| "No --agent specified; the job will run with the configured default agent. " + | |
| "Specify --agent to choose a specific agent, or set agents.defaults.systemAgent.agentId.", | |
| ), | |
| ); | |
| } | |
| const params = { | |
| name, | |
| declarationKey, | |
| displayName, | |
| description, | |
| ...(declarationKey && cmd.getOptionValueSource("disabled") !== "cli" | |
| ? {} | |
| : { enabled: !opts.disabled }), | |
| deleteAfterRun: opts.deleteAfterRun ? true : opts.keepAfterRun ? false : undefined, | |
| agentId, | |
| sessionKey, | |
| schedule, | |
| ...(pacingMin || pacingMax | |
| ? { | |
| pacing: { | |
| ...(pacingMin ? { min: pacingMin } : {}), | |
| ...(pacingMax ? { max: pacingMax } : {}), | |
| }, | |
| } | |
| : {}), | |
| trigger, | |
| sessionTarget, | |
| wakeMode, | |
| payload: resolvedPayload, | |
| delivery: deliveryMode | |
| ? { | |
| mode: deliveryMode, | |
| channel: hasWebhook ? undefined : normalizeOptionalString(opts.channel), | |
| to: hasWebhook ? webhookUrl : normalizeOptionalString(opts.to), | |
| threadId: hasWebhook ? undefined : threadId, | |
| accountId: hasWebhook ? 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); | |
| } | |
| }, | |
| ), | |
| ); | |
| } | |