refactor: simplify cli commands

This commit is contained in:
Peter Steinberger
2026-01-08 07:16:05 +01:00
parent 79ac0af719
commit 19595a8f99
33 changed files with 359 additions and 1427 deletions

View File

@@ -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.

View File

@@ -42,7 +42,6 @@ clawdbot [--dev] [--profile <name>] <command>
setup
onboard
configure (alias: config)
update
doctor
login
logout
@@ -65,12 +64,6 @@ clawdbot [--dev] [--profile <name>] <command>
call
health
status
wake
send
agent
stop
restart
gateway-daemon
models
list
status
@@ -106,13 +99,6 @@ clawdbot [--dev] [--profile <name>] <command>
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 <telegram|signal|imessage|discord|slack|whatsapp> [--json]`
- `pairing approve --provider <...> <code> [--notify]`
### `telegram pairing`
Telegram-only pairing helper.
Subcommands:
- `telegram pairing list [--json]`
- `telegram pairing approve <code> [--no-notify]`
### `hooks gmail`
Gmail Pub/Sub hook setup + runner. See [/automation/gmail-pubsub](/automation/gmail-pubsub).
@@ -415,9 +391,6 @@ Options:
- `--ws-log <auto|full|compact>`
- `--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 <subcommand>`
Gateway RPC helpers (use `--url`, `--token`, `--password`, `--timeout`, `--expect-final` for each).
@@ -444,15 +416,6 @@ Subcommands:
- `gateway call <method> [--params <json>]`
- `gateway health`
- `gateway status`
- `gateway wake --text <text> [--mode now|next-heartbeat]`
- `gateway send --to <jidOrPhone> --message <text> [--media-url <url>] [--gif-playback] [--idempotency-key <key>]`
- `gateway agent --message <text> [--to <jidOrPhone>] [--session-id <id>] [--thinking <level>] [--deliver] [--timeout-seconds <n>] [--idempotency-key <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 <id|name|ip> [--format png|jpg|jpeg] [--max-width <px>] [--quality <0-1>] [--invoke-timeout <ms>]`
- `nodes canvas present --node <id|name|ip> [--target <urlOrPath>] [--x <px>] [--y <px>] [--width <px>] [--height <px>] [--invoke-timeout <ms>]`
- `nodes canvas hide --node <id|name|ip> [--invoke-timeout <ms>]`
- `nodes canvas navigate <url> --node <id|name|ip> [--invoke-timeout <ms>]`
- `nodes canvas eval [<js>] --node <id|name|ip> [--js <code>] [--invoke-timeout <ms>]`
- `nodes canvas a2ui push --node <id|name|ip> (--jsonl <path> | --text <text>) [--invoke-timeout <ms>]`
- `nodes canvas a2ui reset --node <id|name|ip> [--invoke-timeout <ms>]`
- `nodes screen record --node <id|name|ip> [--screen <index>] [--duration <ms|10s>] [--fps <n>] [--no-audio] [--out <path>] [--invoke-timeout <ms>]`
Location:
- `nodes location get --node <id|name|ip> [--max-age <ms>] [--accuracy <coarse|balanced|precise>] [--location-timeout <ms>] [--invoke-timeout <ms>]`
## 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 <id|name|ip>] [--format png|jpg] [--max-width <px>] [--quality <0-1>]`
- `canvas present [--node <id|name|ip>] [--target <urlOrPath>] [--x <px>] [--y <px>] [--width <px>] [--height <px>]`
- `canvas hide [--node <id|name|ip>]`
- `canvas navigate <url> [--node <id|name|ip>]`
- `canvas eval [<js>] [--js <code>] [--node <id|name|ip>]`
- `canvas a2ui push (--jsonl <path> | --text <text>) [--node <id|name|ip>]`
- `canvas a2ui reset [--node <id|name|ip>]`
## Browser
Browser control CLI (dedicated Chrome/Chromium). See [/tools/browser](/tools/browser).

View File

@@ -167,7 +167,7 @@ If set, CLAWDBOT derives defaults (only when you havent set them explicitly):
### `wizard`
Metadata written by CLI wizards (`onboard`, `configure`, `doctor`, `update`).
Metadata written by CLI wizards (`onboard`, `configure`, `doctor`).
```json5
{

View File

@@ -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 isnt 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 <num> --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 <num> --message "hi" [--media ...]` — send via Gateway (idempotent for WhatsApp).
- `clawdbot agent --message "hi" --to <num>` — run an agent turn (waits for final by default).
- `clawdbot gateway call <method> --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

View File

@@ -202,7 +202,7 @@ kill -9 <PID>
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
```

View File

@@ -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 youre 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:

View File

@@ -51,13 +51,6 @@ clawdbot nodes canvas snapshot --node <idOrNameOrIp> --format png
clawdbot nodes canvas snapshot --node <idOrNameOrIp> --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`):

View File

@@ -167,7 +167,7 @@ More: [Linux](/platforms/linux)
```bash
npm i -g clawdbot@latest
clawdbot doctor
clawdbot gateway restart
clawdbot daemon restart
clawdbot health
```

View File

@@ -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)

View File

@@ -36,7 +36,7 @@ clawdbot daemon install
Or:
```
clawdbot gateway install
clawdbot daemon install
```
Or:

View File

@@ -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:

View File

@@ -87,12 +87,12 @@ Related:
Use the main `clawdbot` CLI; it invokes canvas commands via `node.invoke`.
- `clawdbot canvas present [--node <id>] [--target <...>] [--x/--y/--width/--height]`
- `clawdbot nodes canvas present --node <id> [--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 <id>]`
- `clawdbot canvas eval --js <code> [--node <id>]`
- `clawdbot canvas snapshot [--node <id>]`
- `clawdbot nodes canvas hide --node <id>`
- `clawdbot nodes canvas eval --js <code> --node <id>`
- `clawdbot nodes canvas snapshot --node <id>`
### Canvas A2UI
@@ -104,8 +104,8 @@ http://<gateway-host>: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 <path> [--node <id>]`
- `clawdbot canvas a2ui reset [--node <id>]`
- `clawdbot nodes canvas a2ui push --jsonl <path> --node <id>`
- `clawdbot nodes canvas a2ui reset --node <id>`
`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 <id>
clawdbot nodes canvas a2ui push --jsonl /tmp/a2ui-v0.8.jsonl --node <id>
```
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 <id> --text "Hello from A2UI"` renders a minimal v0.8 view.
## Triggering agent runs from Canvas (deep links)

View File

@@ -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` (buncompiled relay: CLI + gateway-daemon)
- `clawdbot` (buncompiled 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 macspecific 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 <id> -- <command...>`
- `clawdbot nodes notify --node <id> --title ...`

View File

@@ -37,7 +37,7 @@ clawdbot daemon install
Or:
```
clawdbot gateway install
clawdbot daemon install
```
Or:

View File

@@ -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 <id> --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)

View File

@@ -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

View File

@@ -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");
});
});

View File

@@ -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 <url>",
"Gateway WebSocket URL (defaults to gateway.remote.url when configured)",
)
.option("--token <token>", "Gateway token (if required)")
.option("--timeout <ms>", "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<string, unknown>)
: {};
return Array.isArray(obj.nodes) ? (obj.nodes as NodeListNode[]) : [];
}
function parsePairingList(value: unknown): PairingList {
const obj =
typeof value === "object" && value !== null
? (value as Record<string, unknown>)
: {};
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<string, unknown>;
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<NodeListNode[]> {
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<string, unknown>,
) => {
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:<path>)")
.option("--node <idOrNameOrIp>", "Node id, name, or IP")
.option("--format <png|jpg>", "Output format", "png")
.option("--max-width <px>", "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 <idOrNameOrIp>", "Node id, name, or IP")
.option("--target <urlOrPath>", "Target URL/path (optional)")
.option("--x <px>", "Placement x coordinate")
.option("--y <px>", "Placement y coordinate")
.option("--width <px>", "Placement width")
.option("--height <px>", "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<string, unknown> = {};
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 <idOrNameOrIp>", "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("<url>", "Target URL/path")
.option("--node <idOrNameOrIp>", "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 <code>", "JavaScript to evaluate")
.option("--node <idOrNameOrIp>", "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 <js>");
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>", "Path to JSONL payload")
.option("--text <text>", "Render a quick A2UI text payload")
.option("--node <idOrNameOrIp>", "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 <idOrNameOrIp>", "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);
}
}),
);
}

View File

@@ -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);

View File

@@ -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<T>(
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 () => {

View File

@@ -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>", "Port for the gateway WebSocket")
.option(
"--bind <mode>",
'Bind mode ("loopback"|"tailnet"|"lan"|"auto"). Defaults to config gateway.bind (or loopback).',
)
.option(
"--token <token>",
"Shared token required in connect.params.auth.token (default: CLAWDBOT_GATEWAY_TOKEN env if set)",
)
.option("--auth <mode>", 'Gateway auth mode ("token"|"password")')
.option("--password <password>", "Password for auth mode=password")
.option(
"--tailscale <mode>",
'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 <style>",
'WebSocket log style ("auto"|"full"|"compact")',
"auto",
)
.option("--compact", 'Alias for "--ws-log compact"', false)
.action(async (opts) => {
setVerbose(Boolean(opts.verbose));
const wsLogRaw = (opts.compact ? "compact" : opts.wsLog) as
| string
| undefined;
const wsLogStyle: GatewayWsLogStyle =
wsLogRaw === "compact"
? "compact"
: wsLogRaw === "full"
? "full"
: "auto";
if (
wsLogRaw !== undefined &&
wsLogRaw !== "auto" &&
wsLogRaw !== "compact" &&
wsLogRaw !== "full"
) {
defaultRuntime.error(
'Invalid --ws-log (use "auto", "full", "compact")',
);
defaultRuntime.exit(1);
}
setGatewayWsLogStyle(wsLogStyle);
const cfg = loadConfig();
const portOverride = parsePort(opts.port);
if (opts.port !== undefined && portOverride === null) {
defaultRuntime.error("Invalid port");
defaultRuntime.exit(1);
return;
}
const port = portOverride ?? resolveGatewayPort(cfg);
if (!Number.isFinite(port) || port <= 0) {
defaultRuntime.error("Invalid port");
defaultRuntime.exit(1);
return;
}
if (opts.token) {
process.env.CLAWDBOT_GATEWAY_TOKEN = String(opts.token);
}
const authModeRaw = opts.auth ? String(opts.auth) : undefined;
const authMode =
authModeRaw === "token" || authModeRaw === "password"
? authModeRaw
: null;
if (authModeRaw && !authMode) {
defaultRuntime.error('Invalid --auth (use "token" or "password")');
defaultRuntime.exit(1);
return;
}
const tailscaleRaw = opts.tailscale ? String(opts.tailscale) : undefined;
const tailscaleMode =
tailscaleRaw === "off" ||
tailscaleRaw === "serve" ||
tailscaleRaw === "funnel"
? tailscaleRaw
: null;
if (tailscaleRaw && !tailscaleMode) {
defaultRuntime.error(
'Invalid --tailscale (use "off", "serve", or "funnel")',
);
defaultRuntime.exit(1);
return;
}
const bindRaw = String(opts.bind ?? cfg.gateway?.bind ?? "loopback");
const bind =
bindRaw === "loopback" ||
bindRaw === "tailnet" ||
bindRaw === "lan" ||
bindRaw === "auto"
? bindRaw
: null;
if (!bind) {
defaultRuntime.error(
'Invalid --bind (use "loopback", "tailnet", "lan", or "auto")',
);
defaultRuntime.exit(1);
return;
}
try {
await runGatewayLoop({
runtime: defaultRuntime,
start: async () =>
await startGatewayServer(port, {
bind,
auth:
authMode || opts.password || authModeRaw
? {
mode: authMode ?? undefined,
password: opts.password
? String(opts.password)
: undefined,
}
: undefined,
tailscale:
tailscaleMode || opts.tailscaleResetOnExit
? {
mode: tailscaleMode ?? undefined,
resetOnExit: Boolean(opts.tailscaleResetOnExit),
}
: undefined,
}),
});
} catch (err) {
if (
err instanceof GatewayLockError ||
(err &&
typeof err === "object" &&
(err as { name?: string }).name === "GatewayLockError")
) {
const errMessage = describeUnknownError(err);
defaultRuntime.error(
`Gateway failed to start: ${errMessage}\nIf the gateway is supervised, stop it with: clawdbot gateway stop`,
);
try {
const diagnostics = await inspectPortUsage(port);
if (diagnostics.status === "busy") {
for (const line of formatPortDiagnostics(diagnostics)) {
defaultRuntime.error(line);
}
}
} catch {
// ignore diagnostics failures
}
await maybeExplainGatewayServiceStop();
defaultRuntime.exit(1);
return;
}
defaultRuntime.error(`Gateway failed to start: ${String(err)}`);
defaultRuntime.exit(1);
}
});
const gateway = program
.command("gateway")
.description("Run the WebSocket Gateway")
@@ -596,7 +423,7 @@ export function registerGatewayCli(program: Command) {
) {
const errMessage = describeUnknownError(err);
defaultRuntime.error(
`Gateway failed to start: ${errMessage}\nIf the gateway is supervised, stop it with: clawdbot gateway stop`,
`Gateway failed to start: ${errMessage}\nIf the gateway is supervised, stop it with: clawdbot daemon stop`,
);
try {
const diagnostics = await inspectPortUsage(port);
@@ -617,69 +444,13 @@ export function registerGatewayCli(program: Command) {
}
});
gateway
.command("install")
.description(
"Install the Gateway service (alias for `clawdbot daemon install`)",
)
.option("--port <port>", "Gateway port")
.option("--runtime <runtime>", "Daemon runtime (node|bun). Default: node")
.option("--token <token>", "Gateway token (token auth)")
.action(async (opts) => {
await runDaemonInstall(opts);
});
gateway
.command("uninstall")
.description(
"Uninstall the Gateway service (alias for `clawdbot daemon uninstall`)",
)
.action(async () => {
await runDaemonUninstall();
});
gateway
.command("start")
.description(
"Start the Gateway service (alias for `clawdbot daemon start`)",
)
.action(async () => {
await runDaemonStart();
});
const gatewayDaemon = gateway
.command("daemon")
.description("Daemon helpers (alias for `clawdbot daemon`)");
gatewayDaemon
.command("status")
.description("Show daemon install status + probe the Gateway")
.option(
"--url <url>",
"Gateway WebSocket URL (defaults to config/remote/local)",
)
.option("--token <token>", "Gateway token (if required)")
.option("--password <password>", "Gateway password (password auth)")
.option("--timeout <ms>", "Timeout in ms", "10000")
.option("--no-probe", "Skip RPC probe")
.option("--deep", "Scan system-level services", false)
.option("--json", "Output JSON", false)
.action(async (opts) => {
await runDaemonStatus({
rpc: opts,
probe: Boolean(opts.probe),
deep: Boolean(opts.deep),
json: Boolean(opts.json),
});
});
gatewayCallOpts(
gateway
.command("call")
.description("Call a Gateway method and print JSON")
.argument(
"<method>",
"Method name (health/status/system-presence/send/agent/cron.*)",
"Method name (health/status/system-presence/cron.*)",
)
.option("--params <json>", "JSON object string for params", "{}")
.action(async (method, opts) => {
@@ -724,108 +495,4 @@ export function registerGatewayCli(program: Command) {
}),
);
gatewayCallOpts(
gateway
.command("wake")
.description("Enqueue a system event and optionally trigger a heartbeat")
.requiredOption("--text <text>", "System event text")
.option(
"--mode <mode>",
"Wake mode (now|next-heartbeat)",
"next-heartbeat",
)
.action(async (opts) => {
try {
const result = await callGatewayCli("wake", opts, {
mode: opts.mode,
text: opts.text,
});
defaultRuntime.log(JSON.stringify(result, null, 2));
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
}
}),
);
gatewayCallOpts(
gateway
.command("send")
.description("Send a message via the Gateway")
.requiredOption("--to <jidOrPhone>", "Destination (E.164 or jid)")
.requiredOption("--message <text>", "Message text")
.option("--media-url <url>", "Optional media URL")
.option("--gif-playback", "Treat video media as GIF playback", false)
.option("--idempotency-key <key>", "Idempotency key")
.action(async (opts) => {
try {
const idempotencyKey = opts.idempotencyKey ?? randomIdempotencyKey();
const result = await callGatewayCli("send", opts, {
to: opts.to,
message: opts.message,
mediaUrl: opts.mediaUrl,
gifPlayback: opts.gifPlayback,
idempotencyKey,
});
defaultRuntime.log(JSON.stringify(result, null, 2));
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
}
}),
);
gatewayCallOpts(
gateway
.command("agent")
.description("Run an agent turn via the Gateway (waits for final)")
.requiredOption("--message <text>", "User message")
.option("--to <jidOrPhone>", "Destination")
.option("--session-id <id>", "Session id")
.option("--thinking <level>", "Thinking level")
.option("--deliver", "Deliver response", false)
.option("--timeout-seconds <n>", "Agent timeout seconds")
.option("--idempotency-key <key>", "Idempotency key")
.action(async (opts) => {
try {
const idempotencyKey = opts.idempotencyKey ?? randomIdempotencyKey();
const result = await callGatewayCli(
"agent",
{ ...opts, expectFinal: true },
{
message: opts.message,
to: opts.to,
sessionId: opts.sessionId,
thinking: opts.thinking,
deliver: Boolean(opts.deliver),
timeout: opts.timeoutSeconds
? Number.parseInt(String(opts.timeoutSeconds), 10)
: undefined,
idempotencyKey,
},
);
defaultRuntime.log(JSON.stringify(result, null, 2));
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
}
}),
);
gateway
.command("stop")
.description("Stop the Gateway service (launchd/systemd/schtasks)")
.action(async () => {
await runDaemonStop();
});
gateway
.command("restart")
.description("Restart the Gateway service (launchd/systemd/schtasks)")
.action(async () => {
await runDaemonRestart();
});
// Build default deps (keeps parity with other commands; future-proofing).
void createDefaultDeps();
}

View File

@@ -1,3 +1,4 @@
import fs from "node:fs/promises";
import type { Command } from "commander";
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
import { defaultRuntime } from "../runtime.js";
@@ -31,6 +32,14 @@ type NodesRpcOpts = {
params?: string;
invokeTimeout?: string;
idempotencyKey?: string;
target?: string;
x?: string;
y?: string;
width?: string;
height?: string;
js?: string;
jsonl?: string;
text?: string;
cwd?: string;
env?: string[];
commandTimeout?: string;
@@ -99,6 +108,16 @@ type PairingList = {
paired: PairedNode[];
};
const A2UI_ACTION_KEYS = [
"beginRendering",
"surfaceUpdate",
"dataModelUpdate",
"deleteSurface",
"createSurface",
] as const;
type A2UIVersion = "v0.8" | "v0.9";
const nodesCallOpts = (cmd: Command, defaults?: { timeoutMs?: number }) =>
cmd
.option(
@@ -249,6 +268,86 @@ async function resolveNodeId(opts: NodesRpcOpts, query: string) {
);
}
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<string, unknown>;
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 };
}
export function registerNodesCli(program: Command) {
const nodes = program
.command("nodes")
@@ -750,6 +849,25 @@ export function registerNodesCli(program: Command) {
.command("canvas")
.description("Capture or render canvas content from a paired node");
const invokeCanvas = async (
opts: NodesRpcOpts,
command: string,
params?: Record<string, unknown>,
) => {
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
const invokeParams: Record<string, unknown> = {
nodeId,
command,
params,
idempotencyKey: randomIdempotencyKey(),
};
const timeoutMs = parseTimeoutMs(opts.invokeTimeout);
if (typeof timeoutMs === "number") {
invokeParams.timeoutMs = timeoutMs;
}
return await callGatewayCli("node.invoke", opts, invokeParams);
};
nodesCallOpts(
canvas
.command("snapshot")
@@ -840,6 +958,181 @@ export function registerNodesCli(program: Command) {
{ timeoutMs: 60_000 },
);
nodesCallOpts(
canvas
.command("present")
.description("Show the canvas (optionally with a target URL/path)")
.requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP")
.option("--target <urlOrPath>", "Target URL/path (optional)")
.option("--x <px>", "Placement x coordinate")
.option("--y <px>", "Placement y coordinate")
.option("--width <px>", "Placement width")
.option("--height <px>", "Placement height")
.option("--invoke-timeout <ms>", "Node invoke timeout in ms")
.action(async (opts: NodesRpcOpts) => {
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<string, unknown> = {};
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(`nodes canvas present failed: ${String(err)}`);
defaultRuntime.exit(1);
}
}),
);
nodesCallOpts(
canvas
.command("hide")
.description("Hide the canvas")
.requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP")
.option("--invoke-timeout <ms>", "Node invoke timeout in ms")
.action(async (opts: NodesRpcOpts) => {
try {
await invokeCanvas(opts, "canvas.hide", undefined);
if (!opts.json) {
defaultRuntime.log("canvas hide ok");
}
} catch (err) {
defaultRuntime.error(`nodes canvas hide failed: ${String(err)}`);
defaultRuntime.exit(1);
}
}),
);
nodesCallOpts(
canvas
.command("navigate")
.description("Navigate the canvas to a URL")
.argument("<url>", "Target URL/path")
.requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP")
.option("--invoke-timeout <ms>", "Node invoke timeout in ms")
.action(async (url: string, opts: NodesRpcOpts) => {
try {
await invokeCanvas(opts, "canvas.navigate", { url });
if (!opts.json) {
defaultRuntime.log("canvas navigate ok");
}
} catch (err) {
defaultRuntime.error(`nodes canvas navigate failed: ${String(err)}`);
defaultRuntime.exit(1);
}
}),
);
nodesCallOpts(
canvas
.command("eval")
.description("Evaluate JavaScript in the canvas")
.argument("[js]", "JavaScript to evaluate")
.option("--js <code>", "JavaScript to evaluate")
.requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP")
.option("--invoke-timeout <ms>", "Node invoke timeout in ms")
.action(async (jsArg: string | undefined, opts: NodesRpcOpts) => {
try {
const js = opts.js ?? jsArg;
if (!js) throw new Error("missing --js or <js>");
const raw = await invokeCanvas(opts, "canvas.eval", {
javaScript: js,
});
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(`nodes canvas eval failed: ${String(err)}`);
defaultRuntime.exit(1);
}
}),
);
const a2ui = canvas
.command("a2ui")
.description("Render A2UI content on the canvas");
nodesCallOpts(
a2ui
.command("push")
.description("Push A2UI JSONL to the canvas")
.option("--jsonl <path>", "Path to JSONL payload")
.option("--text <text>", "Render a quick A2UI text payload")
.requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP")
.option("--invoke-timeout <ms>", "Node invoke timeout in ms")
.action(async (opts: NodesRpcOpts) => {
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(`nodes canvas a2ui push failed: ${String(err)}`);
defaultRuntime.exit(1);
}
}),
);
nodesCallOpts(
a2ui
.command("reset")
.description("Reset A2UI renderer state")
.requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP")
.option("--invoke-timeout <ms>", "Node invoke timeout in ms")
.action(async (opts: NodesRpcOpts) => {
try {
await invokeCanvas(opts, "canvas.a2ui.reset", undefined);
if (!opts.json) {
defaultRuntime.log("canvas a2ui reset ok");
}
} catch (err) {
defaultRuntime.error(`nodes canvas a2ui reset failed: ${String(err)}`);
defaultRuntime.exit(1);
}
}),
);
nodesCallOpts(
camera
.command("list")

View File

@@ -644,44 +644,6 @@ describe("cli program", () => {
}
});
it("runs canvas snapshot and prints MEDIA path", async () => {
callGateway
.mockResolvedValueOnce({
ts: Date.now(),
nodes: [
{
nodeId: "mac-1",
displayName: "Mac Node",
platform: "macos",
connected: true,
caps: ["canvas"],
},
],
})
.mockResolvedValueOnce({
ok: true,
nodeId: "mac-1",
command: "canvas.snapshot",
payload: { format: "png", base64: "aGk=" },
});
const program = buildProgram();
runtime.log.mockClear();
await program.parseAsync(["canvas", "snapshot", "--format", "png"], {
from: "user",
});
const out = String(runtime.log.mock.calls[0]?.[0] ?? "");
const mediaPath = out.replace(/^MEDIA:/, "").trim();
expect(mediaPath).toMatch(/clawdbot-canvas-snapshot-.*\.png$/);
try {
await expect(fs.readFile(mediaPath, "utf8")).resolves.toBe("hi");
} finally {
await fs.unlink(mediaPath).catch(() => {});
}
});
it("fails nodes camera snap on invalid facing", async () => {
callGateway.mockResolvedValueOnce({
ts: Date.now(),

View File

@@ -14,7 +14,6 @@ import { sendCommand } from "../commands/send.js";
import { sessionsCommand } from "../commands/sessions.js";
import { setupCommand } from "../commands/setup.js";
import { statusCommand } from "../commands/status.js";
import { updateCommand } from "../commands/update.js";
import {
isNixMode,
loadConfig,
@@ -31,7 +30,6 @@ import { VERSION } from "../version.js";
import { resolveWhatsAppAccount } from "../web/accounts.js";
import { emitCliBanner, formatCliBannerLine } from "./banner.js";
import { registerBrowserCli } from "./browser-cli.js";
import { registerCanvasCli } from "./canvas-cli.js";
import { hasExplicitOptions } from "./command-options.js";
import { registerCronCli } from "./cron-cli.js";
import { registerDaemonCli } from "./daemon-cli.js";
@@ -45,7 +43,6 @@ import { registerNodesCli } from "./nodes-cli.js";
import { registerPairingCli } from "./pairing-cli.js";
import { forceFreePort } from "./ports.js";
import { registerProvidersCli } from "./providers-cli.js";
import { registerTelegramCli } from "./telegram-cli.js";
import { registerTuiCli } from "./tui-cli.js";
export { forceFreePort };
@@ -345,17 +342,6 @@ export function buildProgram() {
});
program
.command("update")
.description("Audit and modernize the local configuration")
.action(async () => {
try {
await updateCommand(defaultRuntime);
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
}
});
program
.command("login")
.description("Link your personal WhatsApp via QR (web provider)")
@@ -633,7 +619,6 @@ Examples:
}
});
registerCanvasCli(program);
registerDaemonCli(program);
registerGatewayCli(program);
registerModelsCli(program);
@@ -645,7 +630,6 @@ Examples:
registerHooksCli(program);
registerPairingCli(program);
registerProvidersCli(program);
registerTelegramCli(program);
program
.command("status")

View File

@@ -1,74 +0,0 @@
import type { Command } from "commander";
import { loadConfig } from "../config/config.js";
import {
approveTelegramPairingCode,
listTelegramPairingRequests,
} from "../telegram/pairing-store.js";
import { sendMessageTelegram } from "../telegram/send.js";
import { resolveTelegramToken } from "../telegram/token.js";
export function registerTelegramCli(program: Command) {
const telegram = program
.command("telegram")
.description("Telegram helpers (pairing, allowlists)");
const pairing = telegram
.command("pairing")
.description("Secure DM pairing (approve inbound requests)");
pairing
.command("list")
.description("List pending Telegram pairing requests")
.option("--json", "Print JSON", false)
.action(async (opts) => {
const requests = await listTelegramPairingRequests();
if (opts.json) {
console.log(JSON.stringify({ requests }, null, 2));
return;
}
if (requests.length === 0) {
console.log("No pending Telegram pairing requests.");
return;
}
for (const r of requests) {
const name = [r.firstName, r.lastName].filter(Boolean).join(" ").trim();
const username = r.username ? `@${r.username}` : "";
const who = [name, username].filter(Boolean).join(" ").trim();
console.log(
`${r.code} chatId=${r.chatId}${who ? ` ${who}` : ""} ${r.createdAt}`,
);
}
});
pairing
.command("approve")
.description("Approve a pairing code and allow that chatId")
.argument("<code>", "Pairing code (shown to the requester)")
.option("--no-notify", "Do not notify the requester on Telegram")
.action(async (code, opts) => {
const approved = await approveTelegramPairingCode({ code: String(code) });
if (!approved) {
throw new Error(`No pending pairing request found for code: ${code}`);
}
console.log(`Approved Telegram chatId ${approved.chatId}.`);
if (opts.notify === false) return;
const cfg = loadConfig();
const { token } = resolveTelegramToken(cfg);
if (!token) {
console.log(
"Telegram token not configured; skipping requester notification.",
);
return;
}
await sendMessageTelegram(
approved.chatId,
"✅ Clawdbot access approved. Send a message to start chatting.",
{ token },
).catch((err) => {
console.log(`Failed to notify requester: ${String(err)}`);
});
});
}

View File

@@ -87,7 +87,7 @@ const uninstallLegacyGatewayServices = vi.fn().mockResolvedValue([]);
const findExtraGatewayServices = vi.fn().mockResolvedValue([]);
const renderGatewayServiceCleanupHints = vi.fn().mockReturnValue(["cleanup"]);
const resolveGatewayProgramArguments = vi.fn().mockResolvedValue({
programArguments: ["node", "cli", "gateway-daemon", "--port", "18789"],
programArguments: ["node", "cli", "gateway", "--port", "18789"],
});
const serviceInstall = vi.fn().mockResolvedValue(undefined);
const serviceIsLoaded = vi.fn().mockResolvedValue(false);

View File

@@ -244,7 +244,7 @@ export async function doctorCommand(
}
if (process.platform === "darwin") {
note(
`LaunchAgent loaded; stopping requires "clawdbot gateway stop" or launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}.`,
`LaunchAgent loaded; stopping requires "clawdbot daemon stop" or launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}.`,
"Gateway",
);
}

View File

@@ -1,21 +0,0 @@
import type { RuntimeEnv } from "../runtime.js";
import { defaultRuntime } from "../runtime.js";
import { runConfigureWizard } from "./configure.js";
export async function updateCommand(runtime: RuntimeEnv = defaultRuntime) {
await runConfigureWizard(
{
command: "update",
sections: [
"workspace",
"model",
"gateway",
"daemon",
"providers",
"skills",
"health",
],
},
runtime,
);
}

View File

@@ -23,7 +23,7 @@ export type FindExtraGatewayServicesOptions = {
deep?: boolean;
};
const EXTRA_MARKERS = ["clawdbot", "clawdis", "gateway-daemon"];
const EXTRA_MARKERS = ["clawdbot", "clawdis"];
const execFileAsync = promisify(execFile);
export function renderGatewayServiceCleanupHints(): string[] {

View File

@@ -43,7 +43,7 @@ describe("resolveGatewayProgramArguments", () => {
expect(result.programArguments).toEqual([
process.execPath,
entryPath,
"gateway-daemon",
"gateway",
"--port",
"18789",
]);
@@ -70,7 +70,7 @@ describe("resolveGatewayProgramArguments", () => {
expect(result.programArguments).toEqual([
process.execPath,
indexPath,
"gateway-daemon",
"gateway",
"--port",
"18789",
]);

View File

@@ -147,7 +147,7 @@ export async function resolveGatewayProgramArguments(params: {
dev?: boolean;
runtime?: GatewayRuntimePreference;
}): Promise<GatewayProgramArgs> {
const gatewayArgs = ["gateway-daemon", "--port", String(params.port)];
const gatewayArgs = ["gateway", "--port", String(params.port)];
const execPath = process.execPath;
const runtime = params.runtime ?? "auto";

View File

@@ -42,8 +42,7 @@ export function resolveBonjourCliPath(
const argv = opts.argv ?? process.argv;
const argvPath = argv[1];
if (argvPath && isFile(argvPath)) {
const base = path.basename(argvPath);
if (!base.includes("gateway-daemon")) return argvPath;
return argvPath;
}
const cwd = opts.cwd ?? process.cwd();

View File

@@ -34,7 +34,7 @@ export function buildPortHints(
const hints: string[] = [];
if (kinds.has("gateway")) {
hints.push(
"Gateway already running locally. Stop it (clawdbot gateway stop) or use a different port.",
"Gateway already running locally. Stop it (clawdbot daemon stop) or use a different port.",
);
}
if (kinds.has("ssh")) {