From 0398f684e7c7f77ac652c4193f8e2a671d7b9d92 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 6 Jan 2026 03:25:21 +0100 Subject: [PATCH] fix: add gateway stop/restart commands --- CHANGELOG.md | 1 + docs/faq.md | 12 +- docs/gateway.md | 3 + docs/troubleshooting.md | 7 + src/agents/tools/cron-tool.ts | 7 +- src/cli/cron-cli.ts | 1 - src/cli/gateway-cli.coverage.test.ts | 62 +++++++++ src/cli/gateway-cli.ts | 151 ++++++++++++++++++++- src/commands/doctor.test.ts | 2 + src/commands/doctor.ts | 6 + src/cron/cron-protocol-conformance.test.ts | 4 +- src/cron/normalize.ts | 17 ++- src/daemon/launchd.ts | 29 ++++ src/daemon/schtasks.ts | 22 +++ src/daemon/service.ts | 13 ++ src/daemon/systemd.ts | 16 +++ src/gateway/server-methods/cron.ts | 12 +- src/wizard/onboarding.ts | 2 +- 18 files changed, 339 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9af4fe803..0ce0c6d0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Onboarding: resolve CLI entrypoint when running via `npx` so gateway daemon install works without a build step. - Onboarding: when OpenAI Codex OAuth is used, default to `openai-codex/gpt-5.2` and warn if the selected model lacks auth. - CLI: auto-migrate legacy config entries on command start (same behavior as gateway startup). +- Gateway: add `gateway stop|restart` helpers and surface launchd/systemd/schtasks stop hints when the gateway is already running. - Auth: prioritize OAuth profiles but fall back to API keys when refresh fails; stored profiles now load without explicit auth order. - Control UI: harden config Form view with schema normalization, map editing, and guardrails to prevent data loss on save. - Cron: normalize cron.add/update inputs, align channel enums/status fields across gateway/CLI/UI/macOS, and add protocol conformance tests. Thanks @mneves75 for PR #256. diff --git a/docs/faq.md b/docs/faq.md index ea12dcf30..f51fe824d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -492,6 +492,9 @@ The gateway runs under a supervisor that auto-restarts it. You need to stop the # Check if running launchctl list | grep clawdbot +# Stop (disable does NOT stop a running job) +clawdbot gateway stop + # Stop and disable launchctl disable gui/$UID/com.clawdbot.gateway launchctl bootout gui/$UID/com.clawdbot.gateway @@ -499,6 +502,9 @@ launchctl bootout gui/$UID/com.clawdbot.gateway # Re-enable later launchctl enable gui/$UID/com.clawdbot.gateway launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.clawdbot.gateway.plist + +# Or just restart +clawdbot gateway restart ``` **Linux (systemd)** @@ -508,7 +514,11 @@ launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.clawdbot.gateway.plist systemctl list-units | grep -i clawdbot # Stop and disable -sudo systemctl disable --now clawdbot +clawdbot gateway stop +systemctl --user disable --now clawdbot-gateway.service + +# Or just restart +clawdbot gateway restart ``` **pm2 (if used)** diff --git a/docs/gateway.md b/docs/gateway.md index f278f4756..6a3eb4e54 100644 --- a/docs/gateway.md +++ b/docs/gateway.md @@ -159,6 +159,8 @@ See also: `docs/presence.md` for how presence is produced/deduped and why `insta Bundled mac app: - Clawdbot.app can bundle a bun-compiled gateway binary and install a per-user LaunchAgent labeled `com.clawdbot.gateway`. +- To stop it cleanly, use `clawdbot gateway stop` (or `launchctl bootout gui/$UID/com.clawdbot.gateway`). +- To restart, use `clawdbot gateway restart` (or `launchctl kickstart -k gui/$UID/com.clawdbot.gateway`). ## Supervision (systemd user unit) Create `~/.config/systemd/user/clawdbot-gateway.service`: @@ -217,6 +219,7 @@ sudo systemctl enable --now clawdbot-gateway.service - `clawdbot gateway send --to --message "hi" [--media-url ...]` — send via Gateway (idempotent). - `clawdbot gateway agent --message "hi" [--to ...]` — run an agent turn (waits for final by default). - `clawdbot gateway call --params '{"k":"v"}'` — raw method invoker for debugging. +- `clawdbot gateway stop|restart` — stop/restart the supervised gateway service (launchd/systemd/schtasks). - Gateway helper subcommands assume a running gateway on `--url`; they no longer auto-spawn one. ## Migration guidance diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index ec56836eb..c8cbcab88 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -160,6 +160,13 @@ lsof -nP -i :18789 kill -9 ``` +If the gateway is supervised by launchd, killing the PID will just respawn it. +Stop the supervisor instead: +```bash +clawdbot gateway stop +# Or: launchctl bootout gui/$UID/com.clawdbot.gateway +``` + **Fix 2: Check embedded gateway** Ensure the gateway relay was properly bundled. Run `./scripts/package-mac-app.sh` and ensure `bun` is installed. diff --git a/src/agents/tools/cron-tool.ts b/src/agents/tools/cron-tool.ts index f03260762..38e7ee9e9 100644 --- a/src/agents/tools/cron-tool.ts +++ b/src/agents/tools/cron-tool.ts @@ -1,12 +1,11 @@ import { Type } from "@sinclair/typebox"; - -import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js"; -import { callGatewayTool, type GatewayCallOptions } from "./gateway.js"; -import { CronAddParamsSchema } from "../../gateway/protocol/schema.js"; import { normalizeCronJobCreate, normalizeCronJobPatch, } from "../../cron/normalize.js"; +import { CronAddParamsSchema } from "../../gateway/protocol/schema.js"; +import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js"; +import { callGatewayTool, type GatewayCallOptions } from "./gateway.js"; const CronJobPatchSchema = Type.Partial(CronAddParamsSchema); diff --git a/src/cli/cron-cli.ts b/src/cli/cron-cli.ts index 8b09ec269..94076ea3d 100644 --- a/src/cli/cron-cli.ts +++ b/src/cli/cron-cli.ts @@ -4,7 +4,6 @@ import { defaultRuntime } from "../runtime.js"; import type { GatewayRpcOpts } from "./gateway-rpc.js"; import { addGatewayClientOptions, callGatewayFromCli } from "./gateway-rpc.js"; - async function warnIfCronSchedulerDisabled(opts: GatewayRpcOpts) { try { const res = (await callGatewayFromCli("cron.status", opts, {})) as { diff --git a/src/cli/gateway-cli.coverage.test.ts b/src/cli/gateway-cli.coverage.test.ts index 4d46e70dd..c4a134a6d 100644 --- a/src/cli/gateway-cli.coverage.test.ts +++ b/src/cli/gateway-cli.coverage.test.ts @@ -13,6 +13,9 @@ const forceFreePortAndWait = vi.fn(async () => ({ waitedMs: 0, escalatedToSigkill: false, })); +const serviceStop = vi.fn().mockResolvedValue(undefined); +const serviceRestart = vi.fn().mockResolvedValue(undefined); +const serviceIsLoaded = vi.fn().mockResolvedValue(true); const runtimeLogs: string[] = []; const runtimeErrors: string[] = []; @@ -74,6 +77,20 @@ vi.mock("./ports.js", () => ({ forceFreePortAndWait: (port: number) => forceFreePortAndWait(port), })); +vi.mock("../daemon/service.js", () => ({ + resolveGatewayService: () => ({ + label: "LaunchAgent", + loadedText: "loaded", + notLoadedText: "not loaded", + install: vi.fn(), + uninstall: vi.fn(), + stop: serviceStop, + restart: serviceRestart, + isLoaded: serviceIsLoaded, + readCommand: vi.fn(), + }), +})); + describe("gateway-cli coverage", () => { it("registers call/health/status/send/agent commands and routes to callGateway", async () => { runtimeLogs.length = 0; @@ -228,6 +245,51 @@ describe("gateway-cli coverage", () => { } }); + it("supports gateway stop/restart via service helper", async () => { + runtimeLogs.length = 0; + runtimeErrors.length = 0; + serviceStop.mockClear(); + serviceRestart.mockClear(); + serviceIsLoaded.mockResolvedValue(true); + + const { registerGatewayCli } = await import("./gateway-cli.js"); + const program = new Command(); + program.exitOverride(); + registerGatewayCli(program); + + await program.parseAsync(["gateway", "stop"], { from: "user" }); + await program.parseAsync(["gateway", "restart"], { from: "user" }); + + expect(serviceStop).toHaveBeenCalledTimes(1); + expect(serviceRestart).toHaveBeenCalledTimes(1); + }); + + it("prints stop hints on GatewayLockError when service is loaded", async () => { + runtimeLogs.length = 0; + runtimeErrors.length = 0; + serviceIsLoaded.mockResolvedValue(true); + + const { GatewayLockError } = await import("../infra/gateway-lock.js"); + startGatewayServer.mockRejectedValueOnce( + new GatewayLockError("another gateway instance is already listening"), + ); + + const { registerGatewayCli } = await import("./gateway-cli.js"); + const program = new Command(); + program.exitOverride(); + registerGatewayCli(program); + + await expect( + program.parseAsync(["gateway", "--allow-unconfigured"], { + from: "user", + }), + ).rejects.toThrow("__exit__:1"); + + expect(startGatewayServer).toHaveBeenCalled(); + expect(runtimeErrors.join("\n")).toContain("Gateway failed to start:"); + expect(runtimeErrors.join("\n")).toContain("clawdbot gateway stop"); + }); + it("uses env/config port when --port is omitted", async () => { await withEnvOverride({ CLAWDBOT_GATEWAY_PORT: "19001" }, async () => { runtimeLogs.length = 0; diff --git a/src/cli/gateway-cli.ts b/src/cli/gateway-cli.ts index 85dff78fd..7daf257ca 100644 --- a/src/cli/gateway-cli.ts +++ b/src/cli/gateway-cli.ts @@ -6,6 +6,12 @@ import { loadConfig, resolveGatewayPort, } from "../config/config.js"; +import { + GATEWAY_LAUNCH_AGENT_LABEL, + GATEWAY_SYSTEMD_SERVICE_NAME, + GATEWAY_WINDOWS_TASK_NAME, +} from "../daemon/constants.js"; +import { resolveGatewayService } from "../daemon/service.js"; import { callGateway, randomIdempotencyKey } from "../gateway/call.js"; import { startGatewayServer } from "../gateway/server.js"; import { @@ -45,6 +51,62 @@ function parsePort(raw: unknown): number | null { return parsed; } +function renderGatewayServiceStopHints(): string[] { + switch (process.platform) { + case "darwin": + return [ + "Tip: clawdbot gateway stop", + `Or: launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}`, + ]; + case "linux": + return [ + "Tip: clawdbot gateway stop", + `Or: systemctl --user stop ${GATEWAY_SYSTEMD_SERVICE_NAME}.service`, + ]; + case "win32": + return [ + "Tip: clawdbot gateway stop", + `Or: schtasks /End /TN "${GATEWAY_WINDOWS_TASK_NAME}"`, + ]; + default: + return ["Tip: clawdbot gateway stop"]; + } +} + +function renderGatewayServiceStartHints(): string[] { + switch (process.platform) { + case "darwin": + return [ + `launchctl bootstrap gui/$UID ~/Library/LaunchAgents/${GATEWAY_LAUNCH_AGENT_LABEL}.plist`, + ]; + case "linux": + return [`systemctl --user start ${GATEWAY_SYSTEMD_SERVICE_NAME}.service`]; + case "win32": + return [`schtasks /Run /TN "${GATEWAY_WINDOWS_TASK_NAME}"`]; + default: + return []; + } +} + +async function maybeExplainGatewayServiceStop() { + const service = resolveGatewayService(); + let loaded: boolean | null = null; + try { + loaded = await service.isLoaded({ env: process.env }); + } catch { + loaded = null; + } + if (loaded === false) return; + defaultRuntime.error( + loaded + ? `Gateway service appears ${service.loadedText}. Stop it first.` + : "Gateway service status unknown; if supervised, stop it first.", + ); + for (const hint of renderGatewayServiceStopHints()) { + defaultRuntime.error(hint); + } +} + async function runGatewayLoop(params: { start: () => Promise>>; runtime: typeof defaultRuntime; @@ -285,8 +347,22 @@ export function registerGatewayCli(program: Command) { }), }); } catch (err) { - if (err instanceof GatewayLockError) { - defaultRuntime.error(`Gateway failed to start: ${err.message}`); + if ( + err instanceof GatewayLockError || + (err && + typeof err === "object" && + (err as { name?: string }).name === "GatewayLockError") + ) { + const errMessage = + err instanceof Error + ? err.message + : typeof err === "object" && err !== null && "message" in err + ? String((err as { message?: unknown }).message ?? "") + : String(err); + defaultRuntime.error( + `Gateway failed to start: ${errMessage}\nIf the gateway is supervised, stop it with: clawdbot gateway stop`, + ); + await maybeExplainGatewayServiceStop(); defaultRuntime.exit(1); return; } @@ -486,8 +562,22 @@ export function registerGatewayCli(program: Command) { }), }); } catch (err) { - if (err instanceof GatewayLockError) { - defaultRuntime.error(`Gateway failed to start: ${err.message}`); + if ( + err instanceof GatewayLockError || + (err && + typeof err === "object" && + (err as { name?: string }).name === "GatewayLockError") + ) { + const errMessage = + err instanceof Error + ? err.message + : typeof err === "object" && err !== null && "message" in err + ? String((err as { message?: unknown }).message ?? "") + : String(err); + defaultRuntime.error( + `Gateway failed to start: ${errMessage}\nIf the gateway is supervised, stop it with: clawdbot gateway stop`, + ); + await maybeExplainGatewayServiceStop(); defaultRuntime.exit(1); return; } @@ -635,6 +725,59 @@ export function registerGatewayCli(program: Command) { }), ); + gateway + .command("stop") + .description("Stop the Gateway service (launchd/systemd/schtasks)") + .action(async () => { + const service = resolveGatewayService(); + let loaded = false; + try { + loaded = await service.isLoaded({ env: process.env }); + } catch (err) { + defaultRuntime.error(`Gateway service check failed: ${String(err)}`); + defaultRuntime.exit(1); + return; + } + if (!loaded) { + defaultRuntime.log(`Gateway service ${service.notLoadedText}.`); + return; + } + try { + await service.stop({ stdout: process.stdout }); + } catch (err) { + defaultRuntime.error(`Gateway stop failed: ${String(err)}`); + defaultRuntime.exit(1); + } + }); + + gateway + .command("restart") + .description("Restart the Gateway service (launchd/systemd/schtasks)") + .action(async () => { + const service = resolveGatewayService(); + let loaded = false; + try { + loaded = await service.isLoaded({ env: process.env }); + } catch (err) { + defaultRuntime.error(`Gateway service check failed: ${String(err)}`); + defaultRuntime.exit(1); + return; + } + if (!loaded) { + defaultRuntime.log(`Gateway service ${service.notLoadedText}.`); + for (const hint of renderGatewayServiceStartHints()) { + defaultRuntime.log(`Start with: ${hint}`); + } + return; + } + try { + await service.restart({ stdout: process.stdout }); + } catch (err) { + defaultRuntime.error(`Gateway restart failed: ${String(err)}`); + defaultRuntime.exit(1); + } + }); + // Build default deps (keeps parity with other commands; future-proofing). void createDefaultDeps(); } diff --git a/src/commands/doctor.test.ts b/src/commands/doctor.test.ts index be4efaf6e..8902edfe0 100644 --- a/src/commands/doctor.test.ts +++ b/src/commands/doctor.test.ts @@ -38,6 +38,7 @@ const resolveGatewayProgramArguments = vi.fn().mockResolvedValue({ }); const serviceInstall = vi.fn().mockResolvedValue(undefined); const serviceIsLoaded = vi.fn().mockResolvedValue(false); +const serviceStop = vi.fn().mockResolvedValue(undefined); const serviceRestart = vi.fn().mockResolvedValue(undefined); const serviceUninstall = vi.fn().mockResolvedValue(undefined); @@ -85,6 +86,7 @@ vi.mock("../daemon/service.js", () => ({ notLoadedText: "not loaded", install: serviceInstall, uninstall: serviceUninstall, + stop: serviceStop, restart: serviceRestart, isLoaded: serviceIsLoaded, readCommand: vi.fn(), diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index b3e886b30..9c960ce2a 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -660,6 +660,12 @@ export async function doctorCommand(runtime: RuntimeEnv = defaultRuntime) { if (!loaded) { note("Gateway daemon not installed.", "Gateway"); } else { + if (process.platform === "darwin") { + note( + `LaunchAgent loaded; stopping requires "clawdbot gateway stop" or launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}.`, + "Gateway", + ); + } const restart = guardCancel( await confirm({ message: "Restart gateway daemon now?", diff --git a/src/cron/cron-protocol-conformance.test.ts b/src/cron/cron-protocol-conformance.test.ts index 19a9599ec..f7b57c551 100644 --- a/src/cron/cron-protocol-conformance.test.ts +++ b/src/cron/cron-protocol-conformance.test.ts @@ -33,9 +33,7 @@ const UI_FILES = [ "ui/src/ui/views/cron.ts", ]; -const SWIFT_FILES = [ - "apps/macos/Sources/Clawdbot/GatewayConnection.swift", -]; +const SWIFT_FILES = ["apps/macos/Sources/Clawdbot/GatewayConnection.swift"]; describe("cron protocol conformance", () => { it("ui + swift include all cron channels from gateway schema", async () => { diff --git a/src/cron/normalize.ts b/src/cron/normalize.ts index b6040a345..8586d56f8 100644 --- a/src/cron/normalize.ts +++ b/src/cron/normalize.ts @@ -60,7 +60,8 @@ export function normalizeCronJobInput( if (options.applyDefaults) { if (!next.wakeMode) next.wakeMode = "next-heartbeat"; if (!next.sessionTarget && isRecord(next.payload)) { - const kind = typeof next.payload.kind === "string" ? next.payload.kind : ""; + const kind = + typeof next.payload.kind === "string" ? next.payload.kind : ""; if (kind === "systemEvent") next.sessionTarget = "main"; if (kind === "agentTurn") next.sessionTarget = "isolated"; } @@ -73,16 +74,18 @@ export function normalizeCronJobCreate( raw: unknown, options?: NormalizeOptions, ): CronJobCreate | null { - return normalizeCronJobInput(raw, { applyDefaults: true, ...options }) as - | CronJobCreate - | null; + return normalizeCronJobInput(raw, { + applyDefaults: true, + ...options, + }) as CronJobCreate | null; } export function normalizeCronJobPatch( raw: unknown, options?: NormalizeOptions, ): CronJobPatch | null { - return normalizeCronJobInput(raw, { applyDefaults: false, ...options }) as - | CronJobPatch - | null; + return normalizeCronJobInput(raw, { + applyDefaults: false, + ...options, + }) as CronJobPatch | null; } diff --git a/src/daemon/launchd.ts b/src/daemon/launchd.ts index 6fb6a0391..3a6fc3ced 100644 --- a/src/daemon/launchd.ts +++ b/src/daemon/launchd.ts @@ -307,6 +307,35 @@ export async function uninstallLaunchAgent({ } } +function isLaunchctlNotLoaded(res: { + stdout: string; + stderr: string; + code: number; +}): boolean { + const detail = `${res.stderr || res.stdout}`.toLowerCase(); + return ( + detail.includes("no such process") || + detail.includes("could not find service") || + detail.includes("not found") + ); +} + +export async function stopLaunchAgent({ + stdout, +}: { + stdout: NodeJS.WritableStream; +}): Promise { + const domain = resolveGuiDomain(); + const label = GATEWAY_LAUNCH_AGENT_LABEL; + const res = await execLaunchctl(["bootout", `${domain}/${label}`]); + if (res.code !== 0 && !isLaunchctlNotLoaded(res)) { + throw new Error( + `launchctl bootout failed: ${res.stderr || res.stdout}`.trim(), + ); + } + stdout.write(`Stopped LaunchAgent: ${domain}/${label}\n`); +} + export async function installLaunchAgent({ env, stdout, diff --git a/src/daemon/schtasks.ts b/src/daemon/schtasks.ts index 3d8da28f7..94ba8cb7a 100644 --- a/src/daemon/schtasks.ts +++ b/src/daemon/schtasks.ts @@ -233,6 +233,28 @@ export async function uninstallScheduledTask({ } } +function isTaskNotRunning(res: { + stdout: string; + stderr: string; + code: number; +}): boolean { + const detail = `${res.stderr || res.stdout}`.toLowerCase(); + return detail.includes("not running"); +} + +export async function stopScheduledTask({ + stdout, +}: { + stdout: NodeJS.WritableStream; +}): Promise { + await assertSchtasksAvailable(); + const res = await execSchtasks(["/End", "/TN", GATEWAY_WINDOWS_TASK_NAME]); + if (res.code !== 0 && !isTaskNotRunning(res)) { + throw new Error(`schtasks end failed: ${res.stderr || res.stdout}`.trim()); + } + stdout.write(`Stopped Scheduled Task: ${GATEWAY_WINDOWS_TASK_NAME}\n`); +} + export async function restartScheduledTask({ stdout, }: { diff --git a/src/daemon/service.ts b/src/daemon/service.ts index 0ce53469e..c2799cc71 100644 --- a/src/daemon/service.ts +++ b/src/daemon/service.ts @@ -3,6 +3,7 @@ import { isLaunchAgentLoaded, readLaunchAgentProgramArguments, restartLaunchAgent, + stopLaunchAgent, uninstallLaunchAgent, } from "./launchd.js"; import { @@ -10,6 +11,7 @@ import { isScheduledTaskInstalled, readScheduledTaskCommand, restartScheduledTask, + stopScheduledTask, uninstallScheduledTask, } from "./schtasks.js"; import { @@ -17,6 +19,7 @@ import { isSystemdServiceEnabled, readSystemdServiceExecStart, restartSystemdService, + stopSystemdService, uninstallSystemdService, } from "./systemd.js"; @@ -37,6 +40,7 @@ export type GatewayService = { env: Record; stdout: NodeJS.WritableStream; }) => Promise; + stop: (args: { stdout: NodeJS.WritableStream }) => Promise; restart: (args: { stdout: NodeJS.WritableStream }) => Promise; isLoaded: (args: { env: Record; @@ -59,6 +63,9 @@ export function resolveGatewayService(): GatewayService { uninstall: async (args) => { await uninstallLaunchAgent(args); }, + stop: async (args) => { + await stopLaunchAgent(args); + }, restart: async (args) => { await restartLaunchAgent(args); }, @@ -78,6 +85,9 @@ export function resolveGatewayService(): GatewayService { uninstall: async (args) => { await uninstallSystemdService(args); }, + stop: async (args) => { + await stopSystemdService(args); + }, restart: async (args) => { await restartSystemdService(args); }, @@ -97,6 +107,9 @@ export function resolveGatewayService(): GatewayService { uninstall: async (args) => { await uninstallScheduledTask(args); }, + stop: async (args) => { + await stopScheduledTask(args); + }, restart: async (args) => { await restartScheduledTask(args); }, diff --git a/src/daemon/systemd.ts b/src/daemon/systemd.ts index 0906b2294..5d8875c5f 100644 --- a/src/daemon/systemd.ts +++ b/src/daemon/systemd.ts @@ -331,6 +331,22 @@ export async function uninstallSystemdService({ } } +export async function stopSystemdService({ + stdout, +}: { + stdout: NodeJS.WritableStream; +}): Promise { + await assertSystemdAvailable(); + const unitName = `${GATEWAY_SYSTEMD_SERVICE_NAME}.service`; + const res = await execSystemctl(["--user", "stop", unitName]); + if (res.code !== 0) { + throw new Error( + `systemctl stop failed: ${res.stderr || res.stdout}`.trim(), + ); + } + stdout.write(`Stopped systemd service: ${unitName}\n`); +} + export async function restartSystemdService({ stdout, }: { diff --git a/src/gateway/server-methods/cron.ts b/src/gateway/server-methods/cron.ts index 3c9a74a3b..705338d35 100644 --- a/src/gateway/server-methods/cron.ts +++ b/src/gateway/server-methods/cron.ts @@ -1,3 +1,7 @@ +import { + normalizeCronJobCreate, + normalizeCronJobPatch, +} from "../../cron/normalize.js"; import { readCronRunLogEntries, resolveCronRunLogPath, @@ -17,10 +21,6 @@ import { validateWakeParams, } from "../protocol/index.js"; import type { GatewayRequestHandlers } from "./types.js"; -import { - normalizeCronJobCreate, - normalizeCronJobPatch, -} from "../../cron/normalize.js"; export const cronHandlers: GatewayRequestHandlers = { wake: ({ params, respond, context }) => { @@ -88,9 +88,7 @@ export const cronHandlers: GatewayRequestHandlers = { ); return; } - const job = await context.cron.add( - normalized as unknown as CronJobCreate, - ); + const job = await context.cron.add(normalized as unknown as CronJobCreate); respond(true, job, undefined); }, "cron.update": async ({ params, respond, context }) => { diff --git a/src/wizard/onboarding.ts b/src/wizard/onboarding.ts index 4077ac579..9f99c62c7 100644 --- a/src/wizard/onboarding.ts +++ b/src/wizard/onboarding.ts @@ -60,6 +60,7 @@ import { resolveGatewayPort, writeConfigFile, } from "../config/config.js"; +import type { AgentModelListConfig } from "../config/types.js"; import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js"; import { resolveGatewayProgramArguments } from "../daemon/program-args.js"; import { resolveGatewayService } from "../daemon/service.js"; @@ -68,7 +69,6 @@ import type { RuntimeEnv } from "../runtime.js"; import { defaultRuntime } from "../runtime.js"; import { resolveUserPath, sleep } from "../utils.js"; import type { WizardPrompter } from "./prompts.js"; -import type { AgentModelListConfig } from "../config/types.js"; const OPENAI_CODEX_DEFAULT_MODEL = "openai-codex/gpt-5.2";