UNPKG

@gguf/claw

Version:

WhatsApp gateway CLI (Baileys web) with Pi RPC agent

453 lines (447 loc) 23.5 kB
import { D as colorize, O as isRich, k as theme, p as defaultRuntime, v as danger } from "./entry.js"; import "./auth-profiles-CfFGCDJa.js"; import { L as sanitizeAgentId } from "./agent-scope-jm0ZdXwM.js"; import "./utils-PmTbZoD1.js"; import "./exec-BIMFe4XS.js"; import "./github-copilot-token-rP-6QdKv.js"; import "./config-DCT1RAo6.js"; import "./manifest-registry-tuAcHxrV.js"; import "./client-cU7Xg1MO.js"; import "./call-CfqL-4Nc.js"; import "./message-channel-CAFcg7mw.js"; import { t as formatDocsLink } from "./links-jGisPfXW.js"; import { n as listChannelPlugins } from "./plugins-TrKFfrLt.js"; import "./logging-fywhKCmE.js"; import "./accounts-B5QZU96b.js"; import "./progress-Dn3kWpaL.js"; import { t as parseAbsoluteTimeMs } from "./parse-BB0Cqon8.js"; import { n as parsePositiveIntOrUndefined } from "./helpers-uGhPZ_kK.js"; import { n as callGatewayFromCli, t as addGatewayClientOptions } from "./gateway-rpc-CMAcradB.js"; //#region src/cli/cron-cli/shared.ts const getCronChannelOptions = () => ["last", ...listChannelPlugins().map((plugin) => plugin.id)].join("|"); async function warnIfCronSchedulerDisabled(opts) { try { const res = await callGatewayFromCli("cron.status", opts, {}); if (res?.enabled === true) return; const store = typeof res?.storePath === "string" ? res.storePath : ""; defaultRuntime.error([ "warning: cron scheduler is disabled in the Gateway; jobs are saved but will not run automatically.", "Re-enable with `cron.enabled: true` (or remove `cron.enabled: false`) and restart the Gateway.", store ? `store: ${store}` : "" ].filter(Boolean).join("\n")); } catch {} } function parseDurationMs(input) { const raw = input.trim(); if (!raw) return null; const match = raw.match(/^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/i); if (!match) return null; const n = Number.parseFloat(match[1] ?? ""); if (!Number.isFinite(n) || n <= 0) return null; const unit = (match[2] ?? "").toLowerCase(); const factor = unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5; return Math.floor(n * factor); } function parseAt(input) { const raw = input.trim(); if (!raw) return null; const absolute = parseAbsoluteTimeMs(raw); if (absolute !== null) return new Date(absolute).toISOString(); const dur = parseDurationMs(raw); if (dur !== null) return new Date(Date.now() + dur).toISOString(); return null; } const CRON_ID_PAD = 36; const CRON_NAME_PAD = 24; const CRON_SCHEDULE_PAD = 32; 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, width) => value.padEnd(width); const truncate = (value, width) => { if (value.length <= width) return value; if (width <= 3) return value.slice(0, width); return `${value.slice(0, width - 3)}...`; }; const formatIsoMinute = (iso) => { const parsed = parseAbsoluteTimeMs(iso); const d = new Date(parsed ?? NaN); if (Number.isNaN(d.getTime())) return "-"; const isoStr = d.toISOString(); return `${isoStr.slice(0, 10)} ${isoStr.slice(11, 16)}Z`; }; const formatDuration = (ms) => { if (ms < 6e4) return `${Math.max(1, Math.round(ms / 1e3))}s`; if (ms < 36e5) return `${Math.round(ms / 6e4)}m`; if (ms < 864e5) return `${Math.round(ms / 36e5)}h`; return `${Math.round(ms / 864e5)}d`; }; const formatSpan = (ms) => { if (ms < 6e4) return "<1m"; if (ms < 36e5) return `${Math.round(ms / 6e4)}m`; if (ms < 864e5) return `${Math.round(ms / 36e5)}h`; return `${Math.round(ms / 864e5)}d`; }; const formatRelative = (ms, nowMs) => { if (!ms) return "-"; const delta = ms - nowMs; const label = formatSpan(Math.abs(delta)); return delta >= 0 ? `in ${label}` : `${label} ago`; }; const formatSchedule = (schedule) => { if (schedule.kind === "at") return `at ${formatIsoMinute(schedule.at)}`; if (schedule.kind === "every") return `every ${formatDuration(schedule.everyMs)}`; return schedule.tz ? `cron ${schedule.expr} @ ${schedule.tz}` : `cron ${schedule.expr}`; }; const formatStatus = (job) => { if (!job.enabled) return "disabled"; if (job.state.runningAtMs) return "running"; return job.state.lastStatus ?? "idle"; }; function printCronList(jobs, runtime = defaultRuntime) { if (jobs.length === 0) { runtime.log("No cron jobs."); return; } const rich = isRich(); const header = [ pad("ID", CRON_ID_PAD), pad("Name", CRON_NAME_PAD), pad("Schedule", CRON_SCHEDULE_PAD), pad("Next", CRON_NEXT_PAD), 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); const now = Date.now(); for (const job of jobs) { const idLabel = pad(job.id, CRON_ID_PAD); const nameLabel = pad(truncate(job.name, CRON_NAME_PAD), CRON_NAME_PAD); const scheduleLabel = pad(truncate(formatSchedule(job.schedule), CRON_SCHEDULE_PAD), CRON_SCHEDULE_PAD); const nextLabel = pad(job.enabled ? formatRelative(job.state.nextRunAtMs, now) : "-", CRON_NEXT_PAD); const lastLabel = pad(formatRelative(job.state.lastRunAtMs, now), CRON_LAST_PAD); 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); if (statusRaw === "error") return colorize(rich, theme.error, statusLabel); if (statusRaw === "running") return colorize(rich, theme.warn, statusLabel); if (statusRaw === "skipped") return colorize(rich, theme.muted, statusLabel); return colorize(rich, theme.muted, statusLabel); })(); const coloredTarget = 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), colorize(rich, theme.info, nameLabel), colorize(rich, theme.info, scheduleLabel), colorize(rich, theme.muted, nextLabel), colorize(rich, theme.muted, lastLabel), coloredStatus, coloredTarget, coloredAgent ].join(" "); runtime.log(line.trimEnd()); } } //#endregion //#region src/cli/cron-cli/register.cron-add.ts function registerCronStatusCommand(cron) { 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, {}); defaultRuntime.log(JSON.stringify(res, null, 2)); } catch (err) { defaultRuntime.error(danger(String(err))); defaultRuntime.exit(1); } })); } function registerCronListCommand(cron) { 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) { defaultRuntime.log(JSON.stringify(res, null, 2)); return; } printCronList(res?.jobs ?? [], defaultRuntime); } catch (err) { defaultRuntime.error(danger(String(err))); defaultRuntime.exit(1); } })); } function registerCronAddCommand(cron) { 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("--wake <mode>", "Wake mode (now|next-heartbeat)", "next-heartbeat").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)").option("--tz <iana>", "Timezone for cron expressions (IANA)", "").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)").option("--model <model>", "Model override for agent jobs (provider/model or alias)").option("--timeout-seconds <n>", "Timeout seconds for agent jobs").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("--best-effort-deliver", "Do not fail the job if delivery fails", false).option("--json", "Output JSON", false).action(async (opts, cmd) => { try { 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 : ""; if ([ Boolean(at), Boolean(every), Boolean(cronExpr) ].filter(Boolean).length !== 1) throw new Error("Choose exactly one schedule: --at, --every, or --cron"); if (at) { const atIso = parseAt(at); if (!atIso) throw new Error("Invalid --at; use ISO time or duration like 20m"); return { kind: "at", at: atIso }; } if (every) { const everyMs = parseDurationMs(every); if (!everyMs) throw new Error("Invalid --every; use e.g. 10m, 1h, 1d"); return { kind: "every", everyMs }; } return { kind: "cron", expr: cronExpr, tz: typeof opts.tz === "string" && opts.tz.trim() ? opts.tz.trim() : void 0 }; })(); const wakeMode = (typeof opts.wake === "string" ? opts.wake : "next-heartbeat").trim() || "next-heartbeat"; 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()) : void 0; const hasAnnounce = Boolean(opts.announce) || opts.deliver === true; const hasNoDeliver = opts.deliver === false; if ([hasAnnounce, hasNoDeliver].filter(Boolean).length > 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() : ""; if ([Boolean(systemEvent), Boolean(message)].filter(Boolean).length !== 1) throw new Error("Choose exactly one payload: --system-event or --message"); if (systemEvent) return { kind: "systemEvent", text: systemEvent }; const timeoutSeconds = parsePositiveIntOrUndefined(opts.timeoutSeconds); return { kind: "agentTurn", message, model: typeof opts.model === "string" && opts.model.trim() ? opts.model.trim() : void 0, thinking: typeof opts.thinking === "string" && opts.thinking.trim() ? opts.thinking.trim() : void 0, timeoutSeconds: timeoutSeconds && Number.isFinite(timeoutSeconds) ? timeoutSeconds : void 0 }; })(); const sessionSource = (typeof cmd?.getOptionValueSource === "function" ? (name) => cmd.getOptionValueSource(name) : () => void 0)("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 deliveryMode = sessionTarget === "isolated" && payload.kind === "agentTurn" ? hasAnnounce ? "announce" : hasNoDeliver ? "none" : "announce" : void 0; const name = (typeof opts.name === "string" ? opts.name : "").trim(); if (!name) throw new Error("--name is required"); const res = await callGatewayFromCli("cron.add", opts, { name, description: typeof opts.description === "string" && opts.description.trim() ? opts.description.trim() : void 0, enabled: !opts.disabled, deleteAfterRun: opts.deleteAfterRun ? true : opts.keepAfterRun ? false : void 0, agentId, schedule, sessionTarget, wakeMode, payload, delivery: deliveryMode ? { mode: deliveryMode, channel: typeof opts.channel === "string" && opts.channel.trim() ? opts.channel.trim() : void 0, to: typeof opts.to === "string" && opts.to.trim() ? opts.to.trim() : void 0, bestEffort: opts.bestEffortDeliver ? true : void 0 } : void 0 }); defaultRuntime.log(JSON.stringify(res, null, 2)); await warnIfCronSchedulerDisabled(opts); } catch (err) { defaultRuntime.error(danger(String(err))); defaultRuntime.exit(1); } })); } //#endregion //#region src/cli/cron-cli/register.cron-edit.ts const assignIf = (target, key, value, shouldAssign) => { if (shouldAssign) target[key] = value; }; function registerCronEditCommand(cron) { addGatewayClientOptions(cron.command("edit").description("Edit a cron job (patch fields)").argument("<id>", "Job id").option("--name <name>", "Set name").option("--description <text>", "Set description").option("--enable", "Enable job", false).option("--disable", "Disable job", 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("--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").option("--cron <expr>", "Set cron expression").option("--tz <iana>", "Timezone for cron expressions (IANA)").option("--system-event <text>", "Set systemEvent payload").option("--message <text>", "Set agentTurn payload message").option("--thinking <level>", "Thinking level for agent jobs").option("--model <model>", "Model override for agent jobs").option("--timeout-seconds <n>", "Timeout seconds for agent jobs").option("--announce", "Announce summary to a chat (subagent-style)").option("--deliver", "Deprecated (use --announce). Announces a summary to a chat.").option("--no-deliver", "Disable announce delivery").option("--channel <channel>", `Delivery channel (${getCronChannelOptions()})`).option("--to <dest>", "Delivery destination (E.164, Telegram chatId, or Discord channel/user)").option("--best-effort-deliver", "Do not fail job if delivery fails").option("--no-best-effort-deliver", "Fail job when delivery fails").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.announce && typeof opts.deliver === "boolean") throw new Error("Choose --announce or --no-deliver (not multiple)."); const patch = {}; if (typeof opts.name === "string") patch.name = opts.name; if (typeof opts.description === "string") patch.description = opts.description; if (opts.enable && opts.disable) throw new Error("Choose --enable or --disable, not both"); if (opts.enable) patch.enabled = true; if (opts.disable) patch.enabled = false; if (opts.deleteAfterRun && opts.keepAfterRun) throw new Error("Choose --delete-after-run or --keep-after-run, not both"); if (opts.deleteAfterRun) patch.deleteAfterRun = true; if (opts.keepAfterRun) patch.deleteAfterRun = false; 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 = sanitizeAgentId(opts.agent.trim()); if (opts.clearAgent) patch.agentId = null; if ([ opts.at, opts.every, opts.cron ].filter(Boolean).length > 1) throw new Error("Choose at most one schedule change"); if (opts.at) { const atIso = parseAt(String(opts.at)); if (!atIso) throw new Error("Invalid --at"); patch.schedule = { kind: "at", at: atIso }; } else if (opts.every) { const everyMs = parseDurationMs(String(opts.every)); if (!everyMs) throw new Error("Invalid --every"); patch.schedule = { kind: "every", everyMs }; } else if (opts.cron) patch.schedule = { kind: "cron", expr: String(opts.cron), tz: typeof opts.tz === "string" && opts.tz.trim() ? opts.tz.trim() : void 0 }; const hasSystemEventPatch = typeof opts.systemEvent === "string"; const model = typeof opts.model === "string" && opts.model.trim() ? opts.model.trim() : void 0; const thinking = typeof opts.thinking === "string" && opts.thinking.trim() ? opts.thinking.trim() : void 0; const timeoutSeconds = opts.timeoutSeconds ? Number.parseInt(String(opts.timeoutSeconds), 10) : void 0; const hasTimeoutSeconds = Boolean(timeoutSeconds && Number.isFinite(timeoutSeconds)); const hasDeliveryModeFlag = opts.announce || typeof opts.deliver === "boolean"; const hasDeliveryTarget = typeof opts.channel === "string" || typeof opts.to === "string"; const hasBestEffort = typeof opts.bestEffortDeliver === "boolean"; const hasAgentTurnPatch = typeof opts.message === "string" || Boolean(model) || Boolean(thinking) || hasTimeoutSeconds || hasDeliveryModeFlag || hasDeliveryTarget || hasBestEffort; if (hasSystemEventPatch && hasAgentTurnPatch) throw new Error("Choose at most one payload change"); if (hasSystemEventPatch) patch.payload = { kind: "systemEvent", text: String(opts.systemEvent) }; else if (hasAgentTurnPatch) { const payload = { kind: "agentTurn" }; assignIf(payload, "message", String(opts.message), typeof opts.message === "string"); assignIf(payload, "model", model, Boolean(model)); assignIf(payload, "thinking", thinking, Boolean(thinking)); assignIf(payload, "timeoutSeconds", timeoutSeconds, hasTimeoutSeconds); patch.payload = payload; } if (hasDeliveryModeFlag || hasDeliveryTarget || hasBestEffort) { const delivery = { mode: opts.announce || opts.deliver === true ? "announce" : opts.deliver === false ? "none" : "announce" }; if (typeof opts.channel === "string") { const channel = opts.channel.trim(); delivery.channel = channel ? channel : void 0; } if (typeof opts.to === "string") { const to = opts.to.trim(); delivery.to = to ? to : void 0; } if (typeof opts.bestEffortDeliver === "boolean") delivery.bestEffort = opts.bestEffortDeliver; patch.delivery = delivery; } const res = await callGatewayFromCli("cron.update", opts, { id, patch }); defaultRuntime.log(JSON.stringify(res, null, 2)); await warnIfCronSchedulerDisabled(opts); } catch (err) { defaultRuntime.error(danger(String(err))); defaultRuntime.exit(1); } })); } //#endregion //#region src/cli/cron-cli/register.cron-simple.ts function registerCronSimpleCommands(cron) { addGatewayClientOptions(cron.command("rm").alias("remove").alias("delete").description("Remove a cron job").argument("<id>", "Job id").option("--json", "Output JSON", false).action(async (id, opts) => { try { const res = await callGatewayFromCli("cron.remove", opts, { id }); defaultRuntime.log(JSON.stringify(res, null, 2)); } catch (err) { defaultRuntime.error(danger(String(err))); defaultRuntime.exit(1); } })); 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)").requiredOption("--id <id>", "Job id").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 res = await callGatewayFromCli("cron.runs", opts, { id: String(opts.id), limit }); defaultRuntime.log(JSON.stringify(res, null, 2)); } catch (err) { defaultRuntime.error(danger(String(err))); defaultRuntime.exit(1); } })); addGatewayClientOptions(cron.command("run").description("Run a cron job now (debug)").argument("<id>", "Job id").option("--force", "Run even if not due", false).action(async (id, opts) => { try { const res = await callGatewayFromCli("cron.run", opts, { id, mode: opts.force ? "force" : "due" }); defaultRuntime.log(JSON.stringify(res, null, 2)); } catch (err) { defaultRuntime.error(danger(String(err))); defaultRuntime.exit(1); } })); } //#endregion //#region src/cli/cron-cli/register.ts function registerCronCli(program) { const cron = program.command("cron").description("Manage cron jobs (via Gateway)").addHelpText("after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/cron", "docs.openclaw.ai/cli/cron")}\n`); registerCronStatusCommand(cron); registerCronListCommand(cron); registerCronAddCommand(cron); registerCronSimpleCommands(cron); registerCronEditCommand(cron); } //#endregion export { registerCronCli };