From 19595a8f99c467c8d5122fee0afb2f95c92a1f04 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 8 Jan 2026 07:16:05 +0100 Subject: [PATCH] refactor: simplify cli commands --- CHANGELOG.md | 1 + docs/cli/index.md | 59 +-- docs/gateway/configuration.md | 2 +- docs/gateway/index.md | 12 +- docs/gateway/troubleshooting.md | 2 +- docs/install/updating.md | 12 +- docs/nodes/index.md | 7 - docs/platforms/exe-dev.md | 2 +- docs/platforms/index.md | 2 +- docs/platforms/linux.md | 2 +- docs/platforms/mac/bun.md | 4 +- docs/platforms/mac/canvas.md | 20 +- docs/platforms/macos.md | 4 +- docs/platforms/windows.md | 2 +- docs/tools/index.md | 4 +- scripts/e2e/onboard-docker.sh | 4 +- src/cli/canvas-cli.coverage.test.ts | 166 -------- src/cli/canvas-cli.ts | 544 --------------------------- src/cli/daemon-cli.coverage.test.ts | 2 +- src/cli/gateway-cli.coverage.test.ts | 129 +------ src/cli/gateway-cli.ts | 347 +---------------- src/cli/nodes-cli.ts | 293 +++++++++++++++ src/cli/program.test.ts | 38 -- src/cli/program.ts | 16 - src/cli/telegram-cli.ts | 74 ---- src/commands/doctor.test.ts | 2 +- src/commands/doctor.ts | 2 +- src/commands/update.ts | 21 -- src/daemon/inspect.ts | 2 +- src/daemon/program-args.test.ts | 4 +- src/daemon/program-args.ts | 2 +- src/gateway/server-discovery.ts | 3 +- src/infra/ports-format.ts | 2 +- 33 files changed, 359 insertions(+), 1427 deletions(-) delete mode 100644 src/cli/canvas-cli.coverage.test.ts delete mode 100644 src/cli/canvas-cli.ts delete mode 100644 src/cli/telegram-cli.ts delete mode 100644 src/commands/update.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index bfa9f0644..e619200ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Commands: gate all slash commands to authorized senders; add `/compact` to manually compact session context. - Groups: `whatsapp.groups`, `telegram.groups`, and `imessage.groups` now act as allowlists when set. Add `"*"` to keep allow-all behavior. - Auto-reply: removed `autoReply` from Discord/Slack/Telegram channel configs; use `requireMention` instead (Telegram topics now support `requireMention` overrides). +- CLI: remove `update`, `gateway-daemon`, `gateway {install|uninstall|start|stop|restart|daemon status|wake|send|agent}`, and `telegram` commands; use `daemon` for service control, `send`/`agent`/`wake` for RPC, and `nodes canvas` for canvas ops. ### Fixes - macOS: harden Voice Wake tester/runtime (pause trigger, mic persistence, local-only tester) and keep transcript logs private. Thanks @xadenryan for PR #438. diff --git a/docs/cli/index.md b/docs/cli/index.md index c425302e5..d9f9361bb 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -42,7 +42,6 @@ clawdbot [--dev] [--profile ] setup onboard configure (alias: config) - update doctor login logout @@ -65,12 +64,6 @@ clawdbot [--dev] [--profile ] call health status - wake - send - agent - stop - restart - gateway-daemon models list status @@ -106,13 +99,6 @@ clawdbot [--dev] [--profile ] canvas snapshot screen record location get - canvas - snapshot - present - hide - navigate - eval - a2ui push|reset browser status start @@ -198,9 +184,6 @@ Options: ### `configure` / `config` Interactive configuration wizard (models, providers, skills, gateway). -### `update` -Audit and modernize the local configuration. - ### `doctor` Health checks + quick fixes (config + gateway + legacy services). @@ -261,13 +244,6 @@ Subcommands: - `pairing list --provider [--json]` - `pairing approve --provider <...> [--notify]` -### `telegram pairing` -Telegram-only pairing helper. - -Subcommands: -- `telegram pairing list [--json]` -- `telegram pairing approve [--no-notify]` - ### `hooks gmail` Gmail Pub/Sub hook setup + runner. See [/automation/gmail-pubsub](/automation/gmail-pubsub). @@ -415,9 +391,6 @@ Options: - `--ws-log ` - `--compact` (alias for `--ws-log compact`) -### `gateway-daemon` -Run the Gateway as a long-lived daemon (same options as `gateway`, minus `--allow-unconfigured` and `--force`). - ### `daemon` Manage the Gateway service (launchd/systemd/schtasks). @@ -435,7 +408,6 @@ Notes: - `daemon status` also surfaces legacy or extra gateway services when it can detect them (`--deep` adds system-level scans). - `daemon install` defaults to Node runtime; use `--runtime bun` only when WhatsApp is disabled. - `daemon install` options: `--port`, `--runtime`, `--token`. -- `gateway install|uninstall|start|stop|restart` remain as service aliases; `daemon` is the dedicated manager. ### `gateway ` Gateway RPC helpers (use `--url`, `--token`, `--password`, `--timeout`, `--expect-final` for each). @@ -444,15 +416,6 @@ Subcommands: - `gateway call [--params ]` - `gateway health` - `gateway status` -- `gateway wake --text [--mode now|next-heartbeat]` -- `gateway send --to --message [--media-url ] [--gif-playback] [--idempotency-key ]` -- `gateway agent --message [--to ] [--session-id ] [--thinking ] [--deliver] [--timeout-seconds ] [--idempotency-key ]` -- `gateway install` -- `gateway uninstall` -- `gateway start` -- `gateway stop` -- `gateway restart` -- `gateway daemon status` (alias for `clawdbot daemon status`) Common RPCs: - `config.apply` (validate + write config + restart + wake) @@ -573,27 +536,17 @@ Camera: Canvas + screen: - `nodes canvas snapshot --node [--format png|jpg|jpeg] [--max-width ] [--quality <0-1>] [--invoke-timeout ]` +- `nodes canvas present --node [--target ] [--x ] [--y ] [--width ] [--height ] [--invoke-timeout ]` +- `nodes canvas hide --node [--invoke-timeout ]` +- `nodes canvas navigate --node [--invoke-timeout ]` +- `nodes canvas eval [] --node [--js ] [--invoke-timeout ]` +- `nodes canvas a2ui push --node (--jsonl | --text ) [--invoke-timeout ]` +- `nodes canvas a2ui reset --node [--invoke-timeout ]` - `nodes screen record --node [--screen ] [--duration ] [--fps ] [--no-audio] [--out ] [--invoke-timeout ]` Location: - `nodes location get --node [--max-age ] [--accuracy ] [--location-timeout ] [--invoke-timeout ]` -## Canvas - -Canvas RPC helper (top-level wrapper for `node.invoke`). See [/platforms/mac/canvas](/platforms/mac/canvas). - -Common options: -- `--url`, `--token`, `--timeout`, `--json` - -Subcommands: -- `canvas snapshot [--node ] [--format png|jpg] [--max-width ] [--quality <0-1>]` -- `canvas present [--node ] [--target ] [--x ] [--y ] [--width ] [--height ]` -- `canvas hide [--node ]` -- `canvas navigate [--node ]` -- `canvas eval [] [--js ] [--node ]` -- `canvas a2ui push (--jsonl | --text ) [--node ]` -- `canvas a2ui reset [--node ]` - ## Browser Browser control CLI (dedicated Chrome/Chromium). See [/tools/browser](/tools/browser). diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index 86412fcaa..03805f105 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -167,7 +167,7 @@ If set, CLAWDBOT derives defaults (only when you haven’t set them explicitly): ### `wizard` -Metadata written by CLI wizards (`onboard`, `configure`, `doctor`, `update`). +Metadata written by CLI wizards (`onboard`, `configure`, `doctor`). ```json5 { diff --git a/docs/gateway/index.md b/docs/gateway/index.md index 8afd556b4..2a0a73121 100644 --- a/docs/gateway/index.md +++ b/docs/gateway/index.md @@ -172,15 +172,13 @@ Notes: - `daemon status` probes the Gateway RPC by default (same URL/token defaults as `gateway status`). - `daemon status --deep` adds system-level scans (LaunchDaemons/system units). - `daemon status` now reports runtime state (PID/exit status) and port collisions when the gateway isn’t reachable. -- `gateway install|uninstall|start|stop|restart` remain supported as aliases; `daemon` is the dedicated manager. -- `gateway daemon status` is an alias for `clawdbot daemon status`. - If other gateway-like services are detected, the CLI warns. We recommend **one gateway per machine**; one gateway can host multiple agents. - Cleanup: `clawdbot daemon uninstall` (current service) and `clawdbot doctor` (legacy migrations). 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`). +- To stop it cleanly, use `clawdbot daemon stop` (or `launchctl bootout gui/$UID/com.clawdbot.gateway`). +- To restart, use `clawdbot daemon restart` (or `launchctl kickstart -k gui/$UID/com.clawdbot.gateway`). ## Supervision (systemd user unit) Create `~/.config/systemd/user/clawdbot-gateway.service`: @@ -236,10 +234,10 @@ Windows installs should use **WSL2** and follow the Linux systemd section above. ## CLI helpers - `clawdbot gateway health|status` — request health/status over the Gateway WS. -- `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 send --to --message "hi" [--media ...]` — send via Gateway (idempotent for WhatsApp). +- `clawdbot 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). +- `clawdbot daemon stop|restart` — stop/restart the supervised gateway service (launchd/systemd). - Gateway helper subcommands assume a running gateway on `--url`; they no longer auto-spawn one. ## Migration guidance diff --git a/docs/gateway/troubleshooting.md b/docs/gateway/troubleshooting.md index 481d22a18..4deba39cf 100644 --- a/docs/gateway/troubleshooting.md +++ b/docs/gateway/troubleshooting.md @@ -202,7 +202,7 @@ kill -9 If the gateway is supervised by launchd, killing the PID will just respawn it. Stop the supervisor instead: ```bash -clawdbot gateway stop +clawdbot daemon stop # Or: launchctl bootout gui/$UID/com.clawdbot.gateway ``` diff --git a/docs/install/updating.md b/docs/install/updating.md index a589b6b48..b20ac04cd 100644 --- a/docs/install/updating.md +++ b/docs/install/updating.md @@ -34,12 +34,12 @@ Then: ```bash clawdbot doctor -clawdbot gateway restart +clawdbot daemon restart clawdbot health ``` Notes: -- If your Gateway runs as a service, `clawdbot gateway restart` is preferred over killing PIDs. +- If your Gateway runs as a service, `clawdbot daemon restart` is preferred over killing PIDs. - If you’re pinned to a specific version, see “Rollback / pinning” below. ## Update (Control UI / RPC) @@ -87,8 +87,8 @@ Details: [Doctor](/gateway/doctor) CLI (works regardless of OS): ```bash -clawdbot gateway stop -clawdbot gateway restart +clawdbot daemon stop +clawdbot daemon restart clawdbot gateway --port 18789 ``` @@ -113,7 +113,7 @@ Then restart + re-run doctor: ```bash clawdbot doctor -clawdbot gateway restart +clawdbot daemon restart ``` ### Pin (source) by date @@ -130,7 +130,7 @@ Then reinstall deps + restart: ```bash pnpm install pnpm build -clawdbot gateway restart +clawdbot daemon restart ``` If you want to go back to latest later: diff --git a/docs/nodes/index.md b/docs/nodes/index.md index ad4ffd073..f27483d1d 100644 --- a/docs/nodes/index.md +++ b/docs/nodes/index.md @@ -51,13 +51,6 @@ clawdbot nodes canvas snapshot --node --format png clawdbot nodes canvas snapshot --node --format jpg --max-width 1200 --quality 0.9 ``` -Simple shortcut (auto-picks a single connected node if possible): - -```bash -clawdbot canvas snapshot --format png -clawdbot canvas snapshot --format jpg --max-width 1200 --quality 0.9 -``` - ## Photos + videos (node camera) Photos (`jpg`): diff --git a/docs/platforms/exe-dev.md b/docs/platforms/exe-dev.md index ba9556a59..0261be2fb 100644 --- a/docs/platforms/exe-dev.md +++ b/docs/platforms/exe-dev.md @@ -167,7 +167,7 @@ More: [Linux](/platforms/linux) ```bash npm i -g clawdbot@latest clawdbot doctor -clawdbot gateway restart +clawdbot daemon restart clawdbot health ``` diff --git a/docs/platforms/index.md b/docs/platforms/index.md index 9d388140f..73db4884f 100644 --- a/docs/platforms/index.md +++ b/docs/platforms/index.md @@ -31,7 +31,7 @@ Linux companion apps are planned, but the core Gateway is fully supported today. Use one of these (all supported): - Wizard (recommended): `clawdbot onboard --install-daemon` -- Direct: `clawdbot daemon install` (alias: `clawdbot gateway install`) +- Direct: `clawdbot daemon install` - Configure flow: `clawdbot configure` → select **Gateway daemon** - Repair/migrate: `clawdbot doctor` (offers to install or fix the service) diff --git a/docs/platforms/linux.md b/docs/platforms/linux.md index 78348d698..819462199 100644 --- a/docs/platforms/linux.md +++ b/docs/platforms/linux.md @@ -36,7 +36,7 @@ clawdbot daemon install Or: ``` -clawdbot gateway install +clawdbot daemon install ``` Or: diff --git a/docs/platforms/mac/bun.md b/docs/platforms/mac/bun.md index 92910ca7a..629d56a44 100644 --- a/docs/platforms/mac/bun.md +++ b/docs/platforms/mac/bun.md @@ -18,7 +18,7 @@ App bundle layout: - bun `--compile` relay executable built from [`dist/macos/relay.js`](https://github.com/clawdbot/clawdbot/blob/main/dist/macos/relay.js) - Supports: - `clawdbot …` (CLI) - - `clawdbot gateway-daemon …` (LaunchAgent daemon) + - `clawdbot gateway …` (LaunchAgent daemon) - `Clawdbot.app/Contents/Resources/Relay/package.json` - tiny “p runtime compatibility” file (see below) - `Clawdbot.app/Contents/Resources/Relay/theme/` @@ -109,7 +109,7 @@ dist/Clawdbot.app/Contents/Resources/Relay/clawdbot --version CLAWDBOT_SKIP_PROVIDERS=1 \ CLAWDBOT_SKIP_CANVAS_HOST=1 \ -dist/Clawdbot.app/Contents/Resources/Relay/clawdbot gateway-daemon --port 18999 --bind loopback +dist/Clawdbot.app/Contents/Resources/Relay/clawdbot gateway --port 18999 --bind loopback ``` Then, in another shell: diff --git a/docs/platforms/mac/canvas.md b/docs/platforms/mac/canvas.md index 6a685b6e2..5e9eff473 100644 --- a/docs/platforms/mac/canvas.md +++ b/docs/platforms/mac/canvas.md @@ -87,12 +87,12 @@ Related: Use the main `clawdbot` CLI; it invokes canvas commands via `node.invoke`. -- `clawdbot canvas present [--node ] [--target <...>] [--x/--y/--width/--height]` +- `clawdbot nodes canvas present --node [--target <...>] [--x/--y/--width/--height]` - Local targets map into the session directory via the custom scheme (directory targets resolve `index.html|index.htm`). - If `/` has no index file, Canvas shows the built-in scaffold page and returns `status: "welcome"`. -- `clawdbot canvas hide [--node ]` -- `clawdbot canvas eval --js [--node ]` -- `clawdbot canvas snapshot [--node ]` +- `clawdbot nodes canvas hide --node ` +- `clawdbot nodes canvas eval --js --node ` +- `clawdbot nodes canvas snapshot --node ` ### Canvas A2UI @@ -104,8 +104,8 @@ http://:18793/__clawdbot__/a2ui/ The macOS app simply renders that page in the Canvas panel. The agent can drive it with JSONL **server→client protocol messages** (one JSON object per line): -- `clawdbot canvas a2ui push --jsonl [--node ]` -- `clawdbot canvas a2ui reset [--node ]` +- `clawdbot nodes canvas a2ui push --jsonl --node ` +- `clawdbot nodes canvas a2ui reset --node ` `push` expects a JSONL file where **each line is a single JSON object** (parsed and forwarded to the in-page A2UI renderer). @@ -113,18 +113,18 @@ Minimal example (v0.8): ```bash cat > /tmp/a2ui-v0.8.jsonl <<'EOF' -{"surfaceUpdate":{"surfaceId":"main","components":[{"id":"root","component":{"Column":{"children":{"explicitList":["title","content"]}}}},{"id":"title","component":{"Text":{"text":{"literalString":"Canvas (A2UI v0.8)"},"usageHint":"h1"}}},{"id":"content","component":{"Text":{"text":{"literalString":"If you can read this, `canvas a2ui push` works."},"usageHint":"body"}}}]}} +{"surfaceUpdate":{"surfaceId":"main","components":[{"id":"root","component":{"Column":{"children":{"explicitList":["title","content"]}}}},{"id":"title","component":{"Text":{"text":{"literalString":"Canvas (A2UI v0.8)"},"usageHint":"h1"}}},{"id":"content","component":{"Text":{"text":{"literalString":"If you can read this, `nodes canvas a2ui push` works."},"usageHint":"body"}}}]}} {"beginRendering":{"surfaceId":"main","root":"root"}} EOF -clawdbot canvas a2ui push --jsonl /tmp/a2ui-v0.8.jsonl --node +clawdbot nodes canvas a2ui push --jsonl /tmp/a2ui-v0.8.jsonl --node ``` Notes: - This does **not** support the A2UI v0.9 examples using `createSurface`. - A2UI **fails** if the Gateway canvas host is unreachable (no local fallback). -- `canvas a2ui push` validates JSONL (line numbers on errors) and rejects v0.9 payloads. -- Quick smoke: `clawdbot canvas a2ui push --text "Hello from A2UI"` renders a minimal v0.8 view. +- `nodes canvas a2ui push` validates JSONL (line numbers on errors) and rejects v0.9 payloads. +- Quick smoke: `clawdbot nodes canvas a2ui push --node --text "Hello from A2UI"` renders a minimal v0.8 view. ## Triggering agent runs from Canvas (deep links) diff --git a/docs/platforms/macos.md b/docs/platforms/macos.md index a1daa37cc..7e3671e37 100644 --- a/docs/platforms/macos.md +++ b/docs/platforms/macos.md @@ -40,7 +40,7 @@ Details: [Gateway runbook](/gateway) and [Bundled bun Gateway](/platforms/mac/bu - `Clawdbot` (LSUIElement MenuBarExtra app; hosts Gateway + node bridge + PeekabooBridgeHost). - Bundle ID: `com.clawdbot.mac`. - Bundled runtime binaries live under `Contents/Resources/Relay/`: - - `clawdbot` (bun‑compiled relay: CLI + gateway-daemon) + - `clawdbot` (bun‑compiled relay: CLI + gateway) - The app symlinks `clawdbot` into `/usr/local/bin` and `/opt/homebrew/bin`. ## Gateway + node bridge @@ -65,7 +65,7 @@ Details: [Gateway runbook](/gateway) and [Bundled bun Gateway](/platforms/mac/bu ## CLI (`clawdbot`) - The **only** CLI is `clawdbot` (TS/bun). There is no `clawdbot-mac` helper. - For mac‑specific actions, the CLI uses `node.invoke`: - - `clawdbot canvas present|navigate|eval|snapshot|a2ui push|a2ui reset` + - `clawdbot nodes canvas present|navigate|eval|snapshot|a2ui push|a2ui reset` - `clawdbot nodes run --node -- ` - `clawdbot nodes notify --node --title ...` diff --git a/docs/platforms/windows.md b/docs/platforms/windows.md index b97906295..ce55d0076 100644 --- a/docs/platforms/windows.md +++ b/docs/platforms/windows.md @@ -37,7 +37,7 @@ clawdbot daemon install Or: ``` -clawdbot gateway install +clawdbot daemon install ``` Or: diff --git a/docs/tools/index.md b/docs/tools/index.md index 1d71039b4..3b43f112a 100644 --- a/docs/tools/index.md +++ b/docs/tools/index.md @@ -100,7 +100,7 @@ Notes: - Uses gateway `node.invoke` under the hood. - If no `node` is provided, the tool picks a default (single connected node or local mac node). - A2UI is v0.8 only (no `createSurface`); the CLI rejects v0.9 JSONL with line errors. -- Quick smoke: `clawdbot canvas a2ui push --text "Hello from A2UI"`. +- Quick smoke: `clawdbot nodes canvas a2ui push --node --text "Hello from A2UI"`. ### `nodes` Discover and target paired nodes; send notifications; capture camera/screen. @@ -162,7 +162,7 @@ Notes: Restart or apply updates to the running Gateway process (in-place). Core actions: -- `restart` (sends `SIGUSR1` to the current process; `clawdbot gateway`/`gateway-daemon` restart in-place) +- `restart` (sends `SIGUSR1` to the current process; `clawdbot gateway` restart in-place) - `config.get` / `config.schema` - `config.apply` (validate + write config + restart + wake) - `update.run` (run update + restart + wake) diff --git a/scripts/e2e/onboard-docker.sh b/scripts/e2e/onboard-docker.sh index f132b82f7..468434042 100755 --- a/scripts/e2e/onboard-docker.sh +++ b/scripts/e2e/onboard-docker.sh @@ -42,7 +42,7 @@ TRASH } start_gateway() { - node dist/index.js gateway-daemon --port 18789 --bind loopback > /tmp/gateway-e2e.log 2>&1 & + node dist/index.js gateway --port 18789 --bind loopback > /tmp/gateway-e2e.log 2>&1 & GATEWAY_PID="$!" } @@ -268,7 +268,7 @@ if (errors.length > 0) { } NODE - node dist/index.js gateway-daemon --port 18789 --bind loopback > /tmp/gateway.log 2>&1 & + node dist/index.js gateway --port 18789 --bind loopback > /tmp/gateway.log 2>&1 & GW_PID=$! # Gate on gateway readiness, then run health. for _ in $(seq 1 10); do diff --git a/src/cli/canvas-cli.coverage.test.ts b/src/cli/canvas-cli.coverage.test.ts deleted file mode 100644 index 9b4962b44..000000000 --- a/src/cli/canvas-cli.coverage.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { Command } from "commander"; -import { describe, expect, it, vi } from "vitest"; - -const callGateway = vi.fn( - async (opts: { method?: string; params?: { command?: string } }) => { - if (opts.method === "node.list") { - return { - nodes: [ - { - nodeId: "mac-1", - displayName: "Mac", - platform: "macos", - caps: ["canvas"], - connected: true, - }, - ], - }; - } - if (opts.method === "node.invoke") { - if (opts.params?.command === "canvas.eval") { - return { payload: { result: "ok" } }; - } - return { ok: true }; - } - return { ok: true }; - }, -); - -const randomIdempotencyKey = vi.fn(() => "rk_test"); - -const runtimeLogs: string[] = []; -const runtimeErrors: string[] = []; -const defaultRuntime = { - log: (msg: string) => runtimeLogs.push(msg), - error: (msg: string) => runtimeErrors.push(msg), - exit: (code: number) => { - throw new Error(`__exit__:${code}`); - }, -}; - -vi.mock("../gateway/call.js", () => ({ - callGateway: (opts: unknown) => callGateway(opts as { method?: string }), - randomIdempotencyKey: () => randomIdempotencyKey(), -})); - -vi.mock("../runtime.js", () => ({ - defaultRuntime, -})); - -describe("canvas-cli coverage", () => { - it("invokes canvas.present with placement and target", async () => { - runtimeLogs.length = 0; - runtimeErrors.length = 0; - callGateway.mockClear(); - randomIdempotencyKey.mockClear(); - - const { registerCanvasCli } = await import("./canvas-cli.js"); - const program = new Command(); - program.exitOverride(); - registerCanvasCli(program); - - await program.parseAsync( - [ - "canvas", - "present", - "--node", - "mac-1", - "--target", - "https://example.com", - "--x", - "10", - "--y", - "20", - "--width", - "800", - "--height", - "600", - ], - { from: "user" }, - ); - - const invoke = callGateway.mock.calls.find( - (call) => call[0]?.method === "node.invoke", - )?.[0]; - - expect(invoke).toBeTruthy(); - expect(invoke?.params?.command).toBe("canvas.present"); - expect(invoke?.params?.idempotencyKey).toBe("rk_test"); - expect(invoke?.params?.params).toEqual({ - url: "https://example.com", - placement: { x: 10, y: 20, width: 800, height: 600 }, - }); - }); - - it("prints canvas.eval result", async () => { - runtimeLogs.length = 0; - runtimeErrors.length = 0; - callGateway.mockClear(); - - const { registerCanvasCli } = await import("./canvas-cli.js"); - const program = new Command(); - program.exitOverride(); - registerCanvasCli(program); - - await program.parseAsync(["canvas", "eval", "1+1"], { from: "user" }); - - expect(runtimeErrors).toHaveLength(0); - expect(runtimeLogs.join("\n")).toContain("ok"); - }); - - it("pushes A2UI text payload", async () => { - runtimeLogs.length = 0; - runtimeErrors.length = 0; - callGateway.mockClear(); - - const { registerCanvasCli } = await import("./canvas-cli.js"); - const program = new Command(); - program.exitOverride(); - registerCanvasCli(program); - - await program.parseAsync( - ["canvas", "a2ui", "push", "--node", "mac-1", "--text", "Hello A2UI"], - { from: "user" }, - ); - - const invoke = callGateway.mock.calls.find( - (call) => call[0]?.method === "node.invoke", - )?.[0]; - - expect(invoke?.params?.command).toBe("canvas.a2ui.pushJSONL"); - expect(invoke?.params?.params?.jsonl).toContain("Hello A2UI"); - }); - - it("rejects invalid A2UI JSONL", async () => { - runtimeLogs.length = 0; - runtimeErrors.length = 0; - callGateway.mockClear(); - - vi.resetModules(); - vi.doMock("node:fs/promises", () => ({ - default: { readFile: vi.fn(async () => "{broken") }, - })); - - const { registerCanvasCli } = await import("./canvas-cli.js"); - const program = new Command(); - program.exitOverride(); - registerCanvasCli(program); - - await expect( - program.parseAsync( - [ - "canvas", - "a2ui", - "push", - "--node", - "mac-1", - "--jsonl", - "/tmp/a2ui.jsonl", - ], - { from: "user" }, - ), - ).rejects.toThrow("__exit__:1"); - - expect(runtimeErrors.join("\n")).toContain("Invalid A2UI JSONL"); - }); -}); diff --git a/src/cli/canvas-cli.ts b/src/cli/canvas-cli.ts deleted file mode 100644 index bde5ced6b..000000000 --- a/src/cli/canvas-cli.ts +++ /dev/null @@ -1,544 +0,0 @@ -import fs from "node:fs/promises"; - -import type { Command } from "commander"; -import { callGateway, randomIdempotencyKey } from "../gateway/call.js"; -import { defaultRuntime } from "../runtime.js"; -import { writeBase64ToFile } from "./nodes-camera.js"; -import { - canvasSnapshotTempPath, - parseCanvasSnapshotPayload, -} from "./nodes-canvas.js"; -import { withProgress } from "./progress.js"; - -type CanvasOpts = { - url?: string; - token?: string; - timeout?: string; - json?: boolean; - node?: string; - target?: string; - x?: string; - y?: string; - width?: string; - height?: string; - js?: string; - jsonl?: string; - text?: string; - format?: string; - maxWidth?: string; - quality?: string; -}; - -type NodeListNode = { - nodeId: string; - displayName?: string; - platform?: string; - remoteIp?: string; - caps?: string[]; - connected?: boolean; -}; - -type PendingRequest = { - requestId: string; - nodeId: string; - displayName?: string; - remoteIp?: string; -}; - -type PairedNode = { - nodeId: string; - displayName?: string; - remoteIp?: string; -}; - -type PairingList = { - pending: PendingRequest[]; - paired: PairedNode[]; -}; - -const A2UI_ACTION_KEYS = [ - "beginRendering", - "surfaceUpdate", - "dataModelUpdate", - "deleteSurface", - "createSurface", -] as const; - -type A2UIVersion = "v0.8" | "v0.9"; - -const canvasCallOpts = (cmd: Command) => - cmd - .option( - "--url ", - "Gateway WebSocket URL (defaults to gateway.remote.url when configured)", - ) - .option("--token ", "Gateway token (if required)") - .option("--timeout ", "Timeout in ms", "10000") - .option("--json", "Output JSON", false); - -const callGatewayCli = async ( - method: string, - opts: CanvasOpts, - params?: unknown, -) => - withProgress( - { - label: `Canvas ${method}`, - indeterminate: true, - enabled: opts.json !== true, - }, - async () => - await callGateway({ - url: opts.url, - token: opts.token, - method, - params, - timeoutMs: Number(opts.timeout ?? 10_000), - clientName: "cli", - mode: "cli", - }), - ); - -function parseNodeList(value: unknown): NodeListNode[] { - const obj = - typeof value === "object" && value !== null - ? (value as Record) - : {}; - return Array.isArray(obj.nodes) ? (obj.nodes as NodeListNode[]) : []; -} - -function parsePairingList(value: unknown): PairingList { - const obj = - typeof value === "object" && value !== null - ? (value as Record) - : {}; - const pending = Array.isArray(obj.pending) - ? (obj.pending as PendingRequest[]) - : []; - const paired = Array.isArray(obj.paired) ? (obj.paired as PairedNode[]) : []; - return { pending, paired }; -} - -function normalizeNodeKey(value: string) { - return value - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+/, "") - .replace(/-+$/, ""); -} - -function buildA2UITextJsonl(text: string) { - const surfaceId = "main"; - const rootId = "root"; - const textId = "text"; - const payloads = [ - { - surfaceUpdate: { - surfaceId, - components: [ - { - id: rootId, - component: { Column: { children: { explicitList: [textId] } } }, - }, - { - id: textId, - component: { - Text: { text: { literalString: text }, usageHint: "body" }, - }, - }, - ], - }, - }, - { beginRendering: { surfaceId, root: rootId } }, - ]; - return payloads.map((payload) => JSON.stringify(payload)).join("\n"); -} - -function validateA2UIJsonl(jsonl: string) { - const lines = jsonl.split(/\r?\n/); - const errors: string[] = []; - let sawV08 = false; - let sawV09 = false; - let messageCount = 0; - - lines.forEach((line, idx) => { - const trimmed = line.trim(); - if (!trimmed) return; - messageCount += 1; - let obj: unknown; - try { - obj = JSON.parse(trimmed) as unknown; - } catch (err) { - errors.push(`line ${idx + 1}: ${String(err)}`); - return; - } - if (!obj || typeof obj !== "object" || Array.isArray(obj)) { - errors.push(`line ${idx + 1}: expected JSON object`); - return; - } - const record = obj as Record; - const actionKeys = A2UI_ACTION_KEYS.filter((key) => key in record); - if (actionKeys.length !== 1) { - errors.push( - `line ${idx + 1}: expected exactly one action key (${A2UI_ACTION_KEYS.join( - ", ", - )})`, - ); - return; - } - if (actionKeys[0] === "createSurface") { - sawV09 = true; - } else { - sawV08 = true; - } - }); - - if (messageCount === 0) { - errors.push("no JSONL messages found"); - } - if (sawV08 && sawV09) { - errors.push("mixed A2UI v0.8 and v0.9 messages in one file"); - } - if (errors.length > 0) { - throw new Error(`Invalid A2UI JSONL:\n- ${errors.join("\n- ")}`); - } - - const version: A2UIVersion = sawV09 ? "v0.9" : "v0.8"; - return { version, messageCount }; -} - -async function loadNodes(opts: CanvasOpts): Promise { - try { - const res = (await callGatewayCli("node.list", opts, {})) as unknown; - return parseNodeList(res); - } catch { - const res = (await callGatewayCli("node.pair.list", opts, {})) as unknown; - const { paired } = parsePairingList(res); - return paired.map((n) => ({ - nodeId: n.nodeId, - displayName: n.displayName, - remoteIp: n.remoteIp, - })); - } -} - -function pickDefaultNode(nodes: NodeListNode[]): NodeListNode | null { - const withCanvas = nodes.filter((n) => - Array.isArray(n.caps) ? n.caps.includes("canvas") : true, - ); - if (withCanvas.length === 0) return null; - - const connected = withCanvas.filter((n) => n.connected); - const candidates = connected.length > 0 ? connected : withCanvas; - if (candidates.length === 1) return candidates[0]; - - const local = candidates.filter( - (n) => - n.platform?.toLowerCase().startsWith("mac") && - typeof n.nodeId === "string" && - n.nodeId.startsWith("mac-"), - ); - if (local.length === 1) return local[0]; - - return null; -} - -async function resolveNodeId(opts: CanvasOpts, query?: string) { - const nodes = await loadNodes(opts); - const q = String(query ?? "").trim(); - if (!q) { - const picked = pickDefaultNode(nodes); - if (picked) return picked.nodeId; - throw new Error( - "node required (use --node or ensure only one connected node is available)", - ); - } - - const qNorm = normalizeNodeKey(q); - const matches = nodes.filter((n) => { - if (n.nodeId === q) return true; - if (typeof n.remoteIp === "string" && n.remoteIp === q) return true; - const name = typeof n.displayName === "string" ? n.displayName : ""; - if (name && normalizeNodeKey(name) === qNorm) return true; - if (q.length >= 6 && n.nodeId.startsWith(q)) return true; - return false; - }); - - if (matches.length === 1) return matches[0].nodeId; - if (matches.length === 0) { - const known = nodes - .map((n) => n.displayName || n.remoteIp || n.nodeId) - .filter(Boolean) - .join(", "); - throw new Error(`unknown node: ${q}${known ? ` (known: ${known})` : ""}`); - } - throw new Error( - `ambiguous node: ${q} (matches: ${matches - .map((n) => n.displayName || n.remoteIp || n.nodeId) - .join(", ")})`, - ); -} - -function normalizeFormat(format: string) { - const trimmed = format.trim().toLowerCase(); - if (trimmed === "jpg") return "jpeg"; - return trimmed; -} - -export function registerCanvasCli(program: Command) { - const canvas = program - .command("canvas") - .description("Control node canvases (present/navigate/eval/snapshot/a2ui)"); - - const invokeCanvas = async ( - opts: CanvasOpts, - command: string, - params?: Record, - ) => { - const nodeId = await resolveNodeId(opts, opts.node); - await callGatewayCli("node.invoke", opts, { - nodeId, - command, - params, - idempotencyKey: randomIdempotencyKey(), - }); - }; - - canvasCallOpts( - canvas - .command("snapshot") - .description("Capture a canvas snapshot (prints MEDIA:)") - .option("--node ", "Node id, name, or IP") - .option("--format ", "Output format", "png") - .option("--max-width ", "Max width (px)") - .option("--quality <0-1>", "JPEG quality (default 0.82)") - .action(async (opts: CanvasOpts) => { - try { - const nodeId = await resolveNodeId(opts, opts.node); - const format = normalizeFormat(String(opts.format ?? "png")); - if (format !== "png" && format !== "jpeg") { - throw new Error("invalid format (use png or jpg)"); - } - const maxWidth = opts.maxWidth - ? Number.parseInt(String(opts.maxWidth), 10) - : undefined; - const quality = opts.quality - ? Number.parseFloat(String(opts.quality)) - : undefined; - - const raw = (await callGatewayCli("node.invoke", opts, { - nodeId, - command: "canvas.snapshot", - params: { - format, - maxWidth: Number.isFinite(maxWidth) ? maxWidth : undefined, - quality: Number.isFinite(quality) ? quality : undefined, - }, - idempotencyKey: randomIdempotencyKey(), - })) as unknown; - - const res = - typeof raw === "object" && raw !== null - ? (raw as { payload?: unknown }) - : {}; - const payload = parseCanvasSnapshotPayload(res.payload); - const filePath = canvasSnapshotTempPath({ - ext: payload.format === "jpeg" ? "jpg" : payload.format, - }); - await writeBase64ToFile(filePath, payload.base64); - - if (opts.json) { - defaultRuntime.log( - JSON.stringify( - { - file: { - path: filePath, - }, - }, - null, - 2, - ), - ); - return; - } - defaultRuntime.log(`MEDIA:${filePath}`); - } catch (err) { - defaultRuntime.error(`canvas snapshot failed: ${String(err)}`); - defaultRuntime.exit(1); - } - }), - ); - - canvasCallOpts( - canvas - .command("present") - .description("Show the canvas (optionally with a target URL/path)") - .option("--node ", "Node id, name, or IP") - .option("--target ", "Target URL/path (optional)") - .option("--x ", "Placement x coordinate") - .option("--y ", "Placement y coordinate") - .option("--width ", "Placement width") - .option("--height ", "Placement height") - .action(async (opts: CanvasOpts) => { - try { - const placement = { - x: opts.x ? Number.parseFloat(opts.x) : undefined, - y: opts.y ? Number.parseFloat(opts.y) : undefined, - width: opts.width ? Number.parseFloat(opts.width) : undefined, - height: opts.height ? Number.parseFloat(opts.height) : undefined, - }; - const params: Record = {}; - if (opts.target) params.url = String(opts.target); - if ( - Number.isFinite(placement.x) || - Number.isFinite(placement.y) || - Number.isFinite(placement.width) || - Number.isFinite(placement.height) - ) { - params.placement = placement; - } - await invokeCanvas(opts, "canvas.present", params); - if (!opts.json) { - defaultRuntime.log("canvas present ok"); - } - } catch (err) { - defaultRuntime.error(`canvas present failed: ${String(err)}`); - defaultRuntime.exit(1); - } - }), - ); - - canvasCallOpts( - canvas - .command("hide") - .description("Hide the canvas") - .option("--node ", "Node id, name, or IP") - .action(async (opts: CanvasOpts) => { - try { - await invokeCanvas(opts, "canvas.hide", undefined); - if (!opts.json) { - defaultRuntime.log("canvas hide ok"); - } - } catch (err) { - defaultRuntime.error(`canvas hide failed: ${String(err)}`); - defaultRuntime.exit(1); - } - }), - ); - - canvasCallOpts( - canvas - .command("navigate") - .description("Navigate the canvas to a URL") - .argument("", "Target URL/path") - .option("--node ", "Node id, name, or IP") - .action(async (url: string, opts: CanvasOpts) => { - try { - await invokeCanvas(opts, "canvas.navigate", { url }); - if (!opts.json) { - defaultRuntime.log("canvas navigate ok"); - } - } catch (err) { - defaultRuntime.error(`canvas navigate failed: ${String(err)}`); - defaultRuntime.exit(1); - } - }), - ); - - canvasCallOpts( - canvas - .command("eval") - .description("Evaluate JavaScript in the canvas") - .argument("[js]", "JavaScript to evaluate") - .option("--js ", "JavaScript to evaluate") - .option("--node ", "Node id, name, or IP") - .action(async (jsArg: string | undefined, opts: CanvasOpts) => { - try { - const js = opts.js ?? jsArg; - if (!js) throw new Error("missing --js or "); - const nodeId = await resolveNodeId(opts, opts.node); - const raw = (await callGatewayCli("node.invoke", opts, { - nodeId, - command: "canvas.eval", - params: { javaScript: js }, - idempotencyKey: randomIdempotencyKey(), - })) as unknown; - if (opts.json) { - defaultRuntime.log(JSON.stringify(raw, null, 2)); - return; - } - const payload = - typeof raw === "object" && raw !== null - ? (raw as { payload?: { result?: string } }).payload - : undefined; - if (payload?.result) { - defaultRuntime.log(payload.result); - } else { - defaultRuntime.log("canvas eval ok"); - } - } catch (err) { - defaultRuntime.error(`canvas eval failed: ${String(err)}`); - defaultRuntime.exit(1); - } - }), - ); - - const a2ui = canvas - .command("a2ui") - .description("Render A2UI content on the canvas"); - - canvasCallOpts( - a2ui - .command("push") - .description("Push A2UI JSONL to the canvas") - .option("--jsonl ", "Path to JSONL payload") - .option("--text ", "Render a quick A2UI text payload") - .option("--node ", "Node id, name, or IP") - .action(async (opts: CanvasOpts) => { - try { - const hasJsonl = Boolean(opts.jsonl); - const hasText = typeof opts.text === "string"; - if (hasJsonl === hasText) { - throw new Error("provide exactly one of --jsonl or --text"); - } - - const jsonl = hasText - ? buildA2UITextJsonl(String(opts.text ?? "")) - : await fs.readFile(String(opts.jsonl), "utf8"); - const { version, messageCount } = validateA2UIJsonl(jsonl); - if (version === "v0.9") { - throw new Error( - "Detected A2UI v0.9 JSONL (createSurface). Clawdbot currently supports v0.8 only.", - ); - } - await invokeCanvas(opts, "canvas.a2ui.pushJSONL", { jsonl }); - if (!opts.json) { - defaultRuntime.log( - `canvas a2ui push ok (v0.8, ${messageCount} message${messageCount === 1 ? "" : "s"})`, - ); - } - } catch (err) { - defaultRuntime.error(`canvas a2ui push failed: ${String(err)}`); - defaultRuntime.exit(1); - } - }), - ); - - canvasCallOpts( - a2ui - .command("reset") - .description("Reset A2UI renderer state") - .option("--node ", "Node id, name, or IP") - .action(async (opts: CanvasOpts) => { - try { - await invokeCanvas(opts, "canvas.a2ui.reset", undefined); - if (!opts.json) { - defaultRuntime.log("canvas a2ui reset ok"); - } - } catch (err) { - defaultRuntime.error(`canvas a2ui reset failed: ${String(err)}`); - defaultRuntime.exit(1); - } - }), - ); -} diff --git a/src/cli/daemon-cli.coverage.test.ts b/src/cli/daemon-cli.coverage.test.ts index e7fe47396..ac1de8161 100644 --- a/src/cli/daemon-cli.coverage.test.ts +++ b/src/cli/daemon-cli.coverage.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it, vi } from "vitest"; const callGateway = vi.fn(async () => ({ ok: true })); const resolveGatewayProgramArguments = vi.fn(async () => ({ - programArguments: ["/bin/node", "cli", "gateway-daemon", "--port", "18789"], + programArguments: ["/bin/node", "cli", "gateway", "--port", "18789"], })); const serviceInstall = vi.fn().mockResolvedValue(undefined); const serviceUninstall = vi.fn().mockResolvedValue(undefined); diff --git a/src/cli/gateway-cli.coverage.test.ts b/src/cli/gateway-cli.coverage.test.ts index 3a70d4086..c7b28d6a1 100644 --- a/src/cli/gateway-cli.coverage.test.ts +++ b/src/cli/gateway-cli.coverage.test.ts @@ -2,21 +2,15 @@ import { Command } from "commander"; import { describe, expect, it, vi } from "vitest"; const callGateway = vi.fn(async () => ({ ok: true })); -const randomIdempotencyKey = vi.fn(() => "rk_test"); const startGatewayServer = vi.fn(async () => ({ close: vi.fn(async () => {}), })); const setVerbose = vi.fn(); -const createDefaultDeps = vi.fn(); const forceFreePortAndWait = vi.fn(async () => ({ killed: [], waitedMs: 0, escalatedToSigkill: false, })); -const serviceInstall = vi.fn().mockResolvedValue(undefined); -const serviceStop = vi.fn().mockResolvedValue(undefined); -const serviceUninstall = vi.fn().mockResolvedValue(undefined); -const serviceRestart = vi.fn().mockResolvedValue(undefined); const serviceIsLoaded = vi.fn().mockResolvedValue(true); const runtimeLogs: string[] = []; @@ -53,7 +47,7 @@ async function withEnvOverride( vi.mock("../gateway/call.js", () => ({ callGateway: (opts: unknown) => callGateway(opts), - randomIdempotencyKey: () => randomIdempotencyKey(), + randomIdempotencyKey: () => "rk_test", })); vi.mock("../gateway/server.js", () => ({ @@ -71,10 +65,6 @@ vi.mock("../runtime.js", () => ({ defaultRuntime, })); -vi.mock("./deps.js", () => ({ - createDefaultDeps: () => createDefaultDeps(), -})); - vi.mock("./ports.js", () => ({ forceFreePortAndWait: (port: number) => forceFreePortAndWait(port), })); @@ -84,10 +74,10 @@ vi.mock("../daemon/service.js", () => ({ label: "LaunchAgent", loadedText: "loaded", notLoadedText: "not loaded", - install: serviceInstall, - uninstall: serviceUninstall, - stop: serviceStop, - restart: serviceRestart, + install: vi.fn(), + uninstall: vi.fn(), + stop: vi.fn(), + restart: vi.fn(), isLoaded: serviceIsLoaded, readCommand: vi.fn(), readRuntime: vi.fn().mockResolvedValue({ status: "running" }), @@ -96,12 +86,12 @@ vi.mock("../daemon/service.js", () => ({ vi.mock("../daemon/program-args.js", () => ({ resolveGatewayProgramArguments: async () => ({ - programArguments: ["/bin/node", "cli", "gateway-daemon", "--port", "18789"], + programArguments: ["/bin/node", "cli", "gateway", "--port", "18789"], }), })); describe("gateway-cli coverage", () => { - it("registers call/health/status/send/agent commands and routes to callGateway", async () => { + it("registers call/health/status commands and routes to callGateway", async () => { runtimeLogs.length = 0; runtimeErrors.length = 0; callGateway.mockClear(); @@ -141,66 +131,6 @@ describe("gateway-cli coverage", () => { expect(runtimeErrors.join("\n")).toContain("Gateway call failed:"); }); - it("fills idempotency keys for send/agent when missing", async () => { - runtimeLogs.length = 0; - runtimeErrors.length = 0; - callGateway.mockClear(); - randomIdempotencyKey.mockClear(); - - const { registerGatewayCli } = await import("./gateway-cli.js"); - const program = new Command(); - program.exitOverride(); - registerGatewayCli(program); - - await program.parseAsync( - ["gateway", "send", "--to", "+1555", "--message", "hi"], - { from: "user" }, - ); - - await program.parseAsync( - ["gateway", "agent", "--message", "hello", "--deliver"], - { from: "user" }, - ); - - expect(randomIdempotencyKey).toHaveBeenCalled(); - const callArgs = callGateway.mock.calls.map((c) => c[0]) as Array<{ - method: string; - params?: { idempotencyKey?: string }; - expectFinal?: boolean; - }>; - expect(callArgs.some((c) => c.method === "send")).toBe(true); - expect( - callArgs.some((c) => c.method === "agent" && c.expectFinal === true), - ).toBe(true); - expect(callArgs.every((c) => c.params?.idempotencyKey === "rk_test")).toBe( - true, - ); - }); - - it("passes gifPlayback for gateway send when flag set", async () => { - runtimeLogs.length = 0; - runtimeErrors.length = 0; - callGateway.mockClear(); - randomIdempotencyKey.mockClear(); - - const { registerGatewayCli } = await import("./gateway-cli.js"); - const program = new Command(); - program.exitOverride(); - registerGatewayCli(program); - - await program.parseAsync( - ["gateway", "send", "--to", "+1555", "--message", "hi", "--gif-playback"], - { from: "user" }, - ); - - expect(callGateway).toHaveBeenCalledWith( - expect.objectContaining({ - method: "send", - params: expect.objectContaining({ gifPlayback: true }), - }), - ); - }); - it("validates gateway ports and handles force/start errors", async () => { runtimeLogs.length = 0; runtimeErrors.length = 0; @@ -254,49 +184,6 @@ 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("supports gateway install/uninstall/start via daemon helpers", async () => { - runtimeLogs.length = 0; - runtimeErrors.length = 0; - serviceInstall.mockClear(); - serviceUninstall.mockClear(); - serviceRestart.mockClear(); - serviceIsLoaded.mockResolvedValueOnce(false); - - const { registerGatewayCli } = await import("./gateway-cli.js"); - const program = new Command(); - program.exitOverride(); - registerGatewayCli(program); - - await program.parseAsync(["gateway", "install", "--port", "18789"], { - from: "user", - }); - await program.parseAsync(["gateway", "uninstall"], { from: "user" }); - await program.parseAsync(["gateway", "start"], { from: "user" }); - - expect(serviceInstall).toHaveBeenCalledTimes(1); - expect(serviceUninstall).toHaveBeenCalledTimes(1); - expect(serviceRestart).toHaveBeenCalledTimes(1); - }); - it("prints stop hints on GatewayLockError when service is loaded", async () => { runtimeLogs.length = 0; runtimeErrors.length = 0; @@ -320,7 +207,7 @@ describe("gateway-cli coverage", () => { expect(startGatewayServer).toHaveBeenCalled(); expect(runtimeErrors.join("\n")).toContain("Gateway failed to start:"); - expect(runtimeErrors.join("\n")).toContain("clawdbot gateway stop"); + expect(runtimeErrors.join("\n")).toContain("clawdbot daemon stop"); }); it("uses env/config port when --port is omitted", async () => { diff --git a/src/cli/gateway-cli.ts b/src/cli/gateway-cli.ts index a61bbdde3..814eef12b 100644 --- a/src/cli/gateway-cli.ts +++ b/src/cli/gateway-cli.ts @@ -12,7 +12,7 @@ import { GATEWAY_WINDOWS_TASK_NAME, } from "../daemon/constants.js"; import { resolveGatewayService } from "../daemon/service.js"; -import { callGateway, randomIdempotencyKey } from "../gateway/call.js"; +import { callGateway } from "../gateway/call.js"; import { startGatewayServer } from "../gateway/server.js"; import { type GatewayWsLogStyle, @@ -23,15 +23,6 @@ import { GatewayLockError } from "../infra/gateway-lock.js"; import { formatPortDiagnostics, inspectPortUsage } from "../infra/ports.js"; import { createSubsystemLogger } from "../logging.js"; import { defaultRuntime } from "../runtime.js"; -import { - runDaemonInstall, - runDaemonRestart, - runDaemonStart, - runDaemonStatus, - runDaemonStop, - runDaemonUninstall, -} from "./daemon-cli.js"; -import { createDefaultDeps } from "./deps.js"; import { forceFreePortAndWait } from "./ports.js"; import { withProgress } from "./progress.js"; @@ -83,21 +74,21 @@ function renderGatewayServiceStopHints(): string[] { switch (process.platform) { case "darwin": return [ - "Tip: clawdbot gateway stop", + "Tip: clawdbot daemon stop", `Or: launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}`, ]; case "linux": return [ - "Tip: clawdbot gateway stop", + "Tip: clawdbot daemon stop", `Or: systemctl --user stop ${GATEWAY_SYSTEMD_SERVICE_NAME}.service`, ]; case "win32": return [ - "Tip: clawdbot gateway stop", + "Tip: clawdbot daemon stop", `Or: schtasks /End /TN "${GATEWAY_WINDOWS_TASK_NAME}"`, ]; default: - return ["Tip: clawdbot gateway stop"]; + return ["Tip: clawdbot daemon stop"]; } } @@ -233,170 +224,6 @@ const callGatewayCli = async ( ); export function registerGatewayCli(program: Command) { - program - .command("gateway-daemon") - .description("Run the WebSocket Gateway as a long-lived daemon") - .option("--port ", "Port for the gateway WebSocket") - .option( - "--bind ", - 'Bind mode ("loopback"|"tailnet"|"lan"|"auto"). Defaults to config gateway.bind (or loopback).', - ) - .option( - "--token ", - "Shared token required in connect.params.auth.token (default: CLAWDBOT_GATEWAY_TOKEN env if set)", - ) - .option("--auth ", 'Gateway auth mode ("token"|"password")') - .option("--password ", "Password for auth mode=password") - .option( - "--tailscale ", - 'Tailscale exposure mode ("off"|"serve"|"funnel")', - ) - .option( - "--tailscale-reset-on-exit", - "Reset Tailscale serve/funnel configuration on shutdown", - false, - ) - .option("--verbose", "Verbose logging to stdout/stderr", false) - .option( - "--ws-log