Compare commits
111 Commits
fix/node-i
...
feat/model
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4917c2cfa | ||
|
|
205a5fd522 | ||
|
|
9ba7c8874b | ||
|
|
acd40e1780 | ||
|
|
b5fd66c92d | ||
|
|
45c1ccdfcf | ||
|
|
76600e80ba | ||
|
|
483a50f107 | ||
|
|
31943dcecb | ||
|
|
717fb9e413 | ||
|
|
ad7ef27f66 | ||
|
|
0d3b8f6ac3 | ||
|
|
6492e90c1b | ||
|
|
e4b3c8b98d | ||
|
|
8b8e078ef8 | ||
|
|
44a3539ffa | ||
|
|
0daaa5b592 | ||
|
|
6866cca6d7 | ||
|
|
c145a0d116 | ||
|
|
6c0a01dc90 | ||
|
|
41c9c214fc | ||
|
|
9f999f6554 | ||
|
|
9f59ff325b | ||
|
|
c415ccaed5 | ||
|
|
403904ecd1 | ||
|
|
32550154f9 | ||
|
|
6996c0f330 | ||
|
|
cf4f1ed03a | ||
|
|
c913f05fb5 | ||
|
|
88d76d4be5 | ||
|
|
b52ab96e2c | ||
|
|
f0a8b34198 | ||
|
|
64d29b0c31 | ||
|
|
9b47f463b7 | ||
|
|
9605ad76c5 | ||
|
|
c129f0bbaa | ||
|
|
9e22f019db | ||
|
|
6f58d508b8 | ||
|
|
84eadd92a1 | ||
|
|
fd918bf6bf | ||
|
|
4e1806947d | ||
|
|
8aca606a6f | ||
|
|
56799a21be | ||
|
|
d2a0e416ea | ||
|
|
43afad9f51 | ||
|
|
5d73a412c6 | ||
|
|
d0e8faea97 | ||
|
|
cd25d69b4d | ||
|
|
c3adc50cb2 | ||
|
|
cbb9872478 | ||
|
|
39e24c9937 | ||
|
|
fa1bc589e4 | ||
|
|
0e003cb7f1 | ||
|
|
fb164b321e | ||
|
|
884211a924 | ||
|
|
9bd6b3fd54 | ||
|
|
dc06b225cd | ||
|
|
cdb35c3aae | ||
|
|
4e4f5558fc | ||
|
|
8479dc97da | ||
|
|
86ddd3c69c | ||
|
|
49d53ff0bb | ||
|
|
97e8f9d619 | ||
|
|
5392fa0dfa | ||
|
|
63d017c3af | ||
|
|
40646c73af | ||
|
|
43ea7665ef | ||
|
|
ba131b0164 | ||
|
|
0693c7804f | ||
|
|
6c69ea2c91 | ||
|
|
1e10dc1d3b | ||
|
|
c22a37976d | ||
|
|
9b9bbae501 | ||
|
|
7bfc32fe33 | ||
|
|
b073deee20 | ||
|
|
89c5035aa2 | ||
|
|
cb7791c8a4 | ||
|
|
9a14267dfa | ||
|
|
010d305401 | ||
|
|
3210c91f6b | ||
|
|
e3cea55d72 | ||
|
|
687a902f3e | ||
|
|
fe860de148 | ||
|
|
bc8a59faa4 | ||
|
|
91bcdad503 | ||
|
|
ab97c6880b | ||
|
|
65dd73b4c3 | ||
|
|
b69aa011fe | ||
|
|
e3a44b10bc | ||
|
|
5b8007784b | ||
|
|
0d6e78b718 | ||
|
|
46ab4cb19e | ||
|
|
32edaad823 | ||
|
|
5dcd48544a | ||
|
|
1e05925e47 | ||
|
|
fb47f1cbeb | ||
|
|
15d1421cf2 | ||
|
|
899bbd40d7 | ||
|
|
555b2578a8 | ||
|
|
0229b8bbd8 | ||
|
|
552f9eff7b | ||
|
|
36e0cffaaf | ||
|
|
e17a9c6abf | ||
|
|
6180603ef4 | ||
|
|
810374d648 | ||
|
|
968b967854 | ||
|
|
110079d99d | ||
|
|
34a126a6d7 | ||
|
|
31462f64d8 | ||
|
|
b46855d8c4 | ||
|
|
feaad8250b |
@@ -100,7 +100,7 @@
|
||||
- CLI progress: use `src/cli/progress.ts` (`osc-progress` + `@clack/prompts` spinner); don’t hand-roll spinners/bars.
|
||||
- Status output: keep tables + ANSI-safe wrapping (`src/terminal/table.ts`); `status --all` = read-only/pasteable, `status --deep` = probes.
|
||||
- Gateway currently runs only as the menubar app; there is no separate LaunchAgent/helper label installed. Restart via the Clawdbot Mac app or `scripts/restart-mac.sh`; to verify/kill use `launchctl print gui/$UID | grep clawdbot` rather than assuming a fixed label. **When debugging on macOS, start/stop the gateway via the app, not ad-hoc tmux sessions; kill any temporary tunnels before handoff.**
|
||||
- macOS logs: use `./scripts/clawlog.sh` (aka `vtlog`) to query unified logs for the Clawdbot subsystem; it supports follow/tail/category filters and expects passwordless sudo for `/usr/bin/log`.
|
||||
- macOS logs: use `./scripts/clawlog.sh` to query unified logs for the Clawdbot subsystem; it supports follow/tail/category filters and expects passwordless sudo for `/usr/bin/log`.
|
||||
- If shared guardrails are available locally, review them; otherwise follow this repo's guidance.
|
||||
- SwiftUI state management (iOS/macOS): prefer the `Observation` framework (`@Observable`, `@Bindable`) over `ObservableObject`/`@StateObject`; don’t introduce new `ObservableObject` unless required for compatibility, and migrate existing usages when touching related code.
|
||||
- Connection providers: when adding a new connection, update every UI surface and docs (macOS app, web UI, mobile if applicable, onboarding/overview docs) and add matching status + configuration forms so provider lists and settings stay in sync.
|
||||
|
||||
452
CHANGELOG.md
452
CHANGELOG.md
@@ -2,290 +2,210 @@
|
||||
|
||||
Docs: https://docs.clawd.bot
|
||||
|
||||
## 2026.1.21
|
||||
|
||||
### Changes
|
||||
- CLI: default exec approvals to the local host, add gateway/node targeting flags, and show target details in allowlist output.
|
||||
- CLI: exec approvals mutations render tables instead of raw JSON.
|
||||
- Exec approvals: support wildcard agent allowlists (`*`) across all agents.
|
||||
- Nodes: expose node PATH in status/describe and bootstrap PATH for node-host execution.
|
||||
- CLI: flatten node service commands under `clawdbot node` and remove `service node` docs.
|
||||
- CLI: move gateway service commands under `clawdbot gateway` and add `gateway probe` for reachability.
|
||||
- Sessions: add per-channel reset overrides via `session.resetByChannel`. (#1353) Thanks @cash-echo-bot.
|
||||
|
||||
### Fixes
|
||||
- Gateway: keep auto bind loopback-first and add explicit tailnet binding to avoid Tailscale taking over local UI. (#1380)
|
||||
- Embedded runner: persist injected history images so attachments aren’t reloaded each turn. (#1374) Thanks @Nicell.
|
||||
- Nodes tool: include agent/node/gateway context in tool failure logs to speed approval debugging.
|
||||
- macOS: exec approvals now respect wildcard agent allowlists (`*`).
|
||||
- macOS: allow SSH agent auth when no identity file is set. (#1384) Thanks @ameno-.
|
||||
- UI: remove the chat stop button and keep the composer aligned to the bottom edge.
|
||||
- Typing: start instant typing indicators at run start so DMs and mentions show immediately.
|
||||
- Configure: restrict the model allowlist picker to OAuth-compatible Anthropic models and preselect Opus 4.5.
|
||||
- Configure: seed model fallbacks from the allowlist selection when multiple models are chosen.
|
||||
- Model picker: list the full catalog when no model allowlist is configured.
|
||||
- Chat: include configured defaults/providers in `/models` output and normalize all-mode paging. (#1398) Thanks @vignesh07.
|
||||
- Discord: honor wildcard channel configs via shared match helpers. (#1334) Thanks @pvoo.
|
||||
- BlueBubbles: resolve short message IDs safely and expose full IDs in templates. (#1387) Thanks @tyler6204.
|
||||
- Infra: preserve fetch helper methods when wrapping abort signals. (#1387)
|
||||
|
||||
## 2026.1.20
|
||||
|
||||
### Changes
|
||||
- Deps: update workspace + memory-lancedb dependencies.
|
||||
- Dev: use tsgo for dev/watch builds by default; set `CLAWDBOT_TS_COMPILER=tsc` to opt out.
|
||||
- Repo: remove the Peekaboo git submodule now that the SPM release is used.
|
||||
- Update: sync plugin sources on channel switches and update npm-installed plugins during `clawdbot update`.
|
||||
- Control UI: add copy-as-markdown with error feedback. (#1345) https://docs.clawd.bot/web/control-ui
|
||||
- Control UI: drop the legacy list view. (#1345) https://docs.clawd.bot/web/control-ui
|
||||
- TUI: add syntax highlighting for code blocks. (#1200) https://docs.clawd.bot/tui
|
||||
- TUI: session picker shows derived titles, fuzzy search, relative times, and last message preview. (#1271) https://docs.clawd.bot/tui
|
||||
- TUI: add a searchable model picker for quicker model selection. (#1198) https://docs.clawd.bot/tui
|
||||
- TUI: add input history (up/down) for submitted messages. (#1348) https://docs.clawd.bot/tui
|
||||
- ACP: add `clawdbot acp` for IDE integrations. https://docs.clawd.bot/cli/acp
|
||||
- ACP: add `clawdbot acp client` interactive harness for debugging. https://docs.clawd.bot/cli/acp
|
||||
- Skills: add download installs with OS-filtered options. https://docs.clawd.bot/tools/skills
|
||||
- Skills: add the local sherpa-onnx-tts skill. https://docs.clawd.bot/tools/skills
|
||||
- Memory: add hybrid BM25 + vector search (FTS5) with weighted merging and fallback. https://docs.clawd.bot/concepts/memory
|
||||
- Memory: add SQLite embedding cache to speed up reindexing and frequent updates. https://docs.clawd.bot/concepts/memory
|
||||
- Memory: add OpenAI batch indexing for embeddings when configured. https://docs.clawd.bot/concepts/memory
|
||||
- Memory: enable OpenAI batch indexing by default for OpenAI embeddings. https://docs.clawd.bot/concepts/memory
|
||||
- Memory: allow parallel OpenAI batch indexing jobs (default concurrency: 2). https://docs.clawd.bot/concepts/memory
|
||||
- Memory: render progress immediately, color batch statuses in verbose logs, and poll OpenAI batch status every 2s by default. https://docs.clawd.bot/concepts/memory
|
||||
- Memory: add `--verbose` logging for memory status + batch indexing details. https://docs.clawd.bot/concepts/memory
|
||||
- Memory: add native Gemini embeddings provider for memory search. (#1151) https://docs.clawd.bot/concepts/memory
|
||||
- Browser: allow config defaults for efficient snapshots in the tool/CLI. (#1336) https://docs.clawd.bot/tools/browser
|
||||
- Nostr: add the Nostr channel plugin with profile management + onboarding defaults. (#1323) https://docs.clawd.bot/channels/nostr
|
||||
- Matrix: migrate to matrix-bot-sdk with E2EE support, location handling, and group allowlist upgrades. (#1298) https://docs.clawd.bot/channels/matrix
|
||||
- Slack: add HTTP webhook mode via Bolt HTTP receiver. (#1143) https://docs.clawd.bot/channels/slack
|
||||
- Telegram: enrich forwarded-message context with normalized origin details + legacy fallback. (#1090) https://docs.clawd.bot/channels/telegram
|
||||
- Discord: fall back to `/skill` when native command limits are exceeded. (#1287)
|
||||
- Discord: expose `/skill` globally. (#1287)
|
||||
- Zalouser: add channel dock metadata, config schema, setup wiring, probe, and status issues. (#1219) https://docs.clawd.bot/plugins/zalouser
|
||||
- Plugins: require manifest-embedded config schemas with preflight validation warnings. (#1272) https://docs.clawd.bot/plugins/manifest
|
||||
- Plugins: move channel catalog metadata into plugin manifests. (#1290) https://docs.clawd.bot/plugins/manifest
|
||||
- Plugins: align Nextcloud Talk policy helpers with core patterns. (#1290) https://docs.clawd.bot/plugins/manifest
|
||||
- Plugins/UI: let channel plugin metadata drive UI labels/icons and cron channel options. (#1306) https://docs.clawd.bot/web/control-ui
|
||||
- Plugins: add plugin slots with a dedicated memory slot selector. https://docs.clawd.bot/plugins/agent-tools
|
||||
- Plugins: ship the bundled BlueBubbles channel plugin (disabled by default). https://docs.clawd.bot/channels/bluebubbles
|
||||
- Plugins: migrate bundled messaging extensions to the plugin SDK and resolve plugin-sdk imports in the loader.
|
||||
- Plugins: migrate the Zalo plugin to the shared plugin SDK runtime. https://docs.clawd.bot/channels/zalo
|
||||
- Plugins: migrate the Zalo Personal plugin to the shared plugin SDK runtime. https://docs.clawd.bot/plugins/zalouser
|
||||
- Plugins: allow optional agent tools with explicit allowlists and add the plugin tool authoring guide. https://docs.clawd.bot/plugins/agent-tools
|
||||
- Plugins: auto-enable bundled channel/provider plugins when configuration is present.
|
||||
- Plugins: sync plugin sources on channel switches and update npm-installed plugins during `clawdbot update`.
|
||||
- Plugins: share npm plugin update logic between `clawdbot update` and `clawdbot plugins update`.
|
||||
- Browser: allow config defaults for efficient snapshots in the tool/CLI. (#1336) — thanks @sebslight.
|
||||
- Channels: add the Nostr plugin channel with profile management + onboarding install defaults. (#1323) — thanks @joelklabo.
|
||||
- Plugins: require manifest-embedded config schemas, validate configs without loading plugin code, and surface plugin config warnings. (#1272) — thanks @thewilloftheshadow.
|
||||
- Plugins: move channel catalog metadata into plugin manifests; align Nextcloud Talk policy helpers with core patterns. (#1290) — thanks @NicholaiVogel.
|
||||
- Discord: fall back to /skill when native command limits are exceeded; expose /skill globally. (#1287) — thanks @thewilloftheshadow.
|
||||
- Docs: refresh bird skill install metadata and usage notes. (#1302) — thanks @odysseus0.
|
||||
- Matrix: migrate to matrix-bot-sdk with E2EE support, location handling, and group allowlist upgrades. (#1298) — thanks @sibbl.
|
||||
- Plugins/UI: let channel plugin metadata drive UI labels/icons and cron channel options. (#1306) — thanks @steipete.
|
||||
- Zalouser: add channel dock metadata, config schema, setup wiring, probe, and status issues. (#1219) — thanks @suminhthanh.
|
||||
- Security: warn when <=300B models run without sandboxing and with web tools enabled.
|
||||
- Skills: add download installs with OS-filtered install options; add local sherpa-onnx-tts skill.
|
||||
- Docs: clarify WhatsApp voice notes and Windows WSL portproxy LAN access notes.
|
||||
- UI: add copy-as-markdown with error feedback and drop legacy list view. (#1345) — thanks @bradleypriest.
|
||||
- TUI: add input history (up/down) for submitted messages. (#1348) — thanks @vignesh07.
|
||||
### Fixes
|
||||
- Discovery: shorten Bonjour DNS-SD service type to `_clawdbot-gw._tcp` and update discovery clients/docs.
|
||||
- Agents: preserve subagent announce thread/topic routing + queued replies across channels. (#1241) — thanks @gnarco.
|
||||
- Agents: avoid treating timeout errors with "aborted" messages as user aborts, so model fallback still runs.
|
||||
- Diagnostics: export OTLP logs, correct queue depth tracking, and document message-flow telemetry.
|
||||
- Diagnostics: emit message-flow diagnostics across channels via shared dispatch; gate heartbeat/webhook logging. (#1244) — thanks @oscargavin.
|
||||
- CLI: preserve cron delivery settings when editing message payloads. (#1322) — thanks @KrauseFx.
|
||||
- CLI: keep `clawdbot logs` output resilient to broken pipes while preserving progress output.
|
||||
- Model catalog: avoid caching import failures, log transient discovery errors, and keep partial results. (#1332) — thanks @dougvk.
|
||||
- Doctor: clarify plugin auto-enable hint text in the startup banner.
|
||||
- Gateway: clarify unauthorized handshake responses with token/password mismatch guidance.
|
||||
- Gateway: clarify connect/validation errors for gateway params. (#1347) — thanks @vignesh07.
|
||||
- Gateway: preserve restart wake routing + thread replies across restarts. (#1337) — thanks @John-Rood.
|
||||
- Gateway: reschedule per-agent heartbeats on config hot reload without restarting the runner.
|
||||
- Config: log invalid config issues once per run and keep invalid-config errors stackless.
|
||||
- Exec: default gateway/node exec security to allowlist when unset (sandbox stays deny).
|
||||
- UI: keep config form enums typed, preserve empty strings, protect sensitive defaults, and deepen config search. (#1315) — thanks @MaudeBot.
|
||||
- UI: preserve ordered list numbering in chat markdown. (#1341) — thanks @bradleypriest.
|
||||
- UI: allow Control UI to read gatewayUrl from URL params for remote WebSocket targets. (#1342) — thanks @ameno-.
|
||||
- Web search: infer Perplexity base URL from API key source (direct vs OpenRouter).
|
||||
- Web fetch: harden SSRF protection with shared hostname checks and redirect limits. (#1346) — thanks @fogboots.
|
||||
- TUI: keep thinking blocks ordered before content during streaming and isolate per-run assembly. (#1202) — thanks @aaronveklabs.
|
||||
- TUI: align custom editor initialization with the latest pi-tui API. (#1298) — thanks @sibbl.
|
||||
- CLI: avoid duplicating --profile/--dev flags when formatting commands.
|
||||
- Status: route native `/status` to the active agent so model selection reflects the correct profile. (#1301)
|
||||
- Exec: prefer bash when fish is default shell, falling back to sh if bash is missing. (#1297) — thanks @ysqander.
|
||||
- Exec: merge login-shell PATH for host=gateway exec while keeping daemon PATH minimal. (#1304)
|
||||
- Plugins: add Nextcloud Talk manifest for plugin config validation. (#1297) — thanks @ysqander.
|
||||
- Anthropic: default API prompt caching to 1h with configurable TTL override; ignore TTL for OAuth.
|
||||
- Discord: make resolve warnings avoid raw JSON payloads on rate limits.
|
||||
- Discord: process message handlers in parallel across sessions to avoid event queue blocking. (#1295)
|
||||
- Cron: auto-deliver isolated agent output to explicit targets without tool calls. (#1285)
|
||||
|
||||
## 2026.1.19-3
|
||||
|
||||
### Changes
|
||||
- Android: remove legacy bridge transport code now that nodes use the gateway protocol.
|
||||
- Android: send structured payloads in node events/invokes and include user-agent metadata in gateway connects.
|
||||
- Gateway: expand `/v1/responses` to support file/image inputs, tool_choice, usage, and output limits. (#1229) — thanks @RyanLisse.
|
||||
- Docs: surface Amazon Bedrock in provider lists and clarify Bedrock auth env vars. (#1289) — thanks @steipete.
|
||||
|
||||
### Fixes
|
||||
- Gateway: strip inbound envelope headers from chat history messages to keep clients clean.
|
||||
- UI: prevent double-scroll in Control UI chat by locking chat layout to the viewport. (#1283) — thanks @bradleypriest.
|
||||
- Config: allow Perplexity as a web_search provider in config validation. (#1230)
|
||||
- Browser: register AI snapshot refs for act commands. (#1282) — thanks @John-Rood.
|
||||
|
||||
## 2026.1.19-2
|
||||
|
||||
### Changes
|
||||
- Android: migrate node transport to the Gateway WebSocket protocol with TLS pinning support + gateway discovery naming.
|
||||
- Android: bump okhttp + dnsjava to satisfy lint dependency checks.
|
||||
- Docs: refresh Android node discovery docs for the Gateway WS service type.
|
||||
|
||||
### Fixes
|
||||
- Tests: stabilize Windows gateway/CLI tests by skipping sidecars, normalizing argv, and extending timeouts.
|
||||
- CLI: skip runner rebuilds when dist is fresh. (#1231) — thanks @mukhtharcm, @thewilloftheshadow.
|
||||
|
||||
## 2026.1.19-1
|
||||
|
||||
### Breaking
|
||||
- **BREAKING:** Reject invalid/unknown config entries and refuse to start the gateway for safety; run `clawdbot doctor --fix` to repair.
|
||||
|
||||
### Changes
|
||||
- Gateway: add `/v1/responses` endpoint (OpenResponses API) for agentic workflows with item-based input and semantic streaming events. Enable via `gateway.http.endpoints.responses.enabled: true`.
|
||||
- Usage: add `/usage cost` summaries and macOS menu cost submenu with daily charting.
|
||||
- Gateway/API: add `/v1/responses` (OpenResponses) with item-based input + semantic streaming events. (#1229)
|
||||
- Gateway/API: expand `/v1/responses` to support file/image inputs, tool_choice, usage, and output limits. (#1229)
|
||||
- Usage: add `/usage cost` summaries and macOS menu cost charts. https://docs.clawd.bot/reference/api-usage-costs
|
||||
- Security: warn when <=300B models run without sandboxing while web tools are enabled. https://docs.clawd.bot/cli/security
|
||||
- Exec: add host/security/ask routing for gateway + node exec. https://docs.clawd.bot/tools/exec
|
||||
- Exec: add `/exec` directive for per-session exec defaults (host/security/ask/node). https://docs.clawd.bot/tools/exec
|
||||
- Exec approvals: migrate approvals to `~/.clawdbot/exec-approvals.json` with per-agent allowlists + skill auto-allow toggle, and add approvals UI + node exec lifecycle events. https://docs.clawd.bot/tools/exec-approvals
|
||||
- Nodes: add headless node host (`clawdbot node start`) for `system.run`/`system.which`. https://docs.clawd.bot/cli/node
|
||||
- Nodes: add node daemon service install/status/start/stop/restart. https://docs.clawd.bot/cli/node
|
||||
- Bridge: add `skills.bins` RPC to support node host auto-allow skill bins.
|
||||
- Sessions: add daily reset policy with per-type overrides and idle windows (default 4am local), preserving legacy idle-only configs. (#1146) https://docs.clawd.bot/concepts/session
|
||||
- Sessions: allow `sessions_spawn` to override thinking level for sub-agent runs. https://docs.clawd.bot/tools/subagents
|
||||
- Channels: unify thread/topic allowlist matching + command/mention gating helpers across core providers. https://docs.clawd.bot/concepts/groups
|
||||
- Models: add Qwen Portal OAuth provider support. (#1120) https://docs.clawd.bot/providers/qwen
|
||||
- Onboarding: add allowlist prompts and username-to-id resolution across core and extension channels. https://docs.clawd.bot/start/onboarding
|
||||
- Docs: clarify allowlist input types and onboarding behavior for messaging channels. https://docs.clawd.bot/start/onboarding
|
||||
- Docs: refresh Android node discovery docs for the Gateway WS service type. https://docs.clawd.bot/platforms/android
|
||||
- Docs: surface Amazon Bedrock in provider lists and clarify Bedrock auth env vars. (#1289) https://docs.clawd.bot/bedrock
|
||||
- Docs: clarify WhatsApp voice notes. https://docs.clawd.bot/channels/whatsapp
|
||||
- Docs: clarify Windows WSL portproxy LAN access notes. https://docs.clawd.bot/platforms/windows
|
||||
- Docs: refresh bird skill install metadata and usage notes. (#1302) https://docs.clawd.bot/tools/browser-login
|
||||
- Agents: add local docs path resolution and include docs/mirror/source/community pointers in the system prompt.
|
||||
- Agents: clarify node_modules read-only guidance in agent instructions.
|
||||
- TUI: add syntax highlighting for code blocks. (#1200) — thanks @vignesh07.
|
||||
- TUI: session picker shows derived titles, fuzzy search, relative times, and last message preview. (#1271) — thanks @Whoaa512.
|
||||
|
||||
### Fixes
|
||||
- UI: enable shell mode for sync Windows spawns to avoid `pnpm ui:build` EINVAL. (#1212) — thanks @longmaba.
|
||||
- Agents: add `clawdbot agents set-identity` helper and update bootstrap guidance for multi-agent setups. (#1222) — thanks @ThePickle31.
|
||||
- Plugins: surface plugin load/register/config errors in gateway logs with plugin/source context.
|
||||
- Agents: propagate accountId into embedded runs so sub-agent announce routing honors the originating account. (#1058)
|
||||
- Compaction: include tool failure summaries in safeguard compaction to prevent retry loops. (#1084)
|
||||
- Daemon: include HOME in service environments to avoid missing HOME errors. (#1214) — thanks @ameno-.
|
||||
- Memory: show total file counts + scan issues in `clawdbot memory status`; fall back to non-batch embeddings after repeated batch failures.
|
||||
- TUI: show generic empty-state text for searchable pickers. (#1201) — thanks @vignesh07.
|
||||
- Doctor: canonicalize legacy session keys in session stores to prevent stale metadata. (#1169)
|
||||
- CLI: centralize CLI command registration to keep fast-path routing and program wiring in sync. (#1207) — thanks @gumadeiras.
|
||||
- Config: allow custom fields under `skills.entries.<name>.config` for skill credentials/config. (#1226) — thanks @VACInc. (fixes #1225)
|
||||
|
||||
## 2026.1.18-5
|
||||
|
||||
### Changes
|
||||
- Dependencies: update core + plugin deps (grammy, vitest, openai, Microsoft agents hosting, etc.).
|
||||
- Onboarding: add allowlist prompts and username-to-id resolution across core and extension channels.
|
||||
- TUI: add searchable model picker for quicker model selection. (#1198) — thanks @vignesh07.
|
||||
- Docs: clarify allowlist input types and onboarding behavior for messaging channels.
|
||||
|
||||
### Fixes
|
||||
- Configure: hide OpenRouter auto routing model from the model picker. (#1182) — thanks @zerone0x.
|
||||
- Docs: make docs:list fail fast with a clear error if the docs directory is missing.
|
||||
- macOS: load menu session previews asynchronously so items populate while the menu is open.
|
||||
- macOS: use label colors for session preview text so previews render in menu subviews.
|
||||
- macOS: suppress usage error text in the menubar cost view.
|
||||
- Telegram: honor pairing allowlists for native slash commands.
|
||||
- TUI: highlight model search matches and stabilize search ordering.
|
||||
- CLI: keep banners on routed commands, restore config guarding outside fast-path routing, and tighten fast-path flag parsing while skipping console capture for extra speed. (#1195) — thanks @gumadeiras.
|
||||
- Slack: resolve Bolt import interop for Bun + Node. (#1191) — thanks @CoreyH.
|
||||
- Gateway: require authorized restarts for SIGUSR1 (restart/apply/update) so config gating can't be bypassed.
|
||||
- Discord: stop reconnecting the gateway after aborts to prevent duplicate listeners.
|
||||
|
||||
## 2026.1.18-4
|
||||
|
||||
### Changes
|
||||
- Config: stamp last-touched metadata on write and warn if the config is newer than the running build.
|
||||
- macOS: hide usage section when usage is unavailable instead of showing provider errors.
|
||||
- Android: migrate node transport to the Gateway WebSocket protocol with TLS pinning support + gateway discovery naming.
|
||||
- Android: send structured payloads in node events/invokes and include user-agent metadata in gateway connects.
|
||||
- Android: remove legacy bridge transport code now that nodes use the gateway protocol.
|
||||
- Android: bump okhttp + dnsjava to satisfy lint dependency checks.
|
||||
- Build: update workspace + core/plugin deps.
|
||||
- Build: use tsgo for dev/watch builds by default (opt out with `CLAWDBOT_TS_COMPILER=tsc`).
|
||||
- Repo: remove the Peekaboo git submodule now that the SPM release is used.
|
||||
- macOS: switch PeekabooBridge integration to the tagged Swift Package Manager release.
|
||||
- macOS: stop syncing Peekaboo in postinstall.
|
||||
- Swabble: use the tagged Commander Swift package release.
|
||||
- CLI: add `clawdbot acp client` interactive ACP harness for debugging.
|
||||
- Plugins: route command detection/text chunking helpers through the plugin runtime and drop runtime exports from the SDK.
|
||||
- Plugins: auto-enable bundled channel/provider plugins when configuration is present.
|
||||
- Config: stamp last-touched metadata on write and warn if the config is newer than the running build.
|
||||
- macOS: hide usage section when usage is unavailable instead of showing provider errors.
|
||||
- Memory: add native Gemini embeddings provider for memory search. (#1151)
|
||||
- Agents: add local docs path resolution and include docs/mirror/source/community pointers in the system prompt.
|
||||
- Slack: add HTTP webhook mode via Bolt HTTP receiver for Events API deployments. (#1143) — thanks @jdrhyne.
|
||||
|
||||
### Breaking
|
||||
- **BREAKING:** Reject invalid/unknown config entries and refuse to start the gateway for safety. Run `clawdbot doctor --fix` to repair, then update plugins (`clawdbot plugins update`) if you use any.
|
||||
|
||||
### Fixes
|
||||
- Auth profiles: keep auto-pinned preference while allowing rotation on failover; user pins stay locked. (#1138) — thanks @cheeeee.
|
||||
- Discovery: shorten Bonjour DNS-SD service type to `_clawdbot-gw._tcp` and update discovery clients/docs.
|
||||
- Diagnostics: export OTLP logs, correct queue depth tracking, and document message-flow telemetry.
|
||||
- Diagnostics: emit message-flow diagnostics across channels via shared dispatch. (#1244)
|
||||
- Diagnostics: gate heartbeat/webhook logging. (#1244)
|
||||
- Gateway: strip inbound envelope headers from chat history messages to keep clients clean.
|
||||
- Gateway: clarify unauthorized handshake responses with token/password mismatch guidance.
|
||||
- Gateway: allow mobile node client ids for iOS + Android handshake validation. (#1354)
|
||||
- Gateway: clarify connect/validation errors for gateway params. (#1347)
|
||||
- Gateway: preserve restart wake routing + thread replies across restarts. (#1337)
|
||||
- Gateway: reschedule per-agent heartbeats on config hot reload without restarting the runner.
|
||||
- Gateway: require authorized restarts for SIGUSR1 (restart/apply/update) so config gating can't be bypassed.
|
||||
- Cron: auto-deliver isolated agent output to explicit targets without tool calls. (#1285)
|
||||
- Agents: preserve subagent announce thread/topic routing + queued replies across channels. (#1241)
|
||||
- Agents: propagate accountId into embedded runs so sub-agent announce routing honors the originating account. (#1058)
|
||||
- Agents: avoid treating timeout errors with "aborted" messages as user aborts, so model fallback still runs. (#1137)
|
||||
- Agents: sanitize oversized image payloads before send and surface image-dimension errors.
|
||||
- macOS: Doctor repairs LaunchAgent bootstrap issues for Gateway + Node when listed but not loaded. (#1166) — thanks @AlexMikhalev.
|
||||
- macOS: avoid touching launchd in Remote over SSH so quitting the app no longer disables the remote gateway. (#1105)
|
||||
- Sessions: fall back to session labels when listing display names. (#1124)
|
||||
- Compaction: include tool failure summaries in safeguard compaction to prevent retry loops. (#1084)
|
||||
- Config: log invalid config issues once per run and keep invalid-config errors stackless.
|
||||
- Config: allow Perplexity as a web_search provider in config validation. (#1230)
|
||||
- Config: allow custom fields under `skills.entries.<name>.config` for skill credentials/config. (#1226)
|
||||
- Doctor: clarify plugin auto-enable hint text in the startup banner.
|
||||
- Doctor: canonicalize legacy session keys in session stores to prevent stale metadata. (#1169)
|
||||
- Docs: make docs:list fail fast with a clear error if the docs directory is missing.
|
||||
- Plugins: add Nextcloud Talk manifest for plugin config validation. (#1297)
|
||||
- Plugins: surface plugin load/register/config errors in gateway logs with plugin/source context.
|
||||
- CLI: preserve cron delivery settings when editing message payloads. (#1322)
|
||||
- CLI: keep `clawdbot logs` output resilient to broken pipes while preserving progress output.
|
||||
- CLI: avoid duplicating --profile/--dev flags when formatting commands.
|
||||
- CLI: centralize CLI command registration to keep fast-path routing and program wiring in sync. (#1207)
|
||||
- CLI: keep banners on routed commands, restore config guarding outside fast-path routing, and tighten fast-path flag parsing while skipping console capture for extra speed. (#1195)
|
||||
- CLI: skip runner rebuilds when dist is fresh. (#1231)
|
||||
- CLI: add WSL2/systemd unavailable hints in daemon status/doctor output.
|
||||
- Status: route native `/status` to the active agent so model selection reflects the correct profile. (#1301)
|
||||
- Status: show both usage windows with reset hints when usage data is available. (#1101)
|
||||
- UI: keep config form enums typed, preserve empty strings, protect sensitive defaults, and deepen config search. (#1315)
|
||||
- UI: preserve ordered list numbering in chat markdown. (#1341)
|
||||
- UI: allow Control UI to read gatewayUrl from URL params for remote WebSocket targets. (#1342)
|
||||
- UI: prevent double-scroll in Control UI chat by locking chat layout to the viewport. (#1283)
|
||||
- UI: enable shell mode for sync Windows spawns to avoid `pnpm ui:build` EINVAL. (#1212)
|
||||
- TUI: keep thinking blocks ordered before content during streaming and isolate per-run assembly. (#1202)
|
||||
- TUI: align custom editor initialization with the latest pi-tui API. (#1298)
|
||||
- TUI: show generic empty-state text for searchable pickers. (#1201)
|
||||
- TUI: highlight model search matches and stabilize search ordering.
|
||||
- Configure: hide OpenRouter auto routing model from the model picker. (#1182)
|
||||
- Memory: show total file counts + scan issues in `clawdbot memory status`.
|
||||
- Memory: fall back to non-batch embeddings after repeated batch failures.
|
||||
- Memory: apply OpenAI batch defaults even without explicit remote config.
|
||||
- Memory: index atomically so failed reindex preserves the previous memory database. (#1151)
|
||||
- Memory: avoid sqlite-vec unique constraint failures when reindexing duplicate chunk ids. (#1151)
|
||||
|
||||
## 2026.1.18-3
|
||||
|
||||
### Changes
|
||||
- Exec: add host/security/ask routing for gateway + node exec.
|
||||
- Exec: add `/exec` directive for per-session exec defaults (host/security/ask/node).
|
||||
- macOS: migrate exec approvals to `~/.clawdbot/exec-approvals.json` with per-agent allowlists and skill auto-allow toggle.
|
||||
- macOS: add approvals socket UI server + node exec lifecycle events.
|
||||
- Nodes: add headless node host (`clawdbot node start`) for `system.run`/`system.which`.
|
||||
- Nodes: add node daemon service install/status/start/stop/restart.
|
||||
- Bridge: add `skills.bins` RPC to support node host auto-allow skill bins.
|
||||
- Slash commands: replace `/cost` with `/usage off|tokens|full` to control per-response usage footer; `/usage` no longer aliases `/status`. (Supersedes #1140) — thanks @Nachx639.
|
||||
- Sessions: add daily reset policy with per-type overrides and idle windows (default 4am local), preserving legacy idle-only configs. (#1146) — thanks @austinm911.
|
||||
- Agents: auto-inject local image references for vision models and avoid reloading history images. (#1098) — thanks @tyler6204.
|
||||
- Docs: refresh exec/elevated/exec-approvals docs for the new flow. https://docs.clawd.bot/tools/exec-approvals
|
||||
- Docs: add node host CLI + update exec approvals/bridge protocol docs. https://docs.clawd.bot/cli/node
|
||||
- ACP: add experimental ACP support for IDE integrations (`clawdbot acp`). Thanks @visionik.
|
||||
- Tools: allow `sessions_spawn` to override thinking level for sub-agent runs.
|
||||
- Channels: unify thread/topic allowlist matching + command/mention gating helpers across core providers.
|
||||
- Models: add Qwen Portal OAuth provider support. (#1120) — thanks @mukhtharcm.
|
||||
- Memory: add `--verbose` logging for memory status + batch indexing details.
|
||||
- Memory: allow parallel OpenAI batch indexing jobs (default concurrency: 2).
|
||||
- macOS: add per-agent exec approvals with allowlists, skill CLI auto-allow, and settings UI.
|
||||
- Docs: add exec approvals guide and link from tools index. https://docs.clawd.bot/tools/exec-approvals
|
||||
- macOS: add exec-host IPC for node service `system.run` with HMAC + peer UID checks.
|
||||
|
||||
### Fixes
|
||||
- Exec approvals: enforce allowlist when ask is off; prefer raw command for node approvals/events.
|
||||
- Tools: return a companion-app-required message when node exec is requested with no paired node.
|
||||
- Streaming: emit assistant deltas for OpenAI-compatible SSE chunks. (#1147) — thanks @alauppe.
|
||||
- Model fallback: treat timeout aborts as failover while preserving user aborts. (#1137) — thanks @cheeeee.
|
||||
|
||||
## 2026.1.18-2
|
||||
|
||||
### Fixes
|
||||
- Tests: stabilize plugin SDK resolution and embedded agent timeouts.
|
||||
|
||||
## 2026.1.18-1
|
||||
|
||||
### Changes
|
||||
- Tools: allow `sessions_spawn` to override thinking level for sub-agent runs.
|
||||
- Channels: unify thread/topic allowlist matching + command/mention gating helpers across core providers.
|
||||
- Models: add Qwen Portal OAuth provider support. (#1120) — thanks @mukhtharcm.
|
||||
- Memory: add `--verbose` logging for memory status + batch indexing details.
|
||||
- Memory: allow parallel OpenAI batch indexing jobs (default concurrency: 2).
|
||||
- macOS: add per-agent exec approvals with allowlists, skill CLI auto-allow, and settings UI.
|
||||
- Docs: add exec approvals guide and link from tools index. https://docs.clawd.bot/tools/exec-approvals
|
||||
|
||||
### Fixes
|
||||
- Memory: apply OpenAI batch defaults even without explicit remote config.
|
||||
- macOS: bundle Textual resources in packaged app builds to avoid code block crashes. (#1006)
|
||||
- Tools: return a companion-app-required message when `system.run` is requested without a supporting node.
|
||||
- Discord: only emit slow listener warnings after 30s.
|
||||
|
||||
## 2026.1.17-6
|
||||
|
||||
### Changes
|
||||
- Plugins: add exclusive plugin slots with a dedicated memory slot selector.
|
||||
- Memory: ship core memory tools + CLI as the bundled `memory-core` plugin.
|
||||
- Docs: document plugin slots and memory plugin behavior.
|
||||
- Plugins: add the bundled BlueBubbles channel plugin (disabled by default).
|
||||
- Plugins: migrate bundled messaging extensions to the plugin SDK; resolve plugin-sdk imports in loader.
|
||||
- Plugins: migrate the Zalo plugin to the shared plugin SDK runtime.
|
||||
- Plugins: migrate the Zalo Personal plugin to the shared plugin SDK runtime.
|
||||
|
||||
## 2026.1.17-5
|
||||
|
||||
### Changes
|
||||
- Memory: add hybrid BM25 + vector search (FTS5) with weighted merging and fallback.
|
||||
- Memory: add SQLite embedding cache to speed up reindexing and frequent updates.
|
||||
- CLI: surface FTS + embedding cache state in `clawdbot memory status`.
|
||||
- Memory: render progress immediately, color batch statuses in verbose logs, and poll OpenAI batch status every 2s by default.
|
||||
- Plugins: allow optional agent tools with explicit allowlists and add plugin tool authoring guide. https://docs.clawd.bot/plugins/agent-tools
|
||||
- Tools: centralize plugin tool policy helpers.
|
||||
- Commands: add `/subagents info` and show sub-agent counts in `/status`.
|
||||
- Docs: clarify plugin agent tool configuration. https://docs.clawd.bot/plugins/agent-tools
|
||||
|
||||
### Fixes
|
||||
- Voice call: include request query in Twilio webhook verification when publicUrl is set. (#864)
|
||||
|
||||
## 2026.1.18-1
|
||||
|
||||
### Changes
|
||||
- Tools: allow `sessions_spawn` to override thinking level for sub-agent runs.
|
||||
- Channels: unify thread/topic allowlist matching + command/mention gating helpers across core providers.
|
||||
- Models: add Qwen Portal OAuth provider support. (#1120) — thanks @mukhtharcm.
|
||||
- Memory: add `--verbose` logging for memory status + batch indexing details.
|
||||
- Memory: allow parallel OpenAI batch indexing jobs (default concurrency: 2).
|
||||
- macOS: add per-agent exec approvals with allowlists, skill CLI auto-allow, and settings UI.
|
||||
- Docs: add exec approvals guide and link from tools index. https://docs.clawd.bot/tools/exec-approvals
|
||||
|
||||
### Fixes
|
||||
- Memory: apply OpenAI batch defaults even without explicit remote config.
|
||||
- macOS: bundle Textual resources in packaged app builds to avoid code block crashes. (#1006)
|
||||
- Tools: return a companion-app-required message when `system.run` is requested without a supporting node.
|
||||
- Discord: only emit slow listener warnings after 30s.
|
||||
## 2026.1.17-3
|
||||
|
||||
### Changes
|
||||
- Memory: add OpenAI Batch API indexing for embeddings when configured.
|
||||
- Memory: enable OpenAI batch indexing by default for OpenAI embeddings.
|
||||
|
||||
### Fixes
|
||||
- Memory: retry transient 5xx errors (Cloudflare) during embedding indexing.
|
||||
|
||||
## 2026.1.17-2
|
||||
|
||||
### Changes
|
||||
|
||||
### Fixes
|
||||
- Tools: show exec elevated flag before the command and keep it outside markdown in tool summaries.
|
||||
- Memory: parallelize embedding indexing with rate-limit retries.
|
||||
- Memory: split overly long lines to keep embeddings under token limits.
|
||||
- Memory: skip empty chunks to avoid invalid embedding inputs.
|
||||
- Sessions: fall back to session labels when listing display names. (#1124) — thanks @abdaraxus.
|
||||
- Discord: inherit parent channel allowlists for thread slash commands and reactions. (#1123) — thanks @thewilloftheshadow.
|
||||
|
||||
## 2026.1.17-1
|
||||
|
||||
### Changes
|
||||
- Telegram: enrich forwarded message context with normalized origin details + legacy fallback. (#1090) — thanks @sleontenko.
|
||||
- macOS: strip prerelease/build suffixes when parsing gateway semver patches. (#1110) — thanks @zerone0x.
|
||||
- macOS: keep CLI install pinned to the full build suffix. (#1111) — thanks @artuskg.
|
||||
- CLI: surface update availability in `clawdbot status`.
|
||||
- CLI: add `clawdbot memory status --deep/--index` probes.
|
||||
- CLI: add playful update completion quips.
|
||||
|
||||
### Fixes
|
||||
- Doctor: avoid re-adding WhatsApp ack reaction config when only legacy auth files exist. (#1087) — thanks @YuriNachos.
|
||||
- Hooks: parse multi-line/YAML frontmatter metadata blocks (JSON5-friendly). (#1114) — thanks @sebslight.
|
||||
- CLI: add WSL2/systemd unavailable hints in daemon status/doctor output.
|
||||
- Windows: install gateway scheduled task as the current user; show friendly guidance instead of failing on access denied.
|
||||
- Status: show both usage windows with reset hints when usage data is available. (#1101) — thanks @rhjoh.
|
||||
- Memory: probe sqlite-vec availability in `clawdbot memory status`.
|
||||
- Memory: split embedding batches to avoid OpenAI token limits during indexing.
|
||||
- Telegram: preserve hidden text_link URLs by expanding entities in inbound text. (#1118) — thanks @sleontenko.
|
||||
- Memory: probe sqlite-vec availability in `clawdbot memory status`.
|
||||
- Exec approvals: enforce allowlist when ask is off.
|
||||
- Exec approvals: prefer raw command for node approvals/events.
|
||||
- Tools: show exec elevated flag before the command and keep it outside markdown in tool summaries.
|
||||
- Tools: return a companion-app-required message when node exec is requested with no paired node.
|
||||
- Tools: return a companion-app-required message when `system.run` is requested without a supporting node.
|
||||
- Exec: default gateway/node exec security to allowlist when unset (sandbox stays deny).
|
||||
- Exec: prefer bash when fish is default shell, falling back to sh if bash is missing. (#1297)
|
||||
- Exec: merge login-shell PATH for host=gateway exec while keeping daemon PATH minimal. (#1304)
|
||||
- Streaming: emit assistant deltas for OpenAI-compatible SSE chunks. (#1147)
|
||||
- Discord: make resolve warnings avoid raw JSON payloads on rate limits.
|
||||
- Discord: process message handlers in parallel across sessions to avoid event queue blocking. (#1295)
|
||||
- Discord: stop reconnecting the gateway after aborts to prevent duplicate listeners.
|
||||
- Discord: only emit slow listener warnings after 30s.
|
||||
- Discord: inherit parent channel allowlists for thread slash commands and reactions. (#1123)
|
||||
- Telegram: honor pairing allowlists for native slash commands.
|
||||
- Telegram: preserve hidden text_link URLs by expanding entities in inbound text. (#1118)
|
||||
- Slack: resolve Bolt import interop for Bun + Node. (#1191)
|
||||
- Web search: infer Perplexity base URL from API key source (direct vs OpenRouter).
|
||||
- Web fetch: harden SSRF protection with shared hostname checks and redirect limits. (#1346)
|
||||
- Browser: register AI snapshot refs for act commands. (#1282)
|
||||
- Voice call: include request query in Twilio webhook verification when publicUrl is set. (#864)
|
||||
- Anthropic: default API prompt caching to 1h with configurable TTL override.
|
||||
- Anthropic: ignore TTL for OAuth.
|
||||
- Auth profiles: keep auto-pinned preference while allowing rotation on failover. (#1138)
|
||||
- Auth profiles: user pins stay locked. (#1138)
|
||||
- Model catalog: avoid caching import failures, log transient discovery errors, and keep partial results. (#1332)
|
||||
- Tests: stabilize Windows gateway/CLI tests by skipping sidecars, normalizing argv, and extending timeouts.
|
||||
- Tests: stabilize plugin SDK resolution and embedded agent timeouts.
|
||||
- Windows: install gateway scheduled task as the current user.
|
||||
- Windows: show friendly guidance instead of failing on access denied.
|
||||
- macOS: load menu session previews asynchronously so items populate while the menu is open.
|
||||
- macOS: use label colors for session preview text so previews render in menu subviews.
|
||||
- macOS: suppress usage error text in the menubar cost view.
|
||||
- macOS: Doctor repairs LaunchAgent bootstrap issues for Gateway + Node when listed but not loaded. (#1166)
|
||||
- macOS: avoid touching launchd in Remote over SSH so quitting the app no longer disables the remote gateway. (#1105)
|
||||
- macOS: bundle Textual resources in packaged app builds to avoid code block crashes. (#1006)
|
||||
- Daemon: include HOME in service environments to avoid missing HOME errors. (#1214)
|
||||
|
||||
Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @NicholaiVogel, @RyanLisse, @ThePickle31, @VACInc, @Whoaa512, @YuriNachos, @aaronveklabs, @abdaraxus, @alauppe, @ameno-, @artuskg, @austinm911, @bradleypriest, @cheeeee, @dougvk, @fogboots, @gnarco, @gumadeiras, @jdrhyne, @joelklabo, @longmaba, @mukhtharcm, @odysseus0, @oscargavin, @rhjoh, @sebslight, @sibbl, @sleontenko, @steipete, @suminhthanh, @thewilloftheshadow, @tyler6204, @vignesh07, @visionik, @ysqander, @zerone0x.
|
||||
|
||||
## 2026.1.16-2
|
||||
|
||||
|
||||
@@ -471,11 +471,6 @@ by Peter Steinberger and the community.
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines, maintainers, and how to submit PRs.
|
||||
AI/vibe-coded PRs welcome! 🤖
|
||||
|
||||
Special thanks to @andrewting19 for the Anthropic OAuth tool-name fix.
|
||||
|
||||
Core contributors:
|
||||
- @cpojer — Telegram onboarding UX + docs
|
||||
|
||||
Thanks to all clawtributors:
|
||||
|
||||
<p align="left">
|
||||
|
||||
362
appcast.xml
362
appcast.xml
@@ -2,6 +2,196 @@
|
||||
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
||||
<channel>
|
||||
<title>Clawdbot</title>
|
||||
<item>
|
||||
<title>2026.1.20</title>
|
||||
<pubDate>Wed, 21 Jan 2026 08:18:22 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
|
||||
<sparkle:version>7116</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.1.20</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>Clawdbot 2026.1.20</h2>
|
||||
<h3>Changes</h3>
|
||||
<ul>
|
||||
<li>Control UI: add copy-as-markdown with error feedback. (#1345) https://docs.clawd.bot/web/control-ui</li>
|
||||
<li>Control UI: drop the legacy list view. (#1345) https://docs.clawd.bot/web/control-ui</li>
|
||||
<li>TUI: add syntax highlighting for code blocks. (#1200) https://docs.clawd.bot/tui</li>
|
||||
<li>TUI: session picker shows derived titles, fuzzy search, relative times, and last message preview. (#1271) https://docs.clawd.bot/tui</li>
|
||||
<li>TUI: add a searchable model picker for quicker model selection. (#1198) https://docs.clawd.bot/tui</li>
|
||||
<li>TUI: add input history (up/down) for submitted messages. (#1348) https://docs.clawd.bot/tui</li>
|
||||
<li>ACP: add <code>clawdbot acp</code> for IDE integrations. https://docs.clawd.bot/cli/acp</li>
|
||||
<li>ACP: add <code>clawdbot acp client</code> interactive harness for debugging. https://docs.clawd.bot/cli/acp</li>
|
||||
<li>Skills: add download installs with OS-filtered options. https://docs.clawd.bot/tools/skills</li>
|
||||
<li>Skills: add the local sherpa-onnx-tts skill. https://docs.clawd.bot/tools/skills</li>
|
||||
<li>Memory: add hybrid BM25 + vector search (FTS5) with weighted merging and fallback. https://docs.clawd.bot/concepts/memory</li>
|
||||
<li>Memory: add SQLite embedding cache to speed up reindexing and frequent updates. https://docs.clawd.bot/concepts/memory</li>
|
||||
<li>Memory: add OpenAI batch indexing for embeddings when configured. https://docs.clawd.bot/concepts/memory</li>
|
||||
<li>Memory: enable OpenAI batch indexing by default for OpenAI embeddings. https://docs.clawd.bot/concepts/memory</li>
|
||||
<li>Memory: allow parallel OpenAI batch indexing jobs (default concurrency: 2). https://docs.clawd.bot/concepts/memory</li>
|
||||
<li>Memory: render progress immediately, color batch statuses in verbose logs, and poll OpenAI batch status every 2s by default. https://docs.clawd.bot/concepts/memory</li>
|
||||
<li>Memory: add <code>--verbose</code> logging for memory status + batch indexing details. https://docs.clawd.bot/concepts/memory</li>
|
||||
<li>Memory: add native Gemini embeddings provider for memory search. (#1151) https://docs.clawd.bot/concepts/memory</li>
|
||||
<li>Browser: allow config defaults for efficient snapshots in the tool/CLI. (#1336) https://docs.clawd.bot/tools/browser</li>
|
||||
<li>Nostr: add the Nostr channel plugin with profile management + onboarding defaults. (#1323) https://docs.clawd.bot/channels/nostr</li>
|
||||
<li>Matrix: migrate to matrix-bot-sdk with E2EE support, location handling, and group allowlist upgrades. (#1298) https://docs.clawd.bot/channels/matrix</li>
|
||||
<li>Slack: add HTTP webhook mode via Bolt HTTP receiver. (#1143) https://docs.clawd.bot/channels/slack</li>
|
||||
<li>Telegram: enrich forwarded-message context with normalized origin details + legacy fallback. (#1090) https://docs.clawd.bot/channels/telegram</li>
|
||||
<li>Discord: fall back to <code>/skill</code> when native command limits are exceeded. (#1287)</li>
|
||||
<li>Discord: expose <code>/skill</code> globally. (#1287)</li>
|
||||
<li>Zalouser: add channel dock metadata, config schema, setup wiring, probe, and status issues. (#1219) https://docs.clawd.bot/plugins/zalouser</li>
|
||||
<li>Plugins: require manifest-embedded config schemas with preflight validation warnings. (#1272) https://docs.clawd.bot/plugins/manifest</li>
|
||||
<li>Plugins: move channel catalog metadata into plugin manifests. (#1290) https://docs.clawd.bot/plugins/manifest</li>
|
||||
<li>Plugins: align Nextcloud Talk policy helpers with core patterns. (#1290) https://docs.clawd.bot/plugins/manifest</li>
|
||||
<li>Plugins/UI: let channel plugin metadata drive UI labels/icons and cron channel options. (#1306) https://docs.clawd.bot/web/control-ui</li>
|
||||
<li>Plugins: add plugin slots with a dedicated memory slot selector. https://docs.clawd.bot/plugins/agent-tools</li>
|
||||
<li>Plugins: ship the bundled BlueBubbles channel plugin (disabled by default). https://docs.clawd.bot/channels/bluebubbles</li>
|
||||
<li>Plugins: migrate bundled messaging extensions to the plugin SDK and resolve plugin-sdk imports in the loader.</li>
|
||||
<li>Plugins: migrate the Zalo plugin to the shared plugin SDK runtime. https://docs.clawd.bot/channels/zalo</li>
|
||||
<li>Plugins: migrate the Zalo Personal plugin to the shared plugin SDK runtime. https://docs.clawd.bot/plugins/zalouser</li>
|
||||
<li>Plugins: allow optional agent tools with explicit allowlists and add the plugin tool authoring guide. https://docs.clawd.bot/plugins/agent-tools</li>
|
||||
<li>Plugins: auto-enable bundled channel/provider plugins when configuration is present.</li>
|
||||
<li>Plugins: sync plugin sources on channel switches and update npm-installed plugins during <code>clawdbot update</code>.</li>
|
||||
<li>Plugins: share npm plugin update logic between <code>clawdbot update</code> and <code>clawdbot plugins update</code>.</li>
|
||||
<li>Gateway/API: add <code>/v1/responses</code> (OpenResponses) with item-based input + semantic streaming events. (#1229)</li>
|
||||
<li>Gateway/API: expand <code>/v1/responses</code> to support file/image inputs, tool_choice, usage, and output limits. (#1229)</li>
|
||||
<li>Usage: add <code>/usage cost</code> summaries and macOS menu cost charts. https://docs.clawd.bot/reference/api-usage-costs</li>
|
||||
<li>Security: warn when <=300B models run without sandboxing while web tools are enabled. https://docs.clawd.bot/cli/security</li>
|
||||
<li>Exec: add host/security/ask routing for gateway + node exec. https://docs.clawd.bot/tools/exec</li>
|
||||
<li>Exec: add <code>/exec</code> directive for per-session exec defaults (host/security/ask/node). https://docs.clawd.bot/tools/exec</li>
|
||||
<li>Exec approvals: migrate approvals to <code>~/.clawdbot/exec-approvals.json</code> with per-agent allowlists + skill auto-allow toggle, and add approvals UI + node exec lifecycle events. https://docs.clawd.bot/tools/exec-approvals</li>
|
||||
<li>Nodes: add headless node host (<code>clawdbot node start</code>) for <code>system.run</code>/<code>system.which</code>. https://docs.clawd.bot/cli/node</li>
|
||||
<li>Nodes: add node daemon service install/status/start/stop/restart. https://docs.clawd.bot/cli/node</li>
|
||||
<li>Bridge: add <code>skills.bins</code> RPC to support node host auto-allow skill bins.</li>
|
||||
<li>Sessions: add daily reset policy with per-type overrides and idle windows (default 4am local), preserving legacy idle-only configs. (#1146) https://docs.clawd.bot/concepts/session</li>
|
||||
<li>Sessions: allow <code>sessions_spawn</code> to override thinking level for sub-agent runs. https://docs.clawd.bot/tools/subagents</li>
|
||||
<li>Channels: unify thread/topic allowlist matching + command/mention gating helpers across core providers. https://docs.clawd.bot/concepts/groups</li>
|
||||
<li>Models: add Qwen Portal OAuth provider support. (#1120) https://docs.clawd.bot/providers/qwen</li>
|
||||
<li>Onboarding: add allowlist prompts and username-to-id resolution across core and extension channels. https://docs.clawd.bot/start/onboarding</li>
|
||||
<li>Docs: clarify allowlist input types and onboarding behavior for messaging channels. https://docs.clawd.bot/start/onboarding</li>
|
||||
<li>Docs: refresh Android node discovery docs for the Gateway WS service type. https://docs.clawd.bot/platforms/android</li>
|
||||
<li>Docs: surface Amazon Bedrock in provider lists and clarify Bedrock auth env vars. (#1289) https://docs.clawd.bot/bedrock</li>
|
||||
<li>Docs: clarify WhatsApp voice notes. https://docs.clawd.bot/channels/whatsapp</li>
|
||||
<li>Docs: clarify Windows WSL portproxy LAN access notes. https://docs.clawd.bot/platforms/windows</li>
|
||||
<li>Docs: refresh bird skill install metadata and usage notes. (#1302) https://docs.clawd.bot/tools/browser-login</li>
|
||||
<li>Agents: add local docs path resolution and include docs/mirror/source/community pointers in the system prompt.</li>
|
||||
<li>Agents: clarify node_modules read-only guidance in agent instructions.</li>
|
||||
<li>Config: stamp last-touched metadata on write and warn if the config is newer than the running build.</li>
|
||||
<li>macOS: hide usage section when usage is unavailable instead of showing provider errors.</li>
|
||||
<li>Android: migrate node transport to the Gateway WebSocket protocol with TLS pinning support + gateway discovery naming.</li>
|
||||
<li>Android: send structured payloads in node events/invokes and include user-agent metadata in gateway connects.</li>
|
||||
<li>Android: remove legacy bridge transport code now that nodes use the gateway protocol.</li>
|
||||
<li>Android: bump okhttp + dnsjava to satisfy lint dependency checks.</li>
|
||||
<li>Build: update workspace + core/plugin deps.</li>
|
||||
<li>Build: use tsgo for dev/watch builds by default (opt out with <code>CLAWDBOT_TS_COMPILER=tsc</code>).</li>
|
||||
<li>Repo: remove the Peekaboo git submodule now that the SPM release is used.</li>
|
||||
<li>macOS: switch PeekabooBridge integration to the tagged Swift Package Manager release.</li>
|
||||
<li>macOS: stop syncing Peekaboo in postinstall.</li>
|
||||
<li>Swabble: use the tagged Commander Swift package release.</li>
|
||||
</ul>
|
||||
<h3>Breaking</h3>
|
||||
<ul>
|
||||
<li><strong>BREAKING:</strong> Reject invalid/unknown config entries and refuse to start the gateway for safety. Run <code>clawdbot doctor --fix</code> to repair, then update plugins (<code>clawdbot plugins update</code>) if you use any.</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Discovery: shorten Bonjour DNS-SD service type to <code>_clawdbot-gw._tcp</code> and update discovery clients/docs.</li>
|
||||
<li>Diagnostics: export OTLP logs, correct queue depth tracking, and document message-flow telemetry.</li>
|
||||
<li>Diagnostics: emit message-flow diagnostics across channels via shared dispatch. (#1244)</li>
|
||||
<li>Diagnostics: gate heartbeat/webhook logging. (#1244)</li>
|
||||
<li>Gateway: strip inbound envelope headers from chat history messages to keep clients clean.</li>
|
||||
<li>Gateway: clarify unauthorized handshake responses with token/password mismatch guidance.</li>
|
||||
<li>Gateway: allow mobile node client ids for iOS + Android handshake validation. (#1354)</li>
|
||||
<li>Gateway: clarify connect/validation errors for gateway params. (#1347)</li>
|
||||
<li>Gateway: preserve restart wake routing + thread replies across restarts. (#1337)</li>
|
||||
<li>Gateway: reschedule per-agent heartbeats on config hot reload without restarting the runner.</li>
|
||||
<li>Gateway: require authorized restarts for SIGUSR1 (restart/apply/update) so config gating can't be bypassed.</li>
|
||||
<li>Cron: auto-deliver isolated agent output to explicit targets without tool calls. (#1285)</li>
|
||||
<li>Agents: preserve subagent announce thread/topic routing + queued replies across channels. (#1241)</li>
|
||||
<li>Agents: propagate accountId into embedded runs so sub-agent announce routing honors the originating account. (#1058)</li>
|
||||
<li>Agents: avoid treating timeout errors with "aborted" messages as user aborts, so model fallback still runs. (#1137)</li>
|
||||
<li>Agents: sanitize oversized image payloads before send and surface image-dimension errors.</li>
|
||||
<li>Sessions: fall back to session labels when listing display names. (#1124)</li>
|
||||
<li>Compaction: include tool failure summaries in safeguard compaction to prevent retry loops. (#1084)</li>
|
||||
<li>Config: log invalid config issues once per run and keep invalid-config errors stackless.</li>
|
||||
<li>Config: allow Perplexity as a web_search provider in config validation. (#1230)</li>
|
||||
<li>Config: allow custom fields under <code>skills.entries.<name>.config</code> for skill credentials/config. (#1226)</li>
|
||||
<li>Doctor: clarify plugin auto-enable hint text in the startup banner.</li>
|
||||
<li>Doctor: canonicalize legacy session keys in session stores to prevent stale metadata. (#1169)</li>
|
||||
<li>Docs: make docs:list fail fast with a clear error if the docs directory is missing.</li>
|
||||
<li>Plugins: add Nextcloud Talk manifest for plugin config validation. (#1297)</li>
|
||||
<li>Plugins: surface plugin load/register/config errors in gateway logs with plugin/source context.</li>
|
||||
<li>CLI: preserve cron delivery settings when editing message payloads. (#1322)</li>
|
||||
<li>CLI: keep <code>clawdbot logs</code> output resilient to broken pipes while preserving progress output.</li>
|
||||
<li>CLI: avoid duplicating --profile/--dev flags when formatting commands.</li>
|
||||
<li>CLI: centralize CLI command registration to keep fast-path routing and program wiring in sync. (#1207)</li>
|
||||
<li>CLI: keep banners on routed commands, restore config guarding outside fast-path routing, and tighten fast-path flag parsing while skipping console capture for extra speed. (#1195)</li>
|
||||
<li>CLI: skip runner rebuilds when dist is fresh. (#1231)</li>
|
||||
<li>CLI: add WSL2/systemd unavailable hints in daemon status/doctor output.</li>
|
||||
<li>Status: route native <code>/status</code> to the active agent so model selection reflects the correct profile. (#1301)</li>
|
||||
<li>Status: show both usage windows with reset hints when usage data is available. (#1101)</li>
|
||||
<li>UI: keep config form enums typed, preserve empty strings, protect sensitive defaults, and deepen config search. (#1315)</li>
|
||||
<li>UI: preserve ordered list numbering in chat markdown. (#1341)</li>
|
||||
<li>UI: allow Control UI to read gatewayUrl from URL params for remote WebSocket targets. (#1342)</li>
|
||||
<li>UI: prevent double-scroll in Control UI chat by locking chat layout to the viewport. (#1283)</li>
|
||||
<li>UI: enable shell mode for sync Windows spawns to avoid <code>pnpm ui:build</code> EINVAL. (#1212)</li>
|
||||
<li>TUI: keep thinking blocks ordered before content during streaming and isolate per-run assembly. (#1202)</li>
|
||||
<li>TUI: align custom editor initialization with the latest pi-tui API. (#1298)</li>
|
||||
<li>TUI: show generic empty-state text for searchable pickers. (#1201)</li>
|
||||
<li>TUI: highlight model search matches and stabilize search ordering.</li>
|
||||
<li>Configure: hide OpenRouter auto routing model from the model picker. (#1182)</li>
|
||||
<li>Memory: show total file counts + scan issues in <code>clawdbot memory status</code>.</li>
|
||||
<li>Memory: fall back to non-batch embeddings after repeated batch failures.</li>
|
||||
<li>Memory: apply OpenAI batch defaults even without explicit remote config.</li>
|
||||
<li>Memory: index atomically so failed reindex preserves the previous memory database. (#1151)</li>
|
||||
<li>Memory: avoid sqlite-vec unique constraint failures when reindexing duplicate chunk ids. (#1151)</li>
|
||||
<li>Memory: retry transient 5xx errors (Cloudflare) during embedding indexing.</li>
|
||||
<li>Memory: parallelize embedding indexing with rate-limit retries.</li>
|
||||
<li>Memory: split overly long lines to keep embeddings under token limits.</li>
|
||||
<li>Memory: skip empty chunks to avoid invalid embedding inputs.</li>
|
||||
<li>Memory: split embedding batches to avoid OpenAI token limits during indexing.</li>
|
||||
<li>Memory: probe sqlite-vec availability in <code>clawdbot memory status</code>.</li>
|
||||
<li>Exec approvals: enforce allowlist when ask is off.</li>
|
||||
<li>Exec approvals: prefer raw command for node approvals/events.</li>
|
||||
<li>Tools: show exec elevated flag before the command and keep it outside markdown in tool summaries.</li>
|
||||
<li>Tools: return a companion-app-required message when node exec is requested with no paired node.</li>
|
||||
<li>Tools: return a companion-app-required message when <code>system.run</code> is requested without a supporting node.</li>
|
||||
<li>Exec: default gateway/node exec security to allowlist when unset (sandbox stays deny).</li>
|
||||
<li>Exec: prefer bash when fish is default shell, falling back to sh if bash is missing. (#1297)</li>
|
||||
<li>Exec: merge login-shell PATH for host=gateway exec while keeping daemon PATH minimal. (#1304)</li>
|
||||
<li>Streaming: emit assistant deltas for OpenAI-compatible SSE chunks. (#1147)</li>
|
||||
<li>Discord: make resolve warnings avoid raw JSON payloads on rate limits.</li>
|
||||
<li>Discord: process message handlers in parallel across sessions to avoid event queue blocking. (#1295)</li>
|
||||
<li>Discord: stop reconnecting the gateway after aborts to prevent duplicate listeners.</li>
|
||||
<li>Discord: only emit slow listener warnings after 30s.</li>
|
||||
<li>Discord: inherit parent channel allowlists for thread slash commands and reactions. (#1123)</li>
|
||||
<li>Telegram: honor pairing allowlists for native slash commands.</li>
|
||||
<li>Telegram: preserve hidden text_link URLs by expanding entities in inbound text. (#1118)</li>
|
||||
<li>Slack: resolve Bolt import interop for Bun + Node. (#1191)</li>
|
||||
<li>Web search: infer Perplexity base URL from API key source (direct vs OpenRouter).</li>
|
||||
<li>Web fetch: harden SSRF protection with shared hostname checks and redirect limits. (#1346)</li>
|
||||
<li>Browser: register AI snapshot refs for act commands. (#1282)</li>
|
||||
<li>Voice call: include request query in Twilio webhook verification when publicUrl is set. (#864)</li>
|
||||
<li>Anthropic: default API prompt caching to 1h with configurable TTL override.</li>
|
||||
<li>Anthropic: ignore TTL for OAuth.</li>
|
||||
<li>Auth profiles: keep auto-pinned preference while allowing rotation on failover. (#1138)</li>
|
||||
<li>Auth profiles: user pins stay locked. (#1138)</li>
|
||||
<li>Model catalog: avoid caching import failures, log transient discovery errors, and keep partial results. (#1332)</li>
|
||||
<li>Tests: stabilize Windows gateway/CLI tests by skipping sidecars, normalizing argv, and extending timeouts.</li>
|
||||
<li>Tests: stabilize plugin SDK resolution and embedded agent timeouts.</li>
|
||||
<li>Windows: install gateway scheduled task as the current user.</li>
|
||||
<li>Windows: show friendly guidance instead of failing on access denied.</li>
|
||||
<li>macOS: load menu session previews asynchronously so items populate while the menu is open.</li>
|
||||
<li>macOS: use label colors for session preview text so previews render in menu subviews.</li>
|
||||
<li>macOS: suppress usage error text in the menubar cost view.</li>
|
||||
<li>macOS: Doctor repairs LaunchAgent bootstrap issues for Gateway + Node when listed but not loaded. (#1166)</li>
|
||||
<li>macOS: avoid touching launchd in Remote over SSH so quitting the app no longer disables the remote gateway. (#1105)</li>
|
||||
<li>macOS: bundle Textual resources in packaged app builds to avoid code block crashes. (#1006)</li>
|
||||
<li>Daemon: include HOME in service environments to avoid missing HOME errors. (#1214)</li>
|
||||
</ul>
|
||||
Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @NicholaiVogel, @RyanLisse, @ThePickle31, @VACInc, @Whoaa512, @YuriNachos, @aaronveklabs, @abdaraxus, @alauppe, @ameno-, @artuskg, @austinm911, @bradleypriest, @cheeeee, @dougvk, @fogboots, @gnarco, @gumadeiras, @jdrhyne, @joelklabo, @longmaba, @mukhtharcm, @odysseus0, @oscargavin, @rhjoh, @sebslight, @sibbl, @sleontenko, @steipete, @suminhthanh, @thewilloftheshadow, @tyler6204, @vignesh07, @visionik, @ysqander, @zerone0x.
|
||||
<p><a href="https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.20/Clawdbot-2026.1.20.zip" length="12208102" type="application/octet-stream" sparkle:edSignature="hU495Eii8O3qmmUnxYFhXyEGv+qan6KL+GpeuBhPIXf+7B5F/gBh5Oz9cHaqaAPoZ4/3Bo6xgvic0HTkbz6gDw=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.1.16-2</title>
|
||||
<pubDate>Sat, 17 Jan 2026 12:46:22 +0000</pubDate>
|
||||
@@ -99,177 +289,5 @@
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.15/Clawdbot-2026.1.15.zip" length="12127276" type="application/octet-stream" sparkle:edSignature="o79vwTbtW/d91NQFRVfUDhsv6D4zIw7IkhY0N1iLImMu94BURgLcecA6z7Smy3bMobPwOyzN8yfm6mA/Rt8FCA=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.1.14-1</title>
|
||||
<pubDate>Thu, 15 Jan 2026 11:14:40 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
|
||||
<sparkle:version>5825</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.1.14-1</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>Clawdbot 2026.1.14-1</h2>
|
||||
<h3>Highlights</h3>
|
||||
<ul>
|
||||
<li>Web search: <code>web_search</code>/<code>web_fetch</code> tools (Brave API) + first-time setup in onboarding/configure.</li>
|
||||
<li>Browser control: Chrome extension relay takeover mode + remote browser control via <code>clawdbot browser serve</code>.</li>
|
||||
<li>Plugins: channel plugins (gateway HTTP hooks) + Zalo plugin + onboarding install flow. (#854) — thanks @longmaba.</li>
|
||||
<li>Security: expanded <code>clawdbot security audit</code> (+ <code>--fix</code>), detect-secrets CI scan, and a <code>SECURITY.md</code> reporting policy.</li>
|
||||
</ul>
|
||||
<h3>Changes</h3>
|
||||
<h4>Web Tools</h4>
|
||||
<ul>
|
||||
<li>Tools: add <code>web_search</code>/<code>web_fetch</code> (Brave API), including helpful setup hints when the key is missing.</li>
|
||||
<li>Tools: enable <code>web_fetch</code> by default (unless explicitly disabled in config).</li>
|
||||
<li>CLI/Docs: add <code>clawdbot configure --section web</code> for storing Brave API keys and update onboarding tips.</li>
|
||||
</ul>
|
||||
<h4>Browser / Control UI</h4>
|
||||
<ul>
|
||||
<li>Browser: add Chrome extension relay takeover mode (toolbar button) + <code>clawdbot browser serve</code> remote control + <code>browser.controlToken</code>.</li>
|
||||
<li>Browser: ship a built-in <code>chrome</code> profile for extension relay and start the relay automatically when running locally.</li>
|
||||
<li>Browser: default <code>browser.defaultProfile</code> to <code>chrome</code> (existing Chrome takeover mode).</li>
|
||||
<li>Browser: add <code>clawdbot browser extension install/path</code> and copy extension path to clipboard.</li>
|
||||
<li>Browser: add <code>snapshot refs=aria</code> (Playwright aria-ref ids) for self-resolving refs across <code>snapshot</code> → <code>act</code>.</li>
|
||||
<li>Browser: <code>profile="chrome"</code> now defaults to host control and returns clearer “attach a tab” errors.</li>
|
||||
<li>Browser: extension mode recovers when only one tab is attached (stale targetId fallback).</li>
|
||||
<li>Control UI: show raw any-map entries in config views; move Docs link into the left nav.</li>
|
||||
</ul>
|
||||
<h4>Plugins</h4>
|
||||
<ul>
|
||||
<li>Plugins: add plugin HTTP hooks + loader updates to support channel plugins. (#854) — thanks @longmaba.</li>
|
||||
<li>Plugins: add onboarding plugin install flow. (#854) — thanks @longmaba.</li>
|
||||
<li>Channels: add Matrix plugin (external) with docs + onboarding hooks.</li>
|
||||
<li>Voice Call: add Plivo provider (no SDK dependency). (#846) — thanks @vrknetha.</li>
|
||||
</ul>
|
||||
<h4>Security</h4>
|
||||
<ul>
|
||||
<li>Security: expand <code>clawdbot security audit</code> checks and publish a <code>SECURITY.md</code> reporting policy.</li>
|
||||
<li>Security: extend <code>clawdbot security audit --fix</code> to tighten more sensitive state paths.</li>
|
||||
<li>Security: add detect-secrets CI scan and baseline guidance. (#227) — thanks @Hyaxia.</li>
|
||||
</ul>
|
||||
<h4>Onboarding / Daemon</h4>
|
||||
<ul>
|
||||
<li>Onboarding: add a security checkpoint prompt (docs link + sandboxing hint); require <code>--accept-risk</code> for <code>--non-interactive</code>.</li>
|
||||
<li>Daemon: support profile-aware service names for multi-gateway setups. (#671) — thanks @bjesuiter.</li>
|
||||
</ul>
|
||||
<h4>Auth / Usage / Config</h4>
|
||||
<ul>
|
||||
<li>Usage: add MiniMax coding plan usage tracking.</li>
|
||||
<li>Auth: label Claude Code CLI auth options. (#915) — thanks @SeanZoR.</li>
|
||||
<li>Agents: add optional auth-profile copy prompt on <code>agents add</code> and improve auth error messaging.</li>
|
||||
<li>Auth: add dynamic template variables to <code>messages.responsePrefix</code>. (#928) — thanks @sebslight.</li>
|
||||
<li>Config: add <code>channels.<provider>.configWrites</code> gating for channel-initiated config writes; migrate Slack channel IDs.</li>
|
||||
</ul>
|
||||
<h4>Channels</h4>
|
||||
<ul>
|
||||
<li>Telegram: add message delete action in the message tool. (#903) — thanks @sleontenko.</li>
|
||||
<li>WhatsApp: add <code>channels.whatsapp.sendReadReceipts</code> to disable auto read receipts. (#882) — thanks @chrisrodz.</li>
|
||||
</ul>
|
||||
<h4>Docs</h4>
|
||||
<ul>
|
||||
<li>Docs: clarify per-agent auth stores, sandboxed skill binaries, and elevated semantics.</li>
|
||||
<li>Docs: add FAQ entries for missing provider auth after adding agents and Gemini thinking signature errors.</li>
|
||||
<li>Docs: expand gateway security hardening guidance and incident response checklist.</li>
|
||||
<li>Docs: document DM history limits for channel DMs. (#883) — thanks @pkrmf.</li>
|
||||
<li>Docs: standardize Claude Code CLI naming across docs and prompts. (follow-up to #915)</li>
|
||||
<li>Docs: add per-command CLI doc pages and link them from <code>clawdbot <command> --help</code>.</li>
|
||||
<li>Docs: add multi-gateway guide (sidebar + nav).</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<h4>Gateway / Daemon / Sessions</h4>
|
||||
<ul>
|
||||
<li>Gateway: forward termination signals to respawned CLI child processes to avoid orphaned systemd runs. (#933) — thanks @roshanasingh4.</li>
|
||||
<li>Gateway/UI: ship session defaults in the hello snapshot so the Control UI canonicalizes main session keys (no bare <code>main</code> alias).</li>
|
||||
<li>Agents: skip thinking/final tag stripping inside Markdown code spans. (#939) — thanks @ngutman.</li>
|
||||
<li>Browser: add tests for snapshot labels/efficient query params and labeled image responses.</li>
|
||||
<li>Browser: persist role snapshot refs per CDP target so <code>snapshot</code> → <code>act</code> clicks work even if Playwright returns a different Page instance.</li>
|
||||
<li>macOS: ensure launchd log directory exists with a test-only override. (#909) — thanks @roshanasingh4.</li>
|
||||
<li>macOS: format ConnectionsStore config to satisfy SwiftFormat lint. (#852) — thanks @mneves75.</li>
|
||||
<li>Packaging: run <code>pnpm build</code> on <code>prepack</code> so npm publishes include fresh <code>dist/</code> output.</li>
|
||||
<li>Telegram: register dock native commands with underscores to avoid <code>BOT_COMMAND_INVALID</code> (#929, fixes #901) — thanks @grp06.</li>
|
||||
<li>Google: downgrade unsigned thinking blocks before send to avoid missing signature errors.</li>
|
||||
<li>Agents: make user time zone and 24-hour time explicit in the system prompt. (#859) — thanks @CashWilliams.</li>
|
||||
<li>Agents: strip downgraded tool call text without eating adjacent replies and filter thinking-tag leaks. (#905) — thanks @erikpr1994.</li>
|
||||
<li>Agents: cap tool call IDs for OpenAI/OpenRouter to avoid request rejections. (#875) — thanks @j1philli.</li>
|
||||
<li>Doctor: avoid re-adding WhatsApp config when only legacy ack reactions are set. (#927, fixes #900) — thanks @grp06.</li>
|
||||
<li>Agents: scrub tuple <code>items</code> schemas for Gemini tool calls. (#926, fixes #746) — thanks @grp06.</li>
|
||||
<li>Agents: stabilize sub-agent announce status from runtime outcomes and normalize Result/Notes. (#835) — thanks @roshanasingh4.</li>
|
||||
<li>Apps: use canonical main session keys from gateway defaults across macOS/iOS/Android to avoid creating bare <code>main</code> sessions.</li>
|
||||
<li>Embedded runner: suppress raw API error payloads from replies. (#924) — thanks @grp06.</li>
|
||||
<li>Auth: normalize Claude Code CLI profile mode to oauth and auto-migrate config. (#855) — thanks @sebslight.</li>
|
||||
<li>Daemon: clear persisted launchd disabled state before bootstrap (fixes <code>daemon install</code> after uninstall). (#849) — thanks @ndraiman.</li>
|
||||
<li>Sessions: return deep clones (<code>structuredClone</code>) so cached session entries can't be mutated. (#934) — thanks @ronak-guliani.</li>
|
||||
<li>Heartbeat: keep <code>updatedAt</code> monotonic when restoring heartbeat sessions. (#934) — thanks @ronak-guliani.</li>
|
||||
<li>Agent: clear run context after CLI runs (<code>clearAgentRunContext</code>) to avoid runaway contexts. (#934) — thanks @ronak-guliani.</li>
|
||||
<li>Gateway/Dev: ensure <code>pnpm gateway:dev</code> always uses the dev profile config + state (<code>~/.clawdbot-dev</code>).</li>
|
||||
</ul>
|
||||
<h4>CLI / Onboarding</h4>
|
||||
<ul>
|
||||
<li>Onboarding: show web search setup at the end (not the beginning).</li>
|
||||
<li>Onboarding: show daemon install/restart progress (avoid “blinking cursor”) and fix daemon install output formatting.</li>
|
||||
<li>Health: colorize “not configured” provider lines for easier scanning.</li>
|
||||
</ul>
|
||||
<h4>Control UI / TUI</h4>
|
||||
<ul>
|
||||
<li>Control UI: load cron run history on job selection and clarify empty-state messaging. (#866)</li>
|
||||
<li>UI: use application-defined WebSocket close code and fix dashboard auth query items. (#918) — thanks @rahthakor.</li>
|
||||
<li>UI: always apply <code>?token=</code> from URL (fixes unauthorized after re-onboard).</li>
|
||||
<li>Browser: add tests for snapshot labels/efficient query params and labeled image responses.</li>
|
||||
<li>TUI: render picker overlays via the overlay stack so /models and /settings display. (#921) — thanks @grizzdank.</li>
|
||||
<li>TUI: add a bright spinner + elapsed time in the status line for send/stream/run states.</li>
|
||||
<li>TUI: show LLM error messages (rate limits, auth, etc.) instead of <code>(no output)</code>.</li>
|
||||
</ul>
|
||||
<h4>Agents / Auth / Tools / Sandbox</h4>
|
||||
<ul>
|
||||
<li>Agents: make user time zone and 24-hour time explicit in the system prompt. (#859) — thanks @CashWilliams.</li>
|
||||
<li>Agents: strip downgraded tool call text without eating adjacent replies and filter thinking-tag leaks. (#905) — thanks @erikpr1994.</li>
|
||||
<li>Agents: cap tool call IDs for OpenAI/OpenRouter to avoid request rejections. (#875) — thanks @j1philli.</li>
|
||||
<li>Agents: scrub tuple <code>items</code> schemas for Gemini tool calls. (#926, fixes #746) — thanks @grp06.</li>
|
||||
<li>Agents: stabilize sub-agent announce status from runtime outcomes and normalize Result/Notes. (#835) — thanks @roshanasingh4.</li>
|
||||
<li>Auth: normalize Claude Code CLI profile mode to oauth and auto-migrate config. (#855) — thanks @sebslight.</li>
|
||||
<li>Embedded runner: suppress raw API error payloads from replies. (#924) — thanks @grp06.</li>
|
||||
<li>Logging: tolerate <code>EIO</code> from console writes to avoid gateway crashes. (#925, fixes #878) — thanks @grp06.</li>
|
||||
<li>Sandbox: restore <code>docker.binds</code> config validation and preserve configured PATH for <code>docker exec</code>. (#873) — thanks @akonyer.</li>
|
||||
<li>Google: downgrade unsigned thinking blocks before send to avoid missing signature errors.</li>
|
||||
</ul>
|
||||
<h4>macOS / Apps</h4>
|
||||
<ul>
|
||||
<li>macOS: ensure launchd log directory exists with a test-only override. (#909) — thanks @roshanasingh4.</li>
|
||||
<li>macOS: format ConnectionsStore config to satisfy SwiftFormat lint. (#852) — thanks @mneves75.</li>
|
||||
<li>macOS: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor.</li>
|
||||
<li>macOS: reuse launchd gateway auth and skip wizard when gateway config already exists. (#917)</li>
|
||||
<li>Apps: use canonical main session keys from gateway defaults across macOS/iOS/Android to avoid creating bare <code>main</code> sessions.</li>
|
||||
<li>macOS: fix cron preview/testing payload to use <code>channel</code> key. (#867) — thanks @wes-davis.</li>
|
||||
<li>macOS: update cron testing channel arg. (#896) — thanks @ngutman.</li>
|
||||
</ul>
|
||||
<h4>Channels / Messaging</h4>
|
||||
<ul>
|
||||
<li>Slack: isolate thread history and avoid inheriting channel transcripts for new threads by default. (#758)</li>
|
||||
<li>Slack: respect <code>channels.slack.requireMention</code> default when resolving channel mention gating. (#850) — thanks @evalexpr.</li>
|
||||
<li>Slack: drop Socket Mode events with mismatched <code>api_app_id</code>/<code>team_id</code>. (#889) — thanks @roshanasingh4.</li>
|
||||
<li>Commands: add native command argument menus across Discord/Slack/Telegram. (#936) — thanks @thewilloftheshadow.</li>
|
||||
<li>Discord: isolate autoThread thread context. (#856) — thanks @davidguttman.</li>
|
||||
<li>Telegram: honor <code>channels.telegram.timeoutSeconds</code> for grammY API requests. (#863) — thanks @Snaver.</li>
|
||||
<li>Telegram: aggregate split inbound messages into one prompt (reduces “one reply per fragment”).</li>
|
||||
<li>Telegram: let control commands bypass per-chat sequentialization; always allow abort triggers.</li>
|
||||
<li>Telegram: split long captions into media + follow-up text messages. (#907) — thanks @jalehman.</li>
|
||||
<li>Telegram: migrate group config when supergroups change chat IDs. (#906) — thanks @sleontenko.</li>
|
||||
<li>Telegram: register dock native commands with underscores to avoid <code>BOT_COMMAND_INVALID</code> (#929, fixes #901) — thanks @grp06.</li>
|
||||
<li>Messaging: unify markdown formatting + format-first chunking for Slack/Telegram/Signal. (#920) — thanks @TheSethRose.</li>
|
||||
<li>iMessage: prefer handle routing for direct-message replies; include imsg RPC error details. (#935)</li>
|
||||
<li>WhatsApp: fix context isolation using wrong ID (was bot's number, now conversation ID). (#911) — thanks @tristanmanchester.</li>
|
||||
<li>WhatsApp: normalize user JIDs with device suffix for allowlist checks in groups. (#838) — thanks @peschee.</li>
|
||||
<li>WhatsApp: harden owner command auth.</li>
|
||||
<li>Auto-reply: treat trailing <code>NO_REPLY</code> tokens as silent replies.</li>
|
||||
</ul>
|
||||
<h4>Config / Doctor / Packaging</h4>
|
||||
<ul>
|
||||
<li>Config: prevent partial config writes from clobbering unrelated settings (base hash guard + merge patch for connection saves).</li>
|
||||
<li>Config/Doctor: remove legacy Clawdis env fallbacks and config/service migrations (Clawdbot-only).</li>
|
||||
<li>Doctor: avoid re-adding WhatsApp config when only legacy ack reactions are set. (#927, fixes #900) — thanks @grp06.</li>
|
||||
<li>Packaging: run <code>pnpm build</code> on <code>prepack</code> so npm publishes include fresh <code>dist/</code> output.</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.14-1/Clawdbot-2026.1.14-1.zip" length="19887144" type="application/octet-stream" sparkle:edSignature="1irKxBLt2eRtns34m/8JsjL/ZzhZQNjahwrxtArTvzaCnidS/MEnpD4nV2SHnhuo8g+fJZQpV9NoCAoEOAinCw=="/>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -529,7 +529,7 @@ class NodeRuntime(context: Context) {
|
||||
caps = buildCapabilities(),
|
||||
commands = buildInvokeCommands(),
|
||||
permissions = emptyMap(),
|
||||
client = buildClientInfo(clientId = "node-host", clientMode = "node"),
|
||||
client = buildClientInfo(clientId = "clawdbot-android", clientMode = "node"),
|
||||
userAgent = buildUserAgent(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -132,6 +132,12 @@ final class TalkModeManager: NSObject {
|
||||
}
|
||||
|
||||
private func startRecognition() throws {
|
||||
#if targetEnvironment(simulator)
|
||||
throw NSError(domain: "TalkMode", code: 2, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Talk mode is not supported on the iOS simulator",
|
||||
])
|
||||
#endif
|
||||
|
||||
self.stopRecognition()
|
||||
self.speechRecognizer = SFSpeechRecognizer()
|
||||
guard let recognizer = self.speechRecognizer else {
|
||||
@@ -146,6 +152,11 @@ final class TalkModeManager: NSObject {
|
||||
|
||||
let input = self.audioEngine.inputNode
|
||||
let format = input.outputFormat(forBus: 0)
|
||||
guard format.sampleRate > 0, format.channelCount > 0 else {
|
||||
throw NSError(domain: "TalkMode", code: 3, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Invalid audio input format",
|
||||
])
|
||||
}
|
||||
input.removeTap(onBus: 0)
|
||||
let tapBlock = Self.makeAudioTapAppendCallback(request: request)
|
||||
input.installTap(onBus: 0, bufferSize: 2048, format: format, block: tapBlock)
|
||||
|
||||
@@ -284,13 +284,16 @@ enum CommandResolver {
|
||||
|
||||
var args: [String] = [
|
||||
"-o", "BatchMode=yes",
|
||||
"-o", "IdentitiesOnly=yes",
|
||||
"-o", "StrictHostKeyChecking=accept-new",
|
||||
"-o", "UpdateHostKeys=yes",
|
||||
]
|
||||
if parsed.port > 0 { args.append(contentsOf: ["-p", String(parsed.port)]) }
|
||||
if !settings.identity.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
args.append(contentsOf: ["-i", settings.identity])
|
||||
let identity = settings.identity.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !identity.isEmpty {
|
||||
// Only use IdentitiesOnly when an explicit identity file is provided.
|
||||
// This allows 1Password SSH agent and other SSH agents to provide keys.
|
||||
args.append(contentsOf: ["-o", "IdentitiesOnly=yes"])
|
||||
args.append(contentsOf: ["-i", identity])
|
||||
}
|
||||
let userHost = parsed.user.map { "\($0)@\(parsed.host)" } ?? parsed.host
|
||||
args.append(userHost)
|
||||
|
||||
@@ -484,6 +484,22 @@ struct DebugSettings: View {
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(
|
||||
"Note: macOS may require restarting Clawdbot after enabling Accessibility or Screen Recording.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Button {
|
||||
LaunchdManager.startClawdbot()
|
||||
} label: {
|
||||
Label("Restart Clawdbot", systemImage: "arrow.counterclockwise")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Button("Restart app") { DebugActions.restartApp() }
|
||||
Button("Restart onboarding") { DebugActions.restartOnboarding() }
|
||||
|
||||
@@ -276,12 +276,13 @@ enum ExecApprovalsStore {
|
||||
? agentId!.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
: "default"
|
||||
let agentEntry = file.agents?[key] ?? ExecApprovalsAgent()
|
||||
let wildcardEntry = file.agents?["*"] ?? ExecApprovalsAgent()
|
||||
let resolvedAgent = ExecApprovalsResolvedDefaults(
|
||||
security: agentEntry.security ?? resolvedDefaults.security,
|
||||
ask: agentEntry.ask ?? resolvedDefaults.ask,
|
||||
askFallback: agentEntry.askFallback ?? resolvedDefaults.askFallback,
|
||||
autoAllowSkills: agentEntry.autoAllowSkills ?? resolvedDefaults.autoAllowSkills)
|
||||
let allowlist = (agentEntry.allowlist ?? [])
|
||||
security: agentEntry.security ?? wildcardEntry.security ?? resolvedDefaults.security,
|
||||
ask: agentEntry.ask ?? wildcardEntry.ask ?? resolvedDefaults.ask,
|
||||
askFallback: agentEntry.askFallback ?? wildcardEntry.askFallback ?? resolvedDefaults.askFallback,
|
||||
autoAllowSkills: agentEntry.autoAllowSkills ?? wildcardEntry.autoAllowSkills ?? resolvedDefaults.autoAllowSkills)
|
||||
let allowlist = ((wildcardEntry.allowlist ?? []) + (agentEntry.allowlist ?? []))
|
||||
.map { entry in
|
||||
ExecAllowlistEntry(
|
||||
pattern: entry.pattern.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
|
||||
@@ -15,10 +15,6 @@ struct GeneralSettings: View {
|
||||
private let gatewayManager = GatewayProcessManager.shared
|
||||
@State private var gatewayDiscovery = GatewayDiscoveryModel(
|
||||
localDisplayName: InstanceIdentity.displayName)
|
||||
@State private var isInstallingCLI = false
|
||||
@State private var cliStatus: String?
|
||||
@State private var cliInstalled = false
|
||||
@State private var cliInstallLocation: String?
|
||||
@State private var gatewayStatus: GatewayEnvironmentStatus = .checking
|
||||
@State private var remoteStatus: RemoteStatus = .idle
|
||||
@State private var showRemoteAdvanced = false
|
||||
@@ -29,25 +25,6 @@ struct GeneralSettings: View {
|
||||
var body: some View {
|
||||
ScrollView(.vertical) {
|
||||
VStack(alignment: .leading, spacing: 18) {
|
||||
if !self.state.onboardingSeen {
|
||||
Button {
|
||||
DebugActions.restartOnboarding()
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
Label("Complete onboarding to finish setup", systemImage: "arrow.counterclockwise")
|
||||
.font(.callout.weight(.semibold))
|
||||
.foregroundStyle(Color.accentColor)
|
||||
Spacer(minLength: 0)
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.bottom, 2)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
SettingsToggleRow(
|
||||
title: "Clawdbot active",
|
||||
@@ -83,8 +60,6 @@ struct GeneralSettings: View {
|
||||
subtitle: "Allow the agent to capture a photo or short video via the built-in camera.",
|
||||
binding: self.$cameraEnabled)
|
||||
|
||||
SystemRunSettingsView()
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Location Access")
|
||||
.font(.body)
|
||||
@@ -130,7 +105,6 @@ struct GeneralSettings: View {
|
||||
}
|
||||
.onAppear {
|
||||
guard !self.isPreview else { return }
|
||||
self.refreshCLIStatus()
|
||||
self.refreshGatewayStatus()
|
||||
self.lastLocationModeRaw = self.locationModeRaw
|
||||
}
|
||||
@@ -187,13 +161,14 @@ struct GeneralSettings: View {
|
||||
.font(.title3.weight(.semibold))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Picker("", selection: self.$state.connectionMode) {
|
||||
Picker("Mode", selection: self.$state.connectionMode) {
|
||||
Text("Not configured").tag(AppState.ConnectionMode.unconfigured)
|
||||
Text("Local (this Mac)").tag(AppState.ConnectionMode.local)
|
||||
Text("Remote over SSH").tag(AppState.ConnectionMode.remote)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.frame(width: 380, alignment: .leading)
|
||||
.pickerStyle(.menu)
|
||||
.labelsHidden()
|
||||
.frame(width: 260, alignment: .leading)
|
||||
|
||||
if self.state.connectionMode == .unconfigured {
|
||||
Text("Pick Local or Remote to start the Gateway.")
|
||||
@@ -217,7 +192,6 @@ struct GeneralSettings: View {
|
||||
self.remoteCard
|
||||
}
|
||||
|
||||
self.cliInstaller
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,59 +320,6 @@ struct GeneralSettings: View {
|
||||
return message == self.controlStatusLine
|
||||
}
|
||||
|
||||
private var cliInstaller: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(spacing: 10) {
|
||||
Button {
|
||||
Task { await self.installCLI() }
|
||||
} label: {
|
||||
let title = self.cliInstalled ? "Reinstall CLI" : "Install CLI"
|
||||
ZStack {
|
||||
Text(title)
|
||||
.opacity(self.isInstallingCLI ? 0 : 1)
|
||||
if self.isInstallingCLI {
|
||||
ProgressView()
|
||||
.controlSize(.mini)
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 150)
|
||||
}
|
||||
.disabled(self.isInstallingCLI)
|
||||
|
||||
if self.isInstallingCLI {
|
||||
Text("Working...")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
} else if self.cliInstalled {
|
||||
Label("Installed", systemImage: "checkmark.circle.fill")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Text("Not installed")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
if let status = cliStatus {
|
||||
Text(status)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
} else if let installLocation = self.cliInstallLocation {
|
||||
Text("Found at \(installLocation)")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
} else {
|
||||
Text("Installs a user-space Node 22+ runtime and the CLI (no Homebrew).")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var gatewayInstallerCard: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(spacing: 10) {
|
||||
@@ -454,22 +375,6 @@ struct GeneralSettings: View {
|
||||
.cornerRadius(10)
|
||||
}
|
||||
|
||||
private func installCLI() async {
|
||||
guard !self.isInstallingCLI else { return }
|
||||
self.isInstallingCLI = true
|
||||
defer { isInstallingCLI = false }
|
||||
await CLIInstaller.install { status in
|
||||
self.cliStatus = status
|
||||
self.refreshCLIStatus()
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshCLIStatus() {
|
||||
let installLocation = CLIInstaller.installedLocation()
|
||||
self.cliInstallLocation = installLocation
|
||||
self.cliInstalled = installLocation != nil
|
||||
}
|
||||
|
||||
private func refreshGatewayStatus() {
|
||||
Task {
|
||||
let status = await Task.detached(priority: .utility) {
|
||||
@@ -763,9 +668,6 @@ extension GeneralSettings {
|
||||
message: "Gateway ready")
|
||||
view.remoteStatus = .failed("SSH failed")
|
||||
view.showRemoteAdvanced = true
|
||||
view.cliInstalled = true
|
||||
view.cliInstallLocation = "/usr/local/bin/clawdbot"
|
||||
view.cliStatus = "Installed"
|
||||
_ = view.body
|
||||
|
||||
state.connectionMode = .unconfigured
|
||||
|
||||
@@ -2,14 +2,28 @@ import Foundation
|
||||
import JavaScriptCore
|
||||
|
||||
enum ModelCatalogLoader {
|
||||
static let defaultPath: String = FileManager().homeDirectoryForCurrentUser
|
||||
.appendingPathComponent("Projects/pi-mono/packages/ai/src/models.generated.ts").path
|
||||
static var defaultPath: String { self.resolveDefaultPath() }
|
||||
private static let logger = Logger(subsystem: "com.clawdbot", category: "models")
|
||||
private nonisolated static let appSupportDir: URL = {
|
||||
let base = FileManager().urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
||||
return base.appendingPathComponent("Clawdbot", isDirectory: true)
|
||||
}()
|
||||
|
||||
private static var cachePath: URL {
|
||||
self.appSupportDir.appendingPathComponent("model-catalog/models.generated.js", isDirectory: false)
|
||||
}
|
||||
|
||||
static func load(from path: String) async throws -> [ModelChoice] {
|
||||
let expanded = (path as NSString).expandingTildeInPath
|
||||
self.logger.debug("model catalog load start file=\(URL(fileURLWithPath: expanded).lastPathComponent)")
|
||||
let source = try String(contentsOfFile: expanded, encoding: .utf8)
|
||||
guard let resolved = self.resolvePath(preferred: expanded) else {
|
||||
self.logger.error("model catalog load failed: file not found")
|
||||
throw NSError(
|
||||
domain: "ModelCatalogLoader",
|
||||
code: 1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Model catalog file not found"])
|
||||
}
|
||||
self.logger.debug("model catalog load start file=\(URL(fileURLWithPath: resolved.path).lastPathComponent)")
|
||||
let source = try String(contentsOfFile: resolved.path, encoding: .utf8)
|
||||
let sanitized = self.sanitize(source: source)
|
||||
|
||||
let ctx = JSContext()
|
||||
@@ -45,9 +59,82 @@ enum ModelCatalogLoader {
|
||||
return lhs.provider.localizedCaseInsensitiveCompare(rhs.provider) == .orderedAscending
|
||||
}
|
||||
self.logger.debug("model catalog loaded providers=\(rawModels.count) models=\(sorted.count)")
|
||||
if resolved.shouldCache {
|
||||
self.cacheCatalog(sourcePath: resolved.path)
|
||||
}
|
||||
return sorted
|
||||
}
|
||||
|
||||
private static func resolveDefaultPath() -> String {
|
||||
let cache = self.cachePath.path
|
||||
if FileManager().isReadableFile(atPath: cache) { return cache }
|
||||
if let bundlePath = self.bundleCatalogPath() { return bundlePath }
|
||||
if let nodePath = self.nodeModulesCatalogPath() { return nodePath }
|
||||
return cache
|
||||
}
|
||||
|
||||
private static func resolvePath(preferred: String) -> (path: String, shouldCache: Bool)? {
|
||||
if FileManager().isReadableFile(atPath: preferred) {
|
||||
return (preferred, preferred != self.cachePath.path)
|
||||
}
|
||||
|
||||
if let bundlePath = self.bundleCatalogPath(), bundlePath != preferred {
|
||||
self.logger.warning("model catalog path missing; falling back to bundled catalog")
|
||||
return (bundlePath, true)
|
||||
}
|
||||
|
||||
let cache = self.cachePath.path
|
||||
if cache != preferred, FileManager().isReadableFile(atPath: cache) {
|
||||
self.logger.warning("model catalog path missing; falling back to cached catalog")
|
||||
return (cache, false)
|
||||
}
|
||||
|
||||
if let nodePath = self.nodeModulesCatalogPath(), nodePath != preferred {
|
||||
self.logger.warning("model catalog path missing; falling back to node_modules catalog")
|
||||
return (nodePath, true)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func bundleCatalogPath() -> String? {
|
||||
guard let url = Bundle.main.url(forResource: "models.generated", withExtension: "js") else {
|
||||
return nil
|
||||
}
|
||||
return url.path
|
||||
}
|
||||
|
||||
private static func nodeModulesCatalogPath() -> String? {
|
||||
let roots = [
|
||||
URL(fileURLWithPath: CommandResolver.projectRootPath()),
|
||||
URL(fileURLWithPath: FileManager().currentDirectoryPath),
|
||||
]
|
||||
for root in roots {
|
||||
let candidate = root
|
||||
.appendingPathComponent("node_modules/@mariozechner/pi-ai/dist/models.generated.js")
|
||||
if FileManager().isReadableFile(atPath: candidate.path) {
|
||||
return candidate.path
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func cacheCatalog(sourcePath: String) {
|
||||
let destination = self.cachePath
|
||||
do {
|
||||
try FileManager().createDirectory(
|
||||
at: destination.deletingLastPathComponent(),
|
||||
withIntermediateDirectories: true)
|
||||
if FileManager().fileExists(atPath: destination.path) {
|
||||
try FileManager().removeItem(at: destination)
|
||||
}
|
||||
try FileManager().copyItem(atPath: sourcePath, toPath: destination.path)
|
||||
self.logger.debug("model catalog cached file=\(destination.lastPathComponent)")
|
||||
} catch {
|
||||
self.logger.warning("model catalog cache failed: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
private static func sanitize(source: String) -> String {
|
||||
guard let exportRange = source.range(of: "export const MODELS"),
|
||||
let firstBrace = source[exportRange.upperBound...].firstIndex(of: "{"),
|
||||
|
||||
@@ -8,6 +8,8 @@ struct PermissionsSettings: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
SystemRunSettingsView()
|
||||
|
||||
Text("Allow these so Clawdbot can notify and capture when needed.")
|
||||
.padding(.top, 4)
|
||||
|
||||
@@ -46,24 +48,6 @@ struct PermissionStatusList: View {
|
||||
.padding(.top, 2)
|
||||
.help("Refresh status")
|
||||
|
||||
if (self.status[.accessibility] ?? false) == false || (self.status[.screenRecording] ?? false) == false {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(
|
||||
"Note: macOS may require restarting Clawdbot after enabling Accessibility or Screen Recording.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Button {
|
||||
LaunchdManager.startClawdbot()
|
||||
} label: {
|
||||
Label("Restart Clawdbot", systemImage: "arrow.counterclockwise")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
}
|
||||
.padding(.top, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,6 @@ final class RemotePortTunnel {
|
||||
}
|
||||
var args: [String] = [
|
||||
"-o", "BatchMode=yes",
|
||||
"-o", "IdentitiesOnly=yes",
|
||||
"-o", "ExitOnForwardFailure=yes",
|
||||
"-o", "StrictHostKeyChecking=accept-new",
|
||||
"-o", "UpdateHostKeys=yes",
|
||||
@@ -84,7 +83,12 @@ final class RemotePortTunnel {
|
||||
]
|
||||
if parsed.port > 0 { args.append(contentsOf: ["-p", String(parsed.port)]) }
|
||||
let identity = settings.identity.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !identity.isEmpty { args.append(contentsOf: ["-i", identity]) }
|
||||
if !identity.isEmpty {
|
||||
// Only use IdentitiesOnly when an explicit identity file is provided.
|
||||
// This allows 1Password SSH agent and other SSH agents to provide keys.
|
||||
args.append(contentsOf: ["-o", "IdentitiesOnly=yes"])
|
||||
args.append(contentsOf: ["-i", identity])
|
||||
}
|
||||
let userHost = parsed.user.map { "\($0)@\(parsed.host)" } ?? parsed.host
|
||||
args.append(userHost)
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ final class TailscaleService {
|
||||
}
|
||||
}
|
||||
|
||||
private static func isTailnetIPv4(_ address: String) -> Bool {
|
||||
private nonisolated static func isTailnetIPv4(_ address: String) -> Bool {
|
||||
let parts = address.split(separator: ".")
|
||||
guard parts.count == 4 else { return false }
|
||||
let octets = parts.compactMap { Int($0) }
|
||||
@@ -188,7 +188,7 @@ final class TailscaleService {
|
||||
return a == 100 && b >= 64 && b <= 127
|
||||
}
|
||||
|
||||
private static func detectTailnetIPv4() -> String? {
|
||||
private nonisolated static func detectTailnetIPv4() -> String? {
|
||||
var addrList: UnsafeMutablePointer<ifaddrs>?
|
||||
guard getifaddrs(&addrList) == 0, let first = addrList else { return nil }
|
||||
defer { freeifaddrs(addrList) }
|
||||
|
||||
@@ -23,6 +23,35 @@ public actor GatewayNodeSession {
|
||||
private var onConnected: (@Sendable () async -> Void)?
|
||||
private var onDisconnected: (@Sendable (String) async -> Void)?
|
||||
private var onInvoke: (@Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse)?
|
||||
|
||||
static func invokeWithTimeout(
|
||||
request: BridgeInvokeRequest,
|
||||
timeoutMs: Int?,
|
||||
onInvoke: @escaping @Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse
|
||||
) async -> BridgeInvokeResponse {
|
||||
let timeout = max(0, timeoutMs ?? 0)
|
||||
guard timeout > 0 else {
|
||||
return await onInvoke(request)
|
||||
}
|
||||
|
||||
return await withTaskGroup(of: BridgeInvokeResponse.self) { group in
|
||||
group.addTask { await onInvoke(request) }
|
||||
group.addTask {
|
||||
try? await Task.sleep(nanoseconds: UInt64(timeout) * 1_000_000)
|
||||
return BridgeInvokeResponse(
|
||||
id: request.id,
|
||||
ok: false,
|
||||
error: ClawdbotNodeError(
|
||||
code: .unavailable,
|
||||
message: "node invoke timed out")
|
||||
)
|
||||
}
|
||||
|
||||
let first = await group.next()!
|
||||
group.cancelAll()
|
||||
return first
|
||||
}
|
||||
}
|
||||
private var serverEventSubscribers: [UUID: AsyncStream<EventFrame>.Continuation] = [:]
|
||||
private var canvasHostUrl: String?
|
||||
|
||||
@@ -167,7 +196,11 @@ public actor GatewayNodeSession {
|
||||
let request = try self.decoder.decode(NodeInvokeRequestPayload.self, from: data)
|
||||
guard let onInvoke else { return }
|
||||
let req = BridgeInvokeRequest(id: request.id, command: request.command, paramsJSON: request.paramsJSON)
|
||||
let response = await onInvoke(req)
|
||||
let response = await Self.invokeWithTimeout(
|
||||
request: req,
|
||||
timeoutMs: request.timeoutMs,
|
||||
onInvoke: onInvoke
|
||||
)
|
||||
await self.sendInvokeResult(request: request, response: response)
|
||||
} catch {
|
||||
self.logger.error("node invoke decode failed: \(error.localizedDescription, privacy: .public)")
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import ClawdbotKit
|
||||
import ClawdbotProtocol
|
||||
|
||||
struct GatewayNodeSessionTests {
|
||||
@Test
|
||||
func invokeWithTimeoutReturnsUnderlyingResponseBeforeTimeout() async {
|
||||
let request = BridgeInvokeRequest(id: "1", command: "x", paramsJSON: nil)
|
||||
let response = await GatewayNodeSession.invokeWithTimeout(
|
||||
request: request,
|
||||
timeoutMs: 50,
|
||||
onInvoke: { req in
|
||||
#expect(req.id == "1")
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: "{}", error: nil)
|
||||
}
|
||||
)
|
||||
|
||||
#expect(response.ok == true)
|
||||
#expect(response.error == nil)
|
||||
#expect(response.payloadJSON == "{}")
|
||||
}
|
||||
|
||||
@Test
|
||||
func invokeWithTimeoutReturnsTimeoutError() async {
|
||||
let request = BridgeInvokeRequest(id: "abc", command: "x", paramsJSON: nil)
|
||||
let response = await GatewayNodeSession.invokeWithTimeout(
|
||||
request: request,
|
||||
timeoutMs: 10,
|
||||
onInvoke: { _ in
|
||||
try? await Task.sleep(nanoseconds: 200_000_000) // 200ms
|
||||
return BridgeInvokeResponse(id: "abc", ok: true, payloadJSON: "{}", error: nil)
|
||||
}
|
||||
)
|
||||
|
||||
#expect(response.ok == false)
|
||||
#expect(response.error?.code == .unavailable)
|
||||
#expect(response.error?.message.contains("timed out") == true)
|
||||
}
|
||||
|
||||
@Test
|
||||
func invokeWithTimeoutZeroDisablesTimeout() async {
|
||||
let request = BridgeInvokeRequest(id: "1", command: "x", paramsJSON: nil)
|
||||
let response = await GatewayNodeSession.invokeWithTimeout(
|
||||
request: request,
|
||||
timeoutMs: 0,
|
||||
onInvoke: { req in
|
||||
try? await Task.sleep(nanoseconds: 5_000_000)
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: nil, error: nil)
|
||||
}
|
||||
)
|
||||
|
||||
#expect(response.ok == true)
|
||||
#expect(response.error == nil)
|
||||
}
|
||||
}
|
||||
@@ -149,6 +149,19 @@ Available actions:
|
||||
- **leaveGroup**: Leave a group chat (`chatGuid`)
|
||||
- **sendAttachment**: Send media/files (`to`, `buffer`, `filename`)
|
||||
|
||||
### Message IDs (short vs full)
|
||||
Clawdbot may surface *short* message IDs (e.g., `1`, `2`) to save tokens.
|
||||
- `MessageSid` / `ReplyToId` can be short IDs.
|
||||
- `MessageSidFull` / `ReplyToIdFull` contain the provider full IDs.
|
||||
- Short IDs are in-memory; they can expire on restart or cache eviction.
|
||||
- Actions accept short or full `messageId`, but short IDs will error if no longer available.
|
||||
|
||||
Use full IDs for durable automations and storage:
|
||||
- Templates: `{{MessageSidFull}}`, `{{ReplyToIdFull}}`
|
||||
- Context: `MessageSidFull` / `ReplyToIdFull` in inbound payloads
|
||||
|
||||
See [Configuration](/gateway/configuration) for template variables.
|
||||
|
||||
## Block streaming
|
||||
Control whether responses are sent as a single message or streamed in blocks:
|
||||
```json5
|
||||
|
||||
@@ -175,6 +175,7 @@ Notes:
|
||||
- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) also count as mentions for guild messages.
|
||||
- Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
|
||||
- If `channels` is present, any channel not listed is denied by default.
|
||||
- Use a `"*"` channel entry to apply defaults across all channels; explicit channel entries override the wildcard.
|
||||
- Threads inherit parent channel config (allowlist, `requireMention`, skills, prompts, etc.) unless you add the thread channel id explicitly.
|
||||
- Bot-authored messages are ignored by default; set `channels.discord.allowBots=true` to allow them (own messages remain filtered).
|
||||
- Warning: If you allow replies to other bots (`channels.discord.allowBots=true`), prevent bot-to-bot reply loops with `requireMention`, `channels.discord.guilds.*.channels.<id>.users` allowlists, and/or clear guardrails in `AGENTS.md` and `SOUL.md`.
|
||||
|
||||
@@ -484,6 +484,10 @@ The agent sees reactions as **system notifications** in the conversation history
|
||||
- Make sure your Telegram user ID is authorized (via pairing or `channels.telegram.allowFrom`)
|
||||
- Commands require authorization even in groups with `groupPolicy: "open"`
|
||||
|
||||
**Long-polling aborts immediately on Node 22+ (often with proxies/custom fetch):**
|
||||
- Node 22+ is stricter about `AbortSignal` instances; foreign signals can abort `fetch` calls right away.
|
||||
- Upgrade to a Clawdbot build that normalizes abort signals, or run the gateway on Node 20 until you can upgrade.
|
||||
|
||||
**Bot starts, then silently stops responding (or logs `HttpError: Network request ... failed`):**
|
||||
- Some hosts resolve `api.telegram.org` to IPv6 first. If your server does not have working IPv6 egress, grammY can get stuck on IPv6-only requests.
|
||||
- Fix by enabling IPv6 egress **or** forcing IPv4 resolution for `api.telegram.org` (for example, add an `/etc/hosts` entry using the IPv4 A record, or prefer IPv4 in your OS DNS stack), then restart the gateway.
|
||||
|
||||
@@ -7,8 +7,8 @@ read_when:
|
||||
|
||||
# `clawdbot approvals`
|
||||
|
||||
Manage exec approvals for the **gateway host** or a **node host**.
|
||||
By default, commands target the gateway. Use `--node` to edit a node’s approvals.
|
||||
Manage exec approvals for the **local host**, **gateway host**, or a **node host**.
|
||||
By default, commands target the local approvals file on disk. Use `--gateway` to target the gateway, or `--node` to target a specific node.
|
||||
|
||||
Related:
|
||||
- Exec approvals: [Exec approvals](/tools/exec-approvals)
|
||||
@@ -19,6 +19,7 @@ Related:
|
||||
```bash
|
||||
clawdbot approvals get
|
||||
clawdbot approvals get --node <id|name|ip>
|
||||
clawdbot approvals get --gateway
|
||||
```
|
||||
|
||||
## Replace approvals from a file
|
||||
@@ -26,6 +27,7 @@ clawdbot approvals get --node <id|name|ip>
|
||||
```bash
|
||||
clawdbot approvals set --file ./exec-approvals.json
|
||||
clawdbot approvals set --node <id|name|ip> --file ./exec-approvals.json
|
||||
clawdbot approvals set --gateway --file ./exec-approvals.json
|
||||
```
|
||||
|
||||
## Allowlist helpers
|
||||
@@ -33,6 +35,7 @@ clawdbot approvals set --node <id|name|ip> --file ./exec-approvals.json
|
||||
```bash
|
||||
clawdbot approvals allowlist add "~/Projects/**/bin/rg"
|
||||
clawdbot approvals allowlist add --agent main --node <id|name|ip> "/usr/bin/uptime"
|
||||
clawdbot approvals allowlist add --agent "*" "/usr/bin/uname"
|
||||
|
||||
clawdbot approvals allowlist remove "~/Projects/**/bin/rg"
|
||||
```
|
||||
@@ -40,5 +43,6 @@ clawdbot approvals allowlist remove "~/Projects/**/bin/rg"
|
||||
## Notes
|
||||
|
||||
- `--node` uses the same resolver as `clawdbot nodes` (id, name, ip, or id prefix).
|
||||
- `--agent` defaults to `"*"`, which applies to all agents.
|
||||
- The node host must advertise `system.execApprovals.get/set` (macOS app or headless node host).
|
||||
- Approvals files are stored per host at `~/.clawdbot/exec-approvals.json`.
|
||||
|
||||
@@ -8,6 +8,9 @@ read_when:
|
||||
|
||||
Interactive prompt to set up credentials, devices, and agent defaults.
|
||||
|
||||
Note: The **Model** section now includes a multi-select for the
|
||||
`agents.defaults.models` allowlist (what shows up in `/model` and the model picker).
|
||||
|
||||
Tip: `clawdbot config` without a subcommand opens the same wizard. Use
|
||||
`clawdbot config get|set|unset` for non-interactive edits.
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot daemon` (install/uninstall/status for the Gateway service)"
|
||||
read_when:
|
||||
- You want to run the Gateway as a background service
|
||||
- You’re debugging daemon install, status, or logs
|
||||
---
|
||||
|
||||
# `clawdbot daemon`
|
||||
|
||||
Manage the Gateway daemon (background service).
|
||||
|
||||
Note: `clawdbot service gateway …` is the preferred surface; `daemon` remains
|
||||
as a legacy alias for compatibility.
|
||||
|
||||
Related:
|
||||
- Gateway CLI: [Gateway](/cli/gateway)
|
||||
- macOS platform notes: [macOS](/platforms/macos)
|
||||
|
||||
Tip: run `clawdbot daemon --help` for platform-specific flags.
|
||||
|
||||
Notes:
|
||||
- `daemon status` supports `--json` for scripting.
|
||||
- `daemon install|uninstall|start|stop|restart` support `--json` for scripting (default output stays human-friendly).
|
||||
@@ -25,6 +25,12 @@ Run a local Gateway process:
|
||||
clawdbot gateway
|
||||
```
|
||||
|
||||
Foreground alias:
|
||||
|
||||
```bash
|
||||
clawdbot gateway run
|
||||
```
|
||||
|
||||
Notes:
|
||||
- By default, the Gateway refuses to start unless `gateway.mode=local` is set in `~/.clawdbot/clawdbot.json`. Use `--allow-unconfigured` for ad-hoc/dev runs.
|
||||
- Binding beyond loopback without auth is blocked (safety guardrail).
|
||||
@@ -34,7 +40,7 @@ Notes:
|
||||
### Options
|
||||
|
||||
- `--port <port>`: WebSocket port (default comes from config/env; usually `18789`).
|
||||
- `--bind <loopback|lan|tailnet|auto>`: listener bind mode.
|
||||
- `--bind <loopback|lan|tailnet|auto|custom>`: listener bind mode.
|
||||
- `--auth <token|password>`: auth mode override.
|
||||
- `--token <token>`: token override (also sets `CLAWDBOT_GATEWAY_TOKEN` for the process).
|
||||
- `--password <password>`: password override (also sets `CLAWDBOT_GATEWAY_PASSWORD` for the process).
|
||||
@@ -75,15 +81,32 @@ clawdbot gateway health --url ws://127.0.0.1:18789
|
||||
|
||||
### `gateway status`
|
||||
|
||||
`gateway status` is the “debug everything” command. It always probes:
|
||||
`gateway status` shows the Gateway service (launchd/systemd/schtasks) plus an optional RPC probe.
|
||||
|
||||
```bash
|
||||
clawdbot gateway status
|
||||
clawdbot gateway status --json
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--url <url>`: override the probe URL.
|
||||
- `--token <token>`: token auth for the probe.
|
||||
- `--password <password>`: password auth for the probe.
|
||||
- `--timeout <ms>`: probe timeout (default `10000`).
|
||||
- `--no-probe`: skip the RPC probe (service-only view).
|
||||
- `--deep`: scan system-level services too.
|
||||
|
||||
### `gateway probe`
|
||||
|
||||
`gateway probe` is the “debug everything” command. It always probes:
|
||||
- your configured remote gateway (if set), and
|
||||
- localhost (loopback) **even if remote is configured**.
|
||||
|
||||
If multiple gateways are reachable, it prints all of them. Multiple gateways are supported when you use isolated profiles/ports (e.g., a rescue bot), but most installs still run a single gateway.
|
||||
|
||||
```bash
|
||||
clawdbot gateway status
|
||||
clawdbot gateway status --json
|
||||
clawdbot gateway probe
|
||||
clawdbot gateway probe --json
|
||||
```
|
||||
|
||||
#### Remote over SSH (Mac app parity)
|
||||
@@ -93,7 +116,7 @@ The macOS app “Remote over SSH” mode uses a local port-forward so the remote
|
||||
CLI equivalent:
|
||||
|
||||
```bash
|
||||
clawdbot gateway status --ssh user@gateway-host
|
||||
clawdbot gateway probe --ssh user@gateway-host
|
||||
```
|
||||
|
||||
Options:
|
||||
@@ -114,6 +137,20 @@ clawdbot gateway call status
|
||||
clawdbot gateway call logs.tail --params '{"sinceMs": 60000}'
|
||||
```
|
||||
|
||||
## Manage the Gateway service
|
||||
|
||||
```bash
|
||||
clawdbot gateway install
|
||||
clawdbot gateway start
|
||||
clawdbot gateway stop
|
||||
clawdbot gateway restart
|
||||
clawdbot gateway uninstall
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `gateway install` supports `--port`, `--runtime`, `--token`, `--force`, `--json`.
|
||||
- Lifecycle commands accept `--json` for scripting.
|
||||
|
||||
## Discover gateways (Bonjour)
|
||||
|
||||
`gateway discover` scans for Gateway beacons (`_clawdbot-gw._tcp`).
|
||||
|
||||
@@ -28,8 +28,6 @@ This page describes the current CLI behavior. If commands change, update this do
|
||||
- [`health`](/cli/health)
|
||||
- [`sessions`](/cli/sessions)
|
||||
- [`gateway`](/cli/gateway)
|
||||
- [`daemon`](/cli/daemon)
|
||||
- [`service`](/cli/service)
|
||||
- [`logs`](/cli/logs)
|
||||
- [`models`](/cli/models)
|
||||
- [`memory`](/cli/memory)
|
||||
@@ -138,29 +136,14 @@ clawdbot [--dev] [--profile <name>] <command>
|
||||
call
|
||||
health
|
||||
status
|
||||
probe
|
||||
discover
|
||||
daemon
|
||||
status
|
||||
install
|
||||
uninstall
|
||||
start
|
||||
stop
|
||||
restart
|
||||
service
|
||||
gateway
|
||||
status
|
||||
install
|
||||
uninstall
|
||||
start
|
||||
stop
|
||||
restart
|
||||
node
|
||||
status
|
||||
install
|
||||
uninstall
|
||||
start
|
||||
stop
|
||||
restart
|
||||
run
|
||||
logs
|
||||
models
|
||||
list
|
||||
@@ -191,14 +174,13 @@ clawdbot [--dev] [--profile <name>] <command>
|
||||
nodes
|
||||
devices
|
||||
node
|
||||
run
|
||||
status
|
||||
install
|
||||
uninstall
|
||||
start
|
||||
daemon
|
||||
status
|
||||
install
|
||||
uninstall
|
||||
start
|
||||
stop
|
||||
restart
|
||||
stop
|
||||
restart
|
||||
approvals
|
||||
get
|
||||
set
|
||||
@@ -328,7 +310,7 @@ Options:
|
||||
- `--minimax-api-key <key>`
|
||||
- `--opencode-zen-api-key <key>`
|
||||
- `--gateway-port <port>`
|
||||
- `--gateway-bind <loopback|lan|tailnet|auto>`
|
||||
- `--gateway-bind <loopback|lan|tailnet|auto|custom>`
|
||||
- `--gateway-auth <off|token|password>`
|
||||
- `--gateway-token <token>`
|
||||
- `--gateway-password <password>`
|
||||
@@ -544,7 +526,7 @@ Options:
|
||||
- `--debug` (alias for `--verbose`)
|
||||
|
||||
Notes:
|
||||
- Overview includes Gateway + Node service status when available.
|
||||
- Overview includes Gateway + node host service status when available.
|
||||
|
||||
### Usage tracking
|
||||
Clawdbot can surface provider usage/quota when OAuth/API creds are available.
|
||||
@@ -614,7 +596,7 @@ Run the WebSocket Gateway.
|
||||
|
||||
Options:
|
||||
- `--port <port>`
|
||||
- `--bind <loopback|tailnet|lan|auto>`
|
||||
- `--bind <loopback|tailnet|lan|auto|custom>`
|
||||
- `--token <token>`
|
||||
- `--auth <token|password>`
|
||||
- `--password <password>`
|
||||
@@ -631,25 +613,25 @@ Options:
|
||||
- `--raw-stream`
|
||||
- `--raw-stream-path <path>`
|
||||
|
||||
### `daemon`
|
||||
### `gateway service`
|
||||
Manage the Gateway service (launchd/systemd/schtasks).
|
||||
|
||||
Subcommands:
|
||||
- `daemon status` (probes the Gateway RPC by default)
|
||||
- `daemon install` (service install)
|
||||
- `daemon uninstall`
|
||||
- `daemon start`
|
||||
- `daemon stop`
|
||||
- `daemon restart`
|
||||
- `gateway status` (probes the Gateway RPC by default)
|
||||
- `gateway install` (service install)
|
||||
- `gateway uninstall`
|
||||
- `gateway start`
|
||||
- `gateway stop`
|
||||
- `gateway restart`
|
||||
|
||||
Notes:
|
||||
- `daemon status` probes the Gateway RPC by default using the daemon’s resolved port/config (override with `--url/--token/--password`).
|
||||
- `daemon status` supports `--no-probe`, `--deep`, and `--json` for scripting.
|
||||
- `daemon status` also surfaces legacy or extra gateway services when it can detect them (`--deep` adds system-level scans). Profile-named Clawdbot services are treated as first-class and aren't flagged as "extra".
|
||||
- `daemon status` prints which config path the CLI uses vs which config the daemon likely uses (service env), plus the resolved probe target URL.
|
||||
- `daemon install|uninstall|start|stop|restart` support `--json` for scripting (default output stays human-friendly).
|
||||
- `daemon install` defaults to Node runtime; bun is **not recommended** (WhatsApp/Telegram bugs).
|
||||
- `daemon install` options: `--port`, `--runtime`, `--token`, `--force`, `--json`.
|
||||
- `gateway status` probes the Gateway RPC by default using the service’s resolved port/config (override with `--url/--token/--password`).
|
||||
- `gateway status` supports `--no-probe`, `--deep`, and `--json` for scripting.
|
||||
- `gateway status` also surfaces legacy or extra gateway services when it can detect them (`--deep` adds system-level scans). Profile-named Clawdbot services are treated as first-class and aren't flagged as "extra".
|
||||
- `gateway status` prints which config path the CLI uses vs which config the service likely uses (service env), plus the resolved probe target URL.
|
||||
- `gateway install|uninstall|start|stop|restart` support `--json` for scripting (default output stays human-friendly).
|
||||
- `gateway install` defaults to Node runtime; bun is **not recommended** (WhatsApp/Telegram bugs).
|
||||
- `gateway install` options: `--port`, `--runtime`, `--token`, `--force`, `--json`.
|
||||
|
||||
### `logs`
|
||||
Tail Gateway file logs via RPC.
|
||||
@@ -668,13 +650,16 @@ clawdbot logs --no-color
|
||||
```
|
||||
|
||||
### `gateway <subcommand>`
|
||||
Gateway RPC helpers (use `--url`, `--token`, `--password`, `--timeout`, `--expect-final` for each).
|
||||
Gateway CLI helpers (use `--url`, `--token`, `--password`, `--timeout`, `--expect-final` for RPC subcommands).
|
||||
|
||||
Subcommands:
|
||||
- `gateway call <method> [--params <json>]`
|
||||
- `gateway health`
|
||||
- `gateway status`
|
||||
- `gateway probe`
|
||||
- `gateway discover`
|
||||
- `gateway install|uninstall|start|stop|restart`
|
||||
- `gateway run`
|
||||
|
||||
Common RPCs:
|
||||
- `config.apply` (validate + write config + restart + wake)
|
||||
@@ -806,16 +791,13 @@ All `cron` commands accept `--url`, `--token`, `--timeout`, `--expect-final`.
|
||||
[`clawdbot node`](/cli/node).
|
||||
|
||||
Subcommands:
|
||||
- `node start --host <gateway-host> --port 18790`
|
||||
- `node service status`
|
||||
- `node service install [--host <gateway-host>] [--port <port>] [--tls] [--tls-fingerprint <sha256>] [--node-id <id>] [--display-name <name>] [--runtime <node|bun>] [--force]`
|
||||
- `node service uninstall`
|
||||
- `node service start`
|
||||
- `node service stop`
|
||||
- `node service restart`
|
||||
|
||||
Legacy alias:
|
||||
- `node daemon …` (same as `node service …`)
|
||||
- `node run --host <gateway-host> --port 18790`
|
||||
- `node status`
|
||||
- `node install [--host <gateway-host>] [--port <port>] [--tls] [--tls-fingerprint <sha256>] [--node-id <id>] [--display-name <name>] [--runtime <node|bun>] [--force]`
|
||||
- `node uninstall`
|
||||
- `node run`
|
||||
- `node stop`
|
||||
- `node restart`
|
||||
|
||||
## Nodes
|
||||
|
||||
|
||||
@@ -23,10 +23,10 @@ Common use cases:
|
||||
Execution is still guarded by **exec approvals** and per‑agent allowlists on the
|
||||
node host, so you can keep command access scoped and explicit.
|
||||
|
||||
## Start (foreground)
|
||||
## Run (foreground)
|
||||
|
||||
```bash
|
||||
clawdbot node start --host <gateway-host> --port 18790
|
||||
clawdbot node run --host <gateway-host> --port 18790
|
||||
```
|
||||
|
||||
Options:
|
||||
@@ -42,9 +42,7 @@ Options:
|
||||
Install a headless node host as a user service.
|
||||
|
||||
```bash
|
||||
clawdbot node service install --host <gateway-host> --port 18790
|
||||
# or
|
||||
clawdbot service node install --host <gateway-host> --port 18790
|
||||
clawdbot node install --host <gateway-host> --port 18790
|
||||
```
|
||||
|
||||
Options:
|
||||
@@ -61,18 +59,10 @@ Manage the service:
|
||||
|
||||
```bash
|
||||
clawdbot node status
|
||||
clawdbot service node status
|
||||
clawdbot node service status
|
||||
clawdbot node service start
|
||||
clawdbot node service stop
|
||||
clawdbot node service restart
|
||||
clawdbot node service uninstall
|
||||
```
|
||||
|
||||
Legacy alias:
|
||||
|
||||
```bash
|
||||
clawdbot node daemon status
|
||||
clawdbot node run
|
||||
clawdbot node stop
|
||||
clawdbot node restart
|
||||
clawdbot node uninstall
|
||||
```
|
||||
|
||||
## Pairing
|
||||
|
||||
@@ -36,4 +36,19 @@ filter to nodes that connected within a duration (e.g. `24h`, `7d`).
|
||||
```bash
|
||||
clawdbot nodes invoke --node <id|name|ip> --command <command> --params <json>
|
||||
clawdbot nodes run --node <id|name|ip> <command...>
|
||||
clawdbot nodes run --raw "git status"
|
||||
clawdbot nodes run --agent main --node <id|name|ip> --raw "git status"
|
||||
```
|
||||
|
||||
### Exec-style defaults
|
||||
|
||||
`nodes run` mirrors the model’s exec behavior (defaults + approvals):
|
||||
|
||||
- Reads `tools.exec.*` (plus `agents.list[].tools.exec.*` overrides).
|
||||
- Uses exec approvals (`exec.approval.request`) before invoking `system.run`.
|
||||
- `--node` can be omitted when `tools.exec.node` is set.
|
||||
|
||||
Flags:
|
||||
- `--raw <command>`: run a shell string (`/bin/sh -lc` or `cmd.exe /c`).
|
||||
- `--agent <id>`: agent-scoped approvals/allowlists (defaults to configured agent).
|
||||
- `--ask <off|on-miss|always>`, `--security <deny|allowlist|full>`: overrides.
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot service` (manage gateway + node services)"
|
||||
read_when:
|
||||
- You want to manage Gateway or node services cross-platform
|
||||
- You want a single surface for start/stop/install/uninstall
|
||||
---
|
||||
|
||||
# `clawdbot service`
|
||||
|
||||
Manage the **Gateway** service and **node host** services.
|
||||
|
||||
Related:
|
||||
- Gateway daemon (legacy alias): [Daemon](/cli/daemon)
|
||||
- Node host: [Node](/cli/node)
|
||||
|
||||
## Gateway service
|
||||
|
||||
```bash
|
||||
clawdbot service gateway status
|
||||
clawdbot service gateway install --port 18789
|
||||
clawdbot service gateway start
|
||||
clawdbot service gateway stop
|
||||
clawdbot service gateway restart
|
||||
clawdbot service gateway uninstall
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `service gateway status` supports `--json` and `--deep` for system checks.
|
||||
- `service gateway install` supports `--runtime node|bun` and `--token`.
|
||||
|
||||
## Node host service
|
||||
|
||||
```bash
|
||||
clawdbot service node status
|
||||
clawdbot service node install --host <gateway-host> --port 18790
|
||||
clawdbot service node start
|
||||
clawdbot service node stop
|
||||
clawdbot service node restart
|
||||
clawdbot service node uninstall
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `service node install` supports `--runtime node|bun`, `--node-id`, `--display-name`,
|
||||
and TLS options (`--tls`, `--tls-fingerprint`).
|
||||
|
||||
## Aliases
|
||||
|
||||
- `clawdbot daemon …` → `clawdbot service gateway …`
|
||||
- `clawdbot node service …` → `clawdbot service node …`
|
||||
- `clawdbot node status` → `clawdbot service node status`
|
||||
- `clawdbot node daemon …` → `clawdbot service node …` (legacy)
|
||||
@@ -19,6 +19,6 @@ clawdbot status --usage
|
||||
Notes:
|
||||
- `--deep` runs live probes (WhatsApp Web + Telegram + Discord + Slack + Signal).
|
||||
- Output includes per-agent session stores when multiple agents are configured.
|
||||
- Overview includes Gateway + Node service install/runtime status when available.
|
||||
- Overview includes Gateway + node host service install/runtime status when available.
|
||||
- Overview includes update channel + git SHA (for source checkouts).
|
||||
- Update info surfaces in the Overview; if an update is available, status prints a hint to run `clawdbot update` (see [Updating](/install/updating)).
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot update` (safe-ish source update + optional daemon restart)"
|
||||
summary: "CLI reference for `clawdbot update` (safe-ish source update + optional gateway restart)"
|
||||
read_when:
|
||||
- You want to update a source checkout safely
|
||||
- You need to understand `--update` shorthand behavior
|
||||
@@ -7,9 +7,9 @@ read_when:
|
||||
|
||||
# `clawdbot update`
|
||||
|
||||
Safely update a **source checkout** (git install) of Clawdbot.
|
||||
Safely update Clawdbot and switch between stable/beta/dev channels.
|
||||
|
||||
If you installed via **npm/pnpm** (global install, no git metadata), use the package manager flow in [Updating](/install/updating).
|
||||
If you installed via **npm/pnpm** (global install, no git metadata), updates happen via the package manager flow in [Updating](/install/updating).
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -26,7 +26,7 @@ clawdbot --update
|
||||
|
||||
## Options
|
||||
|
||||
- `--restart`: restart the Gateway daemon after a successful update.
|
||||
- `--restart`: restart the Gateway service after a successful update.
|
||||
- `--channel <stable|beta|dev>`: set the update channel (git + npm; persisted in config).
|
||||
- `--tag <dist-tag|version>`: override the npm dist-tag or version for this update only.
|
||||
- `--json`: print machine-readable `UpdateRunResult` JSON.
|
||||
@@ -48,7 +48,16 @@ Options:
|
||||
- `--json`: print machine-readable status JSON.
|
||||
- `--timeout <seconds>`: timeout for checks (default is 3s).
|
||||
|
||||
## What it does (git checkout)
|
||||
## What it does
|
||||
|
||||
When you switch channels explicitly (`--channel ...`), Clawdbot also keeps the
|
||||
install method aligned:
|
||||
|
||||
- `dev` → ensures a git checkout (default: `~/clawdbot`, override with `CLAWDBOT_GIT_DIR`),
|
||||
updates it, and installs the global CLI from that checkout.
|
||||
- `stable`/`beta` → installs from npm using the matching dist-tag.
|
||||
|
||||
## Git checkout flow
|
||||
|
||||
Channels:
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ Local mode:
|
||||
|
||||
- File type: Markdown only (`MEMORY.md`, `memory/**/*.md`).
|
||||
- Index storage: per-agent SQLite at `~/.clawdbot/memory/<agentId>.sqlite` (configurable via `agents.defaults.memorySearch.store.path`, supports `{agentId}` token).
|
||||
- Freshness: watcher on `MEMORY.md` + `memory/` marks the index dirty (debounce 1.5s). Sync runs on session start, on first search when dirty, and optionally on an interval.
|
||||
- Freshness: watcher on `MEMORY.md` + `memory/` marks the index dirty (debounce 1.5s). Sync is scheduled on session start, on search, or on an interval and runs asynchronously. Session transcripts use delta thresholds to trigger background sync.
|
||||
- Reindex triggers: the index stores the embedding **provider/model + endpoint fingerprint + chunking params**. If any of those change, Clawdbot automatically resets and reindexes the entire store.
|
||||
|
||||
### Hybrid search (BM25 + vector)
|
||||
@@ -299,11 +299,29 @@ agents: {
|
||||
|
||||
Notes:
|
||||
- Session indexing is **opt-in** (off by default).
|
||||
- Session updates are debounced and indexed lazily on the next `memory_search` (or manual `clawdbot memory index`).
|
||||
- Session updates are debounced and **indexed asynchronously** once they cross delta thresholds (best-effort).
|
||||
- `memory_search` never blocks on indexing; results can be slightly stale until background sync finishes.
|
||||
- Results still include snippets only; `memory_get` remains limited to memory files.
|
||||
- Session indexing is isolated per agent (only that agent’s session logs are indexed).
|
||||
- Session logs live on disk (`~/.clawdbot/agents/<agentId>/sessions/*.jsonl`). Any process/user with filesystem access can read them, so treat disk access as the trust boundary. For stricter isolation, run agents under separate OS users or hosts.
|
||||
|
||||
Delta thresholds (defaults shown):
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
sync: {
|
||||
sessions: {
|
||||
deltaBytes: 100000, // ~100 KB
|
||||
deltaMessages: 50 // JSONL lines
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SQLite vector acceleration (sqlite-vec)
|
||||
|
||||
When the sqlite-vec extension is available, Clawdbot stores embeddings in a
|
||||
|
||||
@@ -9,8 +9,22 @@ read_when:
|
||||
Session pruning trims **old tool results** from the in-memory context right before each LLM call. It does **not** rewrite the on-disk session history (`*.jsonl`).
|
||||
|
||||
## When it runs
|
||||
- Before each LLM request (context hook).
|
||||
- When `mode: "cache-ttl"` is enabled and the last Anthropic call for the session is older than `ttl`.
|
||||
- Only affects the messages sent to the model for that request.
|
||||
- Only active for Anthropic API calls (and OpenRouter Anthropic models).
|
||||
- For best results, match `ttl` to your model `cacheControlTtl`.
|
||||
- After a prune, the TTL window resets so subsequent requests keep cache until `ttl` expires again.
|
||||
|
||||
## Smart defaults (Anthropic)
|
||||
- **OAuth or setup-token** profiles: enable `cache-ttl` pruning and set heartbeat to `1h`.
|
||||
- **API key** profiles: enable `cache-ttl` pruning, set heartbeat to `30m`, and default `cacheControlTtl` to `1h` on Anthropic models.
|
||||
- If you set any of these values explicitly, Clawdbot does **not** override them.
|
||||
|
||||
## What this improves (cost + cache behavior)
|
||||
- **Why prune:** Anthropic prompt caching only applies within the TTL. If a session goes idle past the TTL, the next request re-caches the full prompt unless you trim it first.
|
||||
- **What gets cheaper:** pruning reduces the **cacheWrite** size for that first request after the TTL expires.
|
||||
- **Why the TTL reset matters:** once pruning runs, the cache window resets, so follow‑up requests can reuse the freshly cached prompt instead of re-caching the full history again.
|
||||
- **What it does not do:** pruning doesn’t add tokens or “double” costs; it only changes what gets cached on that first post‑TTL request.
|
||||
|
||||
## What can be pruned
|
||||
- Only `toolResult` messages.
|
||||
@@ -26,14 +40,10 @@ Pruning uses an estimated context window (chars ≈ tokens × 4). The window siz
|
||||
3) `agents.defaults.contextTokens`.
|
||||
4) Default `200000` tokens.
|
||||
|
||||
## Modes
|
||||
### adaptive
|
||||
- If estimated context ratio ≥ `softTrimRatio`: soft-trim oversized tool results.
|
||||
- If still ≥ `hardClearRatio` **and** prunable tool text ≥ `minPrunableToolChars`: hard-clear oldest eligible tool results.
|
||||
|
||||
### aggressive
|
||||
- Always hard-clears eligible tool results before the cutoff.
|
||||
- Ignores `hardClear.enabled` (always clears when eligible).
|
||||
## Mode
|
||||
### cache-ttl
|
||||
- Pruning only runs if the last Anthropic call is older than `ttl` (default `5m`).
|
||||
- When it runs: same soft-trim + hard-clear behavior as before.
|
||||
|
||||
## Soft vs hard pruning
|
||||
- **Soft-trim**: only for oversized tool results.
|
||||
@@ -52,6 +62,7 @@ Pruning uses an estimated context window (chars ≈ tokens × 4). The window siz
|
||||
- Compaction is separate: compaction summarizes and persists, pruning is transient per request. See [/concepts/compaction](/concepts/compaction).
|
||||
|
||||
## Defaults (when enabled)
|
||||
- `ttl`: `"5m"`
|
||||
- `keepLastAssistants`: `3`
|
||||
- `softTrimRatio`: `0.3`
|
||||
- `hardClearRatio`: `0.5`
|
||||
@@ -60,16 +71,7 @@ Pruning uses an estimated context window (chars ≈ tokens × 4). The window siz
|
||||
- `hardClear`: `{ enabled: true, placeholder: "[Old tool result content cleared]" }`
|
||||
|
||||
## Examples
|
||||
Default (adaptive):
|
||||
```json5
|
||||
{
|
||||
agent: {
|
||||
contextPruning: { mode: "adaptive" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To disable:
|
||||
Default (off):
|
||||
```json5
|
||||
{
|
||||
agent: {
|
||||
@@ -78,11 +80,11 @@ To disable:
|
||||
}
|
||||
```
|
||||
|
||||
Aggressive:
|
||||
Enable TTL-aware pruning:
|
||||
```json5
|
||||
{
|
||||
agent: {
|
||||
contextPruning: { mode: "aggressive" }
|
||||
contextPruning: { mode: "cache-ttl", ttl: "5m" }
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -92,7 +94,7 @@ Restrict pruning to specific tools:
|
||||
{
|
||||
agent: {
|
||||
contextPruning: {
|
||||
mode: "adaptive",
|
||||
mode: "cache-ttl",
|
||||
tools: { allow: ["exec", "read"], deny: ["*image*"] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ the workspace is writable. See [Memory](/concepts/memory) and
|
||||
- Idle reset (optional): `idleMinutes` adds a sliding idle window. When both daily and idle resets are configured, **whichever expires first** forces a new session.
|
||||
- Legacy idle-only: if you set `session.idleMinutes` without any `session.reset`/`resetByType` config, Clawdbot stays in idle-only mode for backward compatibility.
|
||||
- Per-type overrides (optional): `resetByType` lets you override the policy for `dm`, `group`, and `thread` sessions (thread = Slack/Discord threads, Telegram topics, Matrix threads when provided by the connector).
|
||||
- Per-channel overrides (optional): `resetByChannel` overrides the reset policy for a channel (applies to all session types for that channel and takes precedence over `reset`/`resetByType`).
|
||||
- Reset triggers: exact `/new` or `/reset` (plus any extras in `resetTriggers`) start a fresh session id and pass the remainder of the message through. `/new <model>` accepts a model alias, `provider/model`, or provider name (fuzzy match) to set the new session model. If `/new` or `/reset` is sent alone, Clawdbot runs a short “hello” greeting turn to confirm the reset.
|
||||
- Manual reset: delete specific keys from the store or remove the JSONL transcript; the next message recreates them.
|
||||
- Isolated cron jobs always mint a fresh `sessionId` per run (no idle reuse).
|
||||
@@ -109,6 +110,9 @@ Send these as standalone messages so they register.
|
||||
dm: { mode: "idle", idleMinutes: 240 },
|
||||
group: { mode: "idle", idleMinutes: 120 }
|
||||
},
|
||||
resetByChannel: {
|
||||
discord: { mode: "idle", idleMinutes: 10080 }
|
||||
},
|
||||
resetTriggers: ["/new", "/reset"],
|
||||
store: "~/.clawdbot/agents/{agentId}/sessions/sessions.json",
|
||||
mainKey: "main",
|
||||
|
||||
@@ -100,7 +100,7 @@ CLAWDBOT_PROFILE=dev clawdbot gateway --dev --reset
|
||||
Tip: if a non‑dev gateway is already running (launchd/systemd), stop it first:
|
||||
|
||||
```bash
|
||||
clawdbot daemon stop
|
||||
clawdbot gateway stop
|
||||
```
|
||||
|
||||
## Raw stream logging (Clawdbot)
|
||||
|
||||
@@ -829,8 +829,6 @@
|
||||
"cli/nodes",
|
||||
"cli/approvals",
|
||||
"cli/gateway",
|
||||
"cli/daemon",
|
||||
"cli/service",
|
||||
"cli/tui",
|
||||
"cli/voicecall",
|
||||
"cli/wake",
|
||||
|
||||
@@ -151,7 +151,9 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
|
||||
atHour: 4,
|
||||
idleMinutes: 60
|
||||
},
|
||||
heartbeatIdleMinutes: 120,
|
||||
resetByChannel: {
|
||||
discord: { mode: "idle", idleMinutes: 10080 }
|
||||
},
|
||||
resetTriggers: ["/new", "/reset"],
|
||||
store: "~/.clawdbot/agents/default/sessions/sessions.json",
|
||||
typingIntervalSeconds: 5,
|
||||
|
||||
@@ -24,7 +24,7 @@ Unknown keys, malformed types, or invalid values cause the Gateway to **refuse t
|
||||
|
||||
When validation fails:
|
||||
- The Gateway does not boot.
|
||||
- Only diagnostic commands are allowed (for example: `clawdbot doctor`, `clawdbot logs`, `clawdbot health`, `clawdbot status`, `clawdbot service`, `clawdbot help`).
|
||||
- Only diagnostic commands are allowed (for example: `clawdbot doctor`, `clawdbot logs`, `clawdbot health`, `clawdbot status`, `clawdbot gateway status`, `clawdbot gateway probe`, `clawdbot help`).
|
||||
- Run `clawdbot doctor` to see the exact issues.
|
||||
- Run `clawdbot doctor --fix` (or `--yes`) to apply migrations/repairs.
|
||||
|
||||
@@ -1414,7 +1414,7 @@ Each `agents.defaults.models` entry can include:
|
||||
- `alias` (optional model shortcut, e.g. `/opus`).
|
||||
- `params` (optional provider-specific API params passed through to the model request).
|
||||
|
||||
`params` is also applied to streaming runs (embedded agent + compaction). Supported keys today: `temperature`, `maxTokens`, `cacheControlTtl` (`"5m"` or `"1h"`, Anthropic API + OpenRouter Anthropic models only; ignored for Anthropic OAuth/Claude Code tokens). These merge with call-time options; caller-supplied values win. `temperature` is an advanced knob—leave unset unless you know the model’s defaults and need a change. Anthropic API defaults to `"1h"` unless you override (`cacheControlTtl: "5m"`). Clawdbot includes the `extended-cache-ttl-2025-04-11` beta flag for Anthropic API; keep it if you override provider headers.
|
||||
`params` is also applied to streaming runs (embedded agent + compaction). Supported keys today: `temperature`, `maxTokens`, `cacheControlTtl` (`"5m"` or `"1h"`, Anthropic API + OpenRouter Anthropic models only; ignored for Anthropic OAuth/Claude Code tokens). These merge with call-time options; caller-supplied values win. `temperature` is an advanced knob—leave unset unless you know the model’s defaults and need a change. Clawdbot includes the `extended-cache-ttl-2025-04-11` beta flag for Anthropic API; keep it if you override provider headers.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -1569,7 +1569,7 @@ Example:
|
||||
}
|
||||
```
|
||||
|
||||
#### `agents.defaults.contextPruning` (tool-result pruning)
|
||||
#### `agents.defaults.contextPruning` (TTL-aware tool-result pruning)
|
||||
|
||||
`agents.defaults.contextPruning` prunes **old tool results** from the in-memory context right before a request is sent to the LLM.
|
||||
It does **not** modify the session history on disk (`*.jsonl` remains complete).
|
||||
@@ -1580,11 +1580,9 @@ High level:
|
||||
- Never touches user/assistant messages.
|
||||
- Protects the last `keepLastAssistants` assistant messages (no tool results after that point are pruned).
|
||||
- Protects the bootstrap prefix (nothing before the first user message is pruned).
|
||||
- Modes:
|
||||
- `adaptive`: soft-trims oversized tool results (keep head/tail) when the estimated context ratio crosses `softTrimRatio`.
|
||||
Then hard-clears the oldest eligible tool results when the estimated context ratio crosses `hardClearRatio` **and**
|
||||
there’s enough prunable tool-result bulk (`minPrunableToolChars`).
|
||||
- `aggressive`: always replaces eligible tool results before the cutoff with the `hardClear.placeholder` (no ratio checks).
|
||||
- Mode:
|
||||
- `cache-ttl`: pruning only runs when the last Anthropic call for the session is **older** than `ttl`.
|
||||
When it runs, it uses the same soft-trim + hard-clear behavior as before.
|
||||
|
||||
Soft vs hard pruning (what changes in the context sent to the LLM):
|
||||
- **Soft-trim**: only for *oversized* tool results. Keeps the beginning + end and inserts `...` in the middle.
|
||||
@@ -1598,44 +1596,41 @@ Notes / current limitations:
|
||||
- Tool results containing **image blocks are skipped** (never trimmed/cleared) right now.
|
||||
- The estimated “context ratio” is based on **characters** (approximate), not exact tokens.
|
||||
- If the session doesn’t contain at least `keepLastAssistants` assistant messages yet, pruning is skipped.
|
||||
- In `aggressive` mode, `hardClear.enabled` is ignored (eligible tool results are always replaced with `hardClear.placeholder`).
|
||||
- `cache-ttl` only activates for Anthropic API calls (and OpenRouter Anthropic models).
|
||||
- After a prune, the TTL window resets so subsequent requests keep cache until `ttl` expires again.
|
||||
- For best results, match `contextPruning.ttl` to the model `cacheControlTtl` you set in `agents.defaults.models.*.params`.
|
||||
|
||||
Default (adaptive):
|
||||
```json5
|
||||
{
|
||||
agents: { defaults: { contextPruning: { mode: "adaptive" } } }
|
||||
}
|
||||
```
|
||||
|
||||
To disable:
|
||||
Default (off, unless Anthropic auth profiles are detected):
|
||||
```json5
|
||||
{
|
||||
agents: { defaults: { contextPruning: { mode: "off" } } }
|
||||
}
|
||||
```
|
||||
|
||||
Defaults (when `mode` is `"adaptive"` or `"aggressive"`):
|
||||
- `keepLastAssistants`: `3`
|
||||
- `softTrimRatio`: `0.3` (adaptive only)
|
||||
- `hardClearRatio`: `0.5` (adaptive only)
|
||||
- `minPrunableToolChars`: `50000` (adaptive only)
|
||||
- `softTrim`: `{ maxChars: 4000, headChars: 1500, tailChars: 1500 }` (adaptive only)
|
||||
- `hardClear`: `{ enabled: true, placeholder: "[Old tool result content cleared]" }`
|
||||
|
||||
Example (aggressive, minimal):
|
||||
Enable TTL-aware pruning:
|
||||
```json5
|
||||
{
|
||||
agents: { defaults: { contextPruning: { mode: "aggressive" } } }
|
||||
agents: { defaults: { contextPruning: { mode: "cache-ttl" } } }
|
||||
}
|
||||
```
|
||||
|
||||
Example (adaptive tuned):
|
||||
Defaults (when `mode` is `"cache-ttl"`):
|
||||
- `ttl`: `"5m"`
|
||||
- `keepLastAssistants`: `3`
|
||||
- `softTrimRatio`: `0.3`
|
||||
- `hardClearRatio`: `0.5`
|
||||
- `minPrunableToolChars`: `50000`
|
||||
- `softTrim`: `{ maxChars: 4000, headChars: 1500, tailChars: 1500 }`
|
||||
- `hardClear`: `{ enabled: true, placeholder: "[Old tool result content cleared]" }`
|
||||
|
||||
Example (cache-ttl tuned):
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
contextPruning: {
|
||||
mode: "adaptive",
|
||||
mode: "cache-ttl",
|
||||
ttl: "5m",
|
||||
keepLastAssistants: 3,
|
||||
softTrimRatio: 0.3,
|
||||
hardClearRatio: 0.5,
|
||||
@@ -1742,6 +1737,10 @@ Z.AI models are available as `zai/<model>` (e.g. `zai/glm-4.7`) and require
|
||||
`30m`. Set `0m` to disable.
|
||||
- `model`: optional override model for heartbeat runs (`provider/model`).
|
||||
- `includeReasoning`: when `true`, heartbeats will also deliver the separate `Reasoning:` message when available (same shape as `/reasoning on`). Default: `false`.
|
||||
- `activeHours`: optional local-time window that controls when heartbeats run.
|
||||
- `start`: start time (HH:MM, 24h). Inclusive.
|
||||
- `end`: end time (HH:MM, 24h). Exclusive. Use `"24:00"` for end-of-day.
|
||||
- `timezone`: `"user"` (default), `"local"`, or an IANA timezone id.
|
||||
- `target`: optional delivery channel (`last`, `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `none`). Default: `last`.
|
||||
- `to`: optional recipient override (channel-specific id, e.g. E.164 for WhatsApp, chat id for Telegram).
|
||||
- `prompt`: optional override for the heartbeat body (default: `Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`). Overrides are sent verbatim; include a `Read HEARTBEAT.md` line if you still want the file read.
|
||||
@@ -2453,6 +2452,9 @@ Controls session scoping, reset policy, reset triggers, and where the session st
|
||||
dm: { mode: "idle", idleMinutes: 240 },
|
||||
group: { mode: "idle", idleMinutes: 120 }
|
||||
},
|
||||
resetByChannel: {
|
||||
discord: { mode: "idle", idleMinutes: 10080 }
|
||||
},
|
||||
resetTriggers: ["/new", "/reset"],
|
||||
// Default is already per-agent under ~/.clawdbot/agents/<agentId>/sessions/sessions.json
|
||||
// You can override with {agentId} templating:
|
||||
@@ -2488,7 +2490,7 @@ Fields:
|
||||
- `idleMinutes`: sliding idle window in minutes. When daily + idle are both configured, whichever expires first wins.
|
||||
- `resetByType`: per-session overrides for `dm`, `group`, and `thread`.
|
||||
- If you only set legacy `session.idleMinutes` without any `reset`/`resetByType`, Clawdbot stays in idle-only mode for backward compatibility.
|
||||
- `heartbeatIdleMinutes`: optional idle override for heartbeat checks (daily reset still applies when enabled).
|
||||
- `resetByChannel`: channel-specific reset policy overrides (keyed by channel id, applies to all session types for that channel; overrides `reset`/`resetByType`).
|
||||
- `agentToAgent.maxPingPongTurns`: max reply-back turns between requester/target (0–5, default 5).
|
||||
- `sendPolicy.default`: `allow` or `deny` fallback when no rule matches.
|
||||
- `sendPolicy.rules[]`: match by `channel`, `chatType` (`direct|group|room`), or `keyPrefix` (e.g. `cron:`). First deny wins; otherwise allow.
|
||||
@@ -2645,6 +2647,13 @@ Defaults:
|
||||
- bind: `loopback`
|
||||
- port: `18789` (single port for WS + HTTP)
|
||||
|
||||
Bind modes:
|
||||
- `loopback`: `127.0.0.1` (local-only)
|
||||
- `lan`: `0.0.0.0` (all interfaces)
|
||||
- `tailnet`: Tailscale IPv4 address (100.64.0.0/10)
|
||||
- `auto`: prefer loopback, fall back to LAN if loopback cannot bind
|
||||
- `custom`: `gateway.customBindHost` (IPv4), fallback to LAN if unavailable
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
@@ -2675,7 +2684,7 @@ Notes:
|
||||
- OpenAI Chat Completions endpoint: **disabled by default**; enable with `gateway.http.endpoints.chatCompletions.enabled: true`.
|
||||
- OpenResponses endpoint: **disabled by default**; enable with `gateway.http.endpoints.responses.enabled: true`.
|
||||
- Precedence: `--port` > `CLAWDBOT_GATEWAY_PORT` > `gateway.port` > default `18789`.
|
||||
- Non-loopback binds (`lan`/`tailnet`/`auto`) require auth. Use `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`).
|
||||
- Non-loopback binds (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) require auth. Use `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`).
|
||||
- The onboarding wizard generates a gateway token by default (even on loopback).
|
||||
- `gateway.remote.token` is **only** for remote CLI calls; it does not enable local gateway auth. `gateway.token` is ignored.
|
||||
|
||||
@@ -3022,6 +3031,9 @@ Template placeholders are expanded in `tools.media.*.models[].args` and `tools.m
|
||||
| `{{From}}` | Sender identifier (E.164 for WhatsApp; may differ per channel) |
|
||||
| `{{To}}` | Destination identifier |
|
||||
| `{{MessageSid}}` | Channel message id (when available) |
|
||||
| `{{MessageSidFull}}` | Provider-specific full message id when `MessageSid` is shortened |
|
||||
| `{{ReplyToId}}` | Reply-to message id (when available) |
|
||||
| `{{ReplyToIdFull}}` | Provider-specific full reply-to id when `ReplyToId` is shortened |
|
||||
| `{{SessionId}}` | Current session UUID |
|
||||
| `{{IsNewSession}}` | `"true"` when a new session was created |
|
||||
| `{{MediaUrl}}` | Inbound media pseudo-URL (if present) |
|
||||
|
||||
@@ -225,10 +225,10 @@ Notes:
|
||||
- `clawdbot doctor --yes` accepts the default repair prompts.
|
||||
- `clawdbot doctor --repair` applies recommended fixes without prompts.
|
||||
- `clawdbot doctor --repair --force` overwrites custom supervisor configs.
|
||||
- You can always force a full rewrite via `clawdbot daemon install --force`.
|
||||
- You can always force a full rewrite via `clawdbot gateway install --force`.
|
||||
|
||||
### 16) Gateway runtime + port diagnostics
|
||||
Doctor inspects the daemon runtime (PID, last exit status) and warns when the
|
||||
Doctor inspects the service runtime (PID, last exit status) and warns when the
|
||||
service is installed but not actually running. It also checks for port collisions
|
||||
on the gateway port (default `18789`) and reports likely causes (gateway already
|
||||
running, SSH tunnel).
|
||||
@@ -236,7 +236,7 @@ running, SSH tunnel).
|
||||
### 17) Gateway runtime best practices
|
||||
Doctor warns when the gateway service runs on Bun or a version-managed Node path
|
||||
(`nvm`, `fnm`, `volta`, `asdf`, etc.). WhatsApp + Telegram channels require Node,
|
||||
and version-manager paths can break after upgrades because the daemon does not
|
||||
and version-manager paths can break after upgrades because the service does not
|
||||
load your shell init. Doctor offers to migrate to a system Node install when
|
||||
available (Homebrew/apt/choco).
|
||||
|
||||
|
||||
@@ -10,10 +10,11 @@ surface anything that needs attention without spamming you.
|
||||
|
||||
## Quick start (beginner)
|
||||
|
||||
1. Leave heartbeats enabled (default is `30m`) or set your own cadence.
|
||||
1. Leave heartbeats enabled (default is `30m`, or `1h` for Anthropic OAuth/setup-token) or set your own cadence.
|
||||
2. Create a tiny `HEARTBEAT.md` checklist in the agent workspace (optional but recommended).
|
||||
3. Decide where heartbeat messages should go (`target: "last"` is the default).
|
||||
4. Optional: enable heartbeat reasoning delivery for transparency.
|
||||
5. Optional: restrict heartbeats to active hours (local time).
|
||||
|
||||
Example config:
|
||||
|
||||
@@ -24,6 +25,7 @@ Example config:
|
||||
heartbeat: {
|
||||
every: "30m",
|
||||
target: "last",
|
||||
// activeHours: { start: "08:00", end: "24:00" },
|
||||
// includeReasoning: true, // optional: send separate `Reasoning:` message too
|
||||
}
|
||||
}
|
||||
@@ -33,11 +35,13 @@ Example config:
|
||||
|
||||
## Defaults
|
||||
|
||||
- Interval: `30m` (set `agents.defaults.heartbeat.every` or per-agent `agents.list[].heartbeat.every`; use `0m` to disable).
|
||||
- Interval: `30m` (or `1h` when Anthropic OAuth/setup-token is the detected auth mode). Set `agents.defaults.heartbeat.every` or per-agent `agents.list[].heartbeat.every`; use `0m` to disable.
|
||||
- Prompt body (configurable via `agents.defaults.heartbeat.prompt`):
|
||||
`Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`
|
||||
- The heartbeat prompt is sent **verbatim** as the user message. The system
|
||||
prompt includes a “Heartbeat” section and the run is flagged internally.
|
||||
- Active hours (`heartbeat.activeHours`) are checked in the configured timezone.
|
||||
Outside the window, heartbeats are skipped until the next tick inside the window.
|
||||
|
||||
## What the heartbeat prompt is for
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
summary: "Runbook for the Gateway daemon, lifecycle, and operations"
|
||||
summary: "Runbook for the Gateway service, lifecycle, and operations"
|
||||
read_when:
|
||||
- Running or debugging the gateway process
|
||||
---
|
||||
# Gateway (daemon) runbook
|
||||
# Gateway service runbook
|
||||
|
||||
Last updated: 2025-12-09
|
||||
|
||||
@@ -101,10 +101,10 @@ Checklist per instance:
|
||||
- unique `agents.defaults.workspace`
|
||||
- separate WhatsApp numbers (if using WA)
|
||||
|
||||
Daemon install per profile:
|
||||
Service install per profile:
|
||||
```bash
|
||||
clawdbot --profile main daemon install
|
||||
clawdbot --profile rescue daemon install
|
||||
clawdbot --profile main gateway install
|
||||
clawdbot --profile rescue gateway install
|
||||
```
|
||||
|
||||
Example:
|
||||
@@ -175,49 +175,49 @@ See also: [Presence](/concepts/presence) for how presence is produced/deduped an
|
||||
- Events are not replayed. Clients detect seq gaps and should refresh (`health` + `system-presence`) before continuing. WebChat and macOS clients now auto-refresh on gap.
|
||||
|
||||
## Supervision (macOS example)
|
||||
- Use launchd to keep the daemon alive:
|
||||
- Use launchd to keep the service alive:
|
||||
- Program: path to `clawdbot`
|
||||
- Arguments: `gateway`
|
||||
- KeepAlive: true
|
||||
- StandardOut/Err: file paths or `syslog`
|
||||
- On failure, launchd restarts; fatal misconfig should keep exiting so the operator notices.
|
||||
- LaunchAgents are per-user and require a logged-in session; for headless setups use a custom LaunchDaemon (not shipped).
|
||||
- `clawdbot daemon install` writes `~/Library/LaunchAgents/com.clawdbot.gateway.plist`
|
||||
- `clawdbot gateway install` writes `~/Library/LaunchAgents/com.clawdbot.gateway.plist`
|
||||
(or `com.clawdbot.<profile>.plist`).
|
||||
- `clawdbot doctor` audits the LaunchAgent config and can update it to current defaults.
|
||||
|
||||
## Daemon management (CLI)
|
||||
## Gateway service management (CLI)
|
||||
|
||||
Use the CLI daemon manager for install/start/stop/restart/status:
|
||||
Use the Gateway CLI for install/start/stop/restart/status:
|
||||
|
||||
```bash
|
||||
clawdbot daemon status
|
||||
clawdbot daemon install
|
||||
clawdbot daemon stop
|
||||
clawdbot daemon restart
|
||||
clawdbot gateway status
|
||||
clawdbot gateway install
|
||||
clawdbot gateway stop
|
||||
clawdbot gateway restart
|
||||
clawdbot logs --follow
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `daemon status` probes the Gateway RPC by default using the daemon’s resolved port/config (override with `--url`).
|
||||
- `daemon status --deep` adds system-level scans (LaunchDaemons/system units).
|
||||
- `daemon status --no-probe` skips the RPC probe (useful when networking is down).
|
||||
- `daemon status --json` is stable for scripts.
|
||||
- `daemon status` reports **supervisor runtime** (launchd/systemd running) separately from **RPC reachability** (WS connect + status RPC).
|
||||
- `daemon status` prints config path + probe target to avoid “localhost vs LAN bind” confusion and profile mismatches.
|
||||
- `daemon status` includes the last gateway error line when the service looks running but the port is closed.
|
||||
- `gateway status` probes the Gateway RPC by default using the service’s resolved port/config (override with `--url`).
|
||||
- `gateway status --deep` adds system-level scans (LaunchDaemons/system units).
|
||||
- `gateway status --no-probe` skips the RPC probe (useful when networking is down).
|
||||
- `gateway status --json` is stable for scripts.
|
||||
- `gateway status` reports **supervisor runtime** (launchd/systemd running) separately from **RPC reachability** (WS connect + status RPC).
|
||||
- `gateway status` prints config path + probe target to avoid “localhost vs LAN bind” confusion and profile mismatches.
|
||||
- `gateway status` includes the last gateway error line when the service looks running but the port is closed.
|
||||
- `logs` tails the Gateway file log via RPC (no manual `tail`/`grep` needed).
|
||||
- If other gateway-like services are detected, the CLI warns unless they are Clawdbot profile services.
|
||||
We still recommend **one gateway per machine** for most setups; use isolated profiles/ports for redundancy or a rescue bot. See [Multiple gateways](/gateway/multiple-gateways).
|
||||
- Cleanup: `clawdbot daemon uninstall` (current service) and `clawdbot doctor` (legacy migrations).
|
||||
- `daemon install` is a no-op when already installed; use `clawdbot daemon install --force` to reinstall (profile/env/path changes).
|
||||
- Cleanup: `clawdbot gateway uninstall` (current service) and `clawdbot doctor` (legacy migrations).
|
||||
- `gateway install` is a no-op when already installed; use `clawdbot gateway install --force` to reinstall (profile/env/path changes).
|
||||
|
||||
Bundled mac app:
|
||||
- Clawdbot.app can bundle a Node-based gateway relay and install a per-user LaunchAgent labeled
|
||||
`com.clawdbot.gateway` (or `com.clawdbot.<profile>`).
|
||||
- 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`).
|
||||
- `launchctl` only works if the LaunchAgent is installed; otherwise use `clawdbot daemon install` first.
|
||||
- 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`).
|
||||
- `launchctl` only works if the LaunchAgent is installed; otherwise use `clawdbot gateway install` first.
|
||||
- Replace the label with `com.clawdbot.<profile>` when running a named profile.
|
||||
|
||||
## Supervision (systemd user unit)
|
||||
@@ -226,7 +226,7 @@ recommend user services for single-user machines (simpler env, per-user config).
|
||||
Use a **system service** for multi-user or always-on servers (no lingering
|
||||
required, shared supervision).
|
||||
|
||||
`clawdbot daemon install` writes the user unit. `clawdbot doctor` audits the
|
||||
`clawdbot gateway install` writes the user unit. `clawdbot doctor` audits the
|
||||
unit and can update it to match the current recommended defaults.
|
||||
|
||||
Create `~/.config/systemd/user/clawdbot-gateway[-<profile>].service`:
|
||||
@@ -285,7 +285,7 @@ Windows installs should use **WSL2** and follow the Linux systemd section above.
|
||||
- `clawdbot message send --target <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 daemon stop|restart` — stop/restart the supervised gateway service (launchd/systemd).
|
||||
- `clawdbot gateway 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
|
||||
|
||||
@@ -31,10 +31,10 @@ clawdbot --profile rescue setup
|
||||
clawdbot --profile rescue gateway --port 19001
|
||||
```
|
||||
|
||||
Per-profile daemons:
|
||||
Per-profile services:
|
||||
```bash
|
||||
clawdbot --profile main daemon install
|
||||
clawdbot --profile rescue daemon install
|
||||
clawdbot --profile main gateway install
|
||||
clawdbot --profile rescue gateway install
|
||||
```
|
||||
|
||||
## Rescue-bot guide
|
||||
@@ -55,7 +55,7 @@ Port spacing: leave at least 20 ports between base ports so the derived bridge/b
|
||||
# Main bot (existing or fresh, without --profile param)
|
||||
# Runs on port 18789 + Chrome CDC/Canvas/... Ports
|
||||
clawdbot onboard
|
||||
clawdbot daemon install
|
||||
clawdbot gateway install
|
||||
|
||||
# Rescue bot (isolated profile + ports)
|
||||
clawdbot --profile rescue onboard
|
||||
@@ -65,8 +65,8 @@ clawdbot --profile rescue onboard
|
||||
# better choose completely different base port, like 19789,
|
||||
# - rest of the onboarding is the same as normal
|
||||
|
||||
# To install the daemon (if not happened automatically during onboarding)
|
||||
clawdbot --profile rescue daemon install
|
||||
# To install the service (if not happened automatically during onboarding)
|
||||
clawdbot --profile rescue gateway install
|
||||
```
|
||||
|
||||
## Port mapping (derived)
|
||||
|
||||
@@ -50,7 +50,7 @@ Guide: [Tailscale](/gateway/tailscale) and [Web overview](/web).
|
||||
|
||||
## Command flow (what runs where)
|
||||
|
||||
One gateway daemon owns state + channels. Nodes are peripherals.
|
||||
One gateway service owns state + channels. Nodes are peripherals.
|
||||
|
||||
Flow example (Telegram → node):
|
||||
- Telegram message arrives at the **Gateway**.
|
||||
@@ -59,7 +59,7 @@ Flow example (Telegram → node):
|
||||
- Node returns the result; Gateway replies back out to Telegram.
|
||||
|
||||
Notes:
|
||||
- **Nodes do not run the gateway daemon.** Only one gateway should run per host unless you intentionally run isolated profiles (see [Multiple gateways](/gateway/multiple-gateways)).
|
||||
- **Nodes do not run the gateway service.** Only one gateway should run per host unless you intentionally run isolated profiles (see [Multiple gateways](/gateway/multiple-gateways)).
|
||||
- macOS app “node mode” is just a node client over the Bridge.
|
||||
|
||||
## SSH tunnel (CLI + tools)
|
||||
@@ -112,7 +112,7 @@ Runbook: [macOS remote access](/platforms/mac/remote).
|
||||
Short version: **keep the Gateway loopback-only** unless you’re sure you need a bind.
|
||||
|
||||
- **Loopback + SSH/Tailscale Serve** is the safest default (no public exposure).
|
||||
- **Non-loopback binds** (`lan`/`tailnet`/`auto`) must use auth tokens/passwords.
|
||||
- **Non-loopback binds** (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) must use auth tokens/passwords.
|
||||
- `gateway.remote.token` is **only** for remote CLI calls — it does **not** enable local auth.
|
||||
- `gateway.remote.tlsFingerprint` pins the remote TLS cert when using `wss://`.
|
||||
- **Tailscale Serve** can authenticate via identity headers when `gateway.auth.allowTailscale: true`.
|
||||
|
||||
@@ -237,7 +237,7 @@ The Gateway multiplexes **WebSocket + HTTP** on a single port:
|
||||
|
||||
Bind mode controls where the Gateway listens:
|
||||
- `gateway.bind: "loopback"` (default): only local clients can connect.
|
||||
- Non-loopback binds (`"lan"`, `"tailnet"`, `"auto"`) expand the attack surface. Only use them with `gateway.auth` enabled and a real firewall.
|
||||
- Non-loopback binds (`"lan"`, `"tailnet"`, `"custom"`) expand the attack surface. Only use them with `gateway.auth` enabled and a real firewall.
|
||||
|
||||
Rules of thumb:
|
||||
- Prefer Tailscale Serve over LAN binds (Serve keeps the Gateway on loopback, and Tailscale handles access).
|
||||
|
||||
@@ -46,6 +46,25 @@ force `gateway.auth.mode: "password"`.
|
||||
|
||||
Open: `https://<magicdns>/` (or your configured `gateway.controlUi.basePath`)
|
||||
|
||||
### Tailnet-only (bind to Tailnet IP)
|
||||
|
||||
Use this when you want the Gateway to listen directly on the Tailnet IP (no Serve/Funnel).
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
bind: "tailnet",
|
||||
auth: { mode: "token", token: "your-token" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Connect from another Tailnet device:
|
||||
- Control UI: `http://<tailscale-ip>:18789/`
|
||||
- WebSocket: `ws://<tailscale-ip>:18789`
|
||||
|
||||
Note: loopback (`http://127.0.0.1:18789`) will **not** work in this mode.
|
||||
|
||||
### Public internet (Funnel + shared password)
|
||||
|
||||
```json5
|
||||
@@ -73,6 +92,8 @@ clawdbot gateway --tailscale funnel --auth password
|
||||
- `tailscale.mode: "funnel"` refuses to start unless auth mode is `password` to avoid public exposure.
|
||||
- Set `gateway.tailscale.resetOnExit` if you want Clawdbot to undo `tailscale serve`
|
||||
or `tailscale funnel` configuration on shutdown.
|
||||
- `gateway.bind: "tailnet"` is a direct Tailnet bind (no HTTPS, no Serve/Funnel).
|
||||
- `gateway.bind: "auto"` prefers loopback; use `tailnet` if you want Tailnet-only.
|
||||
- Serve/Funnel only expose the **Gateway control UI + WS**. Node **bridge** traffic
|
||||
uses the separate bridge port (default `18790`) and is **not** proxied by Serve.
|
||||
|
||||
|
||||
@@ -17,12 +17,12 @@ Quick triage commands (in order):
|
||||
|
||||
| Command | What it tells you | When to use it |
|
||||
|---|---|---|
|
||||
| `clawdbot status` | Local summary: OS + update, gateway reachability/mode, daemon, agents/sessions, provider config state | First check, quick overview |
|
||||
| `clawdbot status` | Local summary: OS + update, gateway reachability/mode, service, agents/sessions, provider config state | First check, quick overview |
|
||||
| `clawdbot status --all` | Full local diagnosis (read-only, pasteable, safe-ish) incl. log tail | When you need to share a debug report |
|
||||
| `clawdbot status --deep` | Runs gateway health checks (incl. provider probes; requires reachable gateway) | When “configured” doesn’t mean “working” |
|
||||
| `clawdbot gateway status` | Gateway discovery + reachability (local + remote targets) | When you suspect you’re probing the wrong gateway |
|
||||
| `clawdbot gateway probe` | Gateway discovery + reachability (local + remote targets) | When you suspect you’re probing the wrong gateway |
|
||||
| `clawdbot channels status --probe` | Asks the running gateway for channel status (and optionally probes) | When gateway is reachable but channels misbehave |
|
||||
| `clawdbot daemon status` | Supervisor state (launchd/systemd/schtasks), runtime PID/exit, last gateway error | When the daemon “looks loaded” but nothing runs |
|
||||
| `clawdbot gateway status` | Supervisor state (launchd/systemd/schtasks), runtime PID/exit, last gateway error | When the service “looks loaded” but nothing runs |
|
||||
| `clawdbot logs --follow` | Live logs (best signal for runtime issues) | When you need the actual failure reason |
|
||||
|
||||
**Sharing output:** prefer `clawdbot status --all` (it redacts tokens). If you paste `clawdbot status`, consider setting `CLAWDBOT_SHOW_SECRETS=0` first (token previews).
|
||||
@@ -38,16 +38,16 @@ Follow [Secret scanning](/gateway/security#secret-scanning-detect-secrets).
|
||||
|
||||
### Service Installed but Nothing is Running
|
||||
|
||||
If the gateway service is installed but the process exits immediately, the daemon
|
||||
If the gateway service is installed but the process exits immediately, the service
|
||||
can appear “loaded” while nothing is running.
|
||||
|
||||
**Check:**
|
||||
```bash
|
||||
clawdbot daemon status
|
||||
clawdbot gateway status
|
||||
clawdbot doctor
|
||||
```
|
||||
|
||||
Doctor/daemon will show runtime state (PID/last exit) and log hints.
|
||||
Doctor/service will show runtime state (PID/last exit) and log hints.
|
||||
|
||||
**Logs:**
|
||||
- Preferred: `clawdbot logs --follow`
|
||||
@@ -71,12 +71,12 @@ See [/logging](/logging) for a full overview of formats, config, and access.
|
||||
|
||||
### Service Environment (PATH + runtime)
|
||||
|
||||
The gateway daemon runs with a **minimal PATH** to avoid shell/manager cruft:
|
||||
The gateway service runs with a **minimal PATH** to avoid shell/manager cruft:
|
||||
- macOS: `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin`, `/bin`
|
||||
- Linux: `/usr/local/bin`, `/usr/bin`, `/bin`
|
||||
|
||||
This intentionally excludes version managers (nvm/fnm/volta/asdf) and package
|
||||
managers (pnpm/npm) because the daemon does not load your shell init. Runtime
|
||||
managers (pnpm/npm) because the service does not load your shell init. Runtime
|
||||
variables like `DISPLAY` should live in `~/.clawdbot/.env` (loaded early by the
|
||||
gateway).
|
||||
Exec runs on `host=gateway` merge your login-shell `PATH` into the exec environment,
|
||||
@@ -106,31 +106,31 @@ the Gateway likely refused to bind.
|
||||
**What "running" means here**
|
||||
- `Runtime: running` means your supervisor (launchd/systemd/schtasks) thinks the process is alive.
|
||||
- `RPC probe` means the CLI could actually connect to the gateway WebSocket and call `status`.
|
||||
- Always trust `Probe target:` + `Config (daemon):` as the “what did we actually try?” lines.
|
||||
- Always trust `Probe target:` + `Config (service):` as the “what did we actually try?” lines.
|
||||
|
||||
**Check:**
|
||||
- `gateway.mode` must be `local` for `clawdbot gateway` and the daemon.
|
||||
- If you set `gateway.mode=remote`, the **CLI defaults** to a remote URL. The daemon can still be running locally, but your CLI may be probing the wrong place. Use `clawdbot daemon status` to see the daemon’s resolved port + probe target (or pass `--url`).
|
||||
- `clawdbot daemon status` and `clawdbot doctor` surface the **last gateway error** from logs when the service looks running but the port is closed.
|
||||
- Non-loopback binds (`lan`/`tailnet`/`auto`) require auth:
|
||||
- `gateway.mode` must be `local` for `clawdbot gateway` and the service.
|
||||
- If you set `gateway.mode=remote`, the **CLI defaults** to a remote URL. The service can still be running locally, but your CLI may be probing the wrong place. Use `clawdbot gateway status` to see the service’s resolved port + probe target (or pass `--url`).
|
||||
- `clawdbot gateway status` and `clawdbot doctor` surface the **last gateway error** from logs when the service looks running but the port is closed.
|
||||
- Non-loopback binds (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) require auth:
|
||||
`gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`).
|
||||
- `gateway.remote.token` is for remote CLI calls only; it does **not** enable local auth.
|
||||
- `gateway.token` is ignored; use `gateway.auth.token`.
|
||||
|
||||
**If `clawdbot daemon status` shows a config mismatch**
|
||||
- `Config (cli): ...` and `Config (daemon): ...` should normally match.
|
||||
- If they don’t, you’re almost certainly editing one config while the daemon is running another.
|
||||
- Fix: rerun `clawdbot daemon install --force` from the same `--profile` / `CLAWDBOT_STATE_DIR` you want the daemon to use.
|
||||
**If `clawdbot gateway status` shows a config mismatch**
|
||||
- `Config (cli): ...` and `Config (service): ...` should normally match.
|
||||
- If they don’t, you’re almost certainly editing one config while the service is running another.
|
||||
- Fix: rerun `clawdbot gateway install --force` from the same `--profile` / `CLAWDBOT_STATE_DIR` you want the service to use.
|
||||
|
||||
**If `clawdbot daemon status` reports service config issues**
|
||||
**If `clawdbot gateway status` reports service config issues**
|
||||
- The supervisor config (launchd/systemd/schtasks) is missing current defaults.
|
||||
- Fix: run `clawdbot doctor` to update it (or `clawdbot daemon install --force` for a full rewrite).
|
||||
- Fix: run `clawdbot doctor` to update it (or `clawdbot gateway install --force` for a full rewrite).
|
||||
|
||||
**If `Last gateway error:` mentions “refusing to bind … without auth”**
|
||||
- You set `gateway.bind` to a non-loopback mode (`lan`/`tailnet`/`auto`) but left auth off.
|
||||
- Fix: set `gateway.auth.mode` + `gateway.auth.token` (or export `CLAWDBOT_GATEWAY_TOKEN`) and restart the daemon.
|
||||
- You set `gateway.bind` to a non-loopback mode (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) but left auth off.
|
||||
- Fix: set `gateway.auth.mode` + `gateway.auth.token` (or export `CLAWDBOT_GATEWAY_TOKEN`) and restart the service.
|
||||
|
||||
**If `clawdbot daemon status` says `bind=tailnet` but no tailnet interface was found**
|
||||
**If `clawdbot gateway status` says `bind=tailnet` but no tailnet interface was found**
|
||||
- The gateway tried to bind to a Tailscale IP (100.64.0.0/10) but none were detected on the host.
|
||||
- Fix: bring up Tailscale on that machine (or change `gateway.bind` to `loopback`/`lan`).
|
||||
|
||||
@@ -144,7 +144,7 @@ This means something is already listening on the gateway port.
|
||||
|
||||
**Check:**
|
||||
```bash
|
||||
clawdbot daemon status
|
||||
clawdbot gateway status
|
||||
```
|
||||
|
||||
It will show the listener(s) and likely causes (gateway already running, SSH tunnel).
|
||||
@@ -354,7 +354,7 @@ clawdbot doctor --fix
|
||||
Notes:
|
||||
- `clawdbot doctor` reports every invalid entry.
|
||||
- `clawdbot doctor --fix` applies migrations/repairs and rewrites the config.
|
||||
- Diagnostic commands like `clawdbot logs`, `clawdbot health`, `clawdbot status`, and `clawdbot service` still run even if the config is invalid.
|
||||
- Diagnostic commands like `clawdbot logs`, `clawdbot health`, `clawdbot status`, `clawdbot gateway status`, and `clawdbot gateway probe` still run even if the config is invalid.
|
||||
|
||||
### “All models failed” — what should I check first?
|
||||
|
||||
@@ -407,7 +407,7 @@ git status # ensure you’re in the repo root
|
||||
pnpm install
|
||||
pnpm build
|
||||
clawdbot doctor
|
||||
clawdbot daemon restart
|
||||
clawdbot gateway restart
|
||||
```
|
||||
|
||||
Why: pnpm is the configured package manager for this repo.
|
||||
@@ -432,7 +432,7 @@ Notes:
|
||||
- After switching, run:
|
||||
```bash
|
||||
clawdbot doctor
|
||||
clawdbot daemon restart
|
||||
clawdbot gateway restart
|
||||
```
|
||||
|
||||
### Telegram block streaming isn’t splitting text between tool calls. Why?
|
||||
@@ -507,8 +507,8 @@ The app connects to a local gateway on port `18789`. If it stays stuck:
|
||||
**Fix 1: Stop the supervisor (preferred)**
|
||||
If the gateway is supervised by launchd, killing the PID will just respawn it. Stop the supervisor first:
|
||||
```bash
|
||||
clawdbot daemon status
|
||||
clawdbot daemon stop
|
||||
clawdbot gateway status
|
||||
clawdbot gateway stop
|
||||
# Or: launchctl bootout gui/$UID/com.clawdbot.gateway (replace with com.clawdbot.<profile> if needed)
|
||||
```
|
||||
|
||||
@@ -558,9 +558,9 @@ clawdbot channels login --verbose
|
||||
|
||||
```bash
|
||||
# Supervisor + probe target + config paths
|
||||
clawdbot daemon status
|
||||
clawdbot gateway status
|
||||
# Include system-level scans (legacy/extra services, port listeners)
|
||||
clawdbot daemon status --deep
|
||||
clawdbot gateway status --deep
|
||||
|
||||
# Is the gateway reachable?
|
||||
clawdbot health --json
|
||||
@@ -581,13 +581,13 @@ tail -20 /tmp/clawdbot/clawdbot-*.log
|
||||
Nuclear option:
|
||||
|
||||
```bash
|
||||
clawdbot daemon stop
|
||||
clawdbot gateway stop
|
||||
# If you installed a service and want a clean install:
|
||||
# clawdbot daemon uninstall
|
||||
# clawdbot gateway uninstall
|
||||
|
||||
trash "${CLAWDBOT_STATE_DIR:-$HOME/.clawdbot}"
|
||||
clawdbot channels login # re-pair WhatsApp
|
||||
clawdbot daemon restart # or: clawdbot gateway
|
||||
clawdbot gateway restart # or: clawdbot gateway
|
||||
```
|
||||
|
||||
⚠️ This loses all sessions and requires re-pairing WhatsApp.
|
||||
|
||||
@@ -14,7 +14,7 @@ Run these in order:
|
||||
```bash
|
||||
clawdbot status
|
||||
clawdbot status --all
|
||||
clawdbot daemon status
|
||||
clawdbot gateway probe
|
||||
clawdbot logs --follow
|
||||
clawdbot doctor
|
||||
```
|
||||
@@ -38,10 +38,10 @@ Almost always a Node/npm PATH issue. Start here:
|
||||
- [Gateway troubleshooting](/gateway/troubleshooting)
|
||||
- [Gateway authentication](/gateway/authentication)
|
||||
|
||||
### Daemon says running, but RPC probe fails
|
||||
### Service says running, but RPC probe fails
|
||||
|
||||
- [Gateway troubleshooting](/gateway/troubleshooting)
|
||||
- [Background process / daemon](/gateway/background-process)
|
||||
- [Background process / service](/gateway/background-process)
|
||||
|
||||
### Model/auth failures (rate limit, billing, “all models failed”)
|
||||
|
||||
|
||||
@@ -103,13 +103,13 @@ Runtime requirement: **Node ≥ 22**.
|
||||
npm install -g clawdbot@latest
|
||||
# or: pnpm add -g clawdbot@latest
|
||||
|
||||
# Onboard + install the daemon (launchd/systemd user service)
|
||||
# Onboard + install the service (launchd/systemd user service)
|
||||
clawdbot onboard --install-daemon
|
||||
|
||||
# Pair WhatsApp Web (shows QR)
|
||||
clawdbot channels login
|
||||
|
||||
# Gateway runs via daemon after onboarding; manual run is still possible:
|
||||
# Gateway runs via the service after onboarding; manual run is still possible:
|
||||
clawdbot gateway --port 18789
|
||||
```
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ read_when:
|
||||
|
||||
# Development channels
|
||||
|
||||
Last updated: 2026-01-20
|
||||
Last updated: 2026-01-21
|
||||
|
||||
Clawdbot ships three update channels:
|
||||
|
||||
@@ -38,6 +38,13 @@ clawdbot update --channel dev
|
||||
|
||||
This updates via the corresponding npm dist-tag (`latest`, `beta`, `dev`).
|
||||
|
||||
When you **explicitly** switch channels with `--channel`, Clawdbot also aligns
|
||||
the install method:
|
||||
|
||||
- `dev` ensures a git checkout (default `~/clawdbot`, override with `CLAWDBOT_GIT_DIR`),
|
||||
updates it, and installs the global CLI from that checkout.
|
||||
- `stable`/`beta` installs from npm using the matching dist-tag.
|
||||
|
||||
Tip: if you want stable + dev in parallel, keep two clones and point your gateway at the stable one.
|
||||
|
||||
## Plugins and channels
|
||||
|
||||
@@ -31,13 +31,13 @@ Manual steps (same result):
|
||||
1) Stop the gateway service:
|
||||
|
||||
```bash
|
||||
clawdbot daemon stop
|
||||
clawdbot gateway stop
|
||||
```
|
||||
|
||||
2) Uninstall the gateway service (launchd/systemd/schtasks):
|
||||
|
||||
```bash
|
||||
clawdbot daemon uninstall
|
||||
clawdbot gateway uninstall
|
||||
```
|
||||
|
||||
3) Delete state + config:
|
||||
|
||||
@@ -68,12 +68,12 @@ Then:
|
||||
|
||||
```bash
|
||||
clawdbot doctor
|
||||
clawdbot daemon restart
|
||||
clawdbot gateway restart
|
||||
clawdbot health
|
||||
```
|
||||
|
||||
Notes:
|
||||
- If your Gateway runs as a service, `clawdbot daemon restart` is preferred over killing PIDs.
|
||||
- If your Gateway runs as a service, `clawdbot gateway restart` is preferred over killing PIDs.
|
||||
- If you’re pinned to a specific version, see “Rollback / pinning” below.
|
||||
|
||||
## Update (`clawdbot update`)
|
||||
@@ -148,9 +148,9 @@ Details: [Doctor](/gateway/doctor)
|
||||
CLI (works regardless of OS):
|
||||
|
||||
```bash
|
||||
clawdbot daemon status
|
||||
clawdbot daemon stop
|
||||
clawdbot daemon restart
|
||||
clawdbot gateway status
|
||||
clawdbot gateway stop
|
||||
clawdbot gateway restart
|
||||
clawdbot gateway --port 18789
|
||||
clawdbot logs --follow
|
||||
```
|
||||
@@ -159,7 +159,7 @@ If you’re supervised:
|
||||
- macOS launchd (app-bundled LaunchAgent): `launchctl kickstart -k gui/$UID/com.clawdbot.gateway` (use `com.clawdbot.<profile>` if set)
|
||||
- Linux systemd user service: `systemctl --user restart clawdbot-gateway[-<profile>].service`
|
||||
- Windows (WSL2): `systemctl --user restart clawdbot-gateway[-<profile>].service`
|
||||
- `launchctl`/`systemctl` only work if the service is installed; otherwise run `clawdbot daemon install`.
|
||||
- `launchctl`/`systemctl` only work if the service is installed; otherwise run `clawdbot gateway install`.
|
||||
|
||||
Runbook + exact service labels: [Gateway runbook](/gateway)
|
||||
|
||||
@@ -183,7 +183,7 @@ Then restart + re-run doctor:
|
||||
|
||||
```bash
|
||||
clawdbot doctor
|
||||
clawdbot daemon restart
|
||||
clawdbot gateway restart
|
||||
```
|
||||
|
||||
### Pin (source) by date
|
||||
@@ -200,7 +200,7 @@ Then reinstall deps + restart:
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm build
|
||||
clawdbot daemon restart
|
||||
clawdbot gateway restart
|
||||
```
|
||||
|
||||
If you want to go back to latest later:
|
||||
|
||||
@@ -13,7 +13,7 @@ A **node** is a companion device (iOS/Android today) that connects to the Gatewa
|
||||
macOS can also run in **node mode**: the menubar app connects to the Gateway’s bridge and exposes its local canvas/camera commands as a node (so `clawdbot nodes …` works against this Mac).
|
||||
|
||||
Notes:
|
||||
- Nodes are **peripherals**, not gateways. They don’t run the gateway daemon.
|
||||
- Nodes are **peripherals**, not gateways. They don’t run the gateway service.
|
||||
- Telegram/WhatsApp/etc. messages land on the **gateway**, not on nodes.
|
||||
|
||||
## Pairing + status
|
||||
@@ -50,14 +50,14 @@ forwards `exec` calls to the **node host** when `host=node` is selected.
|
||||
On the node machine:
|
||||
|
||||
```bash
|
||||
clawdbot node start --host <gateway-host> --port 18789 --display-name "Build Node"
|
||||
clawdbot node run --host <gateway-host> --port 18789 --display-name "Build Node"
|
||||
```
|
||||
|
||||
### Start a node host (service)
|
||||
|
||||
```bash
|
||||
clawdbot node service install --host <gateway-host> --port 18789 --display-name "Build Node"
|
||||
clawdbot node service start
|
||||
clawdbot node install --host <gateway-host> --port 18789 --display-name "Build Node"
|
||||
clawdbot node start
|
||||
```
|
||||
|
||||
### Pair + name
|
||||
@@ -71,7 +71,7 @@ clawdbot nodes list
|
||||
```
|
||||
|
||||
Naming options:
|
||||
- `--display-name` on `clawdbot node start/service install` (persists in `~/.clawdbot/node.json` on the node).
|
||||
- `--display-name` on `clawdbot node run` / `clawdbot node install` (persists in `~/.clawdbot/node.json` on the node).
|
||||
- `clawdbot nodes rename --node <id|name|ip> --name "Build Node"` (gateway override).
|
||||
|
||||
### Allowlist the commands
|
||||
@@ -281,7 +281,7 @@ or for running a minimal node alongside a server.
|
||||
Start it:
|
||||
|
||||
```bash
|
||||
clawdbot node start --host <gateway-host> --port 18790
|
||||
clawdbot node run --host <gateway-host> --port 18790
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
@@ -97,7 +97,7 @@ It can set up:
|
||||
- `~/.clawdbot/clawdbot.json` config
|
||||
- model auth profiles
|
||||
- model provider config/login
|
||||
- Linux systemd **user** service (daemon)
|
||||
- Linux systemd **user** service (service)
|
||||
|
||||
If you’re doing OAuth on a headless VM: do OAuth on a normal machine first, then copy the auth profile to the VM (see [Help](/help)).
|
||||
|
||||
@@ -125,7 +125,7 @@ export CLAWDBOT_GATEWAY_TOKEN="$(openssl rand -hex 32)"
|
||||
clawdbot gateway --bind lan --port 8080 --token "$CLAWDBOT_GATEWAY_TOKEN"
|
||||
```
|
||||
|
||||
For daemon runs, persist it in `~/.clawdbot/clawdbot.json`:
|
||||
For service runs, persist it in `~/.clawdbot/clawdbot.json`:
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -159,7 +159,7 @@ Notes:
|
||||
|
||||
Control UI details: [Control UI](/web/control-ui)
|
||||
|
||||
## 6) Keep it running (daemon)
|
||||
## 6) Keep it running (service)
|
||||
|
||||
On Linux, Clawdbot uses a systemd **user** service. After `--install-daemon`, verify:
|
||||
|
||||
@@ -180,7 +180,7 @@ More: [Linux](/platforms/linux)
|
||||
```bash
|
||||
npm i -g clawdbot@latest
|
||||
clawdbot doctor
|
||||
clawdbot daemon restart
|
||||
clawdbot gateway restart
|
||||
clawdbot health
|
||||
```
|
||||
|
||||
|
||||
@@ -31,15 +31,15 @@ Native companion apps for Windows are also planned; the Gateway is recommended v
|
||||
- Install guide: [Getting Started](/start/getting-started)
|
||||
- Gateway runbook: [Gateway](/gateway)
|
||||
- Gateway configuration: [Configuration](/gateway/configuration)
|
||||
- Service status: `clawdbot daemon status`
|
||||
- Service status: `clawdbot gateway status`
|
||||
|
||||
## Gateway service install (CLI)
|
||||
|
||||
Use one of these (all supported):
|
||||
|
||||
- Wizard (recommended): `clawdbot onboard --install-daemon`
|
||||
- Direct: `clawdbot daemon install`
|
||||
- Configure flow: `clawdbot configure` → select **Gateway daemon**
|
||||
- Direct: `clawdbot gateway install`
|
||||
- Configure flow: `clawdbot configure` → select **Gateway service**
|
||||
- Repair/migrate: `clawdbot doctor` (offers to install or fix the service)
|
||||
|
||||
The service target depends on OS:
|
||||
|
||||
@@ -41,7 +41,7 @@ clawdbot onboard --install-daemon
|
||||
Or:
|
||||
|
||||
```
|
||||
clawdbot daemon install
|
||||
clawdbot gateway install
|
||||
```
|
||||
|
||||
Or:
|
||||
@@ -50,7 +50,7 @@ Or:
|
||||
clawdbot configure
|
||||
```
|
||||
|
||||
Select **Gateway daemon** when prompted.
|
||||
Select **Gateway service** when prompted.
|
||||
|
||||
Repair/migrate:
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Plist location (per‑user):
|
||||
|
||||
Manager:
|
||||
- The macOS app owns LaunchAgent install/update in Local mode.
|
||||
- The CLI can also install it: `clawdbot daemon install`.
|
||||
- The CLI can also install it: `clawdbot gateway install`.
|
||||
|
||||
Behavior:
|
||||
- “Clawdbot Active” enables/disables the LaunchAgent.
|
||||
|
||||
@@ -82,8 +82,8 @@ If the app crashes when you try to allow **Speech Recognition** or **Microphone*
|
||||
If the gateway status stays on "Starting...", check if a zombie process is holding the port:
|
||||
|
||||
```bash
|
||||
clawdbot daemon status
|
||||
clawdbot daemon stop
|
||||
clawdbot gateway status
|
||||
clawdbot gateway stop
|
||||
|
||||
# If you’re not using a LaunchAgent (dev mode / manual runs), find the listener:
|
||||
lsof -nP -iTCP:18789 -sTCP:LISTEN
|
||||
|
||||
@@ -5,7 +5,7 @@ read_when:
|
||||
---
|
||||
# Clawdbot macOS IPC architecture
|
||||
|
||||
**Current model:** a local Unix socket connects the **node service** to the **macOS app** for exec approvals + `system.run`. A `clawdbot-mac` debug CLI exists for discovery/connect checks; agent actions still flow through the Gateway WebSocket and `node.invoke`. UI automation uses PeekabooBridge.
|
||||
**Current model:** a local Unix socket connects the **node host service** to the **macOS app** for exec approvals + `system.run`. A `clawdbot-mac` debug CLI exists for discovery/connect checks; agent actions still flow through the Gateway WebSocket and `node.invoke`. UI automation uses PeekabooBridge.
|
||||
|
||||
## Goals
|
||||
- Single GUI app instance that owns all TCC-facing work (notifications, screen recording, mic, speech, AppleScript).
|
||||
@@ -18,7 +18,7 @@ read_when:
|
||||
- Agent actions are performed via `node.invoke` (e.g. `system.run`, `system.notify`, `canvas.*`).
|
||||
|
||||
### Node service + app IPC
|
||||
- A headless node service connects to the Gateway bridge.
|
||||
- A headless node host service connects to the Gateway bridge.
|
||||
- `system.run` requests are forwarded to the macOS app over a local Unix socket.
|
||||
- The app performs the exec in UI context, prompts if needed, and returns output.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ capabilities to the agent as a node.
|
||||
## Local vs remote mode
|
||||
|
||||
- **Local** (default): the app attaches to a running local Gateway if present;
|
||||
otherwise it enables the launchd service via `clawdbot daemon`.
|
||||
otherwise it enables the launchd service via `clawdbot gateway install`.
|
||||
- **Remote**: the app connects to a Gateway over SSH/Tailscale and never starts
|
||||
a local process.
|
||||
The app starts the local **node host service** so the remote Gateway can reach this Mac.
|
||||
@@ -43,7 +43,7 @@ launchctl bootout gui/$UID/com.clawdbot.gateway
|
||||
Replace the label with `com.clawdbot.<profile>` when running a named profile.
|
||||
|
||||
If the LaunchAgent isn’t installed, enable it from the app or run
|
||||
`clawdbot daemon install`.
|
||||
`clawdbot gateway install`.
|
||||
|
||||
## Node capabilities (mac)
|
||||
|
||||
@@ -57,7 +57,7 @@ The macOS app presents itself as a node. Common commands:
|
||||
The node reports a `permissions` map so agents can decide what’s allowed.
|
||||
|
||||
Node service + app IPC:
|
||||
- When the headless node service is running (remote mode), it connects to the Gateway WS as a node.
|
||||
- When the headless node host service is running (remote mode), it connects to the Gateway WS as a node.
|
||||
- `system.run` executes in the macOS app (UI/TCC context) over a local Unix socket; prompts + output stay in-app.
|
||||
|
||||
Diagram (SCI):
|
||||
|
||||
@@ -32,7 +32,7 @@ clawdbot onboard --install-daemon
|
||||
Or:
|
||||
|
||||
```
|
||||
clawdbot daemon install
|
||||
clawdbot gateway install
|
||||
```
|
||||
|
||||
Or:
|
||||
@@ -41,7 +41,7 @@ Or:
|
||||
clawdbot configure
|
||||
```
|
||||
|
||||
Select **Gateway daemon** when prompted.
|
||||
Select **Gateway service** when prompted.
|
||||
|
||||
Repair/migrate:
|
||||
|
||||
@@ -108,7 +108,7 @@ wsl --install -d Ubuntu-24.04
|
||||
|
||||
Reboot if Windows asks.
|
||||
|
||||
### 2) Enable systemd (required for daemon install)
|
||||
### 2) Enable systemd (required for gateway install)
|
||||
|
||||
In your WSL terminal:
|
||||
|
||||
|
||||
@@ -36,10 +36,10 @@ clawdbot onboard --anthropic-api-key "$ANTHROPIC_API_KEY"
|
||||
|
||||
## Prompt caching (Anthropic API)
|
||||
|
||||
Clawdbot enables **1-hour prompt caching by default** for Anthropic API keys.
|
||||
Clawdbot does **not** override Anthropic’s default cache TTL unless you set it.
|
||||
This is **API-only**; Claude Code CLI OAuth ignores TTL settings.
|
||||
|
||||
To override the TTL per model, set `cacheControlTtl` in the model `params`:
|
||||
To set the TTL per model, use `cacheControlTtl` in the model `params`:
|
||||
|
||||
```json5
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@ read_when:
|
||||
- **Node identity:** use existing `nodeId`.
|
||||
- **Socket auth:** Unix socket + token (cross-platform); split later if needed.
|
||||
- **Node host state:** `~/.clawdbot/node.json` (node id + pairing token).
|
||||
- **macOS exec host:** run `system.run` inside the macOS app; node service forwards requests over local IPC.
|
||||
- **macOS exec host:** run `system.run` inside the macOS app; node host service forwards requests over local IPC.
|
||||
- **No XPC helper:** stick to Unix socket + token + peer checks.
|
||||
|
||||
## Key concepts
|
||||
|
||||
@@ -54,7 +54,7 @@ Allowed (diagnostic-only):
|
||||
- `clawdbot health`
|
||||
- `clawdbot help`
|
||||
- `clawdbot status`
|
||||
- `clawdbot service`
|
||||
- `clawdbot gateway status`
|
||||
|
||||
Everything else must hard-fail with: “Config invalid. Run `clawdbot doctor --fix`.”
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ When the operator says “release”, immediately do this preflight (no extra qu
|
||||
2) **Build & artifacts**
|
||||
- [ ] If A2UI inputs changed, run `pnpm canvas:a2ui:bundle` and commit any updated [`src/canvas-host/a2ui/a2ui.bundle.js`](https://github.com/clawdbot/clawdbot/blob/main/src/canvas-host/a2ui/a2ui.bundle.js).
|
||||
- [ ] `pnpm run build` (regenerates `dist/`).
|
||||
- [ ] Verify npm package `files` includes all required `dist/*` folders (notably `dist/node-host/**` and `dist/acp/**` for headless node + ACP CLI).
|
||||
- [ ] Confirm `dist/build-info.json` exists and includes the expected `commit` hash (CLI banner uses this for npm installs).
|
||||
- [ ] Optional: `npm pack --pack-destination /tmp` after the build; inspect the tarball contents and keep it handy for the GitHub release (do **not** commit it).
|
||||
|
||||
|
||||
@@ -59,20 +59,21 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
||||
- [How do I use Brave for browser control?](#how-do-i-use-brave-for-browser-control)
|
||||
- [Remote gateways + nodes](#remote-gateways-nodes)
|
||||
- [How do commands propagate between Telegram, the gateway, and nodes?](#how-do-commands-propagate-between-telegram-the-gateway-and-nodes)
|
||||
- [Do nodes run a gateway daemon?](#do-nodes-run-a-gateway-daemon)
|
||||
- [Do nodes run a gateway service?](#do-nodes-run-a-gateway-service)
|
||||
- [Is there an API / RPC way to apply config?](#is-there-an-api-rpc-way-to-apply-config)
|
||||
- [What’s a minimal “sane” config for a first install?](#whats-a-minimal-sane-config-for-a-first-install)
|
||||
- [How do I set up Tailscale on a VPS and connect from my Mac?](#how-do-i-set-up-tailscale-on-a-vps-and-connect-from-my-mac)
|
||||
- [How do I connect a Mac node to a remote Gateway (Tailscale Serve)?](#how-do-i-connect-a-mac-node-to-a-remote-gateway-tailscale-serve)
|
||||
- [Env vars and .env loading](#env-vars-and-env-loading)
|
||||
- [How does Clawdbot load environment variables?](#how-does-clawdbot-load-environment-variables)
|
||||
- [“I started the Gateway via a daemon and my env vars disappeared.” What now?](#i-started-the-gateway-via-a-daemon-and-my-env-vars-disappeared-what-now)
|
||||
- [“I started the Gateway via the service and my env vars disappeared.” What now?](#i-started-the-gateway-via-the-service-and-my-env-vars-disappeared-what-now)
|
||||
- [I set `COPILOT_GITHUB_TOKEN`, but models status shows “Shell env: off.” Why?](#i-set-copilot_github_token-but-models-status-shows-shell-env-off-why)
|
||||
- [Sessions & multiple chats](#sessions-multiple-chats)
|
||||
- [How do I start a fresh conversation?](#how-do-i-start-a-fresh-conversation)
|
||||
- [Do sessions reset automatically if I never send `/new`?](#do-sessions-reset-automatically-if-i-never-send-new)
|
||||
- [How do I completely reset Clawdbot but keep it installed?](#how-do-i-completely-reset-clawdbot-but-keep-it-installed)
|
||||
- [I’m getting “context too large” errors — how do I reset or compact?](#im-getting-context-too-large-errors-how-do-i-reset-or-compact)
|
||||
- [Why am I seeing “LLM request rejected: messages.N.content.X.tool_use.input: Field required”?](#why-am-i-seeing-llm-request-rejected-messagesncontentxtool_useinput-field-required)
|
||||
- [Why am I getting heartbeat messages every 30 minutes?](#why-am-i-getting-heartbeat-messages-every-30-minutes)
|
||||
- [Do I need to add a “bot account” to a WhatsApp group?](#do-i-need-to-add-a-bot-account-to-a-whatsapp-group)
|
||||
- [Why doesn’t Clawdbot reply in a group?](#why-doesnt-clawdbot-reply-in-a-group)
|
||||
@@ -99,8 +100,8 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
||||
- [OAuth vs API key: what’s the difference?](#oauth-vs-api-key-whats-the-difference)
|
||||
- [Gateway: ports, “already running”, and remote mode](#gateway-ports-already-running-and-remote-mode)
|
||||
- [What port does the Gateway use?](#what-port-does-the-gateway-use)
|
||||
- [Why does `clawdbot daemon status` say `Runtime: running` but `RPC probe: failed`?](#why-does-clawdbot-daemon-status-say-runtime-running-but-rpc-probe-failed)
|
||||
- [Why does `clawdbot daemon status` show `Config (cli)` and `Config (daemon)` different?](#why-does-clawdbot-daemon-status-show-config-cli-and-config-daemon-different)
|
||||
- [Why does `clawdbot gateway status` say `Runtime: running` but `RPC probe: failed`?](#why-does-clawdbot-gateway-status-say-runtime-running-but-rpc-probe-failed)
|
||||
- [Why does `clawdbot gateway status` show `Config (cli)` and `Config (service)` different?](#why-does-clawdbot-gateway-status-show-config-cli-and-config-service-different)
|
||||
- [What does “another gateway instance is already listening” mean?](#what-does-another-gateway-instance-is-already-listening-mean)
|
||||
- [How do I run Clawdbot in remote mode (client connects to a Gateway elsewhere)?](#how-do-i-run-clawdbot-in-remote-mode-client-connects-to-a-gateway-elsewhere)
|
||||
- [The Control UI says “unauthorized” (or keeps reconnecting). What now?](#the-control-ui-says-unauthorized-or-keeps-reconnecting-what-now)
|
||||
@@ -109,8 +110,8 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
||||
- [What does “invalid handshake” / code 1008 mean?](#what-does-invalid-handshake--code-1008-mean)
|
||||
- [Logging and debugging](#logging-and-debugging)
|
||||
- [Where are logs?](#where-are-logs)
|
||||
- [How do I start/stop/restart the Gateway daemon?](#how-do-i-startstoprestart-the-gateway-daemon)
|
||||
- [ELI5: `clawdbot daemon restart` vs `clawdbot gateway`](#eli5-clawdbot-daemon-restart-vs-clawdbot-gateway)
|
||||
- [How do I start/stop/restart the Gateway service?](#how-do-i-startstoprestart-the-gateway-service)
|
||||
- [ELI5: `clawdbot gateway restart` vs `clawdbot gateway`](#eli5-clawdbot-gateway-restart-vs-clawdbot-gateway)
|
||||
- [What’s the fastest way to get more details when something fails?](#whats-the-fastest-way-to-get-more-details-when-something-fails)
|
||||
- [Media & attachments](#media-attachments)
|
||||
- [My skill generated an image/PDF, but nothing was sent](#my-skill-generated-an-imagepdf-but-nothing-was-sent)
|
||||
@@ -128,7 +129,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
||||
```bash
|
||||
clawdbot status
|
||||
```
|
||||
Fast local summary: OS + update, gateway/daemon reachability, agents/sessions, provider config + runtime issues (when gateway is reachable).
|
||||
Fast local summary: OS + update, gateway/service reachability, agents/sessions, provider config + runtime issues (when gateway is reachable).
|
||||
|
||||
2) **Pasteable report (safe to share)**
|
||||
```bash
|
||||
@@ -138,9 +139,9 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
||||
|
||||
3) **Daemon + port state**
|
||||
```bash
|
||||
clawdbot daemon status
|
||||
clawdbot gateway status
|
||||
```
|
||||
Shows supervisor runtime vs RPC reachability, the probe target URL, and which config the daemon likely used.
|
||||
Shows supervisor runtime vs RPC reachability, the probe target URL, and which config the service likely used.
|
||||
|
||||
4) **Deep probes**
|
||||
```bash
|
||||
@@ -334,7 +335,7 @@ cd clawdbot
|
||||
pnpm install
|
||||
pnpm build
|
||||
clawdbot doctor
|
||||
clawdbot daemon restart
|
||||
clawdbot gateway restart
|
||||
```
|
||||
|
||||
From git → npm:
|
||||
@@ -342,7 +343,7 @@ From git → npm:
|
||||
```bash
|
||||
npm install -g clawdbot@latest
|
||||
clawdbot doctor
|
||||
clawdbot daemon restart
|
||||
clawdbot gateway restart
|
||||
```
|
||||
|
||||
Doctor detects a gateway service entrypoint mismatch and offers to rewrite the service config to match the current install (use `--repair` in automation).
|
||||
@@ -747,7 +748,7 @@ pair devices you trust, and review [Security](/gateway/security).
|
||||
|
||||
Docs: [Nodes](/nodes), [Bridge protocol](/gateway/bridge-protocol), [macOS remote mode](/platforms/mac/remote), [Security](/gateway/security).
|
||||
|
||||
### Do nodes run a gateway daemon?
|
||||
### Do nodes run a gateway service?
|
||||
|
||||
No. Only **one gateway** should run per host unless you intentionally run isolated profiles (see [Multiple gateways](/gateway/multiple-gateways)). Nodes are peripherals that connect
|
||||
to the gateway (iOS/Android nodes, or macOS “node mode” in the menubar app).
|
||||
@@ -839,11 +840,11 @@ You can also define inline env vars in config (applied only if missing from the
|
||||
|
||||
See [/environment](/environment) for full precedence and sources.
|
||||
|
||||
### “I started the Gateway via a daemon and my env vars disappeared.” What now?
|
||||
### “I started the Gateway via a service and my env vars disappeared.” What now?
|
||||
|
||||
Two common fixes:
|
||||
|
||||
1) Put the missing keys in `~/.clawdbot/.env` so they’re picked up even when the daemon doesn’t inherit your shell env.
|
||||
1) Put the missing keys in `~/.clawdbot/.env` so they’re picked up even when the service doesn’t inherit your shell env.
|
||||
2) Enable shell import (opt‑in convenience):
|
||||
|
||||
```json5
|
||||
@@ -866,7 +867,7 @@ This runs your login shell and imports only missing expected keys (never overrid
|
||||
does **not** mean your env vars are missing — it just means Clawdbot won’t load
|
||||
your login shell automatically.
|
||||
|
||||
If the Gateway runs as a daemon (launchd/systemd), it won’t inherit your shell
|
||||
If the Gateway runs as a service (launchd/systemd), it won’t inherit your shell
|
||||
environment. Fix by doing one of these:
|
||||
|
||||
1) Put the token in `~/.clawdbot/.env`:
|
||||
@@ -951,6 +952,14 @@ If it keeps happening:
|
||||
|
||||
Docs: [Compaction](/concepts/compaction), [Session pruning](/concepts/session-pruning), [Session management](/concepts/session).
|
||||
|
||||
### Why am I seeing “LLM request rejected: messages.N.content.X.tool_use.input: Field required”?
|
||||
|
||||
This is a provider validation error: the model emitted a `tool_use` block without the required
|
||||
`input`. It usually means the session history is stale or corrupted (often after long threads
|
||||
or a tool/schema change).
|
||||
|
||||
Fix: start a fresh session with `/new` (standalone message).
|
||||
|
||||
### Why am I getting heartbeat messages every 30 minutes?
|
||||
|
||||
Heartbeats run every **30m** by default. Tune or disable them:
|
||||
@@ -1336,24 +1345,24 @@ Precedence:
|
||||
--port > CLAWDBOT_GATEWAY_PORT > gateway.port > default 18789
|
||||
```
|
||||
|
||||
### Why does `clawdbot daemon status` say `Runtime: running` but `RPC probe: failed`?
|
||||
### Why does `clawdbot gateway status` say `Runtime: running` but `RPC probe: failed`?
|
||||
|
||||
Because “running” is the **supervisor’s** view (launchd/systemd/schtasks). The RPC probe is the CLI actually connecting to the gateway WebSocket and calling `status`.
|
||||
|
||||
Use `clawdbot daemon status` and trust these lines:
|
||||
Use `clawdbot gateway status` and trust these lines:
|
||||
- `Probe target:` (the URL the probe actually used)
|
||||
- `Listening:` (what’s actually bound on the port)
|
||||
- `Last gateway error:` (common root cause when the process is alive but the port isn’t listening)
|
||||
|
||||
### Why does `clawdbot daemon status` show `Config (cli)` and `Config (daemon)` different?
|
||||
### Why does `clawdbot gateway status` show `Config (cli)` and `Config (service)` different?
|
||||
|
||||
You’re editing one config file while the daemon is running another (often a `--profile` / `CLAWDBOT_STATE_DIR` mismatch).
|
||||
You’re editing one config file while the service is running another (often a `--profile` / `CLAWDBOT_STATE_DIR` mismatch).
|
||||
|
||||
Fix:
|
||||
```bash
|
||||
clawdbot daemon install --force
|
||||
clawdbot gateway install --force
|
||||
```
|
||||
Run that from the same `--profile` / environment you want the daemon to use.
|
||||
Run that from the same `--profile` / environment you want the service to use.
|
||||
|
||||
### What does “another gateway instance is already listening” mean?
|
||||
|
||||
@@ -1406,7 +1415,7 @@ Fix:
|
||||
- Start Tailscale on that host (so it has a 100.x address), or
|
||||
- Switch to `gateway.bind: "loopback"` / `"lan"`.
|
||||
|
||||
Note: `tailnet` is legacy and is migrated to `auto` by Doctor. Prefer `gateway.bind: "auto"` when using Tailscale.
|
||||
Note: `tailnet` is explicit. `auto` prefers loopback; use `gateway.bind: "tailnet"` when you want a tailnet-only bind.
|
||||
|
||||
### Can I run multiple Gateways on the same host?
|
||||
|
||||
@@ -1422,7 +1431,7 @@ Yes, but you must isolate:
|
||||
Quick setup (recommended):
|
||||
- Use `clawdbot --profile <name> …` per instance (auto-creates `~/.clawdbot-<name>`).
|
||||
- Set a unique `gateway.port` in each profile config (or pass `--port` for manual runs).
|
||||
- Install a per-profile daemon: `clawdbot --profile <name> daemon install`.
|
||||
- Install a per-profile service: `clawdbot --profile <name> gateway install`.
|
||||
|
||||
Profiles also suffix service names (`com.clawdbot.<profile>`, `clawdbot-gateway-<profile>.service`, `Clawdbot Gateway (<profile>)`).
|
||||
Full guide: [Multiple gateways](/gateway/multiple-gateways).
|
||||
@@ -1475,23 +1484,23 @@ Service/supervisor logs (when the gateway runs via launchd/systemd):
|
||||
|
||||
See [Troubleshooting](/gateway/troubleshooting#log-locations) for more.
|
||||
|
||||
### How do I start/stop/restart the Gateway daemon?
|
||||
### How do I start/stop/restart the Gateway service?
|
||||
|
||||
Use the daemon helpers:
|
||||
Use the gateway helpers:
|
||||
|
||||
```bash
|
||||
clawdbot daemon status
|
||||
clawdbot daemon restart
|
||||
clawdbot gateway status
|
||||
clawdbot gateway restart
|
||||
```
|
||||
|
||||
If you run the gateway manually, `clawdbot gateway --force` can reclaim the port. See [Gateway](/gateway).
|
||||
|
||||
### ELI5: `clawdbot daemon restart` vs `clawdbot gateway`
|
||||
### ELI5: `clawdbot gateway restart` vs `clawdbot gateway`
|
||||
|
||||
- `clawdbot daemon restart`: restarts the **background service** (launchd/systemd).
|
||||
- `clawdbot gateway restart`: restarts the **background service** (launchd/systemd).
|
||||
- `clawdbot gateway`: runs the gateway **in the foreground** for this terminal session.
|
||||
|
||||
If you installed the daemon, use the daemon commands. Use `clawdbot gateway` when
|
||||
If you installed the service, use the gateway commands. Use `clawdbot gateway` when
|
||||
you want a one-off, foreground run.
|
||||
|
||||
### What’s the fastest way to get more details when something fails?
|
||||
|
||||
@@ -15,7 +15,7 @@ Recommended path: use the **CLI onboarding wizard** (`clawdbot onboard`). It set
|
||||
- channels (WhatsApp/Telegram/Discord/…)
|
||||
- pairing defaults (secure DMs)
|
||||
- workspace bootstrap + skills
|
||||
- optional background daemon
|
||||
- optional background service
|
||||
|
||||
If you want the deeper reference pages, jump to: [Wizard](/start/wizard), [Setup](/start/setup), [Pairing](/start/pairing), [Security](/gateway/security).
|
||||
|
||||
@@ -71,7 +71,7 @@ npm install -g clawdbot@latest
|
||||
pnpm add -g clawdbot@latest
|
||||
```
|
||||
|
||||
## 2) Run the onboarding wizard (and install the daemon)
|
||||
## 2) Run the onboarding wizard (and install the service)
|
||||
|
||||
```bash
|
||||
clawdbot onboard --install-daemon
|
||||
@@ -89,7 +89,7 @@ Wizard doc: [Wizard](/start/wizard)
|
||||
|
||||
### Auth: where it lives (important)
|
||||
|
||||
- **Recommended Anthropic path:** set an API key (wizard can store it for daemon use). `claude setup-token` is also supported if you want to reuse Claude Code credentials.
|
||||
- **Recommended Anthropic path:** set an API key (wizard can store it for service use). `claude setup-token` is also supported if you want to reuse Claude Code credentials.
|
||||
|
||||
- OAuth credentials (legacy import): `~/.clawdbot/credentials/oauth.json`
|
||||
- Auth profiles (OAuth + API keys): `~/.clawdbot/agents/<agentId>/agent/auth-profiles.json`
|
||||
@@ -98,10 +98,10 @@ Headless/server tip: do OAuth on a normal machine first, then copy `oauth.json`
|
||||
|
||||
## 3) Start the Gateway
|
||||
|
||||
If you installed the daemon during onboarding, the Gateway should already be running:
|
||||
If you installed the service during onboarding, the Gateway should already be running:
|
||||
|
||||
```bash
|
||||
clawdbot daemon status
|
||||
clawdbot gateway status
|
||||
```
|
||||
|
||||
Manual run (foreground):
|
||||
|
||||
@@ -60,6 +60,28 @@ Full setup walkthrough (28m) by VelvetShark.
|
||||
|
||||
[Watch on YouTube](https://www.youtube.com/watch?v=mMSKQvlmFuQ)
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
paddingBottom: "56.25%",
|
||||
height: 0,
|
||||
overflow: "hidden",
|
||||
borderRadius: 16,
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/5kkIJNUGFho"
|
||||
title="Clawdbot community showcase"
|
||||
style={{ position: "absolute", top: 0, left: 0, width: "100%", height: "100%" }}
|
||||
frameBorder="0"
|
||||
loading="lazy"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
|
||||
[Watch on YouTube](https://www.youtube.com/watch?v=5kkIJNUGFho)
|
||||
|
||||
## 🆕 Fresh from Discord
|
||||
|
||||
<CardGroup cols={2}>
|
||||
|
||||
@@ -65,6 +65,41 @@ These are **USD per 1M tokens** for `input`, `output`, `cacheRead`, and
|
||||
`cacheWrite`. If pricing is missing, Clawdbot shows tokens only. OAuth tokens
|
||||
never show dollar cost.
|
||||
|
||||
## Cache TTL and pruning impact
|
||||
|
||||
Provider prompt caching only applies within the cache TTL window. Clawdbot can
|
||||
optionally run **cache-ttl pruning**: it prunes the session once the cache TTL
|
||||
has expired, then resets the cache window so subsequent requests can re-use the
|
||||
freshly cached context instead of re-caching the full history. This keeps cache
|
||||
write costs lower when a session goes idle past the TTL.
|
||||
|
||||
Configure it in [Gateway configuration](/gateway/configuration) and see the
|
||||
behavior details in [Session pruning](/concepts/session-pruning).
|
||||
|
||||
Heartbeat can keep the cache **warm** across idle gaps. If your model cache TTL
|
||||
is `1h`, setting the heartbeat interval just under that (e.g., `55m`) can avoid
|
||||
re-caching the full prompt, reducing cache write costs.
|
||||
|
||||
For Anthropic API pricing, cache reads are significantly cheaper than input
|
||||
tokens, while cache writes are billed at a higher multiplier. See Anthropic’s
|
||||
prompt caching pricing for the latest rates and TTL multipliers:
|
||||
https://docs.anthropic.com/docs/build-with-claude/prompt-caching
|
||||
|
||||
### Example: keep 1h cache warm with heartbeat
|
||||
|
||||
```yaml
|
||||
agents:
|
||||
defaults:
|
||||
model:
|
||||
primary: "anthropic/claude-opus-4-5"
|
||||
models:
|
||||
"anthropic/claude-opus-4-5":
|
||||
params:
|
||||
cacheControlTtl: "1h"
|
||||
heartbeat:
|
||||
every: "55m"
|
||||
```
|
||||
|
||||
## Tips for reducing token pressure
|
||||
|
||||
- Use `/compact` to summarize long sessions.
|
||||
|
||||
@@ -23,7 +23,7 @@ Exec approvals are enforced locally on the execution host:
|
||||
- **node host** → node runner (macOS companion app or headless node host)
|
||||
|
||||
Planned macOS split:
|
||||
- **node service** forwards `system.run` to the **macOS app** over local IPC.
|
||||
- **node host service** forwards `system.run` to the **macOS app** over local IPC.
|
||||
- **macOS app** enforces approvals + executes the command in UI context.
|
||||
|
||||
## Settings and storage
|
||||
|
||||
@@ -181,7 +181,7 @@ Notes:
|
||||
- If `process` is disallowed, `exec` runs synchronously and ignores `yieldMs`/`background`.
|
||||
- `elevated` is gated by `tools.elevated` plus any `agents.list[].tools.elevated` override (both must allow) and is an alias for `host=gateway` + `security=full`.
|
||||
- `elevated` only changes behavior when the agent is sandboxed (otherwise it’s a no-op).
|
||||
- `host=node` can target a macOS companion app or a headless node host (`clawdbot node start`).
|
||||
- `host=node` can target a macOS companion app or a headless node host (`clawdbot node run`).
|
||||
- gateway/node approvals and allowlists: [Exec approvals](/tools/exec-approvals).
|
||||
|
||||
### `process`
|
||||
|
||||
@@ -60,6 +60,7 @@ Text + native (when enabled):
|
||||
- `/commands`
|
||||
- `/skill <name> [input]` (run a skill by name)
|
||||
- `/status` (show current status; includes provider usage/quota for the current model provider when available)
|
||||
- `/allowlist` (list/add/remove allowlist entries)
|
||||
- `/context [list|detail|json]` (explain “context”; `detail` shows per-file + per-tool + per-skill + system prompt size)
|
||||
- `/whoami` (show your sender id; alias: `/id`)
|
||||
- `/subagents list|stop|log|info|send` (inspect, stop, log, or message sub-agent runs for the current session)
|
||||
@@ -93,6 +94,7 @@ Notes:
|
||||
- Commands accept an optional `:` between the command and args (e.g. `/think: high`, `/send: on`, `/help:`).
|
||||
- `/new <model>` accepts a model alias, `provider/model`, or a provider name (fuzzy match); if no match, the text is treated as the message body.
|
||||
- For full provider usage breakdown, use `clawdbot status --usage`.
|
||||
- `/allowlist add|remove` requires `commands.config=true` and honors channel `configWrites`.
|
||||
- `/usage` controls the per-response usage footer; `/usage cost` prints a local cost summary from Clawdbot session logs.
|
||||
- `/restart` is disabled by default; set `commands.restart: true` to enable it.
|
||||
- `/verbose` is meant for debugging and extra visibility; keep it **off** in normal use.
|
||||
|
||||
@@ -83,7 +83,7 @@ current limits and pricing.
|
||||
`~/.clawdbot/clawdbot.json` under `tools.web.search.apiKey`.
|
||||
|
||||
**Environment alternative:** set `BRAVE_API_KEY` in the Gateway process
|
||||
environment. For a daemon install, put it in `~/.clawdbot/.env` (or your
|
||||
environment. For a gateway install, put it in `~/.clawdbot/.env` (or your
|
||||
service environment). See [Env vars](/start/faq#how-does-clawdbot-load-environment-variables).
|
||||
|
||||
## Using Perplexity (direct or via OpenRouter)
|
||||
@@ -122,7 +122,7 @@ crypto/prepaid).
|
||||
```
|
||||
|
||||
**Environment alternative:** set `OPENROUTER_API_KEY` or `PERPLEXITY_API_KEY` in the Gateway
|
||||
environment. For a daemon install, put it in `~/.clawdbot/.env`.
|
||||
environment. For a gateway install, put it in `~/.clawdbot/.env`.
|
||||
|
||||
If no base URL is set, Clawdbot chooses a default based on the API key source:
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"name": "@clawdbot/bluebubbles",
|
||||
"version": "2026.1.15",
|
||||
"version": "2026.1.21-1",
|
||||
"type": "module",
|
||||
"description": "Clawdbot BlueBubbles channel plugin",
|
||||
"clawdbot": {
|
||||
"extensions": ["./index.ts"],
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
],
|
||||
"channel": {
|
||||
"id": "bluebubbles",
|
||||
"label": "BlueBubbles",
|
||||
|
||||
@@ -38,6 +38,10 @@ vi.mock("./attachments.js", () => ({
|
||||
sendBlueBubblesAttachment: vi.fn().mockResolvedValue({ messageId: "att-msg-123" }),
|
||||
}));
|
||||
|
||||
vi.mock("./monitor.js", () => ({
|
||||
resolveBlueBubblesMessageId: vi.fn((id: string) => id),
|
||||
}));
|
||||
|
||||
describe("bluebubblesMessageActions", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -358,6 +362,106 @@ describe("bluebubblesMessageActions", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses toolContext currentChannelId when no explicit target is provided", async () => {
|
||||
const { sendBlueBubblesReaction } = await import("./reactions.js");
|
||||
const { resolveChatGuidForTarget } = await import("./send.js");
|
||||
vi.mocked(resolveChatGuidForTarget).mockResolvedValueOnce("iMessage;-;+15550001111");
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
serverUrl: "http://localhost:1234",
|
||||
password: "test-password",
|
||||
},
|
||||
},
|
||||
};
|
||||
await bluebubblesMessageActions.handleAction({
|
||||
action: "react",
|
||||
params: {
|
||||
emoji: "👍",
|
||||
messageId: "msg-456",
|
||||
},
|
||||
cfg,
|
||||
accountId: null,
|
||||
toolContext: {
|
||||
currentChannelId: "bluebubbles:chat_guid:iMessage;-;+15550001111",
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolveChatGuidForTarget).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
target: { kind: "chat_guid", chatGuid: "iMessage;-;+15550001111" },
|
||||
}),
|
||||
);
|
||||
expect(sendBlueBubblesReaction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
chatGuid: "iMessage;-;+15550001111",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves short messageId before reacting", async () => {
|
||||
const { resolveBlueBubblesMessageId } = await import("./monitor.js");
|
||||
const { sendBlueBubblesReaction } = await import("./reactions.js");
|
||||
vi.mocked(resolveBlueBubblesMessageId).mockReturnValueOnce("resolved-uuid");
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
serverUrl: "http://localhost:1234",
|
||||
password: "test-password",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await bluebubblesMessageActions.handleAction({
|
||||
action: "react",
|
||||
params: {
|
||||
emoji: "❤️",
|
||||
messageId: "1",
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
},
|
||||
cfg,
|
||||
accountId: null,
|
||||
});
|
||||
|
||||
expect(resolveBlueBubblesMessageId).toHaveBeenCalledWith("1", { requireKnownShortId: true });
|
||||
expect(sendBlueBubblesReaction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
messageGuid: "resolved-uuid",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("propagates short-id errors from the resolver", async () => {
|
||||
const { resolveBlueBubblesMessageId } = await import("./monitor.js");
|
||||
vi.mocked(resolveBlueBubblesMessageId).mockImplementationOnce(() => {
|
||||
throw new Error("short id expired");
|
||||
});
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
serverUrl: "http://localhost:1234",
|
||||
password: "test-password",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
bluebubblesMessageActions.handleAction({
|
||||
action: "react",
|
||||
params: {
|
||||
emoji: "❤️",
|
||||
messageId: "999",
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
},
|
||||
cfg,
|
||||
accountId: null,
|
||||
}),
|
||||
).rejects.toThrow("short id expired");
|
||||
});
|
||||
|
||||
it("accepts message param for edit action", async () => {
|
||||
const { editBlueBubblesMessage } = await import("./chat.js");
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "clawdbot/plugin-sdk";
|
||||
|
||||
import { resolveBlueBubblesAccount } from "./accounts.js";
|
||||
import { resolveBlueBubblesMessageId } from "./monitor.js";
|
||||
import { isMacOS26OrHigher } from "./probe.js";
|
||||
import { sendBlueBubblesReaction } from "./reactions.js";
|
||||
import { resolveChatGuidForTarget, sendMessageBlueBubbles } from "./send.js";
|
||||
@@ -77,7 +78,7 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
|
||||
const accountId = typeof args.accountId === "string" ? args.accountId.trim() : undefined;
|
||||
return { to, accountId };
|
||||
},
|
||||
handleAction: async ({ action, params, cfg, accountId }) => {
|
||||
handleAction: async ({ action, params, cfg, accountId, toolContext }) => {
|
||||
const account = resolveBlueBubblesAccount({
|
||||
cfg: cfg as ClawdbotConfig,
|
||||
accountId: accountId ?? undefined,
|
||||
@@ -86,7 +87,7 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
|
||||
const password = account.config.password?.trim();
|
||||
const opts = { cfg: cfg as ClawdbotConfig, accountId: accountId ?? undefined };
|
||||
|
||||
// Helper to resolve chatGuid from various params
|
||||
// Helper to resolve chatGuid from various params or session context
|
||||
const resolveChatGuid = async (): Promise<string> => {
|
||||
const chatGuid = readStringParam(params, "chatGuid");
|
||||
if (chatGuid?.trim()) return chatGuid.trim();
|
||||
@@ -94,6 +95,8 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
|
||||
const chatIdentifier = readStringParam(params, "chatIdentifier");
|
||||
const chatId = readNumberParam(params, "chatId", { integer: true });
|
||||
const to = readStringParam(params, "to");
|
||||
// Fall back to session context if no explicit target provided
|
||||
const contextTarget = toolContext?.currentChannelId?.trim();
|
||||
|
||||
const target = chatIdentifier?.trim()
|
||||
? ({
|
||||
@@ -104,7 +107,9 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
|
||||
? ({ kind: "chat_id", chatId } as BlueBubblesSendTarget)
|
||||
: to
|
||||
? mapTarget(to)
|
||||
: null;
|
||||
: contextTarget
|
||||
? mapTarget(contextTarget)
|
||||
: null;
|
||||
|
||||
if (!target) {
|
||||
throw new Error(`BlueBubbles ${action} requires chatGuid, chatIdentifier, chatId, or to.`);
|
||||
@@ -127,16 +132,18 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
|
||||
});
|
||||
if (isEmpty && !remove) {
|
||||
throw new Error(
|
||||
"BlueBubbles react requires emoji parameter. Use action=react with emoji=<emoji> and messageId=<message_guid>.",
|
||||
"BlueBubbles react requires emoji parameter. Use action=react with emoji=<emoji> and messageId=<message_id>.",
|
||||
);
|
||||
}
|
||||
const messageId = readStringParam(params, "messageId");
|
||||
if (!messageId) {
|
||||
const rawMessageId = readStringParam(params, "messageId");
|
||||
if (!rawMessageId) {
|
||||
throw new Error(
|
||||
"BlueBubbles react requires messageId parameter (the message GUID to react to). " +
|
||||
"Use action=react with messageId=<message_guid>, emoji=<emoji>, and to/chatGuid to identify the chat.",
|
||||
"BlueBubbles react requires messageId parameter (the message ID to react to). " +
|
||||
"Use action=react with messageId=<message_id>, emoji=<emoji>, and to/chatGuid to identify the chat.",
|
||||
);
|
||||
}
|
||||
// Resolve short ID (e.g., "1", "2") to full UUID
|
||||
const messageId = resolveBlueBubblesMessageId(rawMessageId, { requireKnownShortId: true });
|
||||
const partIndex = readNumberParam(params, "partIndex", { integer: true });
|
||||
const resolvedChatGuid = await resolveChatGuid();
|
||||
|
||||
@@ -161,20 +168,22 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
|
||||
"Apple removed the ability to edit iMessages in this version.",
|
||||
);
|
||||
}
|
||||
const messageId = readStringParam(params, "messageId");
|
||||
const rawMessageId = readStringParam(params, "messageId");
|
||||
const newText =
|
||||
readStringParam(params, "text") ??
|
||||
readStringParam(params, "newText") ??
|
||||
readStringParam(params, "message");
|
||||
if (!messageId || !newText) {
|
||||
if (!rawMessageId || !newText) {
|
||||
const missing: string[] = [];
|
||||
if (!messageId) missing.push("messageId (the message GUID to edit)");
|
||||
if (!rawMessageId) missing.push("messageId (the message ID to edit)");
|
||||
if (!newText) missing.push("text (the new message content)");
|
||||
throw new Error(
|
||||
`BlueBubbles edit requires: ${missing.join(", ")}. ` +
|
||||
`Use action=edit with messageId=<message_guid>, text=<new_content>.`,
|
||||
`Use action=edit with messageId=<message_id>, text=<new_content>.`,
|
||||
);
|
||||
}
|
||||
// Resolve short ID (e.g., "1", "2") to full UUID
|
||||
const messageId = resolveBlueBubblesMessageId(rawMessageId, { requireKnownShortId: true });
|
||||
const partIndex = readNumberParam(params, "partIndex", { integer: true });
|
||||
const backwardsCompatMessage = readStringParam(params, "backwardsCompatMessage");
|
||||
|
||||
@@ -184,18 +193,20 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
|
||||
backwardsCompatMessage: backwardsCompatMessage ?? undefined,
|
||||
});
|
||||
|
||||
return jsonResult({ ok: true, edited: messageId });
|
||||
return jsonResult({ ok: true, edited: rawMessageId });
|
||||
}
|
||||
|
||||
// Handle unsend action
|
||||
if (action === "unsend") {
|
||||
const messageId = readStringParam(params, "messageId");
|
||||
if (!messageId) {
|
||||
const rawMessageId = readStringParam(params, "messageId");
|
||||
if (!rawMessageId) {
|
||||
throw new Error(
|
||||
"BlueBubbles unsend requires messageId parameter (the message GUID to unsend). " +
|
||||
"Use action=unsend with messageId=<message_guid>.",
|
||||
"BlueBubbles unsend requires messageId parameter (the message ID to unsend). " +
|
||||
"Use action=unsend with messageId=<message_id>.",
|
||||
);
|
||||
}
|
||||
// Resolve short ID (e.g., "1", "2") to full UUID
|
||||
const messageId = resolveBlueBubblesMessageId(rawMessageId, { requireKnownShortId: true });
|
||||
const partIndex = readNumberParam(params, "partIndex", { integer: true });
|
||||
|
||||
await unsendBlueBubblesMessage(messageId, {
|
||||
@@ -203,24 +214,26 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
|
||||
partIndex: typeof partIndex === "number" ? partIndex : undefined,
|
||||
});
|
||||
|
||||
return jsonResult({ ok: true, unsent: messageId });
|
||||
return jsonResult({ ok: true, unsent: rawMessageId });
|
||||
}
|
||||
|
||||
// Handle reply action
|
||||
if (action === "reply") {
|
||||
const messageId = readStringParam(params, "messageId");
|
||||
const rawMessageId = readStringParam(params, "messageId");
|
||||
const text = readMessageText(params);
|
||||
const to = readStringParam(params, "to") ?? readStringParam(params, "target");
|
||||
if (!messageId || !text || !to) {
|
||||
if (!rawMessageId || !text || !to) {
|
||||
const missing: string[] = [];
|
||||
if (!messageId) missing.push("messageId (the message GUID to reply to)");
|
||||
if (!rawMessageId) missing.push("messageId (the message ID to reply to)");
|
||||
if (!text) missing.push("text or message (the reply message content)");
|
||||
if (!to) missing.push("to or target (the chat target)");
|
||||
throw new Error(
|
||||
`BlueBubbles reply requires: ${missing.join(", ")}. ` +
|
||||
`Use action=reply with messageId=<message_guid>, message=<your reply>, target=<chat_target>.`,
|
||||
`Use action=reply with messageId=<message_id>, message=<your reply>, target=<chat_target>.`,
|
||||
);
|
||||
}
|
||||
// Resolve short ID (e.g., "1", "2") to full UUID
|
||||
const messageId = resolveBlueBubblesMessageId(rawMessageId, { requireKnownShortId: true });
|
||||
const partIndex = readNumberParam(params, "partIndex", { integer: true });
|
||||
|
||||
const result = await sendMessageBlueBubbles(to, text, {
|
||||
@@ -229,7 +242,7 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
|
||||
replyToPartIndex: typeof partIndex === "number" ? partIndex : undefined,
|
||||
});
|
||||
|
||||
return jsonResult({ ok: true, messageId: result.messageId, repliedTo: messageId });
|
||||
return jsonResult({ ok: true, messageId: result.messageId, repliedTo: rawMessageId });
|
||||
}
|
||||
|
||||
// Handle sendWithEffect action
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
resolveDefaultBlueBubblesAccountId,
|
||||
} from "./accounts.js";
|
||||
import { BlueBubblesConfigSchema } from "./config-schema.js";
|
||||
import { resolveBlueBubblesMessageId } from "./monitor.js";
|
||||
import { probeBlueBubbles, type BlueBubblesProbe } from "./probe.js";
|
||||
import { sendMessageBlueBubbles } from "./send.js";
|
||||
import {
|
||||
@@ -65,7 +66,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
||||
threading: {
|
||||
buildToolContext: ({ context, hasRepliedRef }) => ({
|
||||
currentChannelId: context.To?.trim() || undefined,
|
||||
currentThreadTs: context.ReplyToId,
|
||||
currentThreadTs: context.ReplyToIdFull ?? context.ReplyToId,
|
||||
hasRepliedRef,
|
||||
}),
|
||||
},
|
||||
@@ -237,7 +238,11 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
||||
return { ok: true, to: trimmed };
|
||||
},
|
||||
sendText: async ({ cfg, to, text, accountId, replyToId }) => {
|
||||
const replyToMessageGuid = typeof replyToId === "string" ? replyToId.trim() : "";
|
||||
const rawReplyToId = typeof replyToId === "string" ? replyToId.trim() : "";
|
||||
// Resolve short ID (e.g., "5") to full UUID
|
||||
const replyToMessageGuid = rawReplyToId
|
||||
? resolveBlueBubblesMessageId(rawReplyToId, { requireKnownShortId: true })
|
||||
: "";
|
||||
const result = await sendMessageBlueBubbles(to, text, {
|
||||
cfg: cfg as ClawdbotConfig,
|
||||
accountId: accountId ?? undefined,
|
||||
|
||||
@@ -4,8 +4,9 @@ import { fileURLToPath } from "node:url";
|
||||
import { resolveChannelMediaMaxBytes, type ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||
|
||||
import { sendBlueBubblesAttachment } from "./attachments.js";
|
||||
import { sendMessageBlueBubbles } from "./send.js";
|
||||
import { resolveBlueBubblesMessageId } from "./monitor.js";
|
||||
import { getBlueBubblesRuntime } from "./runtime.js";
|
||||
import { sendMessageBlueBubbles } from "./send.js";
|
||||
|
||||
const HTTP_URL_RE = /^https?:\/\//i;
|
||||
const MB = 1024 * 1024;
|
||||
@@ -134,12 +135,17 @@ export async function sendBlueBubblesMedia(params: {
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve short ID (e.g., "5") to full UUID
|
||||
const replyToMessageGuid = replyToId?.trim()
|
||||
? resolveBlueBubblesMessageId(replyToId.trim(), { requireKnownShortId: true })
|
||||
: undefined;
|
||||
|
||||
const attachmentResult = await sendBlueBubblesAttachment({
|
||||
to,
|
||||
buffer,
|
||||
filename: resolvedFilename ?? "attachment",
|
||||
contentType: resolvedContentType ?? undefined,
|
||||
replyToMessageGuid: replyToId?.trim() || undefined,
|
||||
replyToMessageGuid,
|
||||
opts: {
|
||||
cfg,
|
||||
accountId,
|
||||
@@ -151,7 +157,7 @@ export async function sendBlueBubblesMedia(params: {
|
||||
await sendMessageBlueBubbles(to, trimmedCaption, {
|
||||
cfg,
|
||||
accountId,
|
||||
replyToMessageGuid: replyToId?.trim() || undefined,
|
||||
replyToMessageGuid,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { ClawdbotConfig, PluginRuntime } from "clawdbot/plugin-sdk";
|
||||
import {
|
||||
handleBlueBubblesWebhookRequest,
|
||||
registerBlueBubblesWebhookTarget,
|
||||
resolveBlueBubblesMessageId,
|
||||
_resetBlueBubblesShortIdState,
|
||||
} from "./monitor.js";
|
||||
import { setBlueBubblesRuntime } from "./runtime.js";
|
||||
import type { ResolvedBlueBubblesAccount } from "./accounts.js";
|
||||
@@ -223,6 +225,8 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Reset short ID state between tests for predictable behavior
|
||||
_resetBlueBubblesShortIdState();
|
||||
mockReadAllowFromStore.mockResolvedValue([]);
|
||||
mockUpsertPairingRequest.mockResolvedValue({ code: "TESTCODE", created: true });
|
||||
mockResolveRequireMention.mockReturnValue(false);
|
||||
@@ -467,6 +471,98 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
|
||||
expect(handled).toBe(false);
|
||||
});
|
||||
|
||||
it("parses chatId when provided as a string (webhook variant)", async () => {
|
||||
const { resolveChatGuidForTarget } = await import("./send.js");
|
||||
vi.mocked(resolveChatGuidForTarget).mockClear();
|
||||
|
||||
const account = createMockAccount({ groupPolicy: "open" });
|
||||
const config: ClawdbotConfig = {};
|
||||
const core = createMockRuntime();
|
||||
setBlueBubblesRuntime(core);
|
||||
|
||||
unregister = registerBlueBubblesWebhookTarget({
|
||||
account,
|
||||
config,
|
||||
runtime: { log: vi.fn(), error: vi.fn() },
|
||||
core,
|
||||
path: "/bluebubbles-webhook",
|
||||
});
|
||||
|
||||
const payload = {
|
||||
type: "new-message",
|
||||
data: {
|
||||
text: "hello from group",
|
||||
handle: { address: "+15551234567" },
|
||||
isGroup: true,
|
||||
isFromMe: false,
|
||||
guid: "msg-1",
|
||||
chatId: "123",
|
||||
date: Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(resolveChatGuidForTarget).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
target: { kind: "chat_id", chatId: 123 },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("extracts chatGuid from nested chat object fields (webhook variant)", async () => {
|
||||
const { sendMessageBlueBubbles, resolveChatGuidForTarget } = await import("./send.js");
|
||||
vi.mocked(sendMessageBlueBubbles).mockClear();
|
||||
vi.mocked(resolveChatGuidForTarget).mockClear();
|
||||
|
||||
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
|
||||
await params.dispatcherOptions.deliver({ text: "replying now" }, { kind: "final" });
|
||||
});
|
||||
|
||||
const account = createMockAccount({ groupPolicy: "open" });
|
||||
const config: ClawdbotConfig = {};
|
||||
const core = createMockRuntime();
|
||||
setBlueBubblesRuntime(core);
|
||||
|
||||
unregister = registerBlueBubblesWebhookTarget({
|
||||
account,
|
||||
config,
|
||||
runtime: { log: vi.fn(), error: vi.fn() },
|
||||
core,
|
||||
path: "/bluebubbles-webhook",
|
||||
});
|
||||
|
||||
const payload = {
|
||||
type: "new-message",
|
||||
data: {
|
||||
text: "hello from group",
|
||||
handle: { address: "+15551234567" },
|
||||
isGroup: true,
|
||||
isFromMe: false,
|
||||
guid: "msg-1",
|
||||
chat: { chatGuid: "iMessage;+;chat123456" },
|
||||
date: Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(resolveChatGuidForTarget).not.toHaveBeenCalled();
|
||||
expect(sendMessageBlueBubbles).toHaveBeenCalledWith(
|
||||
"chat_guid:iMessage;+;chat123456",
|
||||
expect.any(String),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DM pairing behavior vs allowFrom", () => {
|
||||
@@ -1075,13 +1171,86 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalled();
|
||||
const callArgs = mockDispatchReplyWithBufferedBlockDispatcher.mock.calls[0][0];
|
||||
// ReplyToId is the full UUID since it wasn't previously cached
|
||||
expect(callArgs.ctx.ReplyToId).toBe("msg-0");
|
||||
expect(callArgs.ctx.ReplyToBody).toBe("original message");
|
||||
expect(callArgs.ctx.ReplyToSender).toBe("+15550000000");
|
||||
expect(callArgs.ctx.Body).toContain("[Replying to +15550000000 id:msg-0]");
|
||||
// Body uses just the ID (no sender) for token savings
|
||||
expect(callArgs.ctx.Body).toContain("[Replying to id:msg-0]");
|
||||
expect(callArgs.ctx.Body).toContain("original message");
|
||||
});
|
||||
|
||||
it("hydrates missing reply sender/body from the recent-message cache", async () => {
|
||||
const account = createMockAccount({ dmPolicy: "open", groupPolicy: "open" });
|
||||
const config: ClawdbotConfig = {};
|
||||
const core = createMockRuntime();
|
||||
setBlueBubblesRuntime(core);
|
||||
|
||||
unregister = registerBlueBubblesWebhookTarget({
|
||||
account,
|
||||
config,
|
||||
runtime: { log: vi.fn(), error: vi.fn() },
|
||||
core,
|
||||
path: "/bluebubbles-webhook",
|
||||
});
|
||||
|
||||
const chatGuid = "iMessage;+;chat-reply-cache";
|
||||
|
||||
const originalPayload = {
|
||||
type: "new-message",
|
||||
data: {
|
||||
text: "original message (cached)",
|
||||
handle: { address: "+15550000000" },
|
||||
isGroup: true,
|
||||
isFromMe: false,
|
||||
guid: "cache-msg-0",
|
||||
chatGuid,
|
||||
date: Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
const originalReq = createMockRequest("POST", "/bluebubbles-webhook", originalPayload);
|
||||
const originalRes = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(originalReq, originalRes);
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// Only assert the reply message behavior below.
|
||||
mockDispatchReplyWithBufferedBlockDispatcher.mockClear();
|
||||
|
||||
const replyPayload = {
|
||||
type: "new-message",
|
||||
data: {
|
||||
text: "replying now",
|
||||
handle: { address: "+15551234567" },
|
||||
isGroup: true,
|
||||
isFromMe: false,
|
||||
guid: "cache-msg-1",
|
||||
chatGuid,
|
||||
// Only the GUID is provided; sender/body must be hydrated.
|
||||
replyToMessageGuid: "cache-msg-0",
|
||||
date: Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
const replyReq = createMockRequest("POST", "/bluebubbles-webhook", replyPayload);
|
||||
const replyRes = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(replyReq, replyRes);
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalled();
|
||||
const callArgs = mockDispatchReplyWithBufferedBlockDispatcher.mock.calls[0][0];
|
||||
// ReplyToId uses short ID "1" (first cached message) for token savings
|
||||
expect(callArgs.ctx.ReplyToId).toBe("1");
|
||||
expect(callArgs.ctx.ReplyToIdFull).toBe("cache-msg-0");
|
||||
expect(callArgs.ctx.ReplyToBody).toBe("original message (cached)");
|
||||
expect(callArgs.ctx.ReplyToSender).toBe("+15550000000");
|
||||
// Body uses just the short ID (no sender) for token savings
|
||||
expect(callArgs.ctx.Body).toContain("[Replying to id:1]");
|
||||
expect(callArgs.ctx.Body).toContain("original message (cached)");
|
||||
});
|
||||
|
||||
it("falls back to threadOriginatorGuid when reply metadata is absent", async () => {
|
||||
const account = createMockAccount({ dmPolicy: "open" });
|
||||
const config: ClawdbotConfig = {};
|
||||
@@ -1436,8 +1605,9 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// Outbound message ID uses short ID "2" (inbound msg-1 is "1", outbound msg-123 is "2")
|
||||
expect(mockEnqueueSystemEvent).toHaveBeenCalledWith(
|
||||
"BlueBubbles sent message id: msg-123",
|
||||
'Assistant sent "replying now" [message_id:2]',
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:bluebubbles:dm:+15551234567",
|
||||
}),
|
||||
@@ -1605,6 +1775,99 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("short message ID mapping", () => {
|
||||
it("assigns sequential short IDs to messages", async () => {
|
||||
const account = createMockAccount({ dmPolicy: "open" });
|
||||
const config: ClawdbotConfig = {};
|
||||
const core = createMockRuntime();
|
||||
setBlueBubblesRuntime(core);
|
||||
|
||||
unregister = registerBlueBubblesWebhookTarget({
|
||||
account,
|
||||
config,
|
||||
runtime: { log: vi.fn(), error: vi.fn() },
|
||||
core,
|
||||
path: "/bluebubbles-webhook",
|
||||
});
|
||||
|
||||
const payload = {
|
||||
type: "new-message",
|
||||
data: {
|
||||
text: "hello",
|
||||
handle: { address: "+15551234567" },
|
||||
isGroup: false,
|
||||
isFromMe: false,
|
||||
guid: "msg-uuid-12345",
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalled();
|
||||
const callArgs = mockDispatchReplyWithBufferedBlockDispatcher.mock.calls[0][0];
|
||||
// MessageSid should be short ID "1" instead of full UUID
|
||||
expect(callArgs.ctx.MessageSid).toBe("1");
|
||||
expect(callArgs.ctx.MessageSidFull).toBe("msg-uuid-12345");
|
||||
});
|
||||
|
||||
it("resolves short ID back to UUID", async () => {
|
||||
const account = createMockAccount({ dmPolicy: "open" });
|
||||
const config: ClawdbotConfig = {};
|
||||
const core = createMockRuntime();
|
||||
setBlueBubblesRuntime(core);
|
||||
|
||||
unregister = registerBlueBubblesWebhookTarget({
|
||||
account,
|
||||
config,
|
||||
runtime: { log: vi.fn(), error: vi.fn() },
|
||||
core,
|
||||
path: "/bluebubbles-webhook",
|
||||
});
|
||||
|
||||
const payload = {
|
||||
type: "new-message",
|
||||
data: {
|
||||
text: "hello",
|
||||
handle: { address: "+15551234567" },
|
||||
isGroup: false,
|
||||
isFromMe: false,
|
||||
guid: "msg-uuid-12345",
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// The short ID "1" should resolve back to the full UUID
|
||||
expect(resolveBlueBubblesMessageId("1")).toBe("msg-uuid-12345");
|
||||
});
|
||||
|
||||
it("returns UUID unchanged when not in cache", () => {
|
||||
expect(resolveBlueBubblesMessageId("msg-not-cached")).toBe("msg-not-cached");
|
||||
});
|
||||
|
||||
it("returns short ID unchanged when numeric but not in cache", () => {
|
||||
expect(resolveBlueBubblesMessageId("999")).toBe("999");
|
||||
});
|
||||
|
||||
it("throws when numeric short ID is missing and requireKnownShortId is set", () => {
|
||||
expect(() =>
|
||||
resolveBlueBubblesMessageId("999", { requireKnownShortId: true }),
|
||||
).toThrow(/short message id/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromMe messages", () => {
|
||||
it("ignores messages from self (fromMe=true)", async () => {
|
||||
const account = createMockAccount();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
|
||||
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||
import { resolveAckReaction } from "clawdbot/plugin-sdk";
|
||||
import { markBlueBubblesChatRead, sendBlueBubblesTyping } from "./chat.js";
|
||||
import { resolveChatGuidForTarget, sendMessageBlueBubbles } from "./send.js";
|
||||
import { downloadBlueBubblesAttachment } from "./attachments.js";
|
||||
import { formatBlueBubblesChatTarget, isAllowedBlueBubblesSender, normalizeBlueBubblesHandle } from "./targets.js";
|
||||
import { resolveAckReaction } from "../../../src/agents/identity.js";
|
||||
import { sendBlueBubblesMedia } from "./media-send.js";
|
||||
import type { BlueBubblesAccountConfig, BlueBubblesAttachment } from "./types.js";
|
||||
import type { ResolvedBlueBubblesAccount } from "./accounts.js";
|
||||
@@ -31,6 +31,173 @@ const DEFAULT_WEBHOOK_PATH = "/bluebubbles-webhook";
|
||||
const DEFAULT_TEXT_LIMIT = 4000;
|
||||
const invalidAckReactions = new Set<string>();
|
||||
|
||||
const REPLY_CACHE_MAX = 2000;
|
||||
const REPLY_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
||||
|
||||
type BlueBubblesReplyCacheEntry = {
|
||||
accountId: string;
|
||||
messageId: string;
|
||||
shortId: string;
|
||||
chatGuid?: string;
|
||||
chatIdentifier?: string;
|
||||
chatId?: number;
|
||||
senderLabel?: string;
|
||||
body?: string;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
// Best-effort cache for resolving reply context when BlueBubbles webhooks omit sender/body.
|
||||
const blueBubblesReplyCacheByMessageId = new Map<string, BlueBubblesReplyCacheEntry>();
|
||||
|
||||
// Bidirectional maps for short ID ↔ UUID resolution (token savings optimization)
|
||||
const blueBubblesShortIdToUuid = new Map<string, string>();
|
||||
const blueBubblesUuidToShortId = new Map<string, string>();
|
||||
let blueBubblesShortIdCounter = 0;
|
||||
|
||||
function trimOrUndefined(value?: string | null): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function generateShortId(): string {
|
||||
blueBubblesShortIdCounter += 1;
|
||||
return String(blueBubblesShortIdCounter);
|
||||
}
|
||||
|
||||
function rememberBlueBubblesReplyCache(
|
||||
entry: Omit<BlueBubblesReplyCacheEntry, "shortId">,
|
||||
): BlueBubblesReplyCacheEntry {
|
||||
const messageId = entry.messageId.trim();
|
||||
if (!messageId) {
|
||||
return { ...entry, shortId: "" };
|
||||
}
|
||||
|
||||
// Check if we already have a short ID for this UUID
|
||||
let shortId = blueBubblesUuidToShortId.get(messageId);
|
||||
if (!shortId) {
|
||||
shortId = generateShortId();
|
||||
blueBubblesShortIdToUuid.set(shortId, messageId);
|
||||
blueBubblesUuidToShortId.set(messageId, shortId);
|
||||
}
|
||||
|
||||
const fullEntry: BlueBubblesReplyCacheEntry = { ...entry, shortId };
|
||||
|
||||
// Refresh insertion order.
|
||||
blueBubblesReplyCacheByMessageId.delete(messageId);
|
||||
blueBubblesReplyCacheByMessageId.set(messageId, fullEntry);
|
||||
|
||||
// Opportunistic prune.
|
||||
const cutoff = Date.now() - REPLY_CACHE_TTL_MS;
|
||||
for (const [key, value] of blueBubblesReplyCacheByMessageId) {
|
||||
if (value.timestamp < cutoff) {
|
||||
blueBubblesReplyCacheByMessageId.delete(key);
|
||||
// Clean up short ID mappings for expired entries
|
||||
if (value.shortId) {
|
||||
blueBubblesShortIdToUuid.delete(value.shortId);
|
||||
blueBubblesUuidToShortId.delete(key);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
while (blueBubblesReplyCacheByMessageId.size > REPLY_CACHE_MAX) {
|
||||
const oldest = blueBubblesReplyCacheByMessageId.keys().next().value as string | undefined;
|
||||
if (!oldest) break;
|
||||
const oldEntry = blueBubblesReplyCacheByMessageId.get(oldest);
|
||||
blueBubblesReplyCacheByMessageId.delete(oldest);
|
||||
// Clean up short ID mappings for evicted entries
|
||||
if (oldEntry?.shortId) {
|
||||
blueBubblesShortIdToUuid.delete(oldEntry.shortId);
|
||||
blueBubblesUuidToShortId.delete(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
return fullEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a short message ID (e.g., "1", "2") to a full BlueBubbles UUID.
|
||||
* Returns the input unchanged if it's already a UUID or not found in the mapping.
|
||||
*/
|
||||
export function resolveBlueBubblesMessageId(
|
||||
shortOrUuid: string,
|
||||
opts?: { requireKnownShortId?: boolean },
|
||||
): string {
|
||||
const trimmed = shortOrUuid.trim();
|
||||
if (!trimmed) return trimmed;
|
||||
|
||||
// If it looks like a short ID (numeric), try to resolve it
|
||||
if (/^\d+$/.test(trimmed)) {
|
||||
const uuid = blueBubblesShortIdToUuid.get(trimmed);
|
||||
if (uuid) return uuid;
|
||||
if (opts?.requireKnownShortId) {
|
||||
throw new Error(
|
||||
`BlueBubbles short message id "${trimmed}" is no longer available. Use MessageSidFull.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Return as-is (either already a UUID or not found)
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the short ID state. Only use in tests.
|
||||
* @internal
|
||||
*/
|
||||
export function _resetBlueBubblesShortIdState(): void {
|
||||
blueBubblesShortIdToUuid.clear();
|
||||
blueBubblesUuidToShortId.clear();
|
||||
blueBubblesReplyCacheByMessageId.clear();
|
||||
blueBubblesShortIdCounter = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the short ID for a UUID, if one exists.
|
||||
*/
|
||||
function getShortIdForUuid(uuid: string): string | undefined {
|
||||
return blueBubblesUuidToShortId.get(uuid.trim());
|
||||
}
|
||||
|
||||
function resolveReplyContextFromCache(params: {
|
||||
accountId: string;
|
||||
replyToId: string;
|
||||
chatGuid?: string;
|
||||
chatIdentifier?: string;
|
||||
chatId?: number;
|
||||
}): BlueBubblesReplyCacheEntry | null {
|
||||
const replyToId = params.replyToId.trim();
|
||||
if (!replyToId) return null;
|
||||
|
||||
const cached = blueBubblesReplyCacheByMessageId.get(replyToId);
|
||||
if (!cached) return null;
|
||||
if (cached.accountId !== params.accountId) return null;
|
||||
|
||||
const cutoff = Date.now() - REPLY_CACHE_TTL_MS;
|
||||
if (cached.timestamp < cutoff) {
|
||||
blueBubblesReplyCacheByMessageId.delete(replyToId);
|
||||
return null;
|
||||
}
|
||||
|
||||
const chatGuid = trimOrUndefined(params.chatGuid);
|
||||
const chatIdentifier = trimOrUndefined(params.chatIdentifier);
|
||||
const cachedChatGuid = trimOrUndefined(cached.chatGuid);
|
||||
const cachedChatIdentifier = trimOrUndefined(cached.chatIdentifier);
|
||||
const chatId = typeof params.chatId === "number" ? params.chatId : undefined;
|
||||
const cachedChatId = typeof cached.chatId === "number" ? cached.chatId : undefined;
|
||||
|
||||
// Avoid cross-chat collisions if we have identifiers.
|
||||
if (chatGuid && cachedChatGuid && chatGuid !== cachedChatGuid) return null;
|
||||
if (!chatGuid && chatIdentifier && cachedChatIdentifier && chatIdentifier !== cachedChatIdentifier) {
|
||||
return null;
|
||||
}
|
||||
if (!chatGuid && !chatIdentifier && chatId && cachedChatId && chatId !== cachedChatId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return cached;
|
||||
}
|
||||
|
||||
type BlueBubblesCoreRuntime = ReturnType<typeof getBlueBubblesRuntime>;
|
||||
|
||||
function logVerbose(core: BlueBubblesCoreRuntime, runtime: BlueBubblesRuntimeEnv, message: string): void {
|
||||
@@ -217,19 +384,29 @@ function buildMessagePlaceholder(message: NormalizedWebhookMessage): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
const REPLY_BODY_TRUNCATE_LENGTH = 60;
|
||||
|
||||
function formatReplyContext(message: {
|
||||
replyToId?: string;
|
||||
replyToShortId?: string;
|
||||
replyToBody?: string;
|
||||
replyToSender?: string;
|
||||
}): string | null {
|
||||
if (!message.replyToId && !message.replyToBody && !message.replyToSender) return null;
|
||||
const sender = message.replyToSender?.trim() || "unknown sender";
|
||||
const idPart = message.replyToId ? ` id:${message.replyToId}` : "";
|
||||
const body = message.replyToBody?.trim();
|
||||
if (!body) {
|
||||
return `[Replying to ${sender}${idPart}]\n[/Replying]`;
|
||||
// Prefer short ID for token savings
|
||||
const displayId = message.replyToShortId || message.replyToId;
|
||||
// Only include sender if we don't have an ID (fallback)
|
||||
const label = displayId ? `id:${displayId}` : (message.replyToSender?.trim() || "unknown");
|
||||
const rawBody = message.replyToBody?.trim();
|
||||
if (!rawBody) {
|
||||
return `[Replying to ${label}]\n[/Replying]`;
|
||||
}
|
||||
return `[Replying to ${sender}${idPart}]\n${body}\n[/Replying]`;
|
||||
// Truncate long reply bodies for token savings
|
||||
const body =
|
||||
rawBody.length > REPLY_BODY_TRUNCATE_LENGTH
|
||||
? `${rawBody.slice(0, REPLY_BODY_TRUNCATE_LENGTH)}…`
|
||||
: rawBody;
|
||||
return `[Replying to ${label}]\n${body}\n[/Replying]`;
|
||||
}
|
||||
|
||||
function readNumberLike(record: Record<string, unknown> | null, key: string): number | undefined {
|
||||
@@ -404,6 +581,15 @@ function resolveGroupFlagFromChatGuid(chatGuid?: string | null): boolean | undef
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function extractChatIdentifierFromChatGuid(chatGuid?: string | null): string | undefined {
|
||||
const guid = chatGuid?.trim();
|
||||
if (!guid) return undefined;
|
||||
const parts = guid.split(";");
|
||||
if (parts.length < 3) return undefined;
|
||||
const identifier = parts[2]?.trim();
|
||||
return identifier || undefined;
|
||||
}
|
||||
|
||||
function formatGroupAllowlistEntry(params: {
|
||||
chatGuid?: string;
|
||||
chatId?: number;
|
||||
@@ -550,20 +736,31 @@ function normalizeWebhookMessage(payload: Record<string, unknown>): NormalizedWe
|
||||
const chatGuid =
|
||||
readString(message, "chatGuid") ??
|
||||
readString(message, "chat_guid") ??
|
||||
readString(chat, "chatGuid") ??
|
||||
readString(chat, "chat_guid") ??
|
||||
readString(chat, "guid") ??
|
||||
readString(chatFromList, "chatGuid") ??
|
||||
readString(chatFromList, "chat_guid") ??
|
||||
readString(chatFromList, "guid");
|
||||
const chatIdentifier =
|
||||
readString(message, "chatIdentifier") ??
|
||||
readString(message, "chat_identifier") ??
|
||||
readString(chat, "chatIdentifier") ??
|
||||
readString(chat, "chat_identifier") ??
|
||||
readString(chat, "identifier") ??
|
||||
readString(chatFromList, "chatIdentifier") ??
|
||||
readString(chatFromList, "chat_identifier") ??
|
||||
readString(chatFromList, "identifier");
|
||||
readString(chatFromList, "identifier") ??
|
||||
extractChatIdentifierFromChatGuid(chatGuid);
|
||||
const chatId =
|
||||
readNumber(message, "chatId") ??
|
||||
readNumber(message, "chat_id") ??
|
||||
readNumber(chat, "id") ??
|
||||
readNumber(chatFromList, "id");
|
||||
readNumberLike(message, "chatId") ??
|
||||
readNumberLike(message, "chat_id") ??
|
||||
readNumberLike(chat, "chatId") ??
|
||||
readNumberLike(chat, "chat_id") ??
|
||||
readNumberLike(chat, "id") ??
|
||||
readNumberLike(chatFromList, "chatId") ??
|
||||
readNumberLike(chatFromList, "chat_id") ??
|
||||
readNumberLike(chatFromList, "id");
|
||||
const chatName =
|
||||
readString(message, "chatName") ??
|
||||
readString(chat, "displayName") ??
|
||||
@@ -679,19 +876,30 @@ function normalizeWebhookReaction(payload: Record<string, unknown>): NormalizedW
|
||||
const chatGuid =
|
||||
readString(message, "chatGuid") ??
|
||||
readString(message, "chat_guid") ??
|
||||
readString(chat, "chatGuid") ??
|
||||
readString(chat, "chat_guid") ??
|
||||
readString(chat, "guid") ??
|
||||
readString(chatFromList, "chatGuid") ??
|
||||
readString(chatFromList, "chat_guid") ??
|
||||
readString(chatFromList, "guid");
|
||||
const chatIdentifier =
|
||||
readString(message, "chatIdentifier") ??
|
||||
readString(message, "chat_identifier") ??
|
||||
readString(chat, "chatIdentifier") ??
|
||||
readString(chat, "chat_identifier") ??
|
||||
readString(chat, "identifier") ??
|
||||
readString(chatFromList, "chatIdentifier") ??
|
||||
readString(chatFromList, "chat_identifier") ??
|
||||
readString(chatFromList, "identifier");
|
||||
readString(chatFromList, "identifier") ??
|
||||
extractChatIdentifierFromChatGuid(chatGuid);
|
||||
const chatId =
|
||||
readNumberLike(message, "chatId") ??
|
||||
readNumberLike(message, "chat_id") ??
|
||||
readNumberLike(chat, "chatId") ??
|
||||
readNumberLike(chat, "chat_id") ??
|
||||
readNumberLike(chat, "id") ??
|
||||
readNumberLike(chatFromList, "chatId") ??
|
||||
readNumberLike(chatFromList, "chat_id") ??
|
||||
readNumberLike(chatFromList, "id");
|
||||
const chatName =
|
||||
readString(message, "chatName") ??
|
||||
@@ -901,14 +1109,39 @@ async function processMessage(
|
||||
target: WebhookTarget,
|
||||
): Promise<void> {
|
||||
const { account, config, runtime, core, statusSink } = target;
|
||||
if (message.fromMe) return;
|
||||
|
||||
const groupFlag = resolveGroupFlagFromChatGuid(message.chatGuid);
|
||||
const isGroup = typeof groupFlag === "boolean" ? groupFlag : message.isGroup;
|
||||
|
||||
const text = message.text.trim();
|
||||
const attachments = message.attachments ?? [];
|
||||
const placeholder = buildMessagePlaceholder(message);
|
||||
if (!text && !placeholder) {
|
||||
const rawBody = text || placeholder;
|
||||
|
||||
const cacheMessageId = message.messageId?.trim();
|
||||
let messageShortId: string | undefined;
|
||||
const cacheInboundMessage = () => {
|
||||
if (!cacheMessageId) return;
|
||||
const cacheEntry = rememberBlueBubblesReplyCache({
|
||||
accountId: account.accountId,
|
||||
messageId: cacheMessageId,
|
||||
chatGuid: message.chatGuid,
|
||||
chatIdentifier: message.chatIdentifier,
|
||||
chatId: message.chatId,
|
||||
senderLabel: message.fromMe ? "me" : message.senderId,
|
||||
body: rawBody,
|
||||
timestamp: message.timestamp ?? Date.now(),
|
||||
});
|
||||
messageShortId = cacheEntry.shortId;
|
||||
};
|
||||
|
||||
if (message.fromMe) {
|
||||
// Cache from-me messages so reply context can resolve sender/body.
|
||||
cacheInboundMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rawBody) {
|
||||
logVerbose(core, runtime, `drop: empty text sender=${message.senderId}`);
|
||||
return;
|
||||
}
|
||||
@@ -1148,6 +1381,10 @@ async function processMessage(
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache allowed inbound messages so later replies can resolve sender/body without
|
||||
// surfacing dropped content (allowlist/mention/command gating).
|
||||
cacheInboundMessage();
|
||||
|
||||
const baseUrl = account.config.serverUrl?.trim();
|
||||
const password = account.config.password?.trim();
|
||||
const maxBytes =
|
||||
@@ -1199,12 +1436,42 @@ async function processMessage(
|
||||
}
|
||||
}
|
||||
}
|
||||
const rawBody = text.trim() || placeholder;
|
||||
const replyContext = formatReplyContext(message);
|
||||
let replyToId = message.replyToId;
|
||||
let replyToBody = message.replyToBody;
|
||||
let replyToSender = message.replyToSender;
|
||||
let replyToShortId: string | undefined;
|
||||
|
||||
if (replyToId && (!replyToBody || !replyToSender)) {
|
||||
const cached = resolveReplyContextFromCache({
|
||||
accountId: account.accountId,
|
||||
replyToId,
|
||||
chatGuid: message.chatGuid,
|
||||
chatIdentifier: message.chatIdentifier,
|
||||
chatId: message.chatId,
|
||||
});
|
||||
if (cached) {
|
||||
if (!replyToBody && cached.body) replyToBody = cached.body;
|
||||
if (!replyToSender && cached.senderLabel) replyToSender = cached.senderLabel;
|
||||
replyToShortId = cached.shortId;
|
||||
if (core.logging.shouldLogVerbose()) {
|
||||
const preview = (cached.body ?? "").replace(/\s+/g, " ").slice(0, 120);
|
||||
logVerbose(
|
||||
core,
|
||||
runtime,
|
||||
`reply-context cache hit replyToId=${replyToId} sender=${replyToSender ?? ""} body="${preview}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no cached short ID, try to get one from the UUID directly
|
||||
if (replyToId && !replyToShortId) {
|
||||
replyToShortId = getShortIdForUuid(replyToId);
|
||||
}
|
||||
|
||||
const replyContext = formatReplyContext({ replyToId, replyToShortId, replyToBody, replyToSender });
|
||||
const baseBody = replyContext ? `${rawBody}\n\n${replyContext}` : rawBody;
|
||||
const fromLabel = isGroup
|
||||
? `group:${peerId}`
|
||||
: message.senderName || `user:${message.senderId}`;
|
||||
const fromLabel = isGroup ? undefined : message.senderName || `user:${message.senderId}`;
|
||||
const groupSubject = isGroup ? message.chatName?.trim() || undefined : undefined;
|
||||
const groupMembers = isGroup
|
||||
? formatGroupMembers({
|
||||
@@ -1230,12 +1497,12 @@ async function processMessage(
|
||||
});
|
||||
let chatGuidForActions = chatGuid;
|
||||
if (!chatGuidForActions && baseUrl && password) {
|
||||
const target =
|
||||
const target =
|
||||
isGroup && (chatId || chatIdentifier)
|
||||
? chatId
|
||||
? { kind: "chat_id", chatId }
|
||||
: { kind: "chat_identifier", chatIdentifier: chatIdentifier ?? "" }
|
||||
: { kind: "handle", address: message.senderId };
|
||||
? ({ kind: "chat_id", chatId } as const)
|
||||
: ({ kind: "chat_identifier", chatIdentifier: chatIdentifier ?? "" } as const)
|
||||
: ({ kind: "handle", address: message.senderId } as const);
|
||||
if (target.kind !== "chat_identifier" || target.chatIdentifier) {
|
||||
chatGuidForActions =
|
||||
(await resolveChatGuidForTarget({
|
||||
@@ -1316,10 +1583,23 @@ async function processMessage(
|
||||
? formatBlueBubblesChatTarget({ chatGuid: chatGuidForActions })
|
||||
: message.senderId;
|
||||
|
||||
const maybeEnqueueOutboundMessageId = (messageId?: string) => {
|
||||
const maybeEnqueueOutboundMessageId = (messageId?: string, snippet?: string) => {
|
||||
const trimmed = messageId?.trim();
|
||||
if (!trimmed || trimmed === "ok" || trimmed === "unknown") return;
|
||||
core.system.enqueueSystemEvent(`BlueBubbles sent message id: ${trimmed}`, {
|
||||
// Cache outbound message to get short ID
|
||||
const cacheEntry = rememberBlueBubblesReplyCache({
|
||||
accountId: account.accountId,
|
||||
messageId: trimmed,
|
||||
chatGuid: chatGuidForActions ?? chatGuid,
|
||||
chatIdentifier,
|
||||
chatId,
|
||||
senderLabel: "me",
|
||||
body: snippet ?? "",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
const displayId = cacheEntry.shortId || trimmed;
|
||||
const preview = snippet ? ` "${snippet.slice(0, 12)}${snippet.length > 12 ? "…" : ""}"` : "";
|
||||
core.system.enqueueSystemEvent(`Assistant sent${preview} [message_id:${displayId}]`, {
|
||||
sessionKey: route.sessionKey,
|
||||
contextKey: `bluebubbles:outbound:${outboundTarget}:${trimmed}`,
|
||||
});
|
||||
@@ -1343,16 +1623,20 @@ async function processMessage(
|
||||
AccountId: route.accountId,
|
||||
ChatType: isGroup ? "group" : "direct",
|
||||
ConversationLabel: fromLabel,
|
||||
ReplyToId: message.replyToId,
|
||||
ReplyToBody: message.replyToBody,
|
||||
ReplyToSender: message.replyToSender,
|
||||
// Use short ID for token savings (agent can use this to reference the message)
|
||||
ReplyToId: replyToShortId || replyToId,
|
||||
ReplyToIdFull: replyToId,
|
||||
ReplyToBody: replyToBody,
|
||||
ReplyToSender: replyToSender,
|
||||
GroupSubject: groupSubject,
|
||||
GroupMembers: groupMembers,
|
||||
SenderName: message.senderName || undefined,
|
||||
SenderId: message.senderId,
|
||||
Provider: "bluebubbles",
|
||||
Surface: "bluebubbles",
|
||||
MessageSid: message.messageId,
|
||||
// Use short ID for token savings (agent can use this to reference the message)
|
||||
MessageSid: messageShortId || message.messageId,
|
||||
MessageSidFull: message.messageId,
|
||||
Timestamp: message.timestamp,
|
||||
OriginatingChannel: "bluebubbles",
|
||||
OriginatingTo: `bluebubbles:${outboundTarget}`,
|
||||
@@ -1367,6 +1651,11 @@ async function processMessage(
|
||||
cfg: config,
|
||||
dispatcherOptions: {
|
||||
deliver: async (payload) => {
|
||||
const rawReplyToId = typeof payload.replyToId === "string" ? payload.replyToId.trim() : "";
|
||||
// Resolve short ID (e.g., "5") to full UUID
|
||||
const replyToMessageGuid = rawReplyToId
|
||||
? resolveBlueBubblesMessageId(rawReplyToId, { requireKnownShortId: true })
|
||||
: "";
|
||||
const mediaList = payload.mediaUrls?.length
|
||||
? payload.mediaUrls
|
||||
: payload.mediaUrl
|
||||
@@ -1382,10 +1671,11 @@ async function processMessage(
|
||||
to: outboundTarget,
|
||||
mediaUrl,
|
||||
caption: caption ?? undefined,
|
||||
replyToId: payload.replyToId ?? null,
|
||||
replyToId: replyToMessageGuid || null,
|
||||
accountId: account.accountId,
|
||||
});
|
||||
maybeEnqueueOutboundMessageId(result.messageId);
|
||||
const cachedBody = (caption ?? "").trim() || "<media:attachment>";
|
||||
maybeEnqueueOutboundMessageId(result.messageId, cachedBody);
|
||||
sentMessage = true;
|
||||
statusSink?.({ lastOutboundAt: Date.now() });
|
||||
}
|
||||
@@ -1400,14 +1690,12 @@ async function processMessage(
|
||||
if (!chunks.length && payload.text) chunks.push(payload.text);
|
||||
if (!chunks.length) return;
|
||||
for (const chunk of chunks) {
|
||||
const replyToMessageGuid =
|
||||
typeof payload.replyToId === "string" ? payload.replyToId.trim() : "";
|
||||
const result = await sendMessageBlueBubbles(outboundTarget, chunk, {
|
||||
cfg: config,
|
||||
accountId: account.accountId,
|
||||
replyToMessageGuid: replyToMessageGuid || undefined,
|
||||
});
|
||||
maybeEnqueueOutboundMessageId(result.messageId);
|
||||
maybeEnqueueOutboundMessageId(result.messageId, chunk);
|
||||
sentMessage = true;
|
||||
statusSink?.({ lastOutboundAt: Date.now() });
|
||||
}
|
||||
@@ -1541,7 +1829,9 @@ async function processReaction(
|
||||
|
||||
const senderLabel = reaction.senderName || reaction.senderId;
|
||||
const chatLabel = reaction.isGroup ? ` in group:${peerId}` : "";
|
||||
const text = `BlueBubbles reaction ${reaction.action}: ${reaction.emoji} by ${senderLabel}${chatLabel} on msg ${reaction.messageId}`;
|
||||
// Use short ID for token savings
|
||||
const messageDisplayId = getShortIdForUuid(reaction.messageId) || reaction.messageId;
|
||||
const text = `BlueBubbles reaction ${reaction.action}: ${reaction.emoji} by ${senderLabel}${chatLabel} on msg ${messageDisplayId}`;
|
||||
core.system.enqueueSystemEvent(text, {
|
||||
sessionKey: route.sessionKey,
|
||||
contextKey: `bluebubbles:reaction:${reaction.action}:${peerId}:${reaction.messageId}:${reaction.senderId}:${reaction.emoji}`,
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import type { ClawdbotConfig, DmPolicy, WizardPrompter } from "clawdbot/plugin-sdk";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk";
|
||||
import type {
|
||||
ChannelOnboardingAdapter,
|
||||
ChannelOnboardingDmPolicy,
|
||||
} from "../../../src/channels/plugins/onboarding-types.js";
|
||||
import { addWildcardAllowFrom, promptAccountId } from "../../../src/channels/plugins/onboarding/helpers.js";
|
||||
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||
ClawdbotConfig,
|
||||
DmPolicy,
|
||||
WizardPrompter,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
addWildcardAllowFrom,
|
||||
formatDocsLink,
|
||||
normalizeAccountId,
|
||||
promptAccountId,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
import {
|
||||
listBlueBubblesAccountIds,
|
||||
resolveBlueBubblesAccount,
|
||||
|
||||
@@ -20,32 +20,101 @@ const REACTION_TYPES = new Set([
|
||||
]);
|
||||
|
||||
const REACTION_ALIASES = new Map<string, string>([
|
||||
// General
|
||||
["heart", "love"],
|
||||
["love", "love"],
|
||||
["❤", "love"],
|
||||
["❤️", "love"],
|
||||
["red_heart", "love"],
|
||||
["thumbs_up", "like"],
|
||||
["thumbs-down", "dislike"],
|
||||
["thumbsup", "like"],
|
||||
["thumbs-up", "like"],
|
||||
["thumbsup", "like"],
|
||||
["like", "like"],
|
||||
["thumb", "like"],
|
||||
["ok", "like"],
|
||||
["thumbs_down", "dislike"],
|
||||
["thumbsdown", "dislike"],
|
||||
["thumbs-down", "dislike"],
|
||||
["dislike", "dislike"],
|
||||
["boo", "dislike"],
|
||||
["no", "dislike"],
|
||||
// Laugh
|
||||
["haha", "laugh"],
|
||||
["lol", "laugh"],
|
||||
["lmao", "laugh"],
|
||||
["rofl", "laugh"],
|
||||
["😂", "laugh"],
|
||||
["🤣", "laugh"],
|
||||
["xd", "laugh"],
|
||||
["laugh", "laugh"],
|
||||
// Emphasize / exclaim
|
||||
["emphasis", "emphasize"],
|
||||
["emphasize", "emphasize"],
|
||||
["exclaim", "emphasize"],
|
||||
["!!", "emphasize"],
|
||||
["‼", "emphasize"],
|
||||
["‼️", "emphasize"],
|
||||
["❗", "emphasize"],
|
||||
["important", "emphasize"],
|
||||
["bang", "emphasize"],
|
||||
// Question
|
||||
["question", "question"],
|
||||
["?", "question"],
|
||||
["❓", "question"],
|
||||
["❔", "question"],
|
||||
["ask", "question"],
|
||||
// Apple/Messages names
|
||||
["loved", "love"],
|
||||
["liked", "like"],
|
||||
["disliked", "dislike"],
|
||||
["laughed", "laugh"],
|
||||
["emphasized", "emphasize"],
|
||||
["questioned", "question"],
|
||||
// Colloquial / informal
|
||||
["fire", "love"],
|
||||
["🔥", "love"],
|
||||
["wow", "emphasize"],
|
||||
["!", "emphasize"],
|
||||
// Edge: generic emoji name forms
|
||||
["heart_eyes", "love"],
|
||||
["smile", "laugh"],
|
||||
["smiley", "laugh"],
|
||||
["happy", "laugh"],
|
||||
["joy", "laugh"],
|
||||
]);
|
||||
|
||||
const REACTION_EMOJIS = new Map<string, string>([
|
||||
// Love
|
||||
["❤️", "love"],
|
||||
["❤", "love"],
|
||||
["♥️", "love"],
|
||||
["♥", "love"],
|
||||
["😍", "love"],
|
||||
["💕", "love"],
|
||||
// Like
|
||||
["👍", "like"],
|
||||
["👌", "like"],
|
||||
// Dislike
|
||||
["👎", "dislike"],
|
||||
["🙅", "dislike"],
|
||||
// Laugh
|
||||
["😂", "laugh"],
|
||||
["🤣", "laugh"],
|
||||
["😆", "laugh"],
|
||||
["😁", "laugh"],
|
||||
["😹", "laugh"],
|
||||
// Emphasize
|
||||
["‼️", "emphasize"],
|
||||
["‼", "emphasize"],
|
||||
["!!", "emphasize"],
|
||||
["❗", "emphasize"],
|
||||
["❕", "emphasize"],
|
||||
["!", "emphasize"],
|
||||
// Question
|
||||
["❓", "question"],
|
||||
["❔", "question"],
|
||||
["?", "question"],
|
||||
]);
|
||||
|
||||
function resolveAccount(params: BlueBubblesReactionOpts) {
|
||||
|
||||
@@ -96,6 +96,33 @@ describe("send", () => {
|
||||
expect(result).toBe("iMessage;-;chat123");
|
||||
});
|
||||
|
||||
it("matches chat_identifier against the 3rd component of chat GUID", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
data: [
|
||||
{
|
||||
guid: "iMessage;+;chat660250192681427962",
|
||||
participants: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const target: BlueBubblesSendTarget = {
|
||||
kind: "chat_identifier",
|
||||
chatIdentifier: "chat660250192681427962",
|
||||
};
|
||||
const result = await resolveChatGuidForTarget({
|
||||
baseUrl: "http://localhost:1234",
|
||||
password: "test",
|
||||
target,
|
||||
});
|
||||
|
||||
expect(result).toBe("iMessage;+;chat660250192681427962");
|
||||
});
|
||||
|
||||
it("resolves handle target by matching participant", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
|
||||
@@ -133,6 +133,13 @@ function extractChatId(chat: BlueBubblesChatRecord): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractChatIdentifierFromChatGuid(chatGuid: string): string | null {
|
||||
const parts = chatGuid.split(";");
|
||||
if (parts.length < 3) return null;
|
||||
const identifier = parts[2]?.trim();
|
||||
return identifier ? identifier : null;
|
||||
}
|
||||
|
||||
function extractParticipantAddresses(chat: BlueBubblesChatRecord): string[] {
|
||||
const raw =
|
||||
(Array.isArray(chat.participants) ? chat.participants : null) ??
|
||||
@@ -223,7 +230,16 @@ export async function resolveChatGuidForTarget(params: {
|
||||
}
|
||||
if (targetChatIdentifier) {
|
||||
const guid = extractChatGuid(chat);
|
||||
if (guid && guid === targetChatIdentifier) return guid;
|
||||
if (guid) {
|
||||
// Back-compat: some callers might pass a full chat GUID.
|
||||
if (guid === targetChatIdentifier) return guid;
|
||||
|
||||
// Primary match: BlueBubbles `chat_identifier:*` targets correspond to the
|
||||
// third component of the chat GUID: `service;(+|-) ;identifier`.
|
||||
const guidIdentifier = extractChatIdentifierFromChatGuid(guid);
|
||||
if (guidIdentifier && guidIdentifier === targetChatIdentifier) return guid;
|
||||
}
|
||||
|
||||
const identifier =
|
||||
typeof chat.identifier === "string"
|
||||
? chat.identifier
|
||||
@@ -232,7 +248,7 @@ export async function resolveChatGuidForTarget(params: {
|
||||
: typeof chat.chat_identifier === "string"
|
||||
? chat.chat_identifier
|
||||
: "";
|
||||
if (identifier && identifier === targetChatIdentifier) return extractChatGuid(chat);
|
||||
if (identifier && identifier === targetChatIdentifier) return guid ?? extractChatGuid(chat);
|
||||
}
|
||||
if (normalizedHandle) {
|
||||
const guid = extractChatGuid(chat);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/copilot-proxy",
|
||||
"version": "2026.1.17-1",
|
||||
"version": "2026.1.20-2",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Copilot Proxy provider plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"name": "@clawdbot/diagnostics-otel",
|
||||
"version": "2026.1.17-1",
|
||||
"version": "2026.1.20-2",
|
||||
"type": "module",
|
||||
"description": "Clawdbot diagnostics OpenTelemetry exporter",
|
||||
"clawdbot": {
|
||||
"extensions": ["./index.ts"]
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"name": "@clawdbot/discord",
|
||||
"version": "2026.1.17-1",
|
||||
"version": "2026.1.20-2",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Discord channel plugin",
|
||||
"clawdbot": {
|
||||
"extensions": ["./index.ts"]
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/google-antigravity-auth",
|
||||
"version": "2026.1.17-1",
|
||||
"version": "2026.1.20-2",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Google Antigravity OAuth provider plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/google-gemini-cli-auth",
|
||||
"version": "2026.1.17-1",
|
||||
"version": "2026.1.20-2",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Gemini CLI OAuth provider plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"name": "@clawdbot/imessage",
|
||||
"version": "2026.1.17-1",
|
||||
"version": "2026.1.20-2",
|
||||
"type": "module",
|
||||
"description": "Clawdbot iMessage channel plugin",
|
||||
"clawdbot": {
|
||||
"extensions": ["./index.ts"]
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.1.20-2
|
||||
|
||||
### Changes
|
||||
- Version alignment with core Clawdbot release numbers.
|
||||
|
||||
## 2026.1.20
|
||||
|
||||
### Changes
|
||||
- Version alignment with core Clawdbot release numbers.
|
||||
|
||||
## 2026.1.17-1
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/matrix",
|
||||
"version": "2026.1.17-1",
|
||||
"version": "2026.1.20-2",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Matrix channel plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/memory-core",
|
||||
"version": "2026.1.17-1",
|
||||
"version": "2026.1.20-2",
|
||||
"type": "module",
|
||||
"description": "Clawdbot core memory search plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/memory-lancedb",
|
||||
"version": "0.0.1",
|
||||
"version": "2026.1.20-2",
|
||||
"type": "module",
|
||||
"description": "Clawdbot LanceDB-backed long-term memory plugin with auto-recall/capture",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.1.20-2
|
||||
|
||||
### Changes
|
||||
- Version alignment with core Clawdbot release numbers.
|
||||
|
||||
## 2026.1.20
|
||||
|
||||
### Changes
|
||||
- Version alignment with core Clawdbot release numbers.
|
||||
|
||||
## 2026.1.17-1
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/msteams",
|
||||
"version": "2026.1.17-1",
|
||||
"version": "2026.1.20-2",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Microsoft Teams channel plugin",
|
||||
"clawdbot": {
|
||||
@@ -14,7 +14,9 @@
|
||||
"docsPath": "/channels/msteams",
|
||||
"docsLabel": "msteams",
|
||||
"blurb": "Bot Framework; enterprise support.",
|
||||
"aliases": ["teams"],
|
||||
"aliases": [
|
||||
"teams"
|
||||
],
|
||||
"order": 60
|
||||
},
|
||||
"install": {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"name": "@clawdbot/nextcloud-talk",
|
||||
"version": "2026.1.17-1",
|
||||
"version": "2026.1.20-2",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Nextcloud Talk channel plugin",
|
||||
"clawdbot": {
|
||||
"extensions": ["./index.ts"],
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
],
|
||||
"channel": {
|
||||
"id": "nextcloud-talk",
|
||||
"label": "Nextcloud Talk",
|
||||
@@ -12,7 +14,10 @@
|
||||
"docsPath": "/channels/nextcloud-talk",
|
||||
"docsLabel": "nextcloud-talk",
|
||||
"blurb": "Self-hosted chat via Nextcloud Talk webhook bots.",
|
||||
"aliases": ["nc-talk", "nc"],
|
||||
"aliases": [
|
||||
"nc-talk",
|
||||
"nc"
|
||||
],
|
||||
"order": 65,
|
||||
"quickstartAllowFrom": true
|
||||
},
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.1.20-2
|
||||
|
||||
### Changes
|
||||
- Version alignment with core Clawdbot release numbers.
|
||||
|
||||
## 2026.1.20
|
||||
|
||||
### Changes
|
||||
- Version alignment with core Clawdbot release numbers.
|
||||
|
||||
## 2026.1.19-1
|
||||
|
||||
Initial release.
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"name": "@clawdbot/nostr",
|
||||
"version": "2026.1.19-1",
|
||||
"version": "2026.1.20-2",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Nostr channel plugin for NIP-04 encrypted DMs",
|
||||
"clawdbot": {
|
||||
"extensions": ["./index.ts"],
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
],
|
||||
"channel": {
|
||||
"id": "nostr",
|
||||
"label": "Nostr",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"name": "@clawdbot/signal",
|
||||
"version": "2026.1.17-1",
|
||||
"version": "2026.1.20-2",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Signal channel plugin",
|
||||
"clawdbot": {
|
||||
"extensions": ["./index.ts"]
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"name": "@clawdbot/slack",
|
||||
"version": "2026.1.17-1",
|
||||
"version": "2026.1.20-2",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Slack channel plugin",
|
||||
"clawdbot": {
|
||||
"extensions": ["./index.ts"]
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
resolveDefaultSlackAccountId,
|
||||
resolveSlackAccount,
|
||||
resolveSlackGroupRequireMention,
|
||||
buildSlackThreadingToolContext,
|
||||
setAccountEnabledInConfigSection,
|
||||
slackOnboardingAdapter,
|
||||
SlackConfigSchema,
|
||||
@@ -164,18 +165,7 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
|
||||
resolveReplyToMode: ({ cfg, accountId }) =>
|
||||
resolveSlackAccount({ cfg, accountId }).replyToMode ?? "off",
|
||||
allowTagsWhenOff: true,
|
||||
buildToolContext: ({ cfg, accountId, context, hasRepliedRef }) => {
|
||||
const configuredReplyToMode = resolveSlackAccount({ cfg, accountId }).replyToMode ?? "off";
|
||||
const effectiveReplyToMode = context.ThreadLabel ? "all" : configuredReplyToMode;
|
||||
return {
|
||||
currentChannelId: context.To?.startsWith("channel:")
|
||||
? context.To.slice("channel:".length)
|
||||
: undefined,
|
||||
currentThreadTs: context.ReplyToId,
|
||||
replyToMode: effectiveReplyToMode,
|
||||
hasRepliedRef,
|
||||
};
|
||||
},
|
||||
buildToolContext: (params) => buildSlackThreadingToolContext(params),
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeSlackMessagingTarget,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"name": "@clawdbot/telegram",
|
||||
"version": "2026.1.17-1",
|
||||
"version": "2026.1.20-2",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Telegram channel plugin",
|
||||
"clawdbot": {
|
||||
"extensions": ["./index.ts"]
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user