UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,055 lines 59.2 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString, s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js";
import { A as resolvePositiveTimerTimeoutMs, C as resolveExpiresAtMsFromDurationMs, P as timestampMsToIsoString, b as parseStrictPositiveInteger, j as resolveTimerTimeoutMs } from "./number-coercion-CJQ8TR--.js";
import "./parse-finite-number-Z7n6tXLk.js";
import { t as formatDocsLink } from "./links-CsLBrRff.js";
import { n as isRich, r as theme, t as colorize } from "./theme-vjDs9tao.js";
import { n as defaultRuntime } from "./runtime-B4lgFmsS.js";
import { p as formatTimestamp } from "./logger-Brhy0cvC.js";
import { h as sanitizeAgentId } from "./session-key-B_NoIfpX.js";
import { t as danger } from "./globals-GTrXU4s9.js";
import { t as parseDurationMs$1 } from "./parse-duration-oxu_S07c.js";
import { i as listChannelPlugins } from "./registry-MkxNn1Ue.js";
import "./plugins-t2ejWcVy.js";
import { i as parseAbsoluteTimeMs, n as resolveCronStaggerMs } from "./stagger-Bbv08nIY.js";
import { n as formatDurationHuman } from "./format-duration-BrZ-AaEJ.js";
import { n as callGatewayFromCli, t as addGatewayClientOptions } from "./gateway-rpc-DV2_JEP0.js";
import { i as parseStrictPositiveIntOrUndefined, n as parsePositiveIntOrUndefined } from "./helpers-gBVG4H2O.js";
import { t as applyParentDefaultHelpAction } from "./parent-default-help-DQUF3qKA.js";
//#region src/infra/format-time/parse-offsetless-zoned-datetime.ts
const OFFSETLESS_ISO_DATETIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(\.\d+)?$/;
const OFFSETLESS_ISO_DATETIME_PARTS_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?$/;
function isOffsetlessIsoDateTime(raw) {
	return OFFSETLESS_ISO_DATETIME_RE.test(raw);
}
function parseOffsetlessIsoDateTimeInTimeZone(raw, timeZone) {
	const expectedParts = parseOffsetlessIsoDateTimeParts(raw);
	if (!expectedParts) return null;
	try {
		getZonedDateTimeParts(Date.now(), timeZone);
		const naiveMs = (/* @__PURE__ */ new Date(`${raw}Z`)).getTime();
		if (Number.isNaN(naiveMs)) return null;
		const resolvedMs = naiveMs - getTimeZoneOffsetMs(naiveMs - getTimeZoneOffsetMs(naiveMs, timeZone), timeZone);
		if (!matchesOffsetlessIsoDateTimeParts(resolvedMs, timeZone, expectedParts)) return null;
		return new Date(resolvedMs).toISOString();
	} catch {
		return null;
	}
}
function parseOffsetlessIsoDateTimeParts(raw) {
	const match = OFFSETLESS_ISO_DATETIME_PARTS_RE.exec(raw);
	if (!match) return null;
	const fractionalMs = (match[7] ?? "").padEnd(3, "0").slice(0, 3);
	return {
		year: Number.parseInt(match[1] ?? "0", 10),
		month: Number.parseInt(match[2] ?? "0", 10),
		day: Number.parseInt(match[3] ?? "0", 10),
		hour: Number.parseInt(match[4] ?? "0", 10),
		minute: Number.parseInt(match[5] ?? "0", 10),
		second: Number.parseInt(match[6] ?? "0", 10),
		millisecond: Number.parseInt(fractionalMs || "0", 10)
	};
}
function matchesOffsetlessIsoDateTimeParts(utcMs, timeZone, expected) {
	const actual = getZonedDateTimeParts(utcMs, timeZone);
	return actual.year === expected.year && actual.month === expected.month && actual.day === expected.day && actual.hour === expected.hour && actual.minute === expected.minute && actual.second === expected.second && actual.millisecond === expected.millisecond;
}
function getTimeZoneOffsetMs(utcMs, timeZone) {
	const parts = getZonedDateTimeParts(utcMs, timeZone);
	return Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second, parts.millisecond) - utcMs;
}
function getZonedDateTimeParts(utcMs, timeZone) {
	const utcDate = new Date(utcMs);
	const parts = new Intl.DateTimeFormat("en-US", {
		timeZone,
		year: "numeric",
		month: "2-digit",
		day: "2-digit",
		hour: "2-digit",
		minute: "2-digit",
		second: "2-digit",
		hour12: false,
		hourCycle: "h23"
	}).formatToParts(utcDate);
	const getNumericPart = (type) => {
		const part = parts.find((candidate) => candidate.type === type);
		return Number.parseInt(part?.value ?? "0", 10);
	};
	return {
		year: getNumericPart("year"),
		month: getNumericPart("month"),
		day: getNumericPart("day"),
		hour: getNumericPart("hour"),
		minute: getNumericPart("minute"),
		second: getNumericPart("second"),
		millisecond: utcDate.getUTCMilliseconds()
	};
}
//#endregion
//#region src/cli/cron-cli/shared.ts
function parseCronCommandArgv(value) {
	if (typeof value !== "string") return;
	let parsed;
	try {
		parsed = JSON.parse(value);
	} catch {
		throw new Error("--command-argv must be a JSON array of strings");
	}
	if (!Array.isArray(parsed) || parsed.length === 0 || parsed.some((entry) => typeof entry !== "string" || entry.length === 0)) throw new Error("--command-argv must be a non-empty JSON array of non-empty strings");
	return parsed;
}
function parseCronCommandEnv(values) {
	const rawValues = Array.isArray(values) ? values : typeof values === "string" ? [values] : [];
	if (rawValues.length === 0) return;
	const env = {};
	for (const raw of rawValues) {
		if (typeof raw !== "string") throw new Error("--command-env must be KEY=VALUE");
		const idx = raw.indexOf("=");
		const key = idx > 0 ? raw.slice(0, idx).trim() : "";
		if (!key) throw new Error("--command-env must be KEY=VALUE");
		env[key] = raw.slice(idx + 1);
	}
	return env;
}
const getCronChannelOptions = () => {
	const pluginIds = listChannelPlugins().map((plugin) => plugin.id).filter(Boolean);
	return pluginIds.length > 0 ? ["last", ...pluginIds].join("|") : "last|<channel-id>";
};
function toLocalIsoTime(value) {
	return typeof value === "number" && Number.isFinite(value) ? formatTimestamp(new Date(value), { style: "long" }) : void 0;
}
/**
* CLI-only display enrichment for `cron runs` history entries: adds a short
* `cause` alias for `errorReason` plus readable local-offset ISO mirrors of the
* numeric timestamps (matching the diagnostic log `time` format). Stored data
* and the gateway protocol stay unchanged; raw numeric fields are preserved.
*/
function enrichCronRunEntriesForDisplay(value) {
	if (!value || typeof value !== "object") return value;
	const record = value;
	const entries = record.entries;
	if (!Array.isArray(entries)) return value;
	const nextEntries = entries.map((entry) => {
		if (!entry || typeof entry !== "object") return entry;
		const item = entry;
		if (item.action !== "finished") return item;
		const extra = {};
		const cause = typeof item.errorReason === "string" ? item.errorReason.trim() : "";
		if (cause) extra.cause = cause;
		const tsIso = toLocalIsoTime(item.ts);
		if (tsIso) extra.tsIso = tsIso;
		const runAtIso = toLocalIsoTime(item.runAtMs);
		if (runAtIso) extra.runAtIso = runAtIso;
		const nextRunAtIso = toLocalIsoTime(item.nextRunAtMs);
		if (nextRunAtIso) extra.nextRunAtIso = nextRunAtIso;
		return Object.keys(extra).length > 0 ? Object.assign({}, item, extra) : item;
	});
	return {
		...record,
		entries: nextEntries
	};
}
function printCronJson(value) {
	defaultRuntime.writeJson(enrichCronRunEntriesForDisplay(value));
}
/**
* Enrich a CronJob (or list response) with a computed `status` field
* derived from enabled + state.runningAtMs + state.lastRunStatus.
* This mirrors the human-readable status shown by `cron list` / `cron show`.
*/
function enrichCronJsonWithStatus(value) {
	if (!value || typeof value !== "object") return value;
	const obj = value;
	if ("state" in obj && "enabled" in obj) return {
		...obj,
		status: computeStatus(obj)
	};
	if ("jobs" in obj && Array.isArray(obj.jobs)) {
		const enrichedJobs = obj.jobs.map((job) => {
			const status = computeStatus(job);
			return Object.assign({}, job, { status });
		});
		return {
			...obj,
			jobs: enrichedJobs
		};
	}
	return value;
}
function computeStatus(job) {
	if (!job.enabled) return "disabled";
	const state = job.state ?? {};
	if (state.runningAtMs) return "running";
	return state.lastRunStatus ?? state.lastStatus ?? "idle";
}
function handleCronCliError(err) {
	defaultRuntime.error(danger(String(err)));
	defaultRuntime.exit(1);
}
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 = normalizeLowercaseStringOrEmpty(match[2] ?? "");
	const result = Math.floor(n * (unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5));
	if (!Number.isFinite(result)) return null;
	return result;
}
function parseCronStaggerMs(params) {
	if (params.useExact) return 0;
	if (!params.staggerRaw) return;
	const parsed = parseDurationMs(params.staggerRaw);
	if (!parsed) throw new Error("Invalid --stagger; use e.g. 30s, 1m, 5m");
	return parsed;
}
function parseCronToolsAllow(input) {
	const tools = (Array.isArray(input) ? input.map((value) => String(value)).join(" ") : typeof input === "string" ? input : "").split(/[,\s]+/u).map((tool) => normalizeOptionalString(tool)).filter((tool) => Boolean(tool));
	return tools.length > 0 ? tools : void 0;
}
/**
* Parse a one-shot `--at` value into an ISO string (UTC).
*
* When `tz` is provided and the input is an offset-less datetime
* (e.g. `2026-03-23T23:00:00`), the datetime is interpreted in
* that IANA timezone instead of UTC.
*/
function parseAt(input, tz) {
	const raw = input.trim();
	if (!raw) return null;
	if (tz && isOffsetlessIsoDateTime(raw)) return parseOffsetlessIsoDateTimeInTimeZone(raw, tz);
	const absolute = parseAbsoluteTimeMs(raw);
	if (absolute !== null) return timestampMsToIsoString(absolute) ?? null;
	const dur = parseDurationMs(raw.startsWith("+") ? raw.slice(1) : raw);
	if (dur !== null) return timestampMsToIsoString(resolveExpiresAtMsFromDurationMs(dur)) ?? null;
	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_DELIVERY_PAD = 64;
const CRON_AGENT_PAD = 10;
const CRON_MODEL_PAD = 20;
const stringifyCell = (value, fallback = "-") => {
	if (typeof value === "string") return value;
	if (typeof value === "number" || typeof value === "boolean") return String(value);
	return fallback;
};
const pad = (value, width) => stringifyCell(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 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 ${formatDurationHuman(schedule.everyMs)}`;
	if (schedule?.kind !== "cron") return "-";
	const base = schedule.tz ? `cron ${schedule.expr} @ ${schedule.tz}` : `cron ${schedule.expr}`;
	const staggerMs = resolveCronStaggerMs(schedule);
	if (staggerMs <= 0) return `${base} (exact)`;
	return `${base} (stagger ${formatDurationHuman(staggerMs)})`;
};
const formatStatus = (job) => {
	if (!job.enabled) return "disabled";
	const state = job.state ?? {};
	if (state.runningAtMs) return "running";
	return state.lastStatus ?? "idle";
};
function coerceCronDeliveryPreviews(value) {
	const previews = value && typeof value === "object" ? value.deliveryPreviews : void 0;
	if (!previews || typeof previews !== "object") return /* @__PURE__ */ new Map();
	return new Map(Object.entries(previews).flatMap(([jobId, preview]) => {
		if (!preview || typeof preview !== "object") return [];
		const record = preview;
		if (typeof record.label !== "string" || typeof record.detail !== "string") return [];
		return [[jobId, {
			label: record.label,
			detail: record.detail
		}]];
	}));
}
function printCronList(jobs, runtime = defaultRuntime, opts) {
	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("Delivery", CRON_DELIVERY_PAD),
		pad("Agent ID", CRON_AGENT_PAD),
		pad("Model", CRON_MODEL_PAD)
	].join(" ");
	runtime.log(rich ? theme.heading(header) : header);
	const now = Date.now();
	for (const job of jobs) {
		const state = job.state ?? {};
		const idLabel = pad(job.id, CRON_ID_PAD);
		const nameLabel = pad(truncate(stringifyCell(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(state.nextRunAtMs, now) : "-", CRON_NEXT_PAD);
		const lastLabel = pad(formatRelative(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 deliveryPreview = opts?.deliveryPreviews?.get(job.id);
		const deliveryLabel = pad(truncate(deliveryPreview ? `${deliveryPreview.label} (${deliveryPreview.detail})` : "-", CRON_DELIVERY_PAD), CRON_DELIVERY_PAD);
		const agentLabel = pad(truncate(job.agentId ?? "-", CRON_AGENT_PAD), CRON_AGENT_PAD);
		const modelLabel = pad(truncate((job.payload?.kind === "agentTurn" ? job.payload.model : void 0) ?? "-", CRON_MODEL_PAD), CRON_MODEL_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 === "main" ? colorize(rich, theme.accent, targetLabel) : colorize(rich, theme.accentBright, 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,
			deliveryPreview ? colorize(rich, theme.info, deliveryLabel) : colorize(rich, theme.muted, deliveryLabel),
			coloredAgent,
			job.payload?.kind === "agentTurn" && job.payload.model ? colorize(rich, theme.info, modelLabel) : colorize(rich, theme.muted, modelLabel)
		].join(" ");
		runtime.log(line.trimEnd());
	}
}
function printCronShow(job, runtime = defaultRuntime, opts) {
	const preview = opts?.deliveryPreview ?? {
		label: "-",
		detail: "unavailable"
	};
	runtime.log(`id: ${job.id}`);
	runtime.log(`name: ${job.name}`);
	runtime.log(`enabled: ${job.enabled ? "yes" : "no"}`);
	runtime.log(`schedule: ${formatSchedule(job.schedule)}`);
	runtime.log(`session: ${job.sessionTarget ?? "-"}`);
	runtime.log(`agent: ${job.agentId ?? "-"}`);
	runtime.log(`model: ${job.payload.kind === "agentTurn" ? job.payload.model ?? "-" : "-"}`);
	runtime.log(`delivery: ${preview.label} (${preview.detail})`);
	runtime.log(`next: ${formatRelative(job.state.nextRunAtMs, Date.now())}`);
	runtime.log(`last: ${formatRelative(job.state.lastRunAtMs, Date.now())}`);
	runtime.log(`status: ${formatStatus(job)}`);
	runtime.log(`diagnostic: ${job.state.lastDiagnosticSummary ?? "-"}`);
}
//#endregion
//#region src/cli/cron-cli/schedule-options.ts
/** Resolve explicit `--at`, `--every`, or `--cron` options for cron creation. */
function resolveCronCreateSchedule(options) {
	const normalized = normalizeScheduleOptions(options);
	if (countChosenSchedules(normalized) !== 1) throw new Error("Choose exactly one schedule: --at, --every, or --cron");
	const schedule = resolveDirectSchedule(normalized);
	if (!schedule) throw new Error("Choose exactly one schedule: --at, --every, or --cron");
	return schedule;
}
/** Resolve cron creation schedule from either a positional shorthand or explicit flags. */
function resolveCronCreateScheduleFromArgs(options) {
	const positionalSchedule = normalizeOptionalString(options.positionalSchedule);
	if (!positionalSchedule) return resolveCronCreateSchedule(options);
	if (countChosenSchedules(normalizeScheduleOptions(options)) > 0) throw new Error("Choose a positional schedule or one of --at, --every, or --cron.");
	const every = parseEverySchedule(positionalSchedule);
	return resolveCronCreateSchedule({
		...options,
		at: every ? void 0 : looksLikeCronExpression(positionalSchedule) ? void 0 : positionalSchedule,
		cron: looksLikeCronExpression(positionalSchedule) ? positionalSchedule : void 0,
		every
	});
}
/** Resolve a cron edit request, allowing at most one direct schedule replacement. */
function resolveCronEditScheduleRequest(options) {
	const normalized = normalizeScheduleOptions(options);
	if (countChosenSchedules(normalized) > 1) throw new Error("Choose at most one schedule change");
	const schedule = resolveDirectSchedule(normalized);
	if (schedule) return {
		kind: "direct",
		schedule
	};
	if (normalized.requestedStaggerMs !== void 0 || normalized.tz !== void 0) return {
		kind: "patch-existing-cron",
		tz: normalized.tz,
		staggerMs: normalized.requestedStaggerMs
	};
	return { kind: "none" };
}
/** Apply `--tz`, `--stagger`, or `--exact` metadata changes to an existing cron schedule. */
function applyExistingCronSchedulePatch(existingSchedule, request) {
	if (existingSchedule.kind !== "cron") throw new Error("Current job is not a cron schedule; use --cron to convert first");
	return {
		kind: "cron",
		expr: existingSchedule.expr,
		tz: request.tz ?? existingSchedule.tz,
		staggerMs: request.staggerMs !== void 0 ? request.staggerMs : existingSchedule.staggerMs
	};
}
function normalizeScheduleOptions(options) {
	const staggerRaw = normalizeOptionalString(options.stagger) ?? "";
	const useExact = Boolean(options.exact);
	if (staggerRaw && useExact) throw new Error("Choose either --stagger or --exact, not both");
	return {
		at: normalizeOptionalString(options.at) ?? "",
		every: normalizeOptionalString(options.every) ?? "",
		cronExpr: normalizeOptionalString(options.cron) ?? "",
		tz: normalizeOptionalString(options.tz),
		requestedStaggerMs: parseCronStaggerMs({
			staggerRaw,
			useExact
		})
	};
}
function countChosenSchedules(options) {
	return [
		Boolean(options.at),
		Boolean(options.every),
		Boolean(options.cronExpr)
	].filter(Boolean).length;
}
function parseEverySchedule(value) {
	return /^every\s+(.+)$/iu.exec(value.trim())?.[1]?.trim() || void 0;
}
function looksLikeCronExpression(value) {
	const parts = value.trim().split(/\s+/u);
	return parts.length === 5 || parts.length === 6;
}
function resolveDirectSchedule(options) {
	if (options.tz && options.every) throw new Error("--tz is only valid with --cron or offset-less --at");
	if (options.requestedStaggerMs !== void 0 && (options.at || options.every)) throw new Error("--stagger/--exact are only valid for cron schedules");
	if (options.at) {
		const atIso = parseAt(options.at, options.tz);
		if (!atIso) throw new Error("Invalid --at. Use an ISO timestamp or a duration like 20m.");
		return {
			kind: "at",
			at: atIso
		};
	}
	if (options.every) {
		const everyMs = parseDurationMs(options.every);
		if (!everyMs) throw new Error("Invalid --every. Use a duration like 10m, 1h, or 1d.");
		return {
			kind: "every",
			everyMs
		};
	}
	if (options.cronExpr) return {
		kind: "cron",
		expr: options.cronExpr,
		tz: options.tz,
		staggerMs: options.requestedStaggerMs
	};
}
//#endregion
//#region src/cli/cron-cli/thread-id-shared.ts
function parseCronThreadIdOption(value) {
	const raw = normalizeOptionalString(value);
	if (!raw) return;
	if (!/^\d+$/.test(raw)) throw new Error("--thread-id must be a positive integer Telegram topic thread id");
	const parsed = Number.parseInt(raw, 10);
	if (!Number.isSafeInteger(parsed) || parsed <= 0) throw new Error("--thread-id must be a safe positive integer Telegram topic thread id");
	return parsed;
}
function normalizeCronSessionTargetOption(value) {
	const raw = normalizeOptionalString(value);
	if (!raw) return;
	const lower = normalizeLowercaseStringOrEmpty(raw);
	if (lower === "main" || lower === "isolated" || lower === "current") return lower;
	if (lower.startsWith("session:")) {
		const id = normalizeOptionalString(raw.slice(8));
		return id ? `session:${id}` : void 0;
	}
}
//#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 {
			printCronJson(await callGatewayFromCli("cron.status", opts, {}));
		} catch (err) {
			handleCronCliError(err);
		}
	}));
}
function registerCronListCommand(cron) {
	addGatewayClientOptions(cron.command("list").description("List cron jobs").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(opts.all) };
			const agentId = normalizeOptionalString(opts.agent);
			if (agentId) listParams.agentId = sanitizeAgentId(agentId);
			const res = await callGatewayFromCli("cron.list", opts, listParams);
			if (opts.json) {
				printCronJson(enrichCronJsonWithStatus(res));
				return;
			}
			printCronList(res?.jobs ?? [], defaultRuntime, { deliveryPreviews: coerceCronDeliveryPreviews(res) });
		} catch (err) {
			handleCronCliError(err);
		}
	}));
}
function registerCronAddCommand(cron) {
	addGatewayClientOptions(cron.command("add").alias("create").description("Add a cron job").argument("[scheduleOrName]", "Schedule string, or job name when using --at/--every/--cron").argument("[message]", "Agent message when using a positional schedule").option("--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 with offset, or +duration). Use --tz for offset-less datetimes").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; cron default: Gateway host local timezone)", "").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("--command <shell>", "Command payload run as sh -lc <shell> on the Gateway").option("--command-argv <json>", "Command payload argv as JSON array of strings").option("--command-cwd <path>", "Working directory for command payloads").option("--command-env <KEY=VALUE>", "Environment override for command payloads (repeatable)", (value, previous) => [...previous ?? [], value]).option("--command-input <text>", "stdin for command payloads").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 or command jobs").option("--no-output-timeout-seconds <n>", "No-output timeout seconds for command jobs").option("--output-max-bytes <n>", "Maximum captured stdout/stderr bytes for command jobs").option("--light-context", "Use lightweight bootstrap context for agent jobs", false).option("--tools <list>", "Tool allow-list (e.g. exec,read,write or exec read write)").option("--announce", "Fallback-deliver final text to a chat", false).option("--deliver", "Deprecated (use --announce). Fallback-delivers final text to a chat.").option("--no-deliver", "Disable runner fallback delivery").option("--webhook <url>", "POST the finished payload to a webhook URL").option("--channel <channel>", `Delivery channel (${getCronChannelOptions()})`, "last").option("--to <dest>", "Delivery destination (E.164, Telegram chatId, or Discord channel/user)").option("--thread-id <id>", "Telegram forum topic thread id").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 (nameArg, messageArg, opts, cmd) => {
		try {
			const hasScheduleFlag = typeof opts.at === "string" || typeof opts.cron === "string" || typeof opts.every === "string";
			const positionalSchedule = hasScheduleFlag ? void 0 : nameArg;
			const schedule = resolveCronCreateScheduleFromArgs({
				at: opts.at,
				cron: opts.cron,
				every: opts.every,
				exact: opts.exact,
				positionalSchedule,
				stagger: opts.stagger,
				tz: opts.tz
			});
			const wakeMode = normalizeOptionalString(opts.wake) ?? "now";
			if (wakeMode !== "now" && wakeMode !== "next-heartbeat") throw new Error("--wake must be now or next-heartbeat");
			const rawAgentId = normalizeOptionalString(opts.agent);
			const agentId = rawAgentId ? sanitizeAgentId(rawAgentId) : void 0;
			const optionSource = typeof cmd?.getOptionValueSource === "function" ? (name) => cmd.getOptionValueSource(name) : () => void 0;
			const hasAnnounce = Boolean(opts.announce) || opts.deliver === true;
			const hasNoDeliver = opts.deliver === false;
			const webhookUrl = normalizeOptionalString(opts.webhook);
			const hasWebhook = typeof opts.webhook === "string";
			if ([
				hasAnnounce,
				hasNoDeliver,
				hasWebhook
			].filter(Boolean).length > 1) throw new Error("Choose at most one of --announce, --no-deliver, or --webhook");
			const payload = (() => {
				const systemEvent = normalizeOptionalString(opts.systemEvent) ?? "";
				const optionMessage = normalizeOptionalString(opts.message);
				const positionalMessage = normalizeOptionalString(messageArg);
				const commandShell = normalizeOptionalString(opts.command);
				const commandArgv = parseCronCommandArgv(opts.commandArgv);
				if (optionMessage && positionalMessage && optionMessage !== positionalMessage) throw new Error("Pass the cron job message either positionally or with --message, not both.");
				const message = optionMessage ?? positionalMessage ?? "";
				if (commandShell && commandArgv) throw new Error("Pass command payload either with --command or --command-argv, not both.");
				if ([
					Boolean(systemEvent),
					Boolean(message),
					Boolean(commandShell) || Boolean(commandArgv)
				].filter(Boolean).length !== 1) throw new Error("Choose exactly one payload: --system-event, --message, or --command");
				if (systemEvent) return {
					kind: "systemEvent",
					text: systemEvent
				};
				const timeoutSeconds = parseStrictPositiveIntOrUndefined(opts.timeoutSeconds);
				if (opts.timeoutSeconds !== void 0 && timeoutSeconds === void 0) throw new Error("Invalid --timeout-seconds (must be a positive integer).");
				if (commandShell || commandArgv) {
					const noOutputTimeoutSeconds = parsePositiveIntOrUndefined(opts.noOutputTimeoutSeconds ?? (typeof opts.outputTimeoutSeconds === "string" || typeof opts.outputTimeoutSeconds === "number" ? opts.outputTimeoutSeconds : void 0));
					const outputMaxBytes = parsePositiveIntOrUndefined(opts.outputMaxBytes);
					return {
						kind: "command",
						argv: commandArgv ?? [
							"sh",
							"-lc",
							commandShell ?? ""
						],
						cwd: normalizeOptionalString(opts.commandCwd),
						env: parseCronCommandEnv(opts.commandEnv),
						input: typeof opts.commandInput === "string" ? opts.commandInput : void 0,
						timeoutSeconds: timeoutSeconds && Number.isFinite(timeoutSeconds) ? timeoutSeconds : void 0,
						noOutputTimeoutSeconds: noOutputTimeoutSeconds && Number.isFinite(noOutputTimeoutSeconds) ? noOutputTimeoutSeconds : void 0,
						outputMaxBytes: outputMaxBytes && Number.isFinite(outputMaxBytes) ? outputMaxBytes : void 0
					};
				}
				return {
					kind: "agentTurn",
					message,
					model: normalizeOptionalString(opts.model),
					thinking: normalizeOptionalString(opts.thinking),
					timeoutSeconds: timeoutSeconds && Number.isFinite(timeoutSeconds) ? timeoutSeconds : void 0,
					lightContext: opts.lightContext === true ? true : void 0,
					toolsAllow: parseCronToolsAllow(opts.tools)
				};
			})();
			const sessionSource = optionSource("session");
			const sessionTargetRaw = normalizeOptionalString(opts.session) ?? "";
			const inferredSessionTarget = payload.kind === "agentTurn" || payload.kind === "command" ? "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 Error("--session must be main, isolated, current, or session:<id>");
			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 (isIsolatedLikeSessionTarget && payload.kind !== "agentTurn" && payload.kind !== "command") throw new Error("Isolated/current/custom-session jobs require --message (agentTurn) or --command.");
			if ((opts.announce || typeof opts.deliver === "boolean") && (!isIsolatedLikeSessionTarget || payload.kind !== "agentTurn" && payload.kind !== "command")) throw new Error("--announce/--no-deliver require a non-main agentTurn or command session target.");
			const accountId = normalizeOptionalString(opts.account);
			const threadId = parseCronThreadIdOption(opts.threadId);
			const hasThreadId = typeof threadId === "number";
			const hasChatDeliveryTarget = optionSource("channel") === "cli" || typeof opts.to === "string" || Boolean(accountId) || hasThreadId;
			if ((accountId || hasThreadId) && (!isIsolatedLikeSessionTarget || payload.kind !== "agentTurn" && payload.kind !== "command")) throw new Error("--account and --thread-id require a non-main agentTurn or command job with delivery.");
			if (hasWebhook && hasChatDeliveryTarget) throw new Error("--webhook cannot be combined with chat delivery options.");
			const deliveryMode = hasWebhook ? "webhook" : isIsolatedLikeSessionTarget && (payload.kind === "agentTurn" || payload.kind === "command") ? hasAnnounce ? "announce" : hasNoDeliver ? "none" : "announce" : void 0;
			const optionName = normalizeOptionalString(opts.name);
			const positionalName = hasScheduleFlag ? normalizeOptionalString(nameArg) : void 0;
			if (optionName && positionalName && optionName !== positionalName) throw new Error("Pass the cron job name either positionally or with --name, not both.");
			const name = optionName ?? positionalName ?? "";
			if (!name) throw new Error("Cron job name is required. Pass a name or --name <name>.");
			const description = normalizeOptionalString(opts.description);
			const sessionKey = normalizeOptionalString(opts.sessionKey);
			if ((payload.kind === "agentTurn" || payload.kind === "command") && !agentId) defaultRuntime.error(theme.warn("No --agent specified; the job will run with the configured default agent. Specify --agent to choose a specific agent."));
			printCronJson(await callGatewayFromCli("cron.add", opts, {
				name,
				description,
				enabled: !opts.disabled,
				deleteAfterRun: opts.deleteAfterRun ? true : opts.keepAfterRun ? false : void 0,
				agentId,
				sessionKey,
				schedule,
				sessionTarget,
				wakeMode,
				payload,
				delivery: deliveryMode ? {
					mode: deliveryMode,
					channel: hasWebhook ? void 0 : normalizeOptionalString(opts.channel),
					to: hasWebhook ? webhookUrl : normalizeOptionalString(opts.to),
					threadId: hasWebhook ? void 0 : threadId,
					accountId: hasWebhook ? void 0 : accountId,
					bestEffort: opts.bestEffortDeliver ? true : void 0
				} : void 0
			}));
			await warnIfCronSchedulerDisabled(opts);
		} catch (err) {
			handleCronCliError(err);
		}
	}));
}
//#endregion
//#region src/cli/cron-cli/register.cron-edit.ts
const CRON_EDIT_LOOKUP_PAGE_SIZE = 200;
const CRON_EDIT_LOOKUP_MAX_PAGES = 50;
const assignIf = (target, key, value, shouldAssign) => {
	if (shouldAssign) target[key] = value;
};
async function loadCronJobForEditSchedulePatch(opts, id) {
	let offset = 0;
	for (let page = 0; page < CRON_EDIT_LOOKUP_MAX_PAGES; page += 1) {
		const listed = await callGatewayFromCli("cron.list", opts, {
			includeDisabled: true,
			limit: CRON_EDIT_LOOKUP_PAGE_SIZE,
			offset
		});
		const existing = (listed?.jobs ?? []).find((job) => job.id === id);
		if (existing) return existing;
		if (!listed?.hasMore || typeof listed.nextOffset !== "number") return;
		if (listed.nextOffset <= offset) throw new Error("cron.list pagination did not advance while looking up cron job");
		offset = listed.nextOffset;
	}
	throw new Error("cron.list pagination exceeded maximum pages while looking up cron job");
}
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("--session-key <key>", "Set session key for job routing").option("--clear-session-key", "Unset session key", 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; cron default: Gateway host local timezone)").option("--stagger <duration>", "Cron stagger window (e.g. 30s, 5m)").option("--exact", "Disable cron staggering (set stagger to 0)").option("--system-event <text>", "Set systemEvent payload").option("--message <text>", "Set agentTurn payload message").option("--command <shell>", "Set command payload run as sh -lc <shell> on the Gateway").option("--command-argv <json>", "Set command payload argv as JSON array of strings").option("--command-cwd <path>", "Set command payload working directory").option("--command-env <KEY=VALUE>", "Set command payload environment overrides (repeatable)", (value, previous) => [...previous ?? [], value]).option("--command-input <text>", "Set command payload stdin").option("--thinking <level>", "Thinking level for agent jobs (off|minimal|low|medium|high|xhigh)").option("--model <model>", "Model override for agent jobs").option("--timeout-seconds <n>", "Timeout seconds for agent or command jobs").option("--no-output-timeout-seconds <n>", "No-output timeout seconds for command jobs").option("--output-max-bytes <n>", "Maximum captured stdout/stderr bytes for command jobs").option("--light-context", "Enable lightweight bootstrap context for agent jobs").option("--no-light-context", "Disable lightweight bootstrap context for agent jobs").option("--tools <list>", "Tool allow-list (e.g. exec,read,write or exec read write)").option("--clear-tools", "Remove tool allow-list (use all tools)", false).option("--announce", "Fallback-deliver final text to a chat").option("--deliver", "Deprecated (use --announce). Fallback-delivers final text to a chat.").option("--no-deliver", "Disable runner fallback delivery").option("--webhook <url>", "POST the finished payload to a webhook URL").option("--channel <channel>", `Delivery channel (${getCronChannelOptions()})`).option("--to <dest>", "Delivery destination (E.164, Telegram chatId, or Discord channel/user)").option("--thread-id <id>", "Telegram forum topic thread id").option("--account <id>", "Channel account id for delivery (multi-account setups)").option("--best-effort-deliver", "Do not fail job if delivery fails (also implies --announce when used alone)").option("--no-best-effort-deliver", "Fail job when delivery fails").option("--failure-alert", "Enable failure alerts for this job").option("--no-failure-alert", "Disable failure alerts for this job").option("--failure-alert-after <n>", "Alert after N consecutive job errors").option("--failure-alert-channel <channel>", `Failure alert channel (${getCronChannelOptions()})`).option("--failure-alert-to <dest>", "Failure alert destination").option("--failure-alert-cooldown <duration>", "Minimum time between alerts (e.g. 1h, 30m)").option("--failure-alert-include-skipped", "Count consecutive skipped runs toward alerts").option("--failure-alert-exclude-skipped", "Alert only on execution errors").option("--failure-alert-mode <mode>", "Failure alert delivery mode (announce or webhook)").option("--failure-alert-account-id <id>", "Account ID for failure alert channel (multi-account setups)").action(async (id, opts) => {
		try {
			const sessionTarget = typeof opts.session === "string" ? normalizeCronSessionTargetOption(opts.session) : void 0;
			if (typeof opts.session === "string" && !sessionTarget) throw new Error("--session must be main, isolated, current, or session:<id>");
			if (sessionTarget === "main" && (opts.message || opts.command || opts.commandArgv)) throw new Error("Main jobs cannot use --message or --command; use --system-event or --session isolated.");
			if ((sessionTarget === "isolated" || sessionTarget === "current" || sessionTarget?.startsWith("session:")) && opts.systemEvent) throw new Error("Isolated jobs cannot use --system-event; use --message, --command, or --session main.");
			const hasWebhookDelivery = typeof opts.webhook === "string";
			if ([
				Boolean(opts.announce),
				typeof opts.deliver === "boolean",
				hasWebhookDelivery
			].filter(Boolean).length > 1) throw new Error("Choose at most one of --announce, --no-deliver, or --webhook.");
			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 = sessionTarget;
			if (typeof opts.wake === "string") {
				const wakeMode = opts.wake.trim();
				if (wakeMode !== "now" && wakeMode !== "next-heartbeat") throw new Error("--wake must be now or next-heartbeat");
				patch.wakeMode = wakeMode;
			}
			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.sessionKey && opts.clearSessionKey) throw new Error("Use --session-key or --clear-session-key, not both");
			if (typeof opts.sessionKey === "string" && opts.sessionKey.trim()) patch.sessionKey = opts.sessionKey.trim();
			if (opts.clearSessionKey) patch.sessionKey = null;
			const scheduleRequest = resolveCronEditScheduleRequest({
				at: opts.at,
				cron: opts.cron,
				every: opts.every,
				exact: opts.exact,
				stagger: opts.stagger,
				tz: opts.tz
			});
			if (scheduleRequest.kind === "direct") patch.schedule = scheduleRequest.schedule;
			else if (scheduleRequest.kind === "patch-existing-cron") {
				const existing = await loadCronJobForEditSchedulePatch(opts, String(id));
				if (!existing) throw new Error(`unknown cron job id: ${id}`);
				patch.schedule = applyExistingCronSchedulePatch(existing.schedule, scheduleRequest);
			}
			const hasSystemEventPatch = typeof opts.systemEvent === "string";
			const commandShell = normalizeOptionalString(opts.command);
			const commandArgv = parseCronCommandArgv(opts.commandArgv);
			if (commandShell && commandArgv) throw new Error("Pass command payload either with --command or --command-argv, not both.");
			const model = normalizeOptionalString(opts.model);
			const thinking = normalizeOptionalString(opts.thinking);
			const toolsAllow = parseCronToolsAllow(opts.tools);
			const rawTimeoutSeconds = opts.timeoutSeconds === void 0 ? void 0 : String(opts.timeoutSeconds).trim();
			if (rawTimeoutSeconds !== void 0 && !/^\d+$/u.test(rawTimeoutSeconds)) throw new Error("Invalid --timeout-seconds (must be a positive integer).");
			const timeoutSeconds = rawTimeoutSeconds === void 0 ? void 0 : Number(rawTimeoutSeconds);
			const hasTimeoutSeconds = typeof timeoutSeconds === "number" && Number.isSafeInteger(timeoutSeconds) && timeoutSeconds > 0;
			if (rawTimeoutSeconds !== void 0 && !hasTimeoutSeconds) throw new Error("Invalid --timeout-seconds (must be a positive integer).");
			const rawNoOutputTimeoutSeconds = opts.noOutputTimeoutSeconds ?? (typeof opts.outputTimeoutSeconds === "string" || typeof opts.outputTimeoutSeconds === "number" ? opts.outputTimeoutSeconds : void 0);
			const noOutputTimeoutSeconds = parseStrictPositiveInteger(rawNoOutputTimeoutSeconds);
			if (rawNoOutputTimeoutSeconds !== void 0 && noOutputTimeoutSeconds === void 0) throw new Error("Invalid --no-output-timeout-seconds (must be a positive integer).");
			const outputMaxBytes = parseStrictPositiveInteger(opts.outputMaxBytes);
			if (opts.outputMaxBytes !== void 0 && outputMaxBytes === void 0) throw new Error("Invalid --output-max-bytes (must be a positive integer).");
			const hasDeliveryModeFlag = opts.announce || typeof opts.deliver === "boolean" || hasWebhookDelivery;
			const threadId = parseCronThreadIdOption(opts.threadId);
			const hasDeliveryThreadId = typeof threadId === "number";
			const hasDeliveryTarget = typeof opts.channel === "string" || typeof opts.to === "string" || hasDeliveryThreadId;
			const hasDeliveryAccount = typeof opts.account === "string";
			const hasBestEffort = typeof opts.bestEffortDeliver === "boolean";
			if (hasWebhookDelivery && (hasDeliveryTarget || hasDeliveryAccount)) throw new Error("--webhook cannot be combined with chat delivery options.");
			const hasCommandSpecificPayloadField = Boolean(commandShell) || Boolean(commandArgv) || typeof opts.commandCwd === "string" || typeof opts.commandInput === "string" || opts.commandEnv !== void 0 || noOutputTimeoutSeconds !== void 0 || outputMaxBytes !== void 0;
			let timeoutOnlyPayloadKind;
			if (hasTimeoutSeconds && !hasCommandSpecificPayloadField && typeof opts.message !== "string" && !model && !thinking && typeof opts.lightContext !== "boolean" && typeof opts.tools !== "string" && !Array.isArray(opts.tools) && !opts.clearTools) timeoutOnlyPayloadKind = (await loadCronJobForEditSchedulePatch(opts, String(id)))?.payload.kind === "command" ? "command" : "agentTurn";
			const hasAgentTurnPayloadField = typeof opts.message === "string" || Boolean(model) || Boolean(thinking) || hasTimeoutSeconds && !hasCommandSpecificPayloadField && timeoutOnlyPayloadKind !== "command" || typeof opts.lightContext === "boolean" || typeof opts.tools === "string" || Array.isArray(opts.tools) || opts.clearTools;
			const hasCommandPayloadField = hasCommandSpecificPayloadField || hasTimeoutSeconds && (hasCommandSpecificPayloadField || timeoutOnlyPayloadKind === "command");
			const hasAgentTurnPatch = hasAgentTurnPayloadField;
			const hasCommandPatch = hasCommandPayloadField;
			if ([
				hasSystemEventPatch,
				hasAgentTurnPatch,
				hasCommandPatch
			].filter(Boolean).length > 1) 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);
				assignIf(payload, "lightContext", opts.lightContext, typeof opts.lightContext === "boolean");
				if (opts.clearTools) payload.toolsAllow = null;
				else if (toolsAllow) payload.toolsAllow = toolsAllow;
				patch.payload = payload;
			} else if (hasCommandPatch) {
				const payload = { kind: "command" };
				assignIf(payload, "argv", commandArgv, Boolean(commandArgv));
				assignIf(payload, "argv", [
					"sh",
					"-lc",
					commandShell
				], Boolean(commandShell));
				assignIf(payload, "cwd", normalizeOptionalString(opts.commandCwd), typeof opts.commandCwd === "string");
				assignIf(payload, "env", parseCronCommandEnv(opts.commandEnv), opts.commandEnv !== void 0);
				assignIf(payload, "input", opts.commandInput, typeof opts.commandInput === "string");
				assignIf(payload, "timeoutSeconds", timeoutSeconds, hasTimeoutSeconds);
				assignIf(payload, "noOutputTimeoutSeconds", noOutputTimeoutSeconds, noOutputTimeoutSeconds !== void 0);
				assignIf(payload, "outputMaxBytes", outputMaxBytes, outputMaxBytes !== void 0);
				patch.payload = payload;
			}
			if (hasDeliveryModeFlag || hasDeliveryTarget || hasDeliveryAccount || hasBestEffort) {
				const delivery = {};
				if (hasDeliveryModeFlag) delivery.mode = hasWebhookDelivery ? "webhook" : opts.announce || opts.deliver === true ? "announce" : "none";
				else if (opts.bestEffortDeliver === true || (hasAgentTurnPayloadField || hasCommandPayloadField) && hasBestEffort) delivery.mode = "announce";
				if (typeof opts.channel === "string") {
					const channel = opts.channel.trim();
					delivery.channel = channel ? channel : void 0;
				}
				if (hasWebhookDelivery) {
					const webhook = normalizeOptionalString(opts.webhook) ?? "";
					delivery.to = webhook ? webhook : void 0;
				} else if (typeof opts.to === "string") {
					const to = opts.to.trim();
					delivery.to = to ? to : void 0;
				}
				if (hasDeliveryThreadId) delivery.threadId = threadId;
				if (typeof opts.account === "string") {
					const account = opts.account.trim();
					delivery.accountId = account ? account : void 0;
				}
				if (typeof opts.bestEffortDeliver === "boolean") delivery.bestEffort = opts.bestEffortDeliver;
				patch.delivery = delivery;
			}
			const hasFailureAlertAfter = typeof opts.failureAlertAfter === "string";
			const hasFailureAlertChannel = typeof opts.failureAlertChannel === "string";
			const hasFailureAlertTo = typeof opts.failureAlertTo === "string";
			const hasFailureAlertCooldown = typeof opts.failureAlertCooldown === "string";
			const hasFailureAlertIncludeSkipped = typeof opts.failureAlertIncludeSkipped === "boolean";
			const hasFailureAlertExcludeSkipped = typeof opts.failureAlertExcludeSkipped === "boolean";
			const hasFailureAlertMode = typeof opts.failureAlertMode === "string";
			const hasFailureAlertAccountId = typeof opts.failureAlertAccountId === "string";
			if (hasFailureAlertIncludeSkipped && hasFailureAlertExcludeSkipped) throw new Error("Use either --failure-alert-include-skipped or --failure-alert-exclude-skipped.");
			const hasFailureAlertFields = hasFailureAlertAfter || hasFailureAlertChannel || hasFailureAlertTo || hasFailureAlertCooldown || hasFailureAlertIncludeSkipped || hasFailureAlertExcludeSkipped || hasFailureAlertMode || hasFailureAlertAccountId;
			const failureAlertFlag = typeof opts.failureAlert === "boolean" ? opts.failureAlert : void 0;
			if (failureAlertFlag === false && hasFailureAlertFields) throw new Error("Use --no-failure-alert alone (without failure-alert-* options).");
			if (failureAlertFlag === false) patch.failureAlert = false;
			else if (failureAlertFlag === true || hasFailureAlertFields) {
				const failureAlert = {};
				if (hasFailureAlertAfter) {
					const after = parseStrictPositiveInteger(opts.failureAlertAfter);
					if (after === void 0) throw new Error("Invalid --failure-alert-after (must be a positive integer).");
					failureAlert.after = after;
				}
				if (hasFailureAlertChannel) failureAlert.channel = normalizeOptionalLowercaseString(opts.failureAlertChannel);
				if (hasFailureAlertTo) {
					const to = normalizeOptionalString(opts.failureAlertTo) ?? "";
					failureAlert.to = to ? to : void 0;
				}
				if (hasFailureAlertCooldown) {
					const cooldownMs = parseDurationMs(String(opts.failureAlertCooldown));
					if (!cooldownMs && cooldownMs !== 0) throw new Error("Invalid --failure-alert-cooldown.");
					failureAlert.cooldownMs = cooldownMs;
				}
				if (hasFailureAlertIncludeSkipped || hasFailureAlertExcludeSkipped) failureAlert.includeSkipped = hasFailureAlertIncludeSkipped;
				if (hasFailureAlertMode) {
					const mode = normalizeOptionalLowercaseString(opts.failureAlertMode);
					if (mode !== "announce" && mode !== "webhook") throw new Error("Invalid --failure-alert-mode (must be 'announce' or 'webhook').");
					failureAlert.mode = mode;
				}
				if (hasFailureAlertAccountId) {
					const accountId = normalizeOptionalString(opts.failureAlertAccountId) ?? "";
					failureAlert.accountId = accountId ? accountId : void 0;
				}
				patch.failureAlert = failureAlert;
			}
			const res = await callGatewayFromCli("cron.update", opts, {
				id,
				patch
			});
			defaultRuntime.writeJson(res);
			await warnIfCronSchedulerDisabled(opts);
		} catch (err) {
			defaultRuntime.error(danger(String(err)));
			defaultRuntime.exit(1);
		}
	}));
}
//#endregion
//#region src/cli/cron-cli/register.cron-simple.ts
const CRON_SHOW_PAGE_SIZE = 200;
const CRON_SHOW_LOOKUP_MAX_PAGES = 50;
const CRON_RUN_WAIT_TIMEOUT_DEFAULT = "10m";
const CRON_RUN_WAIT_POLL_INTERVAL_DEFAULT = "2s";
function sleep(ms) {
	return new Promise((resolve) => {
		setTimeout(resolve, ms);
	});
}
function parseCronRunWaitDuration(raw, label) {
	const durationMs = parseDurationMs$1(typeof raw === "string" || typeof raw === "number" || typeof raw === "bigint" ? String(raw) : "", { defaultUnit: "ms" });
	if (!Number.isFinite(durationMs) || durationMs < 0) throw new Error(`invalid ${label}`);
	return resolveTimerTimeoutMs(durationMs, 0, 0);
}
function parseCronRunPollInterval(raw) {
	const durationMs = parseCronRunWaitDuration(raw, "--poll-interval");
	if (durationMs <= 0) throw new Error("invalid --poll-interval");
	return resolvePositiveTimerTimeoutMs(durationMs, 2e3);
}
async function waitForCronRunCompletion(params) {
	const startedAt = Date.now();
	for (;;) {
		const entry = (await callGatewayFromCli("cron.runs", params.opts, {
			id: params.jobId,
			runId: params.runId,
			limit: 1
		})).entries?.[0];
		if (entry?.status === "ok" || entry?.status === "error" || entry?.status === "skipped") return entry;
		const elapsedMs = Date.now() - startedAt;
		if (elapsedMs >= params.timeoutMs) throw new Error(`timed out waiting for cron run ${params.runId}`);
		await sleep(Math.min(params.pollIntervalMs, params.timeoutMs - elapsedMs));
	}
}
function findCronJobInPage(jobs, idOrName) {
	const needle = normalizeLowercaseStringOrEmpty(idOrName);
	return jobs.find((job) => normalizeLowercaseStringOrEmpty(job.id) === needle || normalizeLowercaseStringOrEmpty(job.name) === needle);
}
async function loadCronJobForShow(opts, idOrName) {
	let offset = 0;
	for (let page = 0; page < CRON_SHOW_LOOKUP_MAX_PAGES; page += 1) {
		const res = await callGatewayFromCli("cron.list", opts, {
			includeDisabled: true,
			limit: CRON_SHOW_PAGE_SIZE,
			offset
		});
		const listed = res;
		const job = findCronJobInPage(listed.jobs ?? [], idOrName);
		if (job) return {
			job,
			deliveryPreview: coerceCronDeliveryPreviews(res).get(job.id)
		};
		if (!listed.hasMore || typeof listed.nextOffset !== "number") return {};
		if (listed.nextOffset <= offset) throw new Error("cron.list pagination did not advance while looking up cron job");
		offset = listed.nextOffset;
	}
	throw new Error("cron.list pagination exceeded maximum pages while looking up cron job");
}
function registerCronToggleCommand(params) {
	addGatewayClientOptions(params.cron.command(params.name).description(params.description).argument("<id>", "Job id").action(async (id, opts) => {
		try {
			printCronJson(await callGatewayFromCli("cron.update", opts, {
				id,
				patch: { enabled: params.enabled }
			}));
			await warnIfCronSchedulerDisabled(opts);
		} catch (err) {
			handleCronCliError(err);
		}
	}));
}
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 {
			printCronJson(await callGatewayFromCli("cron.remove", opts, { id }));
		} catch (err) {
			handleCronCliError(err);
		}
	}));
	registerCronToggleCommand({
		cron,
		name: "enable",
		description: "Enable a cron job",
		enabled: true
	});
	registerCronToggleCommand({
		cron,
		name: "disable",
		description: "Disable a cron job",
		enabled: false
	});
	addGatewayClientOptions(cron.command("get").description("Get a cron job as JSON").argument("<id>", "Job id").action(async (id, opts) => {
		try {
			printCronJson(await callGatewayFromCli("cron.get", opts, { id: String(id) }));
		} catch (err) {
			handleCronCliError(err);
		}
	}));
	addGatewayClientOptions(cron.command("show").description("Show a cron job").argument("<id>", "Job id or exact name").option("--json", "Output JSON", false).action(async (id, opts) => {
		try {
			const { job, deliveryPreview } = await loadCronJobForShow(opts, String(id));
			if (!job) throw new Error(`cron job not found: ${String(id)}`);
			if (opts.json) {
				printCronJson(enrichCronJsonWithStatus(job));
				return;
			}
			printCronShow(job, defaultRuntime, { deliveryPreview });
		} catch (err) {
			handleCronCliError(err);
		}
	}));
	addGatewayClientOptions(cron.command("runs").description("Show cron run history").requiredOption("--id <id>", "Job id").option("--run-id <runId>", "Filter by cron run id").option("--limit <n>", "Max entries (default 50)", "50").action(async (opts) => {
		try {
			const limit = parseStrictPositiveInteger(opts.limit ?? "50");
			if (limit === void 0) throw new Error("Invalid --limit (must be a positive integer).");
			printCronJson(await callGatewayFromCli("cron.runs", opts, {
				id: String(opts.id),
				...typeof opts.runId === "string" && opts.runId.trim() ? { runId: opts.runId } : {},
				limit
			}));
		} catch (err) {
			handleCronCliError(err);
		}
	}));
	addGatewayClientOptions(cron.command("run").description("Run a cron job now (debug)").argument("<id>", "Job id").option("--due", "Run only when due (default behavior in older versions)", false).option("--wait", "Wait for the queued run to finish", false).option("--wait-timeout <duration>", "Maximum time to wait for --wait", CRON_RUN_WAIT_TIMEOUT_DEFAULT).option("--poll-interval <duration>", "Polling interval for --wait", CRON_RUN_WAIT_POLL_INTERVAL_DEFAULT).action(async (id, opts, command) => {
		try {
			let waitTimeoutMs = 0;
			let pollIntervalMs = 0;
			if (opts.wait) {
				waitTimeoutMs = parseCronRunWaitDuration(opts.waitTimeout, "--wait-timeout");
				pollIntervalMs = parseCronRunPollInterval(opts.pollInterval);
			}
			if (command.getOptionValueSource("timeout") === "default") opts.timeout = "600000";
			const res = await callGatewayFromCli("cron.run", opts, {
				id,
				mode: opts.due ? "due" : "force"
			});
			const result = res;
			if (opts.wait && result?.ok && result.enqueued) {
				if (!result.runId) throw new Error("cron run did not return a runId to wait for");
				const run = await waitForCronRunCompletion({
					opts,
					jobId: String(id),
					runId: result.runId,
					timeoutMs: waitTimeoutMs,
					pollIntervalMs
				});
				printCronJson({
					...res,
					completed: true,
					status: run.status,
					run
				});
				defaultRuntime.exit(run.status === "ok" ? 0 : 1);
				return;
			}
			printCronJson(res);
			defaultRuntime.exit(result?.ok && (result?.ran || result?.enqueued) ? 0 : 1);
		} catch (err) {
			handleCronCliError(err);
		}
	}));
}
//#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${theme.muted("Upgrade tip:")} run \`openclaw doctor --fix\` to normalize legacy cron job storage.\n`);
	registerCronStatusCommand(cron);
	registerCronListCommand(cron);
	registerCronAddCommand(cron);
	registerCronSimpleCommands(cron);
	registerCronEditCommand(cron);
	applyParentDefaultHelpAction(cron);
}
//#endregion
export { registerCronCli };