Compare commits

..
353 changed files with 4945 additions and 19149 deletions

No files matched your search

+11 -21
View File
@@ -5,12 +5,6 @@
- Tests: colocated `*.test.ts`.
- Docs: `docs/` (images, queue, Pi config). Built output lives in `dist/`.
## Docs Linking (Mintlify)
- Docs are hosted on Mintlify (docs.clawd.bot).
- Internal doc links in `docs/**/*.md`: root-relative, no `.md`/`.mdx` (example: `[Config](/configuration)`).
- Section cross-references: use anchors on root-relative paths (example: `[Hooks](/configuration#hooks)`).
- README (GitHub): keep absolute docs URLs (`https://docs.clawd.bot/...`) so links work on GitHub.
## Build, Test, and Development Commands
- Runtime baseline: Node **22+** (keep Node + Bun paths working).
- Install deps: `pnpm install`
@@ -25,7 +19,6 @@
## Coding Style & Naming Conventions
- Language: TypeScript (ESM). Prefer strict typing; avoid `any`.
- Formatting/linting via Biome; run `pnpm lint` before commits.
- Add brief code comments for tricky or non-obvious logic.
- Keep files concise; extract helpers instead of “V2” copies. Use existing patterns for CLI options and dependency injection via `createDefaultDeps`.
- Aim to keep files under ~700 LOC; guideline only (not a hard guardrail). Split/refactor when it improves clarity or testability.
@@ -57,34 +50,31 @@
- Pi sessions live under `~/.clawdbot/sessions/` by default; the base directory is not configurable.
- Never commit or publish real phone numbers, videos, or live configuration values. Use obviously fake placeholders in docs, tests, and examples.
## Troubleshooting
- Rebrand/migration issues (Clawdis → Clawdbot) or legacy config/service warnings: run `clawdbot doctor` (see `docs/gateway/doctor.md`).
## Agent-Specific Notes
- 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`.
- If shared guardrails are available locally, review them; otherwise follow this repo's guidance.
- Gateway currently runs only as the menubar app (launchctl shows `application.com.steipete.clawdbot.debug.*`), 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 expecting `com.steipete.clawdbot`. **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 subsystem `com.steipete.clawdbot`; it supports follow/tail/category filters and expects passwordless sudo for `/usr/bin/log`.
- Also read the shared guardrails at `~/Projects/oracle/AGENTS.md` and `~/Projects/agent-scripts/AGENTS.MD` before making changes; align with any cross-repo rules noted there.
- 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.
- **Restart apps:** “restart iOS/Android apps” means rebuild (recompile/install) and relaunch, not just kill/launch.
- **Device checks:** before testing, verify connected real devices (iOS/Android) before reaching for simulators/emulators.
- iOS Team ID lookup: `security find-identity -p codesigning -v` → use Apple Development (…) TEAMID. Fallback: `defaults read com.apple.dt.Xcode IDEProvisioningTeamIdentifiers`.
- A2UI bundle hash: `src/canvas-host/a2ui/.bundle.hash` is auto-generated; ignore unexpected changes, and only regenerate via `pnpm canvas:a2ui:bundle` (or `scripts/bundle-a2ui.sh`) when needed. Commit the hash as a separate commit.
- Release signing/notary keys are managed outside the repo; follow internal release docs.
- Notary auth env vars (`APP_STORE_CONNECT_ISSUER_ID`, `APP_STORE_CONNECT_KEY_ID`, `APP_STORE_CONNECT_API_KEY_P8`) are expected in your environment (per internal release docs).
- **Multi-agent safety:** do **not** create/apply/drop `git stash` entries unless explicitly requested (this includes `git pull --rebase --autostash`). Assume other agents may be working; keep unrelated WIP untouched and avoid cross-cutting state changes.
- **Multi-agent safety:** when the user says "push", you may `git pull --rebase` to integrate latest changes (never discard other agents' work). When the user says "commit", scope to your changes only. When the user says "commit all", commit everything in grouped chunks.
- **Multi-agent safety:** do **not** create/remove/modify `git worktree` checkouts (or edit `.worktrees/*`) unless explicitly requested.
- **Multi-agent safety:** do **not** switch branches / check out a different branch unless explicitly requested.
- Notary key file lives at `~/Library/CloudStorage/Dropbox/Backup/AppStore/AuthKey_NJF3NFGTS3.p8` (Sparkle keys live under `~/Library/CloudStorage/Dropbox/Backup/Sparkle`).
- Notary auth env vars (`APP_STORE_CONNECT_ISSUER_ID`, `APP_STORE_CONNECT_KEY_ID`, `APP_STORE_CONNECT_API_KEY_P8`) are in `~/.profile`.
- **Multi-agent safety:** do **not** create/apply/drop `git stash` entries unless Peter explicitly asks (this includes `git pull --rebase --autostash`). Assume other agents may be working; keep unrelated WIP untouched and avoid cross-cutting state changes.
- **Multi-agent safety:** when Peter says "push", you may `git pull --rebase` to integrate latest changes (never discard other agents' work). When Peter says "commit", scope to your changes only. When Peter says "commit all", commit everything in grouped chunks.
- **Multi-agent safety:** do **not** create/remove/modify `git worktree` checkouts (or edit `.worktrees/*`) unless Peter explicitly asks.
- **Multi-agent safety:** do **not** switch branches / check out a different branch unless Peter explicitly asks.
- **Multi-agent safety:** running multiple agents is OK as long as each agent has its own session.
- **Multi-agent safety:** when you see unrecognized files, keep going; focus on your changes and commit only those.
- When asked to open a “session” file, open the Pi session logs under `~/.clawdbot/sessions/*.jsonl` (newest unless a specific ID is given), not the default `sessions.json`. If logs are needed from another machine, SSH via Tailscale and read the same path there.
- When asked to open a “session” file, open the Pi session logs under `~/.clawdbot/sessions/*.jsonl` (newest unless a specific ID is given), not the default `sessions.json`. If logs are needed from Mac Studio, SSH via Tailscale and read the same path there.
- Menubar dimming + restart flow mirrors Trimmy: use `scripts/restart-mac.sh` (kills all Clawdbot variants, runs `swift build`, packages, relaunches). Icon dimming depends on MenuBarExtraAccess wiring in AppMain; keep `appearsDisabled` updates intact when touching the status item.
- Do not rebuild the macOS app over SSH; rebuilds must be run directly on the Mac.
- Never send streaming/partial replies to external messaging surfaces (WhatsApp, Telegram); only final replies should be delivered there. Streaming/tool events may still go to internal UIs/control channel.
- Voice wake forwarding tips:
- Command template should stay `clawdbot-mac agent --message "${text}" --thinking low`; `VoiceWakeForwarder` already shell-escapes `${text}`. Don’t add extra quotes.
- launchd PATH is minimal; ensure the app’s launch agent PATH includes standard system paths plus your pnpm bin (typically `$HOME/Library/pnpm`) so `pnpm`/`clawdbot` binaries resolve when invoked via `clawdbot-mac`.
- launchd PATH is minimal; ensure the app’s launch agent sets PATH to include `/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/steipete/Library/pnpm` so `pnpm`/`clawdbot` binaries resolve when invoked via `clawdbot-mac`.
- For manual `clawdbot send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tool’s escaping.
## Exclamation Mark Escaping Workaround
+2 -52
View File
@@ -10,67 +10,31 @@
- New default: DM pairing (`dmPolicy="pairing"` / `discord.dm.policy="pairing"` / `slack.dm.policy="pairing"`).
- To keep old “open to everyone” behavior: set `dmPolicy="open"` and include `"*"` in the relevant `allowFrom` (Discord/Slack: `discord.dm.allowFrom` / `slack.dm.allowFrom`).
- Approve requests via `clawdbot pairing list --provider <provider>` + `clawdbot pairing approve --provider <provider> <code>` (Telegram also supports `clawdbot telegram pairing ...`).
- Sandbox: default `agent.sandbox.scope` to `"agent"` (one container/workspace per agent). Use `"session"` for per-session isolation; `"shared"` disables cross-session isolation.
- Timestamps in agent envelopes are now UTC (compact `YYYY-MM-DDTHH:mmZ`); removed `messages.timestampPrefix`. Add `agent.userTimezone` to tell the model the user’s local time (system prompt only).
- Model config schema changes (auth profiles + model lists); doctor auto-migrates and the gateway rewrites legacy configs on startup.
- Commands: gate all slash commands to authorized senders; add `/compact` to manually compact session context.
- Groups: `whatsapp.groups`, `telegram.groups`, and `imessage.groups` now act as allowlists when set. Add `"*"` to keep allow-all behavior.
- Auto-reply: removed `autoReply` from Discord/Slack/Telegram channel configs; use `requireMention` instead (Telegram topics now support `requireMention` overrides).
### Fixes
- Pairing: generate DM pairing codes with CSPRNG, expire pending codes after 1 hour, and avoid re-sending codes for already pending requests.
- Pairing: lock + atomically write pairing stores with 0600 perms and stop logging pairing codes in provider logs.
- Discord: include all inbound attachments in `MediaPaths`/`MediaUrls` (back-compat `MediaPath`/`MediaUrl` still first).
- Sandbox: add `agent.sandbox.workspaceAccess` (`none`/`ro`/`rw`) to control agent workspace visibility inside the container; `ro` hard-disables `write`/`edit`.
- Tools: add Telegram/WhatsApp reaction tools (with per-provider gating). Thanks @zats for PR #353.
- Tools: unify reaction removal semantics across Discord/Slack/Telegram/WhatsApp and allow WhatsApp reaction routing across accounts.
- Gateway/CLI: add daemon runtime selection (Node recommended; Bun optional) and document WhatsApp/Baileys Bun WebSocket instability on reconnect.
- CLI: add `clawdbot docs` live docs search with pretty output.
- CLI: add `clawdbot agents` (list/add/delete) with wizarded workspace/setup, provider login, and full prune on delete.
- Agent: treat compaction retry AbortError as a fallback trigger without swallowing non-abort errors. Thanks @erikpr1994 for PR #341.
- Agent: deliver final replies for non-streaming models when block chunking is enabled. Thank you @mneves75 for PR #369!
- Agent: trim bootstrap context injections and keep group guidance concise (emoji reactions allowed). Thanks @tobiasbischoff for PR #370.
- Sub-agents: allow `sessions_spawn` model overrides and error on invalid models. Thanks @azade-c for PR #298.
- Sub-agents: skip invalid model overrides with a warning and keep the run alive; tool exceptions now return tool errors instead of crashing the agent.
- Heartbeat: default interval 30m; clarified default prompt usage and HEARTBEAT.md template behavior.
- Skills: harden skill installs with process-group timeouts, clearer timeout messaging, and logged rerun commands when installs fail.
- Heartbeat: default interval now 30m with a new default prompt + HEARTBEAT.md template.
- Onboarding: write auth profiles to the multi-agent path (`~/.clawdbot/agents/main/agent/`) so the gateway finds credentials on first startup. Thanks @minghinmatthewlam for PR #327.
- Docs: add missing `ui:install` setup step in the README. Thanks @hugobarauna for PR #300.
- Docs: sanitize AGENTS guidance and add Clawdis migration troubleshooting note. Thanks @buddyh for PR #348.
- Docs: add ClawdHub guide and hubs link for browsing, install, and sync workflows.
- Docs: add FAQ for PNPM/Bun lockfile migration warning; link AgentSkills spec + ClawdHub guide (`/clawdhub`) from skills docs.
- Build: import tool-display JSON as a module instead of runtime file reads. Thanks @mukhtharcm for PR #312.
- Status: add provider usage snapshots to `/status`, `clawdbot status --usage`, and the macOS menu bar.
- Build: fix macOS packaging QR smoke test for the bun-compiled relay. Thanks @dbhurley for PR #358.
- Browser: fix `browser snapshot`/`browser act` timeouts under Bun by patching Playwright’s CDP WebSocket selection. Thanks @azade-c for PR #307.
- Browser: add `--browser-profile` flag and honor profile in tabs routes + browser tool. Thanks @jamesgroat for PR #324.
- Gmail: include tailscale command exit codes/output when hook setup fails (easier debugging).
- Telegram: stop typing after tool results. Thanks @AbhisekBasu1 for PR #322.
- Telegram: include sender identity in group envelope headers. (#336)
- Telegram: support forum topics with topic-isolated sessions and message_thread_id routing. Thanks @HazAT, @nachoiacovino, @RandyVentures for PR #321/#333/#334.
- Telegram: add draft streaming via `sendMessageDraft` with `telegram.streamMode`, plus `/reasoning stream` for draft-only reasoning.
- Telegram: honor `/activation` session mode for group mention gating and clarify group activation docs. Thanks @julianengel for PR #377.
- iMessage: ignore disconnect errors during shutdown (avoid unhandled promise rejections). Thanks @antons for PR #359.
- Messages: stop defaulting ack reactions to 👀 when identity emoji is missing.
- Auto-reply: require slash for control commands to avoid false triggers in normal text.
- Auto-reply: add `/reasoning on|off` to expose model reasoning blocks (italic).
- Auto-reply: place reasoning blocks before the final reply text when appended.
- Auto-reply: flag error payloads and improve Bun socket error messaging. Thanks @emanuelst for PR #331.
- Auto-reply: add per-channel/topic skill filters + system prompts for Discord/Slack/Telegram. Thanks @kitze for PR #286.
- Auto-reply: refresh `/status` output with build info, compact context, and queue depth.
- Commands: add `/stop` to the registry and route native aborts to the active chat session. Thanks @nachoiacovino for PR #295.
- Commands: unify native + text chat commands behind `commands.*` config (Discord/Slack/Telegram). Thanks @thewilloftheshadow for PR #275.
- Auto-reply: treat steer during compaction as a follow-up, queued until compaction completes.
- Auth: lock auth profile refreshes to avoid multi-instance OAuth logouts; keep credentials on refresh failure.
- Auth/Doctor: migrate Anthropic OAuth configs from `anthropic:default` → `anthropic:<email>` and surface a doctor hint on refresh failures. Thanks @RandyVentures for PR #361. (#363)
- Auth: delete legacy `auth.json` after migration to prevent stale OAuth token overwrites. Thanks @reeltimeapps for PR #368.
- Auth: auto-sync OAuth creds from Claude CLI/Codex CLI into `anthropic:claude-cli`/`openai-codex:codex-cli` and offer them as onboarding/config choices (avoids `refresh_token_reused`). Thanks @pepicrft for PR #374.
- Gateway/CLI: stop forcing localhost URL in remote mode so remote gateway config works. Thanks @oswalpalash for PR #293.
- Onboarding: prompt immediately for OpenAI Codex redirect URL on remote/headless logins.
- Configure: add OpenAI Codex (ChatGPT OAuth) auth choice (align with onboarding).
- Doctor: suggest adding the workspace memory system when missing (opt-out via `--no-workspace-suggestions`).
- Doctor: normalize default workspace path to `~/clawd` (avoid `~/clawdbot`).
- Doctor: add `--yes` and `--non-interactive` for headless/automation runs (`--non-interactive` only applies safe migrations).
- Gateway/CLI: auto-migrate legacy sessions + agent state layouts on startup (safe; WhatsApp auth still requires `clawdbot doctor`).
- Workspace: only create `BOOTSTRAP.md` for brand-new workspaces (don’t recreate after deletion).
- Build: fix duplicate protocol export, align Codex OAuth options, and add proper-lockfile typings.
- Build: install Bun in the Dockerfile so `pnpm build` can run Bun scripts. Thanks @loukotal for PR #284.
@@ -85,21 +49,17 @@
- Onboarding: when OpenAI Codex OAuth is used, default to `openai-codex/gpt-5.2` and warn if the selected model lacks auth.
- CLI: auto-migrate legacy config entries on command start (same behavior as gateway startup).
- Gateway: add `gateway stop|restart` helpers and surface launchd/systemd/schtasks stop hints when the gateway is already running.
- Cron/Heartbeat: enqueue cron system events in the resolved main session so heartbeats drain the right queue. Thanks @zats for PR #350.
- Gateway: honor `agent.timeoutSeconds` for `chat.send` and share timeout defaults across chat/cron/auto-reply. Thanks @MSch for PR #229.
- Auth: prioritize OAuth profiles but fall back to API keys when refresh fails; stored profiles now load without explicit auth order.
- Auth/CLI: normalize provider ids and Z.AI aliases across auth profile ordering and models list/status. Thanks @mneves75 for PR #303.
- Control UI: harden config Form view with schema normalization, map editing, and guardrails to prevent data loss on save.
- Cron: normalize cron.add/update inputs, align channel enums/status fields across gateway/CLI/UI/macOS, and add protocol conformance tests. Thanks @mneves75 for PR #256.
- Docs: add group chat participation guidance to the AGENTS template.
- Gmail: stop restart loop when `gog gmail watch serve` fails to bind (address already in use).
- Linux: auto-attempt lingering during onboarding (try without sudo, fallback to sudo) and prompt on install/restart to keep the gateway alive after logout/idle. Thanks @tobiasbischoff for PR #237.
- Skills: add Linuxbrew paths to gateway PATH bootstrap so the Skills UI can run brew installers under systemd/minimal environments.
- TUI: migrate key handling to the updated pi-tui Key matcher API.
- TUI: add `/elev` alias for `/elevated`.
- Logging: redact sensitive tokens in verbose tool summaries by default (configurable patterns).
- macOS: keep app connection settings local in remote mode to avoid overwriting gateway config. Thanks @ngutman for PR #310.
- macOS: honor discovered gateway ports (Bonjour TXT) so remote tunnels connect to the right ports. Thanks @kkarimi for PR #375.
- macOS: prefer gateway config reads/writes in local mode (fall back to disk if the gateway is unavailable).
- macOS: local gateway now connects via tailnet IP when bind mode is `tailnet`/`auto`.
- macOS: Connections settings now use a custom sidebar to avoid toolbar toggle issues, with rounded styling and full-width row hit targets.
@@ -111,7 +71,6 @@
- Model: avoid duplicate `missing (missing)` auth labels in `/model` list output.
- Auth: when `openai` has no API key but Codex OAuth exists, suggest `openai-codex/gpt-5.2` vs `OPENAI_API_KEY`.
- Docs: clarify auth storage, migration, and OpenAI Codex OAuth onboarding.
- Docs: clarify per-session sandbox isolation and `perSession` sharing risks.
- Sandbox: copy inbound media into sandbox workspaces so agent tools can read attachments.
- Sandbox: enable session tools in sandboxed sessions with spawned-only visibility by default (opt-in `agent.sandbox.sessionToolsVisibility = "all"`).
- Control UI: show a reading indicator bubble while the assistant is responding.
@@ -129,7 +88,6 @@
- Block streaming: preserve leading indentation in block replies (lists, indented fences).
- Docs: document systemd lingering and logged-in session requirements on macOS/Windows.
- Auto-reply: centralize tool/block/final dispatch across providers for consistent streaming + heartbeat/prefix handling. Thanks @MSch for PR #225.
- Routing: route replies back to the originating provider/chat when multiple providers share the same session. Thanks @jalehman for PR #328.
- Heartbeat: make HEARTBEAT_OK ack padding configurable across heartbeat and cron delivery. (#238) — thanks @jalehman
- Skills: emit MEDIA token after Nano Banana Pro image generation. Thanks @Iamadig for PR #271.
- WhatsApp: set sender E.164 for direct chats so owner commands work in DMs.
@@ -138,12 +96,10 @@
- Slack: fix Slack provider startup under Bun by using a named import for Bolt `App`. Thanks @snopoke for PR #299.
- Discord: surface missing-permission hints (muted/role overrides) when replies fail.
- Discord: use channel IDs for DMs instead of user IDs. Thanks @VACInc for PR #261.
- Discord: treat empty message content as media placeholder so voice messages are not dropped; enables `routing.transcribeAudio`. Thanks @VACInc for PR #339.
- Docs: clarify Slack manifest scopes (current vs optional) with references. Thanks @jarvis-medmatic for PR #235.
- Control UI: avoid Slack config ReferenceError by reading slack config snapshots. Thanks @sreekaransrinath for PR #249.
- Auth: rotate across multiple OAuth profiles with cooldown tracking and email-based profile IDs. Thanks @mukhtharcm for PR #269.
- Auth: fix multi-account OAuth rotation so round-robin alternates instead of pinning to lastGood. Thanks @mukhtharcm for PR #281.
- Auth: lock auth profile usage updates and fail fast on 429s during rotation. Thanks @mukhtharcm for PR #342.
- Configure: stop auto-writing `auth.order` for newly added auth profiles (round-robin default unless explicitly pinned).
- Telegram: honor routing.groupChat.mentionPatterns for group mention gating. Thanks Kevin Kern (@regenrek) for PR #242.
- Telegram: gate groups via `telegram.groups` allowlist (align with WhatsApp/iMessage). Thanks @kitze for PR #241.
@@ -160,7 +116,6 @@
- Models: extend `clawdbot models` status output with a masked auth overview (profiles, env sources, and OAuth counts).
### Maintenance
- Skills: add Himalaya email CLI skill. Thanks @dantelex for PR #335.
- Agent: add `skipBootstrap` config option. Thanks @onutc for PR #292.
- UI: add favicon.ico derived from the macOS app icon. Thanks @jeffersonwarrior for PR #305.
- Tooling: replace tsx with bun for TypeScript execution. Thanks @obviyus for PR #278.
@@ -171,11 +126,6 @@
- Refactor: centralize group allowlist/mention policy across providers.
- Deps: update to latest across the repo.
## 2026.1.7
### Fixes
- Android: bump version to 2026.1.7, add version code, and name APK outputs. Thanks @fcatuhe for PR #402.
## 2026.1.5-3
### Fixes
+29 -19
View File
@@ -22,7 +22,7 @@ If you want a personal, single-user assistant that feels local, fast, and always
[Website](https://clawdbot.com) · [Docs](https://docs.clawd.bot) · Getting Started: [https://docs.clawd.bot/getting-started](https://docs.clawd.bot/getting-started) · Updating: [https://docs.clawd.bot/updating](https://docs.clawd.bot/updating) · Showcase: [https://docs.clawd.bot/showcase](https://docs.clawd.bot/showcase) · FAQ: [https://docs.clawd.bot/faq](https://docs.clawd.bot/faq) · Wizard: [https://docs.clawd.bot/wizard](https://docs.clawd.bot/wizard) · Nix: [https://github.com/clawdbot/nix-clawdbot](https://github.com/clawdbot/nix-clawdbot) · Docker: [https://docs.clawd.bot/docker](https://docs.clawd.bot/docker) · Discord: [https://discord.gg/clawd](https://discord.gg/clawd)
Preferred setup: run the onboarding wizard (`clawdbot onboard`). It walks through gateway, workspace, providers, and skills. The CLI wizard is the recommended path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**.
Preferred setup: run the onboarding wizard (`clawdbot onboard`). It walks through gateway, workspace, providers, and skills. The CLI wizard is the recommended path and works on **macOS, Windows, and Linux**.
Works with npm, pnpm, or bun.
New install? Start here: https://docs.clawd.bot/getting-started
@@ -41,21 +41,21 @@ Model note: while any model is supported, I strongly recommend **Anthropic Pro/M
Do **not** download prebuilt binaries. Run from source.
Prefer `pnpm` for builds from source. Bun is optional for running TypeScript directly.
Prefer **Bun**. `pnpm` is also supported (see https://docs.clawd.bot/getting-started).
```bash
# Clone this repo
git clone https://github.com/clawdbot/clawdbot.git
cd clawdbot
pnpm install
pnpm ui:install
pnpm ui:build
pnpm build
pnpm clawdbot onboard
bun install
bun run ui:install
bun run ui:build
bun run build
bun run clawdbot onboard
```
Note: `pnpm clawdbot ...` runs TypeScript directly (via `tsx`). `pnpm build` produces `dist/` for running via Node / the packaged `clawdbot` binary.
Note: `bun run clawdbot ...` runs TypeScript directly. `bun run build` produces `dist/` for running via Node / the packaged `clawdbot` binary.
## Quick start (TL;DR)
@@ -64,23 +64,23 @@ Runtime: **Node ≥22**.
Full beginner guide (auth, pairing, providers): https://docs.clawd.bot/getting-started
```bash
pnpm clawdbot onboard
bun run clawdbot onboard
pnpm clawdbot gateway --port 18789 --verbose
bun run clawdbot gateway --port 18789 --verbose
# Dev loop (auto-reload on TS changes)
pnpm gateway:watch
bun run gateway:watch
# Send a message
pnpm clawdbot send --to +1234567890 --message "Hello from Clawdbot"
bun run clawdbot send --to +1234567890 --message "Hello from Clawdbot"
# Talk to the assistant (optionally deliver back to WhatsApp/Telegram/Slack/Discord)
pnpm clawdbot agent --message "Ship checklist" --thinking high
bun run clawdbot agent --message "Ship checklist" --thinking high
```
Upgrading? https://docs.clawd.bot/updating (and run `clawdbot doctor`).
If you run from source, prefer `pnpm clawdbot …` (not global `clawdbot`).
If you run from source, prefer `bun run clawdbot …` or `pnpm clawdbot …` (not global `clawdbot`).
## Security defaults (DM access)
@@ -138,6 +138,17 @@ Run `clawdbot doctor` to surface risky/misconfigured DM policies.
- [Nix mode](https://docs.clawd.bot/nix) for declarative config; [Docker](https://docs.clawd.bot/docker)-based installs.
- [Doctor](https://docs.clawd.bot/doctor) migrations, [logging](https://docs.clawd.bot/logging).
#### Gateway service (optional)
Install a supervised service (launchd/systemd/Scheduled Task):
```bash
clawdbot gateway install
clawdbot gateway service-status
```
Aliases: `clawdbot service ...` or `clawdbot daemon ...`.
## How it works (short)
```
@@ -369,7 +380,7 @@ Use these when you’re past the onboarding flow and want the deeper reference.
- [Wire external triggers via the webhook surface.](https://docs.clawd.bot/webhook)
- [Set up Gmail Pub/Sub triggers.](https://docs.clawd.bot/gmail-pubsub)
- [Learn the macOS menu bar companion details.](https://docs.clawd.bot/mac/menu-bar)
- [Platform guides: Windows (WSL2)](https://docs.clawd.bot/windows), [Linux](https://docs.clawd.bot/linux), [macOS](https://docs.clawd.bot/macos), [iOS](https://docs.clawd.bot/ios), [Android](https://docs.clawd.bot/android)
- [Platform guides: Windows](https://docs.clawd.bot/windows), [Linux](https://docs.clawd.bot/linux), [macOS](https://docs.clawd.bot/macos), [iOS](https://docs.clawd.bot/ios), [Android](https://docs.clawd.bot/android)
- [Debug common failures with the troubleshooting guide.](https://docs.clawd.bot/troubleshooting)
- [Review security guidance before exposing anything.](https://docs.clawd.bot/security)
@@ -416,7 +427,7 @@ Use these when you’re past the onboarding flow and want the deeper reference.
- [macOS voice wake](https://docs.clawd.bot/mac/voicewake)
- [iOS node](https://docs.clawd.bot/ios)
- [Android node](https://docs.clawd.bot/android)
- [Windows (WSL2)](https://docs.clawd.bot/windows)
- [Windows app](https://docs.clawd.bot/windows)
- [Linux app](https://docs.clawd.bot/linux)
## Email hooks (Gmail)
@@ -453,6 +464,5 @@ Thanks to all clawtributors:
<a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a>
<a href="https://github.com/adamgall"><img src="https://avatars.githubusercontent.com/u/706929?v=4&s=48" width="48" height="48" alt="adamgall" title="adamgall"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/regenrek"><img src="https://avatars.githubusercontent.com/u/5182020?v=4&s=48" width="48" height="48" alt="regenrek" title="regenrek"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="tobiasbischoff" title="tobiasbischoff"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a>
<a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="Iamadig" title="Iamadig"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a>
<a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="ManuelHettich" title="ManuelHettich"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="AbhisekBasu1" title="AbhisekBasu1"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a>
<a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a>
</p>
<a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="ManuelHettich" title="ManuelHettich"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="AbhisekBasu1" title="AbhisekBasu1"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a>
</p>
+2 -12
View File
@@ -19,8 +19,8 @@ android {
applicationId = "com.clawdbot.android"
minSdk = 31
targetSdk = 36
versionCode = 20260107
versionName = "2026.1.7"
versionCode = 1
versionName = "2.0.0-beta3"
}
buildTypes {
@@ -54,16 +54,6 @@ android {
}
}
androidComponents {
onVariants { variant ->
variant.outputs.forEach { output ->
val apkOutput = output as? com.android.build.api.variant.ApkVariantOutput ?: return@forEach
val versionName = variant.versionName.orNull ?: "0"
apkOutput.outputFileName.set("clawdbot-${versionName}-${variant.name}.apk")
}
}
}
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
@@ -0,0 +1,197 @@
{
"version": 1,
"fallback": {
"emoji": "🧩",
"detailKeys": [
"command",
"path",
"url",
"targetUrl",
"targetId",
"ref",
"element",
"node",
"nodeId",
"id",
"requestId",
"to",
"channelId",
"guildId",
"userId",
"name",
"query",
"pattern",
"messageId"
]
},
"tools": {
"bash": {
"emoji": "🛠️",
"title": "Bash",
"detailKeys": ["command"]
},
"process": {
"emoji": "🧰",
"title": "Process",
"detailKeys": ["sessionId"]
},
"read": {
"emoji": "📖",
"title": "Read",
"detailKeys": ["path"]
},
"write": {
"emoji": "✍️",
"title": "Write",
"detailKeys": ["path"]
},
"edit": {
"emoji": "📝",
"title": "Edit",
"detailKeys": ["path"]
},
"attach": {
"emoji": "📎",
"title": "Attach",
"detailKeys": ["path", "url", "fileName"]
},
"browser": {
"emoji": "🌐",
"title": "Browser",
"actions": {
"status": { "label": "status" },
"start": { "label": "start" },
"stop": { "label": "stop" },
"tabs": { "label": "tabs" },
"open": { "label": "open", "detailKeys": ["targetUrl"] },
"focus": { "label": "focus", "detailKeys": ["targetId"] },
"close": { "label": "close", "detailKeys": ["targetId"] },
"snapshot": {
"label": "snapshot",
"detailKeys": ["targetUrl", "targetId", "ref", "element", "format"]
},
"screenshot": {
"label": "screenshot",
"detailKeys": ["targetUrl", "targetId", "ref", "element"]
},
"navigate": {
"label": "navigate",
"detailKeys": ["targetUrl", "targetId"]
},
"console": { "label": "console", "detailKeys": ["level", "targetId"] },
"pdf": { "label": "pdf", "detailKeys": ["targetId"] },
"upload": {
"label": "upload",
"detailKeys": ["paths", "ref", "inputRef", "element", "targetId"]
},
"dialog": {
"label": "dialog",
"detailKeys": ["accept", "promptText", "targetId"]
},
"act": {
"label": "act",
"detailKeys": ["request.kind", "request.ref", "request.selector", "request.text", "request.value"]
}
}
},
"canvas": {
"emoji": "🖼️",
"title": "Canvas",
"actions": {
"present": { "label": "present", "detailKeys": ["target", "node", "nodeId"] },
"hide": { "label": "hide", "detailKeys": ["node", "nodeId"] },
"navigate": { "label": "navigate", "detailKeys": ["url", "node", "nodeId"] },
"eval": { "label": "eval", "detailKeys": ["javaScript", "node", "nodeId"] },
"snapshot": { "label": "snapshot", "detailKeys": ["format", "node", "nodeId"] },
"a2ui_push": { "label": "A2UI push", "detailKeys": ["jsonlPath", "node", "nodeId"] },
"a2ui_reset": { "label": "A2UI reset", "detailKeys": ["node", "nodeId"] }
}
},
"nodes": {
"emoji": "📱",
"title": "Nodes",
"actions": {
"status": { "label": "status" },
"describe": { "label": "describe", "detailKeys": ["node", "nodeId"] },
"pending": { "label": "pending" },
"approve": { "label": "approve", "detailKeys": ["requestId"] },
"reject": { "label": "reject", "detailKeys": ["requestId"] },
"notify": { "label": "notify", "detailKeys": ["node", "nodeId", "title", "body"] },
"camera_snap": { "label": "camera snap", "detailKeys": ["node", "nodeId", "facing", "deviceId"] },
"camera_list": { "label": "camera list", "detailKeys": ["node", "nodeId"] },
"camera_clip": { "label": "camera clip", "detailKeys": ["node", "nodeId", "facing", "duration", "durationMs"] },
"screen_record": {
"label": "screen record",
"detailKeys": ["node", "nodeId", "duration", "durationMs", "fps", "screenIndex"]
}
}
},
"cron": {
"emoji": "⏰",
"title": "Cron",
"actions": {
"status": { "label": "status" },
"list": { "label": "list" },
"add": {
"label": "add",
"detailKeys": ["job.name", "job.id", "job.schedule", "job.cron"]
},
"update": { "label": "update", "detailKeys": ["id"] },
"remove": { "label": "remove", "detailKeys": ["id"] },
"run": { "label": "run", "detailKeys": ["id"] },
"runs": { "label": "runs", "detailKeys": ["id"] },
"wake": { "label": "wake", "detailKeys": ["text", "mode"] }
}
},
"gateway": {
"emoji": "🔌",
"title": "Gateway",
"actions": {
"restart": { "label": "restart", "detailKeys": ["reason", "delayMs"] }
}
},
"whatsapp_login": {
"emoji": "🟢",
"title": "WhatsApp Login",
"actions": {
"start": { "label": "start" },
"wait": { "label": "wait" }
}
},
"discord": {
"emoji": "💬",
"title": "Discord",
"actions": {
"react": { "label": "react", "detailKeys": ["channelId", "messageId", "emoji"] },
"reactions": { "label": "reactions", "detailKeys": ["channelId", "messageId"] },
"sticker": { "label": "sticker", "detailKeys": ["to", "stickerIds"] },
"poll": { "label": "poll", "detailKeys": ["question", "to"] },
"permissions": { "label": "permissions", "detailKeys": ["channelId"] },
"readMessages": { "label": "read messages", "detailKeys": ["channelId", "limit"] },
"sendMessage": { "label": "send", "detailKeys": ["to", "content"] },
"editMessage": { "label": "edit", "detailKeys": ["channelId", "messageId"] },
"deleteMessage": { "label": "delete", "detailKeys": ["channelId", "messageId"] },
"threadCreate": { "label": "thread create", "detailKeys": ["channelId", "name"] },
"threadList": { "label": "thread list", "detailKeys": ["guildId", "channelId"] },
"threadReply": { "label": "thread reply", "detailKeys": ["channelId", "content"] },
"pinMessage": { "label": "pin", "detailKeys": ["channelId", "messageId"] },
"unpinMessage": { "label": "unpin", "detailKeys": ["channelId", "messageId"] },
"listPins": { "label": "list pins", "detailKeys": ["channelId"] },
"searchMessages": { "label": "search", "detailKeys": ["guildId", "content"] },
"memberInfo": { "label": "member", "detailKeys": ["guildId", "userId"] },
"roleInfo": { "label": "roles", "detailKeys": ["guildId"] },
"emojiList": { "label": "emoji list", "detailKeys": ["guildId"] },
"roleAdd": { "label": "role add", "detailKeys": ["guildId", "userId", "roleId"] },
"roleRemove": { "label": "role remove", "detailKeys": ["guildId", "userId", "roleId"] },
"channelInfo": { "label": "channel", "detailKeys": ["channelId"] },
"channelList": { "label": "channels", "detailKeys": ["guildId"] },
"voiceStatus": { "label": "voice", "detailKeys": ["guildId", "userId"] },
"eventList": { "label": "events", "detailKeys": ["guildId"] },
"eventCreate": { "label": "event create", "detailKeys": ["guildId", "name"] },
"timeout": { "label": "timeout", "detailKeys": ["guildId", "userId"] },
"kick": { "label": "kick", "detailKeys": ["guildId", "userId"] },
"ban": { "label": "ban", "detailKeys": ["guildId", "userId"] }
}
}
}
}
@@ -123,70 +123,6 @@ enum ClawdbotConfigFile {
return nil
}
static func remoteGatewayPort() -> Int? {
guard let url = self.remoteGatewayUrl(),
let port = url.port,
port > 0
else { return nil }
return port
}
static func remoteGatewayPort(matchingHost sshHost: String) -> Int? {
let trimmedSshHost = sshHost.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedSshHost.isEmpty,
let url = self.remoteGatewayUrl(),
let port = url.port,
port > 0,
let urlHost = url.host?.trimmingCharacters(in: .whitespacesAndNewlines),
!urlHost.isEmpty
else {
return nil
}
let sshKey = Self.hostKey(trimmedSshHost)
let urlKey = Self.hostKey(urlHost)
guard !sshKey.isEmpty, !urlKey.isEmpty, sshKey == urlKey else { return nil }
return port
}
static func setRemoteGatewayUrl(host: String, port: Int?) {
guard let port, port > 0 else { return }
let trimmedHost = host.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedHost.isEmpty else { return }
self.updateGatewayDict { gateway in
var remote = gateway["remote"] as? [String: Any] ?? [:]
let existingUrl = (remote["url"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let scheme = URL(string: existingUrl)?.scheme ?? "ws"
remote["url"] = "\(scheme)://\(trimmedHost):\(port)"
gateway["remote"] = remote
}
}
private static func remoteGatewayUrl() -> URL? {
let root = self.loadDict()
guard let gateway = root["gateway"] as? [String: Any],
let remote = gateway["remote"] as? [String: Any],
let raw = remote["url"] as? String
else {
return nil
}
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, let url = URL(string: trimmed) else { return nil }
return url
}
private static func hostKey(_ host: String) -> String {
let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard !trimmed.isEmpty else { return "" }
if trimmed.contains(":") { return trimmed }
let digits = CharacterSet(charactersIn: "0123456789.")
if trimmed.rangeOfCharacter(from: digits.inverted) == nil {
return trimmed
}
return trimmed.split(separator: ".").first.map(String.init) ?? trimmed
}
private static func parseConfigData(_ data: Data) -> [String: Any]? {
if let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
return root
@@ -1,27 +0,0 @@
import Foundation
enum GatewayAgentChannel: String, CaseIterable, Sendable {
case last
case webchat
case whatsapp
case telegram
init(raw: String?) {
let trimmed = raw?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased() ?? ""
self = GatewayAgentChannel(rawValue: trimmed) ?? .last
}
func shouldDeliver(_ isLast: Bool) -> Bool {
switch self {
case .webchat:
return false
case .last:
return isLast
case .whatsapp, .telegram:
return true
}
}
}
@@ -18,7 +18,6 @@ final class GatewayDiscoveryModel {
var lanHost: String?
var tailnetDns: String?
var sshPort: Int
var gatewayPort: Int?
var cliPath: String?
var stableID: String
var debugID: String
@@ -139,7 +138,6 @@ final class GatewayDiscoveryModel {
lanHost: parsedTXT.lanHost,
tailnetDns: parsedTXT.tailnetDns,
sshPort: parsedTXT.sshPort,
gatewayPort: parsedTXT.gatewayPort,
cliPath: parsedTXT.cliPath,
stableID: stableID,
debugID: BridgeEndpointID.prettyDescription(result.endpoint),
@@ -209,12 +207,11 @@ final class GatewayDiscoveryModel {
}
static func parseGatewayTXT(_ txt: [String: String])
-> (lanHost: String?, tailnetDns: String?, sshPort: Int, gatewayPort: Int?, cliPath: String?)
-> (lanHost: String?, tailnetDns: String?, sshPort: Int, cliPath: String?)
{
var lanHost: String?
var tailnetDns: String?
var sshPort = 22
var gatewayPort: Int?
var cliPath: String?
if let value = txt["lanHost"] {
@@ -231,18 +228,12 @@ final class GatewayDiscoveryModel {
{
sshPort = parsed
}
if let value = txt["gatewayPort"],
let parsed = Int(value.trimmingCharacters(in: .whitespacesAndNewlines)),
parsed > 0
{
gatewayPort = parsed
}
if let value = txt["cliPath"] {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
cliPath = trimmed.isEmpty ? nil : trimmed
}
return (lanHost, tailnetDns, sshPort, gatewayPort, cliPath)
return (lanHost, tailnetDns, sshPort, cliPath)
}
static func buildSSHTarget(user: String, host: String, port: Int) -> String {
@@ -694,7 +694,6 @@ extension GeneralSettings {
host: host,
port: gateway.sshPort)
self.state.remoteCliPath = gateway.cliPath ?? ""
ClawdbotConfigFile.setRemoteGatewayUrl(host: host, port: gateway.gatewayPort)
}
}
@@ -22,10 +22,6 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
private var cachedErrorText: String?
private var cacheUpdatedAt: Date?
private let refreshIntervalSeconds: TimeInterval = 12
private var cachedUsageSummary: GatewayUsageSummary?
private var cachedUsageErrorText: String?
private var usageCacheUpdatedAt: Date?
private let usageRefreshIntervalSeconds: TimeInterval = 30
private let nodesStore = NodesStore.shared
#if DEBUG
private var testControlChannelConnected: Bool?
@@ -62,7 +58,6 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
self.loadTask = Task { [weak self] in
guard let self else { return }
await self.refreshCache(force: forceRefresh)
await self.refreshUsageCache(force: forceRefresh)
await MainActor.run {
guard self.isMenuOpen else { return }
self.inject(into: menu)
@@ -113,69 +108,65 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
guard self.isControlChannelConnected else { return }
var cursor = insertIndex
var headerView: NSView?
if let snapshot = self.cachedSnapshot {
let now = Date()
let rows = snapshot.rows.filter { row in
if row.key == "main" { return true }
guard let updatedAt = row.updatedAt else { return false }
return now.timeIntervalSince(updatedAt) <= self.activeWindowSeconds
}.sorted { lhs, rhs in
if lhs.key == "main" { return true }
if rhs.key == "main" { return false }
return (lhs.updatedAt ?? .distantPast) > (rhs.updatedAt ?? .distantPast)
}
guard let snapshot = self.cachedSnapshot else {
let headerItem = NSMenuItem()
headerItem.tag = self.tag
headerItem.isEnabled = false
let hosted = self.makeHostedView(
rootView: AnyView(MenuSessionsHeaderView(count: rows.count, statusText: nil)),
width: width,
highlighted: false)
headerItem.view = hosted
headerView = hosted
menu.insertItem(headerItem, at: cursor)
cursor += 1
if rows.isEmpty {
menu.insertItem(
self.makeMessageItem(text: "No active sessions", symbolName: "minus", width: width),
at: cursor)
cursor += 1
} else {
for row in rows {
let item = NSMenuItem()
item.tag = self.tag
item.isEnabled = true
item.submenu = self.buildSubmenu(for: row, storePath: snapshot.storePath)
item.view = self.makeHostedView(
rootView: AnyView(SessionMenuLabelView(row: row, width: width)),
width: width,
highlighted: true)
menu.insertItem(item, at: cursor)
cursor += 1
}
}
} else {
let headerItem = NSMenuItem()
headerItem.tag = self.tag
headerItem.isEnabled = false
let hosted = self.makeHostedView(
headerItem.view = self.makeHostedView(
rootView: AnyView(MenuSessionsHeaderView(
count: 0,
statusText: self.cachedErrorText ?? "Loading sessions…")),
width: width,
highlighted: false)
headerItem.view = hosted
headerView = hosted
menu.insertItem(headerItem, at: cursor)
cursor += 1
menu.insertItem(headerItem, at: insertIndex)
DispatchQueue.main.async { [weak self, weak view = headerItem.view] in
guard let self, let view else { return }
self.captureMenuWidthIfAvailable(from: view)
}
return
}
cursor = self.insertUsageSection(into: menu, at: cursor, width: width)
let now = Date()
let rows = snapshot.rows.filter { row in
if row.key == "main" { return true }
guard let updatedAt = row.updatedAt else { return false }
return now.timeIntervalSince(updatedAt) <= self.activeWindowSeconds
}.sorted { lhs, rhs in
if lhs.key == "main" { return true }
if rhs.key == "main" { return false }
return (lhs.updatedAt ?? .distantPast) > (rhs.updatedAt ?? .distantPast)
}
let headerItem = NSMenuItem()
headerItem.tag = self.tag
headerItem.isEnabled = false
let headerView = self.makeHostedView(
rootView: AnyView(MenuSessionsHeaderView(count: rows.count, statusText: nil)),
width: width,
highlighted: false)
headerItem.view = headerView
menu.insertItem(headerItem, at: insertIndex)
var cursor = insertIndex + 1
if rows.isEmpty {
menu.insertItem(
self.makeMessageItem(text: "No active sessions", symbolName: "minus", width: width),
at: cursor)
return
}
for row in rows {
let item = NSMenuItem()
item.tag = self.tag
item.isEnabled = true
item.submenu = self.buildSubmenu(for: row, storePath: snapshot.storePath)
item.view = self.makeHostedView(
rootView: AnyView(SessionMenuLabelView(row: row, width: width)),
width: width,
highlighted: true)
menu.insertItem(item, at: cursor)
cursor += 1
}
DispatchQueue.main.async { [weak self, weak headerView] in
guard let self, let headerView else { return }
@@ -249,55 +240,6 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
_ = cursor
}
private func insertUsageSection(into menu: NSMenu, at cursor: Int, width: CGFloat) -> Int {
let rows = self.usageRows
let errorText = self.cachedUsageErrorText
if rows.isEmpty && errorText == nil {
return cursor
}
var cursor = cursor
let headerItem = NSMenuItem()
headerItem.tag = self.tag
headerItem.isEnabled = false
headerItem.view = self.makeHostedView(
rootView: AnyView(MenuUsageHeaderView(
count: rows.count,
statusText: errorText)),
width: width,
highlighted: false)
menu.insertItem(headerItem, at: cursor)
cursor += 1
if rows.isEmpty {
menu.insertItem(
self.makeMessageItem(text: errorText ?? "No usage available", symbolName: "minus", width: width),
at: cursor)
cursor += 1
return cursor
}
for row in rows {
let item = NSMenuItem()
item.tag = self.tag
item.isEnabled = false
item.view = self.makeHostedView(
rootView: AnyView(UsageMenuLabelView(row: row, width: width)),
width: width,
highlighted: false)
menu.insertItem(item, at: cursor)
cursor += 1
}
return cursor
}
private var usageRows: [UsageRow] {
guard let summary = self.cachedUsageSummary else { return [] }
return summary.primaryRows()
}
private var isControlChannelConnected: Bool {
#if DEBUG
if let override = self.testControlChannelConnected { return override }
@@ -422,40 +364,6 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
}
}
private func refreshUsageCache(force: Bool) async {
if !force,
let updated = self.usageCacheUpdatedAt,
Date().timeIntervalSince(updated) < self.usageRefreshIntervalSeconds
{
return
}
guard self.isControlChannelConnected else {
self.cachedUsageSummary = nil
self.cachedUsageErrorText = nil
self.usageCacheUpdatedAt = Date()
return
}
do {
self.cachedUsageSummary = try await UsageLoader.loadSummary()
self.cachedUsageErrorText = nil
self.usageCacheUpdatedAt = Date()
} catch {
if self.cachedUsageSummary == nil {
self.cachedUsageErrorText = self.compactUsageError(error)
}
self.usageCacheUpdatedAt = Date()
}
}
private func compactUsageError(_ error: Error) -> String {
let message = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines)
if message.isEmpty { return "Usage unavailable" }
if message.count > 90 { return "\(message.prefix(87))…" }
return message
}
private func compactError(_ error: Error) -> String {
if let loadError = error as? SessionLoadError {
switch loadError {
@@ -1,45 +0,0 @@
import SwiftUI
struct MenuUsageHeaderView: View {
let count: Int
let statusText: String?
private let paddingTop: CGFloat = 8
private let paddingBottom: CGFloat = 6
private let paddingTrailing: CGFloat = 10
private let paddingLeading: CGFloat = 20
var body: some View {
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .firstTextBaseline) {
Text("Usage")
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
Spacer(minLength: 10)
Text(self.subtitle)
.font(.caption)
.foregroundStyle(.secondary)
}
if let statusText, !statusText.isEmpty {
Text(statusText)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.tail)
}
}
.padding(.top, self.paddingTop)
.padding(.bottom, self.paddingBottom)
.padding(.leading, self.paddingLeading)
.padding(.trailing, self.paddingTrailing)
.frame(minWidth: 300, maxWidth: .infinity, alignment: .leading)
.transaction { txn in txn.animation = nil }
}
private var subtitle: String {
if self.count == 1 { return "1 provider" }
return "\(self.count) providers"
}
}
@@ -202,23 +202,6 @@ final class MacNodeModeCoordinator {
return 18790
}
static func remoteBridgePort() -> Int {
let fallback = Int(Self.loopbackBridgePort() ?? 18790)
let settings = CommandResolver.connectionSettings()
let sshHost = CommandResolver.parseSSHTarget(settings.target)?.host ?? ""
let base =
ClawdbotConfigFile.remoteGatewayPort(matchingHost: sshHost) ??
GatewayEnvironment.gatewayPort()
guard base > 0 else { return fallback }
return Self.derivePort(base: base, offset: 1, fallback: fallback)
}
private static func derivePort(base: Int, offset: Int, fallback: Int) -> Int {
let derived = base + offset
guard derived > 0, derived <= Int(UInt16.max) else { return fallback }
return derived
}
static func probeEndpoint(_ endpoint: NWEndpoint, timeoutSeconds: Double) async -> Bool {
let connection = NWConnection(to: endpoint, using: .tcp)
let stream = Self.makeStateStream(for: connection)
@@ -286,10 +269,7 @@ final class MacNodeModeCoordinator {
if mode == .remote {
do {
if self.tunnel == nil || self.tunnel?.process.isRunning == false {
let remotePort = Self.remoteBridgePort()
self.tunnel = try await RemotePortTunnel.create(
remotePort: remotePort,
allowRemoteUrlOverride: false)
self.tunnel = try await RemotePortTunnel.create(remotePort: 18790)
}
if let localPort = self.tunnel?.localPort,
let port = NWEndpoint.Port(rawValue: localPort)
@@ -29,7 +29,6 @@ extension OnboardingView {
user: user,
host: host,
port: gateway.sshPort)
ClawdbotConfigFile.setRemoteGatewayUrl(host: host, port: gateway.gatewayPort)
}
self.state.remoteCliPath = gateway.cliPath ?? ""
@@ -12,7 +12,6 @@ extension OnboardingView {
lanHost: "bridge.local",
tailnetDns: "bridge.ts.net",
sshPort: 2222,
gatewayPort: 18789,
cliPath: "/usr/local/bin/clawdbot",
stableID: "bridge-1",
debugID: "bridge-1",
@@ -38,11 +38,7 @@ final class RemotePortTunnel {
Task { await PortGuardian.shared.removeRecord(pid: pid) }
}
static func create(
remotePort: Int,
preferredLocalPort: UInt16? = nil,
allowRemoteUrlOverride: Bool = true
) async throws -> RemotePortTunnel {
static func create(remotePort: Int, preferredLocalPort: UInt16? = nil) async throws -> RemotePortTunnel {
let settings = CommandResolver.connectionSettings()
guard settings.mode == .remote, let parsed = CommandResolver.parseSSHTarget(settings.target) else {
throw NSError(
@@ -53,7 +49,7 @@ final class RemotePortTunnel {
let localPort = try await Self.findPort(preferred: preferredLocalPort)
let sshHost = parsed.host.trimmingCharacters(in: .whitespacesAndNewlines)
let remotePortOverride = allowRemoteUrlOverride ? Self.resolveRemotePortOverride(for: sshHost) : nil
let remotePortOverride = Self.resolveRemotePortOverride(for: sshHost)
let resolvedRemotePort = remotePortOverride ?? remotePort
if let override = remotePortOverride {
Self.logger.info(
@@ -131,9 +127,42 @@ final class RemotePortTunnel {
}
private static func resolveRemotePortOverride(for sshHost: String) -> Int? {
let trimmed = sshHost.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
return ClawdbotConfigFile.remoteGatewayPort(matchingHost: trimmed)
let root = ClawdbotConfigFile.loadDict()
guard let gateway = root["gateway"] as? [String: Any],
let remote = gateway["remote"] as? [String: Any],
let urlRaw = remote["url"] as? String
else {
return nil
}
let trimmed = urlRaw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, let url = URL(string: trimmed), let port = url.port else {
return nil
}
guard let host = url.host?.trimmingCharacters(in: .whitespacesAndNewlines),
!host.isEmpty
else {
return nil
}
let sshKey = Self.hostKey(sshHost)
let urlKey = Self.hostKey(host)
guard !sshKey.isEmpty, !urlKey.isEmpty else { return nil }
guard sshKey == urlKey else {
Self.logger.debug(
"remote url host mismatch sshHost=\(sshHost, privacy: .public) urlHost=\(host, privacy: .public)")
return nil
}
return port
}
private static func hostKey(_ host: String) -> String {
let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard !trimmed.isEmpty else { return "" }
if trimmed.contains(":") { return trimmed }
let digits = CharacterSet(charactersIn: "0123456789.")
if trimmed.rangeOfCharacter(from: digits.inverted) == nil {
return trimmed
}
return trimmed.split(separator: ".").first.map(String.init) ?? trimmed
}
private static func findPort(preferred: UInt16?) async throws -> UInt16 {
-110
View File
@@ -1,110 +0,0 @@
import Foundation
struct GatewayUsageWindow: Codable {
let label: String
let usedPercent: Double
let resetAt: Double?
}
struct GatewayUsageProvider: Codable {
let provider: String
let displayName: String
let windows: [GatewayUsageWindow]
let plan: String?
let error: String?
}
struct GatewayUsageSummary: Codable {
let updatedAt: Double
let providers: [GatewayUsageProvider]
}
struct UsageRow: Identifiable {
let id: String
let displayName: String
let plan: String?
let windowLabel: String?
let usedPercent: Double?
let resetAt: Date?
let error: String?
var titleText: String {
if let plan, !plan.isEmpty { return "\(displayName) (\(plan))" }
return displayName
}
var remainingPercent: Int? {
guard let usedPercent, usedPercent.isFinite else { return nil }
let remaining = max(0, min(100, Int(round(100 - usedPercent))))
return remaining
}
func detailText(now: Date = .init()) -> String {
if let error, !error.isEmpty { return error }
guard let remaining = self.remainingPercent else { return "No data" }
var parts = ["\(remaining)% left"]
if let windowLabel, !windowLabel.isEmpty { parts.append(windowLabel) }
if let resetAt {
let reset = UsageRow.formatResetRemaining(target: resetAt, now: now)
if let reset { parts.append("⏱\(reset)") }
}
return parts.joined(separator: " · ")
}
private static func formatResetRemaining(target: Date, now: Date) -> String? {
let diff = target.timeIntervalSince(now)
if diff <= 0 { return "now" }
let minutes = Int(floor(diff / 60))
if minutes < 60 { return "\(minutes)m" }
let hours = minutes / 60
let mins = minutes % 60
if hours < 24 { return mins > 0 ? "\(hours)h \(mins)m" : "\(hours)h" }
let days = hours / 24
if days < 7 { return "\(days)d \(hours % 24)h" }
let formatter = DateFormatter()
formatter.dateFormat = "MMM d"
return formatter.string(from: target)
}
}
extension GatewayUsageSummary {
func primaryRows() -> [UsageRow] {
self.providers.compactMap { provider in
if let error = provider.error, provider.windows.isEmpty {
return UsageRow(
id: provider.provider,
displayName: provider.displayName,
plan: provider.plan,
windowLabel: nil,
usedPercent: nil,
resetAt: nil,
error: error)
}
guard let window = provider.windows.max(by: { $0.usedPercent < $1.usedPercent }) else {
return nil
}
return UsageRow(
id: "\(provider.provider)-\(window.label)",
displayName: provider.displayName,
plan: provider.plan,
windowLabel: window.label,
usedPercent: window.usedPercent,
resetAt: window.resetAt.map { Date(timeIntervalSince1970: $0 / 1000) },
error: provider.error)
}
}
}
@MainActor
enum UsageLoader {
static func loadSummary() async throws -> GatewayUsageSummary {
let data = try await ControlChannel.shared.request(
method: "usage.status",
params: nil,
timeoutMs: 5000)
return try JSONDecoder().decode(GatewayUsageSummary.self, from: data)
}
}
@@ -1,46 +0,0 @@
import SwiftUI
struct UsageMenuLabelView: View {
let row: UsageRow
let width: CGFloat
private let paddingLeading: CGFloat = 22
private let paddingTrailing: CGFloat = 14
private let barHeight: CGFloat = 6
private var primaryTextColor: Color { .primary }
private var secondaryTextColor: Color { .secondary }
var body: some View {
VStack(alignment: .leading, spacing: 8) {
if let used = row.usedPercent {
ContextUsageBar(
usedTokens: Int(round(used)),
contextTokens: 100,
width: max(1, self.width - (self.paddingLeading + self.paddingTrailing)),
height: self.barHeight)
}
HStack(alignment: .firstTextBaseline, spacing: 6) {
Text(row.titleText)
.font(.caption.weight(.semibold))
.foregroundStyle(self.primaryTextColor)
.lineLimit(1)
.truncationMode(.middle)
.layoutPriority(1)
Spacer(minLength: 4)
Text(row.detailText())
.font(.caption.monospacedDigit())
.foregroundStyle(self.secondaryTextColor)
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)
.layoutPriority(2)
}
}
.padding(.vertical, 10)
.padding(.leading, self.paddingLeading)
.padding(.trailing, self.paddingTrailing)
}
}
@@ -691,7 +691,6 @@ public struct SessionsPatchParams: Codable, Sendable {
public let key: String
public let thinkinglevel: AnyCodable?
public let verboselevel: AnyCodable?
public let reasoninglevel: AnyCodable?
public let elevatedlevel: AnyCodable?
public let model: AnyCodable?
public let spawnedby: AnyCodable?
@@ -702,7 +701,6 @@ public struct SessionsPatchParams: Codable, Sendable {
key: String,
thinkinglevel: AnyCodable?,
verboselevel: AnyCodable?,
reasoninglevel: AnyCodable?,
elevatedlevel: AnyCodable?,
model: AnyCodable?,
spawnedby: AnyCodable?,
@@ -712,7 +710,6 @@ public struct SessionsPatchParams: Codable, Sendable {
self.key = key
self.thinkinglevel = thinkinglevel
self.verboselevel = verboselevel
self.reasoninglevel = reasoninglevel
self.elevatedlevel = elevatedlevel
self.model = model
self.spawnedby = spawnedby
@@ -723,7 +720,6 @@ public struct SessionsPatchParams: Codable, Sendable {
case key
case thinkinglevel = "thinkingLevel"
case verboselevel = "verboseLevel"
case reasoninglevel = "reasoningLevel"
case elevatedlevel = "elevatedLevel"
case model
case spawnedby = "spawnedBy"
@@ -16,52 +16,6 @@ struct ClawdbotConfigFileTests {
}
}
@MainActor
@Test
func remoteGatewayPortParsesAndMatchesHost() {
let override = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-config-\(UUID().uuidString)")
.appendingPathComponent("clawdbot.json")
.path
self.withEnv("CLAWDBOT_CONFIG_PATH", value: override) {
ClawdbotConfigFile.saveDict([
"gateway": [
"remote": [
"url": "ws://bridge.ts.net:19999",
],
],
])
#expect(ClawdbotConfigFile.remoteGatewayPort() == 19999)
#expect(ClawdbotConfigFile.remoteGatewayPort(matchingHost: "bridge.ts.net") == 19999)
#expect(ClawdbotConfigFile.remoteGatewayPort(matchingHost: "bridge") == 19999)
#expect(ClawdbotConfigFile.remoteGatewayPort(matchingHost: "other.ts.net") == nil)
}
}
@MainActor
@Test
func setRemoteGatewayUrlPreservesScheme() {
let override = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-config-\(UUID().uuidString)")
.appendingPathComponent("clawdbot.json")
.path
self.withEnv("CLAWDBOT_CONFIG_PATH", value: override) {
ClawdbotConfigFile.saveDict([
"gateway": [
"remote": [
"url": "wss://old-host:111",
],
],
])
ClawdbotConfigFile.setRemoteGatewayUrl(host: "new-host", port: 2222)
let root = ClawdbotConfigFile.loadDict()
let url = ((root["gateway"] as? [String: Any])?["remote"] as? [String: Any])?["url"] as? String
#expect(url == "wss://new-host:2222")
}
}
@Test
func stateDirOverrideSetsConfigPath() {
let dir = FileManager.default.temporaryDirectory
@@ -69,13 +69,11 @@ struct GatewayDiscoveryModelTests {
"lanHost": " studio.local ",
"tailnetDns": " peters-mac-studio-1.ts.net ",
"sshPort": " 2222 ",
"gatewayPort": " 18799 ",
"cliPath": " /opt/clawdbot "
])
#expect(parsed.lanHost == "studio.local")
#expect(parsed.tailnetDns == "peters-mac-studio-1.ts.net")
#expect(parsed.sshPort == 2222)
#expect(parsed.gatewayPort == 18799)
#expect(parsed.cliPath == "/opt/clawdbot")
}
@@ -83,13 +81,11 @@ struct GatewayDiscoveryModelTests {
let parsed = GatewayDiscoveryModel.parseGatewayTXT([
"lanHost": " ",
"tailnetDns": "\n",
"gatewayPort": "nope",
"sshPort": "nope"
])
#expect(parsed.lanHost == nil)
#expect(parsed.tailnetDns == nil)
#expect(parsed.sshPort == 22)
#expect(parsed.gatewayPort == nil)
#expect(parsed.cliPath == nil)
}
@@ -45,70 +45,6 @@ import Testing
let ok = await MacNodeModeCoordinator.probeEndpoint(endpoint, timeoutSeconds: 0.4)
#expect(ok == false)
}
@MainActor
@Test func remoteBridgePortUsesMatchingRemoteUrlPort() {
let configPath = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-config-\(UUID().uuidString)")
.appendingPathComponent("clawdbot.json")
.path
let defaults = UserDefaults.standard
let prevTarget = defaults.string(forKey: remoteTargetKey)
defer {
if let prevTarget {
defaults.set(prevTarget, forKey: remoteTargetKey)
} else {
defaults.removeObject(forKey: remoteTargetKey)
}
}
withEnv("CLAWDBOT_CONFIG_PATH", value: configPath) {
withEnv("CLAWDBOT_GATEWAY_PORT", value: "20000") {
defaults.set("user@bridge.ts.net", forKey: remoteTargetKey)
ClawdbotConfigFile.saveDict([
"gateway": [
"remote": [
"url": "ws://bridge.ts.net:25000",
],
],
])
#expect(MacNodeModeCoordinator.remoteBridgePort() == 25001)
}
}
}
@MainActor
@Test func remoteBridgePortFallsBackWhenRemoteUrlHostMismatch() {
let configPath = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-config-\(UUID().uuidString)")
.appendingPathComponent("clawdbot.json")
.path
let defaults = UserDefaults.standard
let prevTarget = defaults.string(forKey: remoteTargetKey)
defer {
if let prevTarget {
defaults.set(prevTarget, forKey: remoteTargetKey)
} else {
defaults.removeObject(forKey: remoteTargetKey)
}
}
withEnv("CLAWDBOT_CONFIG_PATH", value: configPath) {
withEnv("CLAWDBOT_GATEWAY_PORT", value: "20000") {
defaults.set("user@other.ts.net", forKey: remoteTargetKey)
ClawdbotConfigFile.saveDict([
"gateway": [
"remote": [
"url": "ws://bridge.ts.net:25000",
],
],
])
#expect(MacNodeModeCoordinator.remoteBridgePort() == 20001)
}
}
}
}
private struct TestError: Error {
+18 -15
View File
@@ -1,17 +1,20 @@
{
"$schema": "https://biomejs.dev/schemas/biome.json",
"formatter": {
"enabled": true,
"indentWidth": 2,
"indentStyle": "space"
},
"files": {
"includes": ["src/**/*.ts", "test/**/*.ts"]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
}
"$schema": "https://biomejs.dev/schemas/biome.json",
"formatter": {
"enabled": true,
"indentWidth": 2,
"indentStyle": "space"
},
"files": {
"includes": [
"src/**/*.ts",
"test/**/*.ts"
]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
}
}
@@ -19,15 +19,15 @@ mkdir -p ~/clawd
2) Copy the default workspace templates into the workspace:
```bash
cp docs/reference/templates/AGENTS.md ~/clawd/AGENTS.md
cp docs/reference/templates/SOUL.md ~/clawd/SOUL.md
cp docs/reference/templates/TOOLS.md ~/clawd/TOOLS.md
cp docs/templates/AGENTS.md ~/clawd/AGENTS.md
cp docs/templates/SOUL.md ~/clawd/SOUL.md
cp docs/templates/TOOLS.md ~/clawd/TOOLS.md
```
3) Optional: if you want the personal assistant skill roster, replace AGENTS.md with this file:
```bash
cp docs/reference/AGENTS.default.md ~/clawd/AGENTS.md
cp docs/AGENTS.default.md ~/clawd/AGENTS.md
```
4) Optional: choose a different workspace by setting `agent.workspace` (supports `~`):
@@ -36,9 +36,9 @@ Use `pnpm` (Node 22+) from the repo root. Keep the working tree clean before tag
- [ ] Build + sign the macOS app, then zip it for distribution.
- [ ] Generate the Sparkle appcast (HTML notes via [`scripts/make_appcast.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/make_appcast.sh)) and update `appcast.xml`.
- [ ] Keep the app zip (and optional dSYM zip) ready to attach to the GitHub release.
- [ ] Follow [`docs/mac/release.md`](/platforms/mac/release) for the exact commands and required env vars.
- [ ] Follow [`docs/mac/release.md`](https://docs.clawd.bot/mac/release) for the exact commands and required env vars.
- `APP_BUILD` must be numeric + monotonic (no `-beta`) so Sparkle compares versions correctly.
- If notarizing, use the `clawdbot-notary` keychain profile created from App Store Connect API env vars (see [`docs/mac/release.md`](/platforms/mac/release)).
- If notarizing, use the `clawdbot-notary` keychain profile created from App Store Connect API env vars (see [`docs/mac/release.md`](https://docs.clawd.bot/mac/release)).
6) **Publish (npm)**
- [ ] Confirm git status is clean; commit and push as needed.
+12 -12
View File
@@ -23,29 +23,29 @@ nav:
- title: "Home"
url: "/"
- title: "Clawd Setup"
url: "/start/clawd/"
url: "/clawd.html"
- title: "Gateway"
url: "/gateway/"
url: "/gateway.html"
- title: "Remote"
url: "/gateway/remote/"
url: "/remote.html"
- title: "Discovery"
url: "/gateway/discovery/"
url: "/discovery.html"
- title: "Configuration"
url: "/gateway/configuration/"
url: "/configuration.html"
- title: "WebChat"
url: "/web/webchat/"
url: "/webchat.html"
- title: "macOS App"
url: "/platforms/macos/"
url: "/macos.html"
- title: "iOS App"
url: "/platforms/ios/"
url: "/ios.html"
- title: "Android App"
url: "/platforms/android/"
url: "/android.html"
- title: "Telegram"
url: "/providers/telegram/"
url: "/telegram.html"
- title: "Security"
url: "/gateway/security/"
url: "/security.html"
- title: "Troubleshooting"
url: "/gateway/troubleshooting/"
url: "/troubleshooting.html"
kramdown:
input: GFM
File renamed without changes.
File renamed without changes.
+4 -8
View File
@@ -13,11 +13,9 @@ CLAWDBOT uses a single agent workspace directory (`agent.workspace`) as the agen
Recommended: use `clawdbot setup` to create `~/.clawdbot/clawdbot.json` if missing and initialize the workspace files.
Full workspace layout + backup guide: [`docs/agent-workspace.md`](/concepts/agent-workspace)
If `agent.sandbox` is enabled, non-main sessions can override this with
per-session workspaces under `agent.sandbox.workspaceRoot` (see
[`docs/configuration.md`](/gateway/configuration)).
[`docs/configuration.md`](https://docs.clawd.bot/configuration)).
## Bootstrap files (injected)
@@ -31,8 +29,6 @@ Inside `agent.workspace`, CLAWDBOT expects these user-editable files:
On the first turn of a new session, CLAWDBOT injects the contents of these files directly into the agent context.
Blank files are skipped. Large files are trimmed and truncated with a marker so prompts stay lean (read the file for full content).
If a file is missing, CLAWDBOT injects a single “missing file” marker line (and `clawdbot setup` will create a safe default template).
`BOOTSTRAP.md` is only created for a **brand new workspace** (no other bootstrap files present). If you delete it after completing the ritual, it should not be recreated on later restarts.
@@ -54,7 +50,7 @@ Clawdbot loads skills from three locations (workspace wins on name conflict):
- Managed/local: `~/.clawdbot/skills`
- Workspace: `<workspace>/skills`
Skills can be gated by config/env (see `skills` in [`docs/configuration.md`](/gateway/configuration)).
Skills can be gated by config/env (see `skills` in [`docs/configuration.md`](https://docs.clawd.bot/configuration)).
## p-mono integration
@@ -93,7 +89,7 @@ message is injected before the next assistant response.
When queue mode is `followup` or `collect`, inbound messages are held until the
current turn ends, then a new agent turn starts with the queued payloads. See
[`docs/queue.md`](/concepts/queue) for mode + debounce/cap behavior.
[`docs/queue.md`](https://docs.clawd.bot/queue) for mode + debounce/cap behavior.
Block streaming sends completed assistant blocks as soon as they finish; disable
via `agent.blockStreamingDefault: "off"` if you only want the final response.
@@ -111,4 +107,4 @@ At minimum, set:
---
*Next: [Group Chats](/concepts/group-messages)* 🦞
*Next: [Group Chats](https://docs.clawd.bot/group-messages)* 🦞
@@ -28,7 +28,7 @@ The Gateway WebSocket stays loopback-only (`ws://127.0.0.1:18789`). Android talk
Bridge is enabled by default (disable via `CLAWDBOT_BRIDGE_ENABLED=0`).
```bash
clawdbot gateway --port 18789 --verbose
pnpm clawdbot gateway --port 18789 --verbose
```
Confirm in logs you see something like:
@@ -47,7 +47,7 @@ From the gateway machine:
dns-sd -B _clawdbot-bridge._tcp local.
```
More debugging notes: [`docs/bonjour.md`](/gateway/bonjour).
More debugging notes: [`docs/bonjour.md`](https://docs.clawd.bot/bonjour).
#### Tailnet (Vienna ⇄ London) discovery via unicast DNS-SD
@@ -56,7 +56,7 @@ Android NSD/mDNS discovery won’t cross networks. If your Android node and the
1) Set up a DNS-SD zone (example `clawdbot.internal.`) on the gateway host and publish `_clawdbot-bridge._tcp` records.
2) Configure Tailscale split DNS for `clawdbot.internal` pointing at that DNS server.
Details and example CoreDNS config: [`docs/bonjour.md`](/gateway/bonjour).
Details and example CoreDNS config: [`docs/bonjour.md`](https://docs.clawd.bot/bonjour).
### 3) Connect from Android
@@ -80,7 +80,7 @@ clawdbot nodes pending
clawdbot nodes approve <requestId>
```
Pairing details: [`docs/gateway/pairing.md`](/gateway/pairing).
Pairing details: [`docs/gateway/pairing.md`](https://docs.clawd.bot/gateway/pairing).
### 5) Verify the node is connected
@@ -130,4 +130,4 @@ Camera commands (foreground only; permission-gated):
- `camera.snap` (jpg)
- `camera.clip` (mp4)
See [`docs/camera.md`](/nodes/camera) for parameters and CLI helpers.
See [`docs/camera.md`](https://docs.clawd.bot/camera) for parameters and CLI helpers.
@@ -46,7 +46,7 @@ Last updated: 2026-01-05
- **Clients (mac app / CLI / web admin)**
- One WS connection per client.
- Send requests (`health`, `status`, `send`, `agent`, `system-presence`, toggles) and subscribe to events (`tick`, `agent`, `presence`, `shutdown`).
- On macOS, the app can also be invoked via deep links (`clawdbot://agent?...`) which translate into the same Gateway `agent` request path (see [`docs/macos.md`](/platforms/macos)).
- On macOS, the app can also be invoked via deep links (`clawdbot://agent?...`) which translate into the same Gateway `agent` request path (see [`docs/macos.md`](https://docs.clawd.bot/macos)).
- **Agent process (Pi)**
- Spawned by the Gateway on demand for `agent` calls; streams events back over the same WS connection.
- **WebChat**
File renamed without changes.
-132
View File
@@ -1,132 +0,0 @@
---
summary: "Cron jobs + wakeups for the Gateway scheduler"
read_when:
- Scheduling background jobs or wakeups
- Wiring automation that should run with or alongside heartbeats
---
# Cron jobs (Gateway scheduler)
Cron runs inside the Gateway and schedules background work so Clawdbot can
wake itself up, run isolated agent jobs, and deliver reminders on time.
## Update checklist (internal)
- [x] Audit cron + heartbeat behavior in code
- [x] Rewrite cron doc as user-facing feature
- [x] Update heartbeat docs + templates
- [x] Update cron links in docs
- [x] Update changelog
- [x] Run full gate (lint/build/test/docs)
## What cron is
- **Gateway-owned scheduler** that persists jobs under `~/.clawdbot/cron/`.
- **Two execution modes**:
- **Main session jobs** enqueue `System:` events and rely on the heartbeat runner.
- **Isolated jobs** run a dedicated agent turn in `cron:<jobId>` sessions.
- **Wakeups** are first-class: a job can trigger the next heartbeat or run it now.
## When to use it
- Recurring reminders: “every weekday at 7:30” or “every 2h.”
- Background chores: summarize inboxes, check dashboards, watch logs.
- Automation that should not pollute the main chat history.
- Scheduled wakeups that drive the heartbeat pipeline.
## Schedules
Cron supports three schedule kinds:
- `at`: one-shot timestamp in ms.
- `every`: fixed interval (ms).
- `cron`: 5-field cron expression, optional IANA timezone.
Cron expressions use `croner` under the hood. If a timezone is omitted, the
server’s local timezone is used.
## Job types
### Main session jobs
Main jobs enqueue a system event and optionally wake the heartbeat runner.
They **must** use `payload.kind = "systemEvent"`.
- **`wakeMode: "next-heartbeat"`** (default): the event waits for the next
scheduled heartbeat.
- **`wakeMode: "now"`**: the event triggers an immediate heartbeat run.
### Isolated jobs
Isolated jobs run a dedicated agent turn in session `cron:<jobId>` and can
optionally deliver a message.
Key behaviors:
- Prompt is prefixed with `[cron:<jobId> <job name>]` for traceability.
- A summary is posted to the main session with prefix `Cron` (or
`isolation.postToMainPrefix`).
- `wakeMode: "now"` triggers an immediate heartbeat after posting the summary.
- `payload.deliver: true` sends output to a provider; otherwise it stays internal.
## Storage & history
- Job store: `~/.clawdbot/cron/jobs.json` (JSON, Gateway-managed).
- Run history: `~/.clawdbot/cron/runs/<jobId>.jsonl` (JSONL, auto-pruned).
- Override store path: `cron.store` in config.
## Configuration
```json5
{
cron: {
enabled: true, // default true
store: "~/.clawdbot/cron/jobs.json",
maxConcurrentRuns: 1 // default 1
}
}
```
Disable cron entirely:
- `cron.enabled: false` (config)
- or `CLAWDBOT_SKIP_CRON=1` (env)
## CLI quickstart
One-shot reminder (main session, wake immediately):
```bash
clawdbot cron add \
--name "Calendar check" \
--at "20m" \
--session main \
--system-event "Next heartbeat: check calendar." \
--wake now
```
Recurring isolated job (deliver to WhatsApp):
```bash
clawdbot cron add \
--name "Morning status" \
--cron "0 7 * * *" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Summarize inbox + calendar for today." \
--deliver \
--provider whatsapp \
--to "+15551234567"
```
Manual run (debug):
```bash
clawdbot cron run <jobId> --force
```
Run history:
```bash
clawdbot cron runs --id <jobId> --limit 50
```
Immediate wake without creating a job:
```bash
clawdbot wake --mode now --text "Next heartbeat: check battery."
```
## API surface (Gateway)
- `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove`
- `cron.run` (force or due), `cron.runs`
- `wake` (enqueue system event + optional heartbeat)
## Tips
- Use **main session jobs** when you want the heartbeat prompt + existing context.
- Use **isolated jobs** for noisy, frequent, or long-running work.
- Keep messages short; cron turns are full agent runs and can burn tokens.
File renamed without changes.
View File
File renamed without changes.
+2 -2
View File
@@ -155,5 +155,5 @@ Bonjour/DNS-SD often escapes bytes in service instance names as decimal `\\DDD`
## Related docs
- Discovery policy and transport selection: [`docs/discovery.md`](/gateway/discovery)
- Node pairing + approvals: [`docs/gateway/pairing.md`](/gateway/pairing)
- Discovery policy and transport selection: [`docs/discovery.md`](https://docs.clawd.bot/discovery)
- Node pairing + approvals: [`docs/gateway/pairing.md`](https://docs.clawd.bot/gateway/pairing)
+1 -1
View File
@@ -306,4 +306,4 @@ Notes:
## Troubleshooting
For Linux-specific issues (especially Ubuntu with snap Chromium), see [browser-linux-troubleshooting](/tools/browser-linux-troubleshooting).
For Linux-specific issues (especially Ubuntu with snap Chromium), see [browser-linux-troubleshooting](https://docs.clawd.bot/browser-linux-troubleshooting).
+3 -3
View File
@@ -7,12 +7,12 @@ read_when:
# Bun
Goal: run this repo with **Bun** (optional) without losing pnpm patch behavior.
Goal: run this repo with **Bun** (preferred) without losing pnpm patch behavior.
## Status
- Bun is an optional local runtime for running TypeScript directly (`bun run …`, `bun --watch …`).
- `pnpm` is the default for builds and remains fully supported (and used by some docs tooling).
- Bun is the preferred local runtime for running TypeScript directly (`bun run …`, `bun --watch …`).
- `pnpm` is still fully supported (and used by some docs tooling).
- Bun cannot use `pnpm-lock.yaml` and will ignore it.
## Install
File renamed without changes.
+14 -12
View File
@@ -69,6 +69,11 @@ clawdbot login
clawdbot gateway --port 18789
```
Optional (supervised, always-on):
```bash
clawdbot gateway install
```
3) Put a minimal config in `~/.clawdbot/clawdbot.json`:
```json5
@@ -93,8 +98,6 @@ Tip: treat this folder like Clawd’s “memory” and make it a git repo (ideal
clawdbot setup
```
Full workspace layout + backup guide: [`docs/agent-workspace.md`](/concepts/agent-workspace)
Optional: choose a different workspace with `agent.workspace` (supports `~`).
```json5
@@ -168,7 +171,6 @@ By default, CLAWDBOT runs a heartbeat every 30 minutes with the prompt:
Set `agent.heartbeat.every: "0m"` to disable.
- If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agent.heartbeat.ackMaxChars`), CLAWDBOT suppresses outbound delivery for that heartbeat.
- Heartbeats run full agent turns — shorter intervals burn more tokens.
```json5
{
@@ -206,12 +208,12 @@ Logs live under `/tmp/clawdbot/` (default: `clawdbot-YYYY-MM-DD.log`).
## Next steps
- WebChat: [WebChat](/web/webchat)
- Gateway ops: [Gateway runbook](/gateway)
- Cron + wakeups: [Cron jobs](/automation/cron-jobs)
- macOS menu bar companion: [Clawdbot macOS app](/platforms/macos)
- iOS node app: [iOS app](/platforms/ios)
- Android node app: [Android app](/platforms/android)
- Windows status: [Windows (WSL2)](/platforms/windows)
- Linux status: [Linux app](/platforms/linux)
- Security: [Security](/gateway/security)
- WebChat: [WebChat](https://docs.clawd.bot/webchat)
- Gateway ops: [Gateway runbook](https://docs.clawd.bot/gateway)
- Cron + wakeups: [Cron + wakeups](https://docs.clawd.bot/cron)
- macOS menu bar companion: [Clawdbot macOS app](https://docs.clawd.bot/macos)
- iOS node app: [iOS app](https://docs.clawd.bot/ios)
- Android node app: [Android app](https://docs.clawd.bot/android)
- Windows status: [Windows app](https://docs.clawd.bot/windows)
- Linux status: [Linux app](https://docs.clawd.bot/linux)
- Security: [Security](https://docs.clawd.bot/security)
-575
View File
@@ -1,575 +0,0 @@
---
summary: "CLI reference for clawdbot commands, subcommands, and options"
read_when:
- Adding or modifying CLI commands or options
- Documenting new command surfaces
---
# CLI reference
This page mirrors `src/cli/*` and is the source of truth for CLI behavior.
If you change the CLI code, update this doc.
## Global flags
- `--dev`: isolate state under `~/.clawdbot-dev` and shift default ports.
- `--profile <name>`: isolate state under `~/.clawdbot-<name>`.
- `-V`, `--version`, `-v`: print version and exit.
## Command tree
```
clawdbot [--dev] [--profile <name>] <command>
setup
onboard
configure (alias: config)
update
doctor
login
logout
send
poll
agent
agents
list
add
delete
status
health
sessions
gateway
call
health
status
wake
send
agent
stop
restart
gateway-daemon
models
list
status
set
set-image
aliases list|add|remove
fallbacks list|add|remove|clear
image-fallbacks list|add|remove|clear
scan
wake
cron
status
list
add
edit
rm
enable
disable
runs
run
nodes
status
describe
list
pending
approve
reject
rename
invoke
run
notify
camera list|snap|clip
canvas snapshot
screen record
location get
canvas
snapshot
present
hide
navigate
eval
a2ui push|reset
browser
status
start
stop
reset-profile
tabs
open
focus
close
profiles
create-profile
delete-profile
screenshot
snapshot
navigate
resize
click
type
press
hover
drag
select
upload
fill
dialog
wait
evaluate
console
pdf
hooks
gmail setup|run
pairing
list
approve
telegram
pairing list|approve
docs
dns
setup
tui
```
## Setup + onboarding
### `setup`
Initialize config + workspace.
Options:
- `--workspace <dir>`: agent workspace path (default `~/clawd`).
- `--wizard`: run the onboarding wizard.
- `--non-interactive`: run wizard without prompts.
- `--mode <local|remote>`: wizard mode.
- `--remote-url <url>`: remote Gateway URL.
- `--remote-token <token>`: remote Gateway token.
### `onboard`
Interactive wizard to set up gateway, workspace, and skills.
Options:
- `--workspace <dir>`
- `--non-interactive`
- `--mode <local|remote>`
- `--auth-choice <oauth|openai-codex|antigravity|apiKey|minimax|skip>`
- `--anthropic-api-key <key>`
- `--gateway-port <port>`
- `--gateway-bind <loopback|lan|tailnet|auto>`
- `--gateway-auth <off|token|password>`
- `--gateway-token <token>`
- `--gateway-password <password>`
- `--remote-url <url>`
- `--remote-token <token>`
- `--tailscale <off|serve|funnel>`
- `--tailscale-reset-on-exit`
- `--install-daemon`
- `--daemon-runtime <node|bun>`
- `--skip-skills`
- `--skip-health`
- `--node-manager <npm|pnpm|bun>`
- `--json`
### `configure` / `config`
Interactive configuration wizard (models, providers, skills, gateway).
### `update`
Audit and modernize the local configuration.
### `doctor`
Health checks + quick fixes.
Options:
- `--no-workspace-suggestions`: disable workspace memory hints.
## Auth + provider helpers
### `login`
Link a WhatsApp Web account via QR.
Options:
- `--verbose`
- `--provider <provider>` (default `whatsapp`)
- `--account <id>`
### `logout`
Clear cached WhatsApp Web credentials.
Options:
- `--provider <provider>`
- `--account <id>`
### `pairing`
Approve DM pairing requests across providers.
Subcommands:
- `pairing list --provider <telegram|signal|imessage|discord|slack|whatsapp> [--json]`
- `pairing approve --provider <...> <code> [--notify]`
### `telegram pairing`
Telegram-only pairing helper.
Subcommands:
- `telegram pairing list [--json]`
- `telegram pairing approve <code> [--no-notify]`
### `hooks gmail`
Gmail Pub/Sub hook setup + runner. See [/automation/gmail-pubsub](/automation/gmail-pubsub).
Subcommands:
- `hooks gmail setup` (requires `--account <email>`; supports `--project`, `--topic`, `--subscription`, `--label`, `--hook-url`, `--hook-token`, `--push-token`, `--bind`, `--port`, `--path`, `--include-body`, `--max-bytes`, `--renew-minutes`, `--tailscale`, `--tailscale-path`, `--push-endpoint`, `--json`)
- `hooks gmail run` (runtime overrides for the same flags)
### `dns setup`
Wide-area discovery DNS helper (CoreDNS + Tailscale). See [/gateway/discovery](/gateway/discovery).
Options:
- `--apply`: install/update CoreDNS config (requires sudo; macOS only).
## Messaging + agent
### `send`
Send a message through a provider.
Required:
- `--to <dest>`
- `--message <text>`
Options:
- `--media <path-or-url>`
- `--gif-playback`
- `--provider <whatsapp|telegram|discord|slack|signal|imessage>`
- `--account <id>` (WhatsApp)
- `--dry-run`
- `--json`
- `--verbose`
### `poll`
Create a poll (WhatsApp or Discord).
Required:
- `--to <id>`
- `--question <text>`
- `--option <choice>` (repeat 2-12 times)
Options:
- `--max-selections <n>`
- `--duration-hours <n>` (Discord)
- `--provider <whatsapp|discord>`
- `--dry-run`
- `--json`
- `--verbose`
### `agent`
Run one agent turn via the Gateway (or `--local` embedded).
Required:
- `--message <text>`
Options:
- `--to <dest>` (for session key and optional delivery)
- `--session-id <id>`
- `--thinking <off|minimal|low|medium|high>`
- `--verbose <on|off>`
- `--provider <whatsapp|telegram|discord|slack|signal|imessage>`
- `--local`
- `--deliver`
- `--json`
- `--timeout <seconds>`
### `agents`
Manage isolated agents (workspaces + auth + routing).
#### `agents list`
List configured agents.
Options:
- `--json`
#### `agents add [name]`
Add a new isolated agent. If `--workspace` is omitted, runs the guided wizard.
Options:
- `--workspace <dir>`
- `--json`
#### `agents delete <id>`
Delete an agent and prune its workspace + state.
Options:
- `--force`
- `--json`
### `status`
Show linked session health and recent recipients.
Options:
- `--json`
- `--deep` (probe providers)
- `--usage` (show provider usage/quota)
- `--timeout <ms>`
- `--verbose`
### Usage tracking
Clawdbot can surface provider usage/quota when OAuth/API creds are available.
Surfaces:
- `/status` (adds a short usage line when available)
- `clawdbot status --usage` (prints full provider breakdown)
- macOS menu bar (Usage section under Context)
Notes:
- Data comes directly from provider usage endpoints (no estimates).
- Providers: Anthropic, GitHub Copilot, Gemini CLI, Antigravity, OpenAI Codex OAuth, plus z.ai when an API key is configured.
- If no matching credentials exist, usage is hidden.
- Details: see [Usage tracking](/concepts/usage-tracking).
### `health`
Fetch health from the running Gateway.
Options:
- `--json`
- `--timeout <ms>`
- `--verbose`
### `sessions`
List stored conversation sessions.
Options:
- `--json`
- `--verbose`
- `--store <path>`
- `--active <minutes>`
## Gateway
### `gateway`
Run the WebSocket Gateway.
Options:
- `--port <port>`
- `--bind <loopback|tailnet|lan|auto>`
- `--token <token>`
- `--auth <token|password>`
- `--password <password>`
- `--tailscale <off|serve|funnel>`
- `--tailscale-reset-on-exit`
- `--allow-unconfigured`
- `--force` (kill existing listener on port)
- `--verbose`
- `--ws-log <auto|full|compact>`
- `--compact` (alias for `--ws-log compact`)
### `gateway-daemon`
Run the Gateway as a long-lived daemon (same options as `gateway`, minus `--allow-unconfigured` and `--force`).
### `gateway <subcommand>`
Gateway RPC helpers (use `--url`, `--token`, `--password`, `--timeout`, `--expect-final` for each).
Subcommands:
- `gateway call <method> [--params <json>]`
- `gateway health`
- `gateway status`
- `gateway wake --text <text> [--mode now|next-heartbeat]`
- `gateway send --to <jidOrPhone> --message <text> [--media-url <url>] [--gif-playback] [--idempotency-key <key>]`
- `gateway agent --message <text> [--to <jidOrPhone>] [--session-id <id>] [--thinking <level>] [--deliver] [--timeout-seconds <n>] [--idempotency-key <key>]`
- `gateway stop`
- `gateway restart`
## Models
See [/concepts/models](/concepts/models) for fallback behavior and scanning strategy.
### `models` (root)
`clawdbot models` is an alias for `models status`.
### `models list`
Options:
- `--all`
- `--local`
- `--provider <name>`
- `--json`
- `--plain`
### `models status`
Options:
- `--json`
- `--plain`
### `models set <model>`
Set `agent.model.primary`.
### `models set-image <model>`
Set `agent.imageModel.primary`.
### `models aliases list|add|remove`
Options:
- `list`: `--json`, `--plain`
- `add <alias> <model>`
- `remove <alias>`
### `models fallbacks list|add|remove|clear`
Options:
- `list`: `--json`, `--plain`
- `add <model>`
- `remove <model>`
- `clear`
### `models image-fallbacks list|add|remove|clear`
Options:
- `list`: `--json`, `--plain`
- `add <model>`
- `remove <model>`
- `clear`
### `models scan`
Options:
- `--min-params <b>`
- `--max-age-days <days>`
- `--provider <name>`
- `--max-candidates <n>`
- `--timeout <ms>`
- `--concurrency <n>`
- `--yes`
- `--no-input`
- `--set-default`
- `--set-image`
- `--json`
## Cron + wake
### `wake`
Enqueue a system event and optionally trigger a heartbeat (Gateway RPC).
Required:
- `--text <text>`
Options:
- `--mode <now|next-heartbeat>`
- `--json`
- `--url`, `--token`, `--timeout`, `--expect-final`
### `cron`
Manage scheduled jobs (Gateway RPC). See [/automation/cron-jobs](/automation/cron-jobs).
Subcommands:
- `cron status [--json]`
- `cron list [--all] [--json]`
- `cron add` (requires `--name` and exactly one of `--at` | `--every` | `--cron`, and exactly one payload of `--system-event` | `--message`)
- `cron edit <id>` (patch fields)
- `cron rm <id>`
- `cron enable <id>`
- `cron disable <id>`
- `cron runs --id <id> [--limit <n>]`
- `cron run <id> [--force]`
All `cron` commands accept `--url`, `--token`, `--timeout`, `--expect-final`.
## Nodes
`nodes` talks to the Gateway and targets paired nodes. See [/nodes](/nodes).
Common options:
- `--url`, `--token`, `--timeout`, `--json`
Subcommands:
- `nodes status`
- `nodes describe --node <id|name|ip>`
- `nodes list`
- `nodes pending`
- `nodes approve <requestId>`
- `nodes reject <requestId>`
- `nodes rename --node <id|name|ip> --name <displayName>`
- `nodes invoke --node <id|name|ip> --command <command> [--params <json>] [--invoke-timeout <ms>] [--idempotency-key <key>]`
- `nodes run --node <id|name|ip> [--cwd <path>] [--env KEY=VAL] [--command-timeout <ms>] [--needs-screen-recording] [--invoke-timeout <ms>] <command...>` (mac only)
- `nodes notify --node <id|name|ip> [--title <text>] [--body <text>] [--sound <name>] [--priority <passive|active|timeSensitive>] [--delivery <system|overlay|auto>] [--invoke-timeout <ms>]` (mac only)
Camera:
- `nodes camera list --node <id|name|ip>`
- `nodes camera snap --node <id|name|ip> [--facing front|back|both] [--device-id <id>] [--max-width <px>] [--quality <0-1>] [--delay-ms <ms>] [--invoke-timeout <ms>]`
- `nodes camera clip --node <id|name|ip> [--facing front|back] [--device-id <id>] [--duration <ms|10s|1m>] [--no-audio] [--invoke-timeout <ms>]`
Canvas + screen:
- `nodes canvas snapshot --node <id|name|ip> [--format png|jpg|jpeg] [--max-width <px>] [--quality <0-1>] [--invoke-timeout <ms>]`
- `nodes screen record --node <id|name|ip> [--screen <index>] [--duration <ms|10s>] [--fps <n>] [--no-audio] [--out <path>] [--invoke-timeout <ms>]`
Location:
- `nodes location get --node <id|name|ip> [--max-age <ms>] [--accuracy <coarse|balanced|precise>] [--location-timeout <ms>] [--invoke-timeout <ms>]`
## Canvas
Canvas RPC helper (top-level wrapper for `node.invoke`). See [/platforms/mac/canvas](/platforms/mac/canvas).
Common options:
- `--url`, `--token`, `--timeout`, `--json`
Subcommands:
- `canvas snapshot [--node <id|name|ip>] [--format png|jpg] [--max-width <px>] [--quality <0-1>]`
- `canvas present [--node <id|name|ip>] [--target <urlOrPath>] [--x <px>] [--y <px>] [--width <px>] [--height <px>]`
- `canvas hide [--node <id|name|ip>]`
- `canvas navigate <url> [--node <id|name|ip>]`
- `canvas eval [<js>] [--js <code>] [--node <id|name|ip>]`
- `canvas a2ui push (--jsonl <path> | --text <text>) [--node <id|name|ip>]`
- `canvas a2ui reset [--node <id|name|ip>]`
## Browser
Browser control CLI (dedicated Chrome/Chromium). See [/tools/browser](/tools/browser).
Common options:
- `--url <controlUrl>`
- `--browser-profile <name>`
- `--json`
Manage:
- `browser status`
- `browser start`
- `browser stop`
- `browser reset-profile`
- `browser tabs`
- `browser open <url>`
- `browser focus <targetId>`
- `browser close [targetId]`
- `browser profiles`
- `browser create-profile --name <name> [--color <hex>] [--cdp-url <url>]`
- `browser delete-profile --name <name>`
Inspect:
- `browser screenshot [targetId] [--full-page] [--ref <ref>] [--element <selector>] [--type png|jpeg]`
- `browser snapshot [--format aria|ai] [--target-id <id>] [--limit <n>] [--out <path>]`
Actions:
- `browser navigate <url> [--target-id <id>]`
- `browser resize <width> <height> [--target-id <id>]`
- `browser click <ref> [--double] [--button <left|right|middle>] [--modifiers <csv>] [--target-id <id>]`
- `browser type <ref> <text> [--submit] [--slowly] [--target-id <id>]`
- `browser press <key> [--target-id <id>]`
- `browser hover <ref> [--target-id <id>]`
- `browser drag <startRef> <endRef> [--target-id <id>]`
- `browser select <ref> <values...> [--target-id <id>]`
- `browser upload <paths...> [--ref <ref>] [--input-ref <ref>] [--element <selector>] [--target-id <id>] [--timeout-ms <ms>]`
- `browser fill [--fields <json>] [--fields-file <path>] [--target-id <id>]`
- `browser dialog --accept|--dismiss [--prompt <text>] [--target-id <id>] [--timeout-ms <ms>]`
- `browser wait [--time <ms>] [--text <value>] [--text-gone <value>] [--target-id <id>]`
- `browser evaluate --fn <code> [--ref <ref>] [--target-id <id>]`
- `browser console [--level <error|warn|info>] [--target-id <id>]`
- `browser pdf [--target-id <id>]`
## Docs search
### `docs [query...]`
Search the live docs index.
## TUI
### `tui`
Open the terminal UI connected to the Gateway.
Options:
- `--url <url>`
- `--token <token>`
- `--password <password>`
- `--session <key>`
- `--deliver`
- `--thinking <level>`
- `--timeout-ms <ms>`
- `--history-limit <n>`
-187
View File
@@ -1,187 +0,0 @@
---
summary: "Agent workspace: location, layout, and backup strategy"
read_when:
- You need to explain the agent workspace or its file layout
- You want to back up or migrate an agent workspace
---
# Agent workspace
The workspace is the agent's home. It is the only working directory used for
file tools and for workspace context. Keep it private and treat it as memory.
This is separate from `~/.clawdbot/`, which stores config, credentials, and
sessions.
## Default location
- Default: `~/clawd`
- If `CLAWDBOT_PROFILE` is set and not `"default"`, the default becomes
`~/clawd-<profile>`.
- Override in `~/.clawdbot/clawdbot.json`:
```json5
{
agent: {
workspace: "~/clawd"
}
}
```
`clawdbot onboard`, `clawdbot configure`, or `clawdbot setup` will create the
workspace and seed the bootstrap files if they are missing.
If you already manage the workspace files yourself, you can disable bootstrap
file creation:
```json5
{ agent: { skipBootstrap: true } }
```
## Workspace file map (what each file means)
These are the standard files Clawdbot expects inside the workspace:
- `AGENTS.md`
- Operating instructions for the agent and how it should use memory.
- Loaded at the start of every session.
- Good place for rules, priorities, and "how to behave" details.
- `SOUL.md`
- Persona, tone, and boundaries.
- Loaded every session.
- `USER.md`
- Who the user is and how to address them.
- Loaded every session.
- `IDENTITY.md`
- The agent's name, vibe, and emoji.
- Created/updated during the bootstrap ritual.
- `TOOLS.md`
- Notes about your local tools and conventions.
- Does not control tool availability; it is only guidance.
- `HEARTBEAT.md`
- Optional tiny checklist for heartbeat runs.
- Keep it short to avoid token burn.
- `BOOTSTRAP.md`
- One-time first-run ritual.
- Only created for a brand-new workspace.
- Delete it after the ritual is complete.
- `memory/YYYY-MM-DD.md`
- Daily memory log (one file per day).
- Recommended to read today + yesterday on session start.
- `MEMORY.md` (optional)
- Curated long-term memory.
- Only load in the main, private session (not shared/group contexts).
- `skills/` (optional)
- Workspace-specific skills.
- Overrides managed/bundled skills when names collide.
- `canvas/` (optional)
- Canvas UI files for node displays (for example `canvas/index.html`).
If any bootstrap file is missing, Clawdbot injects a "missing file" marker into
the session and continues. `clawdbot setup` can recreate missing defaults
without overwriting existing files.
## What is NOT in the workspace
These live under `~/.clawdbot/` and should NOT be committed to the workspace repo:
- `~/.clawdbot/clawdbot.json` (config)
- `~/.clawdbot/credentials/` (OAuth tokens, API keys)
- `~/.clawdbot/agents/<agentId>/sessions/` (session transcripts + metadata)
- `~/.clawdbot/skills/` (managed skills)
If you need to migrate sessions or config, copy them separately and keep them
out of version control.
## Git backup (recommended, private)
Treat the workspace as private memory. Put it in a **private** git repo so it is
backed up and recoverable.
Run these steps on the machine where the Gateway runs (that is where the
workspace lives).
### 1) Initialize the repo
```bash
cd ~/clawd
git init
git add AGENTS.md SOUL.md TOOLS.md IDENTITY.md USER.md HEARTBEAT.md memory/
git commit -m "Add agent workspace"
```
### 2) Add a private remote (beginner-friendly options)
Option A: GitHub web UI
1. Create a new **private** repository on GitHub.
2. Do not initialize with a README (avoids merge conflicts).
3. Copy the HTTPS remote URL.
4. Add the remote and push:
```bash
git branch -M main
git remote add origin <https-url>
git push -u origin main
```
Option B: GitHub CLI (`gh`)
```bash
gh auth login
gh repo create clawd-workspace --private --source . --remote origin --push
```
### 3) Ongoing updates
```bash
git status
git add .
git commit -m "Update memory"
git push
```
## Do not commit secrets
Even in a private repo, avoid storing secrets in the workspace:
- API keys, OAuth tokens, passwords, or private credentials.
- Anything under `~/.clawdbot/`.
- Raw dumps of chats or sensitive attachments.
If you must store sensitive references, use placeholders and keep the real
secret elsewhere (password manager, environment variables, or `~/.clawdbot/`).
Suggested `.gitignore` starter:
```gitignore
.DS_Store
.env
**/*.key
**/*.pem
**/secrets*
```
## Moving the workspace to a new machine
1. Clone the repo to the desired path (default `~/clawd`).
2. Set `agent.workspace` to that path in `~/.clawdbot/clawdbot.json`.
3. Run `clawdbot setup --workspace <path>` to seed any missing files.
4. If you need sessions, copy `~/.clawdbot/agents/<agentId>/sessions/` from the
old machine separately.
## Advanced notes
- Multi-agent routing can use different workspaces per agent. See
`docs/provider-routing.md` for routing configuration.
- If `agent.sandbox` is enabled, non-main sessions can use per-session sandbox
workspaces under `agent.sandbox.workspaceRoot`.
-108
View File
@@ -1,108 +0,0 @@
---
summary: "Plan for models CLI: scan, list, aliases, fallbacks, status"
read_when:
- Adding or modifying models CLI (models list/set/scan/aliases/fallbacks)
- Changing model fallback behavior or selection UX
- Updating model scan probes (tools/images)
---
# Models CLI plan
See [`docs/model-failover.md`](/concepts/model-failover) for how auth profiles rotate (OAuth vs API keys), cooldowns, and how that interacts with model fallbacks.
Goal: give clear model visibility + control (configured vs available), plus scan tooling
that prefers tool-call + image-capable models and maintains ordered fallbacks.
## Model recommendations
Through testing, we’ve found [Claude Opus 4.5](https://www.anthropic.com/claude/opus) is the most useful general-purpose model for anything coding-related. We suggest [GPT-5.2-Codex](https://developers.openai.com/codex/models) for coding and sub-agents. For personal assistant work, nothing comes close to Opus. If you’re going all-in on Claude, we recommend the [Claude Max $200 subscription](https://www.anthropic.com/pricing/).
## Model discussions (community notes)
Anecdotal notes from the Discord thread on January 4–5, 2026. Treat as “what people reported,” not guarantees.
**Reported working well**
- [Claude Opus 4.5](https://www.anthropic.com/claude/opus): best quality, but expensive and easy to hit limits.
- [Claude Sonnet 4.5](https://www.anthropic.com/claude/sonnet): solid fallback when Opus caps out.
- [GLM](https://www.zhipuai.cn/en/): used as a worker model under orchestration.
- [MiniMax M2.1](https://platform.minimax.io/docs/guides/models-intro): “good enough” fallback for grunt tasks.
- “Temu-Sonnet” (community shorthand) for MiniMax quality vs Claude Sonnet.
- [Gemini 3 Pro](https://deepmind.google/en/models/gemini/pro/): some users said it maps Clawdbot structure well.
**Mixed / unclear**
- [Antigravity](https://blog.google/technology/ai/google-ai-updates-november-2025/) (Claude Opus access): some reported extra Opus quota, pricing/limits unclear.
**Reported weak in Clawdbot**
- [GPT-5.2-Codex](https://developers.openai.com/codex/models) inside Clawdbot: considered rough for conversation or assistant tasks.
- [Grok](https://docs.x.ai/docs/models/grok-4): tried, abandoned.
**Tooling note**
- [Codex CLI](https://developers.openai.com/codex/cli) felt stronger than embedded use.
**Theme**
- Token burn feels higher than expected in long sessions; people suspect context buildup + tool outputs. Pruning/compaction helps. Check session logs before blaming providers. See [/concepts/session](/concepts/session) and [/concepts/model-failover](/concepts/model-failover).
## Models CLI
See [/cli](/cli) for the full command tree and CLI flags.
## Config changes
- `agent.models` (configured model catalog + aliases).
- `agent.model.primary` + `agent.model.fallbacks`.
- `agent.imageModel.primary` + `agent.imageModel.fallbacks` (optional).
- `auth.profiles` + `auth.order` for per-provider auth failover.
## Scan behavior (models scan)
Input
- OpenRouter `/models` list (filter `:free`)
- Requires OpenRouter API key from auth profiles or `OPENROUTER_API_KEY`
- Optional filters: `--max-age-days`, `--min-params`, `--provider`, `--max-candidates`
- Probe controls: `--timeout`, `--concurrency`
Probes (direct pi-ai complete)
- Tool-call probe (required):
- Provide a dummy tool, verify tool call emitted.
- Image probe (preferred):
- Prompt includes 1x1 PNG; success if no "unsupported image" error.
Scoring/selection
- Prefer models passing tool + image for text/tool fallbacks.
- Prefer image-only models for image tool fallback (even if tool probe fails).
- Rank by: image ok, then lower tool latency, then larger context, then params.
Interactive selection (TTY)
- Multiselect list with per-model stats:
- model id, tool ok, image ok, median latency, context, inferred params.
- Pre-select top N (default 6).
- Non-TTY: auto-select; require `--yes`/`--no-input` to apply.
Output
- Writes `agent.model.fallbacks` ordered.
- Writes `agent.imageModel.fallbacks` ordered (image-capable models).
- Ensures `agent.models` entries exist for selected models.
- Optional `--set-default` to set `agent.model.primary`.
- Optional `--set-image` to set `agent.imageModel.primary`.
## Runtime fallback
- On model failure: try `agent.model.fallbacks` in order.
- Per-provider auth failover uses `auth.order` (or stored profile order) **before**
moving to the next model.
- Image routing uses `agent.imageModel` **only when configured** and the primary
model lacks image input.
- Persist last successful provider/model to session entry; auth profile success is global.
- See [`docs/model-failover.md`](/concepts/model-failover) for auth profile rotation, cooldowns, and timeout handling.
## Tests
- Unit: scan selection ordering + probe classification.
- CLI: list/aliases/fallbacks add/remove + scan writes config.
- Status: shows last used model + fallbacks.
## Docs
- Update [`docs/configuration.md`](/gateway/configuration) with `agent.models` + `agent.model` + `agent.imageModel`.
- Keep this doc current when CLI surface or scan logic changes.
- Note provider aliases like `z.ai/*` -> `zai/*` when relevant.
- Provider ids in model refs are normalized to lowercase.
-27
View File
@@ -1,27 +0,0 @@
---
summary: "Usage tracking surfaces and credential requirements"
read_when:
- You are wiring provider usage/quota surfaces
- You need to explain usage tracking behavior or auth requirements
---
# Usage tracking
## What it is
- Pulls provider usage/quota directly from their usage endpoints.
- No estimated costs; only the provider-reported windows.
## Where it shows up
- `/status` in chats: adds a short “Usage” line (only if available).
- CLI: `clawdbot status --usage` prints a full per-provider breakdown.
- macOS menu bar: “Usage” section under Context (only if available).
## Providers + credentials
- **Anthropic (Claude)**: OAuth tokens in auth profiles.
- **GitHub Copilot**: OAuth tokens in auth profiles.
- **Gemini CLI**: OAuth tokens in auth profiles.
- **Antigravity**: OAuth tokens in auth profiles.
- **OpenAI Codex**: OAuth tokens in auth profiles (accountId used when present).
- **z.ai**: API key via env/config/auth store.
Usage is hidden if no matching OAuth/API credentials exist.
@@ -119,11 +119,11 @@ rotation order used for failover.
{
auth: {
profiles: {
"anthropic:me@example.com": { provider: "anthropic", mode: "oauth", email: "me@example.com" },
"anthropic:default": { provider: "anthropic", mode: "oauth", email: "me@example.com" },
"anthropic:work": { provider: "anthropic", mode: "api_key" }
},
order: {
anthropic: ["anthropic:me@example.com", "anthropic:work"]
anthropic: ["anthropic:default", "anthropic:work"]
}
}
}
@@ -195,8 +195,6 @@ Controls how WhatsApp direct chats (DMs) are handled:
- `"open"`: allow all inbound DMs (**requires** `whatsapp.allowFrom` to include `"*"`)
- `"disabled"`: ignore all inbound DMs
Pairing codes expire after 1 hour; the bot only sends a pairing code when a new request is created.
Pairing approvals:
- `clawdbot pairing list --provider whatsapp`
- `clawdbot pairing approve --provider whatsapp <code>`
@@ -330,10 +328,8 @@ Run multiple isolated agents (separate workspace, `agentDir`, sessions) inside o
- `routing.defaultAgentId`: fallback when no binding matches (default: `main`).
- `routing.agents.<agentId>`: per-agent overrides.
- `name`: display name for the agent.
- `workspace`: default `~/clawd-<agentId>` (for `main`, falls back to legacy `agent.workspace`).
- `agentDir`: default `~/.clawdbot/agents/<agentId>/agent`.
- `model`: per-agent default model (provider/model), overrides `agent.model` for that agent.
- `routing.bindings[]`: routes inbound messages to an `agentId`.
- `match.provider` (required)
- `match.accountId` (optional; `*` = any account; omitted = default account)
@@ -467,22 +463,7 @@ Set `telegram.enabled: false` to disable automatic startup.
botToken: "your-bot-token",
dmPolicy: "pairing", // pairing | allowlist | open | disabled
allowFrom: ["tg:123456789"], // optional; "open" requires ["*"]
groups: {
"*": { requireMention: true },
"-1001234567890": {
allowFrom: ["@admin"],
systemPrompt: "Keep answers brief.",
topics: {
"99": {
requireMention: false,
skills: ["search"],
systemPrompt: "Stay on topic."
}
}
}
},
streamMode: "partial", // off | partial | block (draft streaming)
actions: { reactions: true }, // tool action gates (false disables)
groups: { "*": { requireMention: true } },
mediaMaxMb: 5,
proxy: "socks5://localhost:9050",
webhookUrl: "https://example.com/telegram-webhook",
@@ -492,11 +473,6 @@ Set `telegram.enabled: false` to disable automatic startup.
}
```
Draft streaming notes:
- Uses Telegram `sendMessageDraft` (draft bubble, not a real message).
- Requires **private chat topics** (message_thread_id in DMs; bot has topics enabled).
- `/reasoning stream` streams reasoning into the draft, then sends the final answer.
### `discord` (bot transport)
Configure the Discord bot by setting the bot token and optional gating:
@@ -540,13 +516,7 @@ Configure the Discord bot by setting the bot token and optional gating:
users: ["987654321098765432"], // optional per-guild user allowlist
channels: {
general: { allow: true },
help: {
allow: true,
requireMention: true,
users: ["987654321098765432"],
skills: ["docs"],
systemPrompt: "Short answers only."
}
help: { allow: true, requireMention: true }
}
}
},
@@ -582,13 +552,7 @@ Slack runs in Socket Mode and requires both a bot token and app token:
},
channels: {
C123: { allow: true, requireMention: true },
"#general": {
allow: true,
requireMention: true,
users: ["U123"],
skills: ["docs"],
systemPrompt: "Short answers only."
}
"#general": { allow: true, requireMention: false }
},
reactionNotifications: "own", // off | own | all | allowlist
reactionAllowlist: ["U123"],
@@ -665,7 +629,7 @@ Default: `~/clawd`.
```
If `agent.sandbox` is enabled, non-main sessions can override this with their
own per-scope workspaces under `agent.sandbox.workspaceRoot`.
own per-session workspaces under `agent.sandbox.workspaceRoot`.
### `agent.skipBootstrap`
@@ -790,10 +754,6 @@ If you configure the same alias name (case-insensitive) yourself, your value win
target: "last"
},
maxConcurrent: 3,
subagents: {
maxConcurrent: 1,
archiveAfterMinutes: 60
},
bash: {
backgroundMs: 10000,
timeoutSec: 1800,
@@ -831,22 +791,17 @@ Z.AI models are available as `zai/<model>` (e.g. `zai/glm-4.7`) and require
- `model`: optional override model for heartbeat runs (`provider/model`).
- `target`: optional delivery provider (`last`, `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `none`). Default: `last`.
- `to`: optional recipient override (provider-specific id, e.g. E.164 for WhatsApp, chat id for Telegram).
- `prompt`: optional override for the heartbeat body (default: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`). Overrides are sent verbatim; include a `Read HEARTBEAT.md if exists` line if you still want the file read.
- `prompt`: optional override for the heartbeat body (default: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`).
- `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery (default: 30).
Heartbeats run full agent turns. Shorter intervals burn more tokens; be mindful
of `every`, keep `HEARTBEAT.md` tiny, and/or choose a cheaper `model`.
Heartbeats run full agent turns. Shorter intervals burn more tokens; adjust `every`
and/or `model` accordingly.
`agent.bash` configures background bash defaults:
- `backgroundMs`: time before auto-background (ms, default 10000)
- `timeoutSec`: auto-kill after this runtime (seconds, default 1800)
- `cleanupMs`: how long to keep finished sessions in memory (ms, default 1800000)
`agent.subagents` configures sub-agent defaults:
- `maxConcurrent`: max concurrent sub-agent runs (default 1)
- `archiveAfterMinutes`: auto-archive sub-agent sessions after N minutes (default 60; set `0` to disable)
- `tools.allow` / `tools.deny`: per-subagent tool allow/deny policy (deny wins)
`agent.tools` configures a global tool allow/deny policy (deny wins).
This is applied even when the Docker sandbox is **off**.
@@ -892,34 +847,24 @@ per session key at a time). Default: 1.
### `agent.sandbox`
Optional **Docker sandboxing** for the embedded agent. Intended for non-main
sessions so they cannot access your host system.
Optional per-session **Docker sandboxing** for the embedded agent. Intended for
non-main sessions so they cannot access your host system.
Defaults (if enabled):
- scope: `"agent"` (one container + workspace per agent)
- one container per session
- Debian bookworm-slim based image
- agent workspace access: `workspaceAccess: "none"` (default)
- `"none"`: use a per-scope sandbox workspace under `~/.clawdbot/sandboxes`
- `"ro"`: keep the sandbox workspace at `/workspace`, and mount the agent workspace read-only at `/agent` (disables `write`/`edit`)
- `"rw"`: mount the agent workspace read/write at `/workspace`
- workspace per session under `~/.clawdbot/sandboxes`
- auto-prune: idle > 24h OR age > 7d
- tools: allow only `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn` (deny wins)
- optional sandboxed browser (Chromium + CDP, noVNC observer)
- hardening knobs: `network`, `user`, `pidsLimit`, `memory`, `cpus`, `ulimits`, `seccompProfile`, `apparmorProfile`
Warning: `scope: "shared"` means a shared container and shared workspace. No
cross-session isolation. Use `scope: "session"` for per-session isolation.
Legacy: `perSession` is still supported (`true` → `scope: "session"`,
`false` → `scope: "shared"`).
```json5
{
agent: {
sandbox: {
mode: "non-main", // off | non-main | all
scope: "agent", // session | agent | shared (agent is default)
workspaceAccess: "none", // none | ro | rw
perSession: true,
workspaceRoot: "~/.clawdbot/sandboxes",
docker: {
image: "clawdbot-sandbox:bookworm-slim",
@@ -976,8 +921,6 @@ scripts/sandbox-setup.sh
Note: sandbox containers default to `network: "none"`; set `agent.sandbox.docker.network`
to `"bridge"` (or your custom network) if the agent needs outbound access.
Note: inbound attachments are staged into the active workspace at `media/inbound/*`. With `workspaceAccess: "rw"`, that means files are written into the agent workspace.
Build the optional browser image with:
```bash
scripts/sandbox-browser-setup.sh
@@ -1035,31 +978,6 @@ Select the model via `agent.model.primary` (provider/model).
}
```
### Z.AI (GLM-4.7) — provider alias support
Z.AI models are available via the built-in `zai` provider. Set `ZAI_API_KEY`
in your environment and reference the model by provider/model.
```json5
{
agent: {
model: "zai/glm-4.7",
allowedModels: ["zai/glm-4.7"]
}
}
```
Notes:
- `z.ai/*` and `z-ai/*` are accepted aliases and normalize to `zai/*`.
- If `ZAI_API_KEY` is missing, requests to `zai/*` will fail with an auth error at runtime.
- Example error: `No API key found for provider "zai".`
- Z.AI’s general API endpoint is `https://api.z.ai/api/paas/v4`. The GLM Coding
Plan uses the dedicated Coding endpoint `https://api.z.ai/api/coding/paas/v4`.
The built-in `zai` provider uses the Coding endpoint. If you need the general
endpoint, define a custom provider in `models.providers` with the base URL
override (see the custom providers section above).
- Use a fake placeholder in docs/configs; never commit real API keys.
### Local models (LM Studio) — recommended setup
Best current local setup (what we’re running): **MiniMax M2.1** on a beefy Mac Studio
@@ -1273,12 +1191,6 @@ Control UI base path:
- Examples: `"/ui"`, `"/clawdbot"`, `"/apps/clawdbot"`.
- Default: root (`/`) (unchanged).
Related docs:
- [Control UI](/web/control-ui)
- [Web overview](/web)
- [Tailscale](/gateway/tailscale)
- [Remote access](/gateway/remote)
Notes:
- `clawdbot gateway` refuses to start unless `gateway.mode` is set to `local` (or you pass the override flag).
- `gateway.port` controls the single multiplexed port used for WebSocket + HTTP (control UI, hooks, A2UI).
@@ -1371,7 +1283,7 @@ Convenience flags (CLI):
- `clawdbot --dev …` → uses `~/.clawdbot-dev` + shifts ports from base `19001`
- `clawdbot --profile <name> …` → uses `~/.clawdbot-<name>` (port via config/env/flags)
See [`docs/gateway.md`](/gateway) for the derived port mapping (gateway/bridge/browser/canvas).
See [`docs/gateway.md`](https://docs.clawd.bot/gateway) for the derived port mapping (gateway/bridge/browser/canvas).
Example:
```bash
@@ -1567,7 +1479,7 @@ Template placeholders are expanded in `routing.transcribeAudio.command` (and any
## Cron (Gateway scheduler)
Cron is a Gateway-owned scheduler for wakeups and scheduled jobs. See [Cron jobs](/automation/cron-jobs) for the feature overview and CLI examples.
Cron is a Gateway-owned scheduler for wakeups and scheduled jobs. See [Cron + wakeups](https://docs.clawd.bot/cron) for the full RFC and CLI examples.
```json5
{
@@ -1580,4 +1492,4 @@ Cron is a Gateway-owned scheduler for wakeups and scheduled jobs. See [Cron jobs
---
*Next: [Agent Runtime](/concepts/agent)* 🦞
*Next: [Agent Runtime](https://docs.clawd.bot/agent)* 🦞
+5 -13
View File
@@ -13,14 +13,6 @@ The Control UI is a small **Vite + Lit** single-page app served by the Gateway:
It speaks **directly to the Gateway WebSocket** on the same port.
## Quick open (local)
If the Gateway is running on the same computer, open:
- http://127.0.0.1:18789/ (or http://localhost:18789/)
If the page fails to load, start the Gateway first: `clawdbot gateway`.
Auth is supplied during the WebSocket handshake via:
- `connect.params.auth.token`
- `connect.params.auth.password`
@@ -71,21 +63,21 @@ Paste the token into the UI settings (sent as `connect.params.auth.token`).
The Gateway serves static files from `dist/control-ui`. Build them with:
```bash
pnpm ui:install
pnpm ui:build
bun run ui:install
bun run ui:build
```
Optional absolute base (when you want fixed asset URLs):
```bash
CLAWDBOT_CONTROL_UI_BASE_PATH=/clawdbot/ pnpm ui:build
CLAWDBOT_CONTROL_UI_BASE_PATH=/clawdbot/ bun run ui:build
```
For local development (separate dev server):
```bash
pnpm ui:install
pnpm ui:dev
bun run ui:install
bun run ui:dev
```
Then point the UI at your Gateway WS URL (e.g. `ws://127.0.0.1:18789`).
+385
View File
@@ -0,0 +1,385 @@
---
summary: "RFC: Cron jobs + wakeups for Clawd/Clawdbot (main vs isolated sessions)"
read_when:
- Designing scheduled jobs, alarms, or wakeups
- Adding Gateway methods or CLI commands for automation
- Adjusting heartbeat behavior or session routing
---
# RFC: Cron jobs + wakeups for Clawd
Status: Draft
Last updated: 2025-12-13
## Context
Clawdbot already has:
- A **gateway heartbeat runner** that runs the agent with the configured heartbeat prompt (default: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`) and suppresses `HEARTBEAT_OK` ([`src/infra/heartbeat-runner.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/infra/heartbeat-runner.ts)).
- A lightweight, in-memory **system event queue** (`enqueueSystemEvent`) that is injected into the next **main session** turn (`drainSystemEvents` in [`src/auto-reply/reply.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/auto-reply/reply.ts)).
- A WebSocket **Gateway** daemon that is intended to be always-on ([`docs/gateway.md`](https://docs.clawd.bot/gateway)).
This RFC adds a small “cron job system” so Clawd can schedule future work and reliably wake itself up:
- **Delayed**: run on the *next* normal heartbeat tick
- **Immediate**: run *now* (trigger a heartbeat immediately)
- **Isolated jobs**: optionally run in their own session that does not pollute the main session and can run concurrently (within configured limits).
## Goals
- Provide a **persistent job store** and an **in-process scheduler** owned by the Gateway.
- Allow each job to target either:
- `sessionTarget: "main"`: inject as `System:` lines and rely on the main heartbeat (or trigger it immediately).
- `sessionTarget: "isolated"`: run an agent turn in a dedicated session key (job session), optionally delivering a message and/or posting a summary back to main.
- Expose a stable control surface:
- **Gateway methods** (`cron.*`, `wake`) for programmatic usage (mac app, CLI, agents).
- **CLI commands** (`clawdbot cron ...`) to add/remove/edit/list and to debug `run`.
- Produce clear, structured **logs** for job lifecycle and execution outcomes.
## Non-goals (v1)
- Multi-host distributed scheduling.
- Exactly-once semantics across crashes (we aim for “at-least-once with idempotency hooks”).
- A full Unix-cron parser as the only schedule format (we can support it, but v1 should not require complex cron features to be useful).
## Terminology
- **Wake**: a request to ensure the agent gets a turn soon (either right now or next heartbeat).
- **Main session**: the canonical session bucket (default key `"main"`) that receives `System:` events.
- **Isolated session**: a per-job session key (e.g. `cron:<jobId>`) with its own session id / session file.
## User stories
- “Remind me in 20 minutes” → add a one-shot job that triggers an immediate heartbeat at T+20m.
- “Every weekday at 7:30, wake me up and start music” → recurring job, isolated session, deliver to WhatsApp.
- “Every hour, check battery; only interrupt me if < 20%” → isolated job that decides whether to deliver; may also post a brief status to main.
- “Next heartbeat, please check calendar” → delayed wake targeting main session.
## Job model
### Storage schema (v1)
Each job is a JSON object with stable keys (unknown keys ignored for forward compatibility):
- `id: string` (UUID)
- `name: string` (required)
- `description?: string` (optional)
- `enabled: boolean`
- `createdAtMs: number`
- `updatedAtMs: number`
- `schedule` (one of)
- `{"kind":"at","atMs":number}` (one-shot)
- `{"kind":"every","everyMs":number,"anchorMs"?:number}` (simple interval)
- `{"kind":"cron","expr":string,"tz"?:string}` (optional; see “Schedule parsing”)
- `sessionTarget: "main" | "isolated"`
- `wakeMode: "next-heartbeat" | "now"`
- For `sessionTarget:"isolated"`, `wakeMode:"now"` means “run immediately when due”.
- For `sessionTarget:"main"`, `wakeMode` controls whether we trigger the heartbeat immediately or just enqueue and wait.
- `payload` (one of)
- `{"kind":"systemEvent","text":string}` (enqueue as `System:`)
- `{"kind":"agentTurn","message":string,"deliver"?:boolean,"provider"?: "last"|"whatsapp"|"telegram"|"discord"|"signal"|"imessage","to"?:string,"timeoutSeconds"?:number}`
- `isolation` (optional; only meaningful for isolated jobs)
- `{"postToMainPrefix"?: string}`
- `runtime` (optional)
- `{"maxAttempts"?:number,"retryBackoffMs"?:number}` (best-effort retries; defaults off)
- `state` (runtime-maintained)
- `{"nextRunAtMs":number,"lastRunAtMs"?:number,"lastStatus"?: "ok"|"error"|"skipped","lastError"?:string,"lastDurationMs"?:number}`
### Key behavior
- `sessionTarget:"main"` jobs always enqueue `payload.kind:"systemEvent"` (directly or derived from `agentTurn` results; see below).
- `sessionTarget:"isolated"` jobs create/use a stable session key: `cron:<jobId>`.
## Storage location
Cron persists everything under `~/.clawdbot/cron/`:
- Job store: `~/.clawdbot/cron/jobs.json`
- Run history: `~/.clawdbot/cron/runs/<jobId>.jsonl`
You can override the job store path via `cron.store` in config.
The scheduler should never require additional configuration for the base directory (Clawdbot already treats `~/.clawdbot` as fixed).
## Enabling
Cron execution is enabled by default inside the Gateway.
To disable it, set:
```json5
{
cron: {
enabled: false,
// optional:
store: "~/.clawdbot/cron/jobs.json",
maxConcurrentRuns: 1
}
}
```
You can also disable scheduling via the environment variable `CLAWDBOT_SKIP_CRON=1`.
## Scheduler design
### Ownership
The Gateway owns:
- the scheduler timer,
- job store reads/writes,
- job execution (enqueue system events and/or agent turns).
This keeps scheduling unified with the always-on process and prevents “two schedulers” when multiple CLIs run.
### Timer strategy
- Maintain an in-memory heap/array of enabled jobs keyed by `state.nextRunAtMs`.
- Use a **single `setTimeout`** to wake at the earliest next run.
- On wake:
- compute all due jobs (now >= nextRunAtMs),
- mark them “in flight” (in memory),
- persist updated `state` (at least bump `nextRunAtMs` / `lastRunAtMs`) before starting execution to minimize duplicate runs on crash,
- execute jobs (with concurrency limits),
- persist final `lastStatus/lastError/lastDurationMs`,
- re-arm timer for the next earliest run.
### Schedule parsing
V1 can ship with `at` + `every` without extra deps.
If we add `"kind":"cron"`:
- Use a well-maintained parser (we use `croner`) and support:
- 5-field cron (`min hour dom mon dow`) at minimum
- optional `tz`
- Store `nextRunAtMs` computed by the parser; re-compute after each run.
## Execution semantics
### Main session jobs
Main session jobs do not run the agent directly by default.
When due:
1) `enqueueSystemEvent(job.payload.text)` (or a derived message)
2) If `wakeMode:"now"`, trigger an immediate heartbeat run (see “Heartbeat wake hook”).
3) Otherwise do nothing else (the next scheduled heartbeat will pick up the system event).
Why: This keeps the main session’s “proactive” behavior centralized in the heartbeat rules and avoids ad-hoc agent turns that might fight with inbound message processing.
### Isolated session jobs
Isolated jobs run an agent turn in a dedicated session key, intended to be separate from main.
When due:
- Build a message body that includes schedule metadata, e.g.:
- `"[cron:<jobId>] <job.name>: <payload.message>"`
- Execute via the same agent runner path as other command-mode runs, but pinned to:
- `sessionKey = cron:<jobId>`
- `sessionId = store[sessionKey].sessionId` (create if missing)
- Optionally deliver output (`payload.deliver === true`) to the configured provider/to.
- Isolated jobs always enqueue a summary system event to the main session when they finish (derived from the last agent text output).
- Prefix defaults to `Cron`, and can be customized via `isolation.postToMainPrefix`.
- If `deliver` is omitted/false, nothing is sent to external providers; you still get the main-session summary and can inspect the full isolated transcript in `cron:<jobId>`.
### “Run in parallel to main”
Clawdbot currently serializes command execution through a global in-process queue ([`src/process/command-queue.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/process/command-queue.ts)) to avoid collisions.
To support isolated cron jobs running “in parallel”, we should introduce **lanes** (keyed queues) plus a global concurrency cap:
- Lane `"main"`: inbound auto-replies + main heartbeat.
- Lane `"cron"` (or `cron:<jobId>`): isolated jobs.
- Configurable `cron.maxConcurrentRuns` (default 1 or 2).
This yields:
- isolated jobs can overlap with the main lane (up to cap),
- each lane still preserves ordering for its own work (optional),
- we retain safety knobs to prevent runaway resource contention.
## Heartbeat wake hook (immediate vs next heartbeat)
We need a way for the Gateway (or the scheduler) to request an immediate heartbeat without duplicating heartbeat logic.
Design:
- `startHeartbeatRunner` owns the real heartbeat execution and installs a wake handler.
- Wake hook lives in [`src/infra/heartbeat-wake.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/infra/heartbeat-wake.ts):
- `setHeartbeatWakeHandler(fn | null)` installed by the heartbeat runner
- `requestHeartbeatNow({ reason, coalesceMs? })`
- If the handler is absent, the request is stored as “pending”; the next time the handler is installed, it runs once.
- Coalesce rapid calls and respect the “skip when queue busy” behavior (retry soon vs dropping).
## Run history log (JSONL)
In addition to normal structured logs, the Gateway writes an append-only run history “ledger” (JSONL) whenever a job finishes. This is intended for quick debugging (“did the job run, when, and what happened?”).
Path rules:
- Run logs are stored per job next to the store: `.../runs/<jobId>.jsonl`.
Retention:
- Best-effort pruning when the file grows beyond ~2MB; keep the newest ~2000 lines.
Each log line includes (at minimum) job id, status/error, timing, and a `summary` string (systemEvent text for main jobs, and the last agent text output for isolated jobs).
## Compatibility policy (cron.add/cron.update)
To keep older clients working, the Gateway applies **best-effort normalization** for `cron.add` and `cron.update`:
- Accepts wrapped payloads under `data` or `job` and unwraps them.
- Infers `schedule.kind` from `atMs`, `everyMs`, or `expr` if missing.
- Infers `payload.kind` from `text` (systemEvent) or `message` (agentTurn) if missing.
- Defaults `wakeMode` to `"next-heartbeat"` when omitted.
- Defaults `sessionTarget` based on payload kind (`systemEvent` → `"main"`, `agentTurn` → `"isolated"`).
Normalization is **compat-only**. New clients should send the full schema (including `kind`, `sessionTarget`, and `wakeMode`) to avoid ambiguity. Unknown fields are still rejected by schema validation.
## Gateway API
New methods (names can be bikeshed; `cron.*` is suggested):
- `wake`
- params: `{ mode: "now" | "next-heartbeat", text: string }`
- effect: `enqueueSystemEvent(text)`, plus optional immediate heartbeat trigger
- `cron.list`
- params: optional `{ includeDisabled?: boolean }`
- returns: `{ jobs: CronJob[] }`
- `cron.add`
- params: job payload without `id/state` (server generates and returns created job)
- `cron.update`
- params: `{ id: string, patch: Partial<CronJobWritableFields> }`
- `cron.remove`
- params: `{ id: string }`
- `cron.run`
- params: `{ id: string, mode?: "due" | "force" }` (debugging; does not change schedule unless `force` requires it)
- `cron.runs`
- params: `{ id: string, limit?: number }`
- returns: `{ entries: CronRunLogEntry[] }`
- note: `id` is required (runs are stored per-job).
The Gateway should broadcast a `cron` event for UI/debug:
- event: `cron`
- payload: `{ jobId, action: "added"|"updated"|"removed"|"started"|"finished", status?, error?, nextRunAtMs? }`
## CLI surface
Add a `cron` command group (all commands should also support `--json` where sensible):
- `clawdbot cron list [--json] [--all]`
- `clawdbot cron add ...`
- schedule flags:
- `--at <iso8601|ms|relative>` (one-shot)
- `--every <duration>` (e.g. `10m`, `1h`)
- `--cron "<expr>" [--tz "<tz>"]`
- target flags:
- `--session main|isolated`
- `--wake now|next-heartbeat`
- payload flags (choose one):
- `--system-event "<text>"`
- `--message "<agent message>" [--deliver] [--provider last|whatsapp|telegram|discord|slack|signal|imessage] [--to <dest>]`
- `clawdbot cron edit <id> ...` (patch-by-flags, non-interactive)
- `clawdbot cron rm <id>`
- `clawdbot cron enable <id>` / `clawdbot cron disable <id>`
- `clawdbot cron run <id> [--force]` (debug)
- `clawdbot cron runs --id <id> [--limit <n>]` (run history)
- `clawdbot cron status` (scheduler enabled + next wake)
Additionally:
- `clawdbot wake --mode now|next-heartbeat --text "<text>"` as a thin wrapper around `wake` for agents to call.
## Examples
### Run once at a specific time
One-shot reminder that targets the main session and triggers a heartbeat immediately at the scheduled time:
```bash
clawdbot cron add \
--at "2025-12-14T07:00:00-08:00" \
--session main \
--wake now \
--system-event "Alarm: wake up (meeting in 30 minutes)."
```
### Run daily (calendar-accurate)
Daily at 07:00 in a specific timezone (preferred over “every 24h” to avoid DST drift):
```bash
clawdbot cron add \
--cron "0 7 * * *" \
--tz "America/Los_Angeles" \
--session isolated \
--wake now \
--message "Daily check: scan calendar + inbox; deliver only if urgent." \
--deliver \
--provider last
```
### Run weekly (every Wednesday)
Every Wednesday at 09:00:
```bash
clawdbot cron add \
--cron "0 9 * * 3" \
--tz "America/Los_Angeles" \
--session isolated \
--wake now \
--message "Weekly: summarize status and remind me of goals." \
--deliver \
--provider last
```
### “Next heartbeat”
Enqueue a note for the main session but let the existing heartbeat cadence pick it up:
```bash
clawdbot wake --mode next-heartbeat --text "Next heartbeat: check battery + upcoming meetings."
```
## Logging & observability
Logging requirements:
- Use `getChildLogger({ module: "cron", jobId, runId, name })` for every run.
- Log lifecycle:
- store load/save (debug; include job count)
- schedule recompute (debug; include nextRunAt)
- job start/end (info)
- job skipped (info; include reason)
- job error (warn; include error + stack where available)
- Emit a concise user-facing line to stdout when running in CLI mode (similar to heartbeat logs).
Suggested log events:
- `cron: scheduler started` (jobCount, nextWakeAt)
- `cron: job started` (jobId, scheduleKind, sessionTarget, wakeMode)
- `cron: job finished` (status, durationMs, nextRunAtMs)
- When `cron.enabled` is false, the Gateway logs `cron: disabled` and jobs will not run automatically (the CLI warns on `cron add`/`cron edit`).
- Use `clawdbot cron status` to confirm the scheduler is enabled and see the next wake time.
## Safety & security
- Respect existing allowlists/routing rules: delivery defaults should not send to arbitrary destinations unless explicitly configured.
- Provide a global “kill switch”:
- `cron.enabled: boolean` (default `true`).
- `gateway method set-heartbeats` already exists; cron should have similar.
- Avoid persistence of sensitive payloads unless requested; job text may contain private content.
## Testing plan (v1)
- Unit tests:
- schedule computation for `at` and `every`
- job store read/write + migration behavior
- lane concurrency: main vs cron overlap is bounded
- “wake now” coalescing and pending behavior when provider not ready
- Integration tests:
- start Gateway with `CLAWDBOT_SKIP_PROVIDERS=1`, add jobs, list/edit/remove
- simulate due jobs and assert `enqueueSystemEvent` called + cron events broadcast
## Rollout plan
1) Add the `wake` primitive + heartbeat wake hook (no persistent jobs yet).
2) Add `cron.*` API and CLI wrappers with `at` + `every`.
3) Add optional cron expression parsing (`kind:"cron"`) if needed.
4) Add UI surfacing in WebChat/macOS app (optional).
+3 -6
View File
@@ -8,13 +8,10 @@ read_when:
The Gateway dashboard is the browser Control UI served at `/` by default
(override with `gateway.controlUi.basePath`).
Quick open (local Gateway):
- http://127.0.0.1:18789/ (or http://localhost:18789/)
Key references:
- [`docs/control-ui.md`](https://docs.clawd.bot/web/control-ui) for usage and UI capabilities.
- [`docs/tailscale.md`](https://docs.clawd.bot/gateway/tailscale) for Serve/Funnel automation.
- [`docs/control-ui.md`](https://docs.clawd.bot/control-ui) for usage and UI capabilities.
- [`docs/tailscale.md`](https://docs.clawd.bot/tailscale) for Serve/Funnel automation.
- [`docs/web.md`](https://docs.clawd.bot/web) for bind modes and security notes.
Authentication is enforced at the WebSocket handshake via `connect.params.auth`
(token or password). See `gateway.auth` in [`docs/configuration.md`](https://docs.clawd.bot/gateway/configuration).
(token or password). See `gateway.auth` in [`docs/configuration.md`](https://docs.clawd.bot/configuration).
File renamed without changes.
+3 -16
View File
@@ -23,17 +23,16 @@ Status: ready for DM and guild text channels via the official Discord bot gatewa
- If you prefer env vars, still add `discord: { enabled: true }` to `~/.clawdbot/clawdbot.json` and set `DISCORD_BOT_TOKEN`.
5. Direct chats: use `user:<id>` (or a `<@id>` mention) when delivering; all turns land in the shared `main` session.
6. Guild channels: use `channel:<channelId>` for delivery. Mentions are required by default and can be set per guild or per channel.
7. Direct chats: secure by default via `discord.dm.policy` (default: `"pairing"`). Unknown senders get a pairing code (expires after 1 hour); approve via `clawdbot pairing approve --provider discord <code>`.
7. Direct chats: secure by default via `discord.dm.policy` (default: `"pairing"`). Unknown senders get a pairing code; approve via `clawdbot pairing approve --provider discord <code>`.
- To keep old “open to anyone” behavior: set `discord.dm.policy="open"` and `discord.dm.allowFrom=["*"]`.
- To hard-allowlist: set `discord.dm.policy="allowlist"` and list senders in `discord.dm.allowFrom`.
- To ignore all DMs: set `discord.dm.enabled=false` or `discord.dm.policy="disabled"`.
8. Group DMs are ignored by default; enable via `discord.dm.groupEnabled` and optionally restrict by `discord.dm.groupChannels`.
9. Optional guild rules: set `discord.guilds` keyed by guild id (preferred) or slug, with per-channel rules.
10. Optional native commands: set `commands.native: true` to register native commands in Discord; set `commands.native: false` to clear previously registered native commands. Text commands are controlled by `commands.text` and must be sent as standalone `/...` messages. Use `commands.useAccessGroups: false` to bypass access-group checks for commands.
- Full command list + config: [Slash commands](/tools/slash-commands)
- Full command list + config: https://docs.clawd.bot/slash-commands
11. Optional guild context history: set `discord.historyLimit` (default 20) to include the last N guild messages as context when replying to a mention. Set `0` to disable.
12. Reactions: the agent can trigger reactions via the `discord` tool (gated by `discord.actions.*`).
- Reaction removal semantics: see [/tools/reactions](/tools/reactions).
- The `discord` tool is only exposed when the current provider is Discord.
13. Native commands use isolated session keys (`discord:slash:${userId}`) rather than the shared `main` session.
@@ -197,13 +196,7 @@ Notes:
users: ["987654321098765432", "steipete"],
channels: {
general: { allow: true },
help: {
allow: true,
requireMention: true,
users: ["987654321098765432"],
skills: ["search", "docs"],
systemPrompt: "Keep answers short."
}
help: { allow: true, requireMention: true }
}
}
}
@@ -224,12 +217,6 @@ Ack reactions are controlled globally via `messages.ackReaction` +
- `guilds."*"`: default per-guild settings applied when no explicit entry exists.
- `guilds.<id>.slug`: optional friendly slug used for display names.
- `guilds.<id>.users`: optional per-guild user allowlist (ids or names).
- `guilds.<id>.channels.<channel>.allow`: allow/deny the channel when `groupPolicy="allowlist"`.
- `guilds.<id>.channels.<channel>.requireMention`: mention gating for the channel.
- `guilds.<id>.channels.<channel>.users`: optional per-channel user allowlist.
- `guilds.<id>.channels.<channel>.skills`: skill filter (omit = all skills, empty = none).
- `guilds.<id>.channels.<channel>.systemPrompt`: extra system prompt for the channel (combined with channel topic).
- `guilds.<id>.channels.<channel>.enabled`: set `false` to disable the channel.
- `guilds.<id>.channels`: channel rules (keys are channel slugs or ids).
- `guilds.<id>.requireMention`: per-guild mention requirement (overridable per channel).
- `guilds.<id>.reactionNotifications`: reaction system event mode (`off`, `own`, `all`, `allowlist`).
@@ -42,7 +42,7 @@ Target direction:
- The **gateway** advertises its bridge via Bonjour.
- Clients browse and show a “pick a gateway” list, then store the chosen endpoint.
Troubleshooting and beacon details: [`docs/bonjour.md`](/gateway/bonjour).
Troubleshooting and beacon details: [`docs/bonjour.md`](https://docs.clawd.bot/bonjour).
#### Current implementation
@@ -77,7 +77,7 @@ If the gateway can detect it is running under Tailscale, it publishes `tailnetDn
When there is no direct route (or direct is disabled), clients can always connect via SSH by forwarding the loopback gateway port.
See [`docs/remote.md`](/gateway/remote).
See [`docs/remote.md`](https://docs.clawd.bot/remote).
## Transport selection (client policy)
@@ -92,7 +92,7 @@ Recommended client behavior:
The gateway is the source of truth for node/client admission.
- Pairing requests are created/approved/rejected in the gateway (see [`docs/gateway/pairing.md`](/gateway/pairing)).
- Pairing requests are created/approved/rejected in the gateway (see [`docs/gateway/pairing.md`](https://docs.clawd.bot/gateway/pairing)).
- The bridge enforces:
- auth (token / keypair)
- scopes/ACLs (bridge is not a raw proxy to every gateway method)
+7 -15
View File
@@ -70,29 +70,22 @@ pnpm test:docker:qr
- Gateway bind defaults to `lan` for container use.
- The gateway container is the source of truth for sessions (`~/.clawdbot/agents/<agentId>/sessions/`).
## Agent Sandbox (host gateway + Docker tools)
## Per-session Agent Sandbox (host gateway + Docker tools)
### What it does
When `agent.sandbox` is enabled, **non-main sessions** run tools inside a Docker
container. The gateway stays on your host, but the tool execution is isolated:
- scope: `"agent"` by default (one container + workspace per agent)
- scope: `"session"` for per-session isolation
- per-scope workspace folder mounted at `/workspace`
- optional agent workspace access (`agent.sandbox.workspaceAccess`)
- one container per session (hard wall)
- per-session workspace folder mounted at `/workspace`
- allow/deny tool policy (deny wins)
- inbound media is copied into the active sandbox workspace (`media/inbound/*`) so tools can read it (with `workspaceAccess: "rw"`, this lands in the agent workspace)
Warning: `scope: "shared"` disables cross-session isolation. All sessions share
one container and one workspace.
- inbound media is copied into the sandbox workspace (`media/inbound/*`) so tools can read it
### Default behavior
- Image: `clawdbot-sandbox:bookworm-slim`
- One container per agent
- Agent workspace access: `workspaceAccess: "none"` (default) uses `~/.clawdbot/sandboxes`
- `"ro"` keeps the sandbox workspace at `/workspace` and mounts the agent workspace read-only at `/agent` (disables `write`/`edit`)
- `"rw"` mounts the agent workspace read/write at `/workspace`
- One container per session
- Workspace per session under `~/.clawdbot/sandboxes`
- Auto-prune: idle > 24h OR age > 7d
- Network: `none` by default (explicitly opt-in if you need egress)
- Default allow: `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`
@@ -105,8 +98,7 @@ one container and one workspace.
agent: {
sandbox: {
mode: "non-main", // off | non-main | all
scope: "agent", // session | agent | shared (agent is default)
workspaceAccess: "none", // none | ro | rw
perSession: true,
workspaceRoot: "~/.clawdbot/sandboxes",
docker: {
image: "clawdbot-sandbox:bookworm-slim",
+42 -644
View File
@@ -20,680 +20,78 @@
"url": "https://github.com/clawdbot/clawdbot/releases"
}
],
"redirects": [
{
"source": "/cron",
"destination": "/cron-jobs"
},
{
"source": "/cron/",
"destination": "/cron-jobs"
},
{
"source": "/model",
"destination": "/models"
},
{
"source": "/model/",
"destination": "/models"
},
{
"source": "/AGENTS.default",
"destination": "/reference/AGENTS.default"
},
{
"source": "/RELEASING",
"destination": "/reference/RELEASING"
},
{
"source": "/agent",
"destination": "/concepts/agent"
},
{
"source": "/agent-loop",
"destination": "/concepts/agent-loop"
},
{
"source": "/agent-send",
"destination": "/tools/agent-send"
},
{
"source": "/agent-workspace",
"destination": "/concepts/agent-workspace"
},
{
"source": "/android",
"destination": "/platforms/android"
},
{
"source": "/architecture",
"destination": "/concepts/architecture"
},
{
"source": "/audio",
"destination": "/nodes/audio"
},
{
"source": "/background-process",
"destination": "/gateway/background-process"
},
{
"source": "/bash",
"destination": "/tools/bash"
},
{
"source": "/bonjour",
"destination": "/gateway/bonjour"
},
{
"source": "/browser",
"destination": "/tools/browser"
},
{
"source": "/browser-linux-troubleshooting",
"destination": "/tools/browser-linux-troubleshooting"
},
{
"source": "/bun",
"destination": "/install/bun"
},
{
"source": "/camera",
"destination": "/nodes/camera"
},
{
"source": "/clawd",
"destination": "/start/clawd"
},
{
"source": "/clawdhub",
"destination": "/tools/clawdhub"
},
{
"source": "/configuration",
"destination": "/gateway/configuration"
},
{
"source": "/control-ui",
"destination": "/web/control-ui"
},
{
"source": "/cron-jobs",
"destination": "/automation/cron-jobs"
},
{
"source": "/dashboard",
"destination": "/web/dashboard"
},
{
"source": "/device-models",
"destination": "/reference/device-models"
},
{
"source": "/discord",
"destination": "/providers/discord"
},
{
"source": "/discovery",
"destination": "/gateway/discovery"
},
{
"source": "/docker",
"destination": "/install/docker"
},
{
"source": "/doctor",
"destination": "/gateway/doctor"
},
{
"source": "/elevated",
"destination": "/tools/elevated"
},
{
"source": "/faq",
"destination": "/start/faq"
},
{
"source": "/gateway-lock",
"destination": "/gateway/gateway-lock"
},
{
"source": "/getting-started",
"destination": "/start/getting-started"
},
{
"source": "/gmail-pubsub",
"destination": "/automation/gmail-pubsub"
},
{
"source": "/grammy",
"destination": "/providers/grammy"
},
{
"source": "/group-messages",
"destination": "/concepts/group-messages"
},
{
"source": "/groups",
"destination": "/concepts/groups"
},
{
"source": "/health",
"destination": "/gateway/health"
},
{
"source": "/heartbeat",
"destination": "/gateway/heartbeat"
},
{
"source": "/hubs",
"destination": "/start/hubs"
},
{
"source": "/images",
"destination": "/nodes/images"
},
{
"source": "/imessage",
"destination": "/providers/imessage"
},
{
"source": "/ios",
"destination": "/platforms/ios"
},
{
"source": "/linux",
"destination": "/platforms/linux"
},
{
"source": "/location",
"destination": "/providers/location"
},
{
"source": "/location-command",
"destination": "/nodes/location-command"
},
{
"source": "/logging",
"destination": "/gateway/logging"
},
{
"source": "/lore",
"destination": "/start/lore"
},
{
"source": "/mac/bun",
"destination": "/platforms/mac/bun"
},
{
"source": "/mac/canvas",
"destination": "/platforms/mac/canvas"
},
{
"source": "/mac/child-process",
"destination": "/platforms/mac/child-process"
},
{
"source": "/mac/dev-setup",
"destination": "/platforms/mac/dev-setup"
},
{
"source": "/mac/health",
"destination": "/platforms/mac/health"
},
{
"source": "/mac/icon",
"destination": "/platforms/mac/icon"
},
{
"source": "/mac/logging",
"destination": "/platforms/mac/logging"
},
{
"source": "/mac/menu-bar",
"destination": "/platforms/mac/menu-bar"
},
{
"source": "/mac/peekaboo",
"destination": "/platforms/mac/peekaboo"
},
{
"source": "/mac/permissions",
"destination": "/platforms/mac/permissions"
},
{
"source": "/mac/release",
"destination": "/platforms/mac/release"
},
{
"source": "/mac/remote",
"destination": "/platforms/mac/remote"
},
{
"source": "/mac/signing",
"destination": "/platforms/mac/signing"
},
{
"source": "/mac/skills",
"destination": "/platforms/mac/skills"
},
{
"source": "/mac/voice-overlay",
"destination": "/platforms/mac/voice-overlay"
},
{
"source": "/mac/voicewake",
"destination": "/platforms/mac/voicewake"
},
{
"source": "/mac/webchat",
"destination": "/platforms/mac/webchat"
},
{
"source": "/mac/xpc",
"destination": "/platforms/mac/xpc"
},
{
"source": "/macos",
"destination": "/platforms/macos"
},
{
"source": "/model-failover",
"destination": "/concepts/model-failover"
},
{
"source": "/models",
"destination": "/concepts/models"
},
{
"source": "/multi-agent",
"destination": "/concepts/multi-agent"
},
{
"source": "/nix",
"destination": "/install/nix"
},
{
"source": "/onboarding",
"destination": "/start/onboarding"
},
{
"source": "/onboarding-config-protocol",
"destination": "/experiments/onboarding-config-protocol"
},
{
"source": "/pairing",
"destination": "/start/pairing"
},
{
"source": "/plans/cron-add-hardening",
"destination": "/experiments/plans/cron-add-hardening"
},
{
"source": "/plans/group-policy-hardening",
"destination": "/experiments/plans/group-policy-hardening"
},
{
"source": "/poll",
"destination": "/automation/poll"
},
{
"source": "/presence",
"destination": "/concepts/presence"
},
{
"source": "/proposals/model-config",
"destination": "/experiments/proposals/model-config"
},
{
"source": "/provider-routing",
"destination": "/concepts/provider-routing"
},
{
"source": "/queue",
"destination": "/concepts/queue"
},
{
"source": "/remote",
"destination": "/gateway/remote"
},
{
"source": "/remote-gateway-readme",
"destination": "/gateway/remote-gateway-readme"
},
{
"source": "/research/memory",
"destination": "/experiments/research/memory"
},
{
"source": "/rpc",
"destination": "/reference/rpc"
},
{
"source": "/security",
"destination": "/gateway/security"
},
{
"source": "/session",
"destination": "/concepts/session"
},
{
"source": "/session-tool",
"destination": "/concepts/session-tool"
},
{
"source": "/sessions",
"destination": "/concepts/sessions"
},
{
"source": "/setup",
"destination": "/start/setup"
},
{
"source": "/showcase",
"destination": "/start/showcase"
},
{
"source": "/signal",
"destination": "/providers/signal"
},
{
"source": "/skills",
"destination": "/tools/skills"
},
{
"source": "/skills-config",
"destination": "/tools/skills-config"
},
{
"source": "/slack",
"destination": "/providers/slack"
},
{
"source": "/slash-commands",
"destination": "/tools/slash-commands"
},
{
"source": "/subagents",
"destination": "/tools/subagents"
},
{
"source": "/tailscale",
"destination": "/gateway/tailscale"
},
{
"source": "/talk",
"destination": "/nodes/talk"
},
{
"source": "/telegram",
"destination": "/providers/telegram"
},
{
"source": "/templates/AGENTS",
"destination": "/reference/templates/AGENTS"
},
{
"source": "/templates/BOOTSTRAP",
"destination": "/reference/templates/BOOTSTRAP"
},
{
"source": "/templates/HEARTBEAT",
"destination": "/reference/templates/HEARTBEAT"
},
{
"source": "/templates/IDENTITY",
"destination": "/reference/templates/IDENTITY"
},
{
"source": "/templates/SOUL",
"destination": "/reference/templates/SOUL"
},
{
"source": "/templates/TOOLS",
"destination": "/reference/templates/TOOLS"
},
{
"source": "/templates/USER",
"destination": "/reference/templates/USER"
},
{
"source": "/test",
"destination": "/reference/test"
},
{
"source": "/thinking",
"destination": "/tools/thinking"
},
{
"source": "/timezone",
"destination": "/concepts/timezone"
},
{
"source": "/troubleshooting",
"destination": "/gateway/troubleshooting"
},
{
"source": "/tui",
"destination": "/web/tui"
},
{
"source": "/typebox",
"destination": "/concepts/typebox"
},
{
"source": "/updating",
"destination": "/install/updating"
},
{
"source": "/voicewake",
"destination": "/nodes/voicewake"
},
{
"source": "/webchat",
"destination": "/web/webchat"
},
{
"source": "/webhook",
"destination": "/automation/webhook"
},
{
"source": "/whatsapp",
"destination": "/providers/whatsapp"
},
{
"source": "/windows",
"destination": "/platforms/windows"
},
{
"source": "/wizard",
"destination": "/start/wizard"
}
],
"navigation": {
"groups": [
{
"group": "Start Here",
"pages": [
"getting-started",
"wizard",
"index",
"start/getting-started",
"start/wizard",
"start/setup",
"start/pairing",
"start/faq",
"start/clawd",
"start/showcase",
"start/hubs",
"start/onboarding",
"start/lore"
"setup",
"pairing",
"faq",
"clawd",
"showcase",
"hubs",
"onboarding"
]
},
{
"group": "Install & Updates",
"group": "Install Options",
"pages": [
"install/updating",
"install/nix",
"install/docker",
"install/bun"
"nix",
"docker"
]
},
{
"group": "Core Concepts",
"pages": [
"concepts/architecture",
"concepts/agent",
"concepts/agent-loop",
"concepts/agent-workspace",
"concepts/multi-agent",
"concepts/session",
"concepts/sessions",
"concepts/session-tool",
"concepts/presence",
"concepts/provider-routing",
"concepts/groups",
"concepts/group-messages",
"concepts/queue",
"concepts/models",
"concepts/model-failover",
"concepts/timezone",
"concepts/typebox"
"architecture",
"agent",
"session",
"rpc",
"queue",
"tools"
]
},
{
"group": "Gateway & Ops",
"group": "Gateway & Providers",
"pages": [
"gateway",
"gateway/pairing",
"gateway/gateway-lock",
"gateway/configuration",
"gateway/background-process",
"gateway/health",
"gateway/heartbeat",
"gateway/doctor",
"gateway/logging",
"gateway/security",
"gateway/troubleshooting",
"gateway/remote",
"gateway/remote-gateway-readme",
"gateway/discovery",
"gateway/bonjour",
"gateway/tailscale"
]
},
{
"group": "Web & Interfaces",
"pages": [
"web",
"web/control-ui",
"web/dashboard",
"web/webchat",
"web/tui"
]
},
{
"group": "Providers",
"pages": [
"providers/whatsapp",
"providers/telegram",
"providers/grammy",
"providers/discord",
"providers/slack",
"providers/signal",
"providers/imessage",
"providers/location"
]
},
{
"group": "Automation & Hooks",
"pages": [
"automation/webhook",
"automation/gmail-pubsub",
"automation/cron-jobs",
"automation/poll"
]
},
{
"group": "Tools & Skills",
"pages": [
"tools",
"tools/bash",
"tools/elevated",
"tools/browser",
"tools/browser-linux-troubleshooting",
"tools/slash-commands",
"tools/thinking",
"tools/agent-send",
"tools/subagents",
"tools/skills",
"tools/skills-config",
"tools/clawdhub"
]
},
{
"group": "Nodes & Media",
"pages": [
"nodes",
"nodes/camera",
"nodes/images",
"nodes/audio",
"nodes/location-command",
"nodes/voicewake",
"nodes/talk"
"whatsapp",
"telegram",
"discord",
"imessage",
"webhook",
"gmail-pubsub"
]
},
{
"group": "Platforms",
"pages": [
"platforms/macos",
"platforms/mac/dev-setup",
"platforms/mac/menu-bar",
"platforms/mac/voicewake",
"platforms/mac/voice-overlay",
"platforms/mac/webchat",
"platforms/mac/canvas",
"platforms/mac/child-process",
"platforms/mac/health",
"platforms/mac/icon",
"platforms/mac/logging",
"platforms/mac/permissions",
"platforms/mac/remote",
"platforms/mac/signing",
"platforms/mac/release",
"platforms/mac/bun",
"platforms/mac/xpc",
"platforms/mac/skills",
"platforms/mac/peekaboo",
"platforms/ios",
"platforms/android",
"platforms/windows",
"platforms/linux"
"mac/dev-setup",
"mac/menu-bar",
"mac/voicewake",
"macos",
"ios",
"android",
"webchat",
"web"
]
},
{
"group": "Reference & Templates",
"group": "Operations",
"pages": [
"reference/rpc",
"reference/device-models",
"reference/test",
"reference/RELEASING",
"reference/AGENTS.default",
"reference/templates/AGENTS",
"reference/templates/BOOTSTRAP",
"reference/templates/HEARTBEAT",
"reference/templates/IDENTITY",
"reference/templates/SOUL",
"reference/templates/TOOLS",
"reference/templates/USER"
]
},
{
"group": "Experiments & Proposals",
"pages": [
"experiments/onboarding-config-protocol",
"experiments/plans/cron-add-hardening",
"experiments/plans/group-policy-hardening",
"experiments/proposals/model-config",
"experiments/research/memory"
"doctor",
"troubleshooting",
"logging",
"security",
"health",
"background-process",
"gateway-lock",
"remote"
]
}
]
+2 -17
View File
@@ -14,7 +14,7 @@ read_when:
- Detects deprecated config keys and offers to migrate them.
- Migrates legacy `~/.clawdis/clawdis.json` when no Clawdbot config exists.
- Checks sandbox Docker images when sandboxing is enabled (offers to build or switch to legacy names).
- Detects legacy Clawdis services (launchd/systemd; legacy schtasks for native Windows) and offers to migrate them.
- Detects legacy Clawdis services (launchd/systemd/schtasks) and offers to migrate them.
- On Linux, checks if systemd user lingering is enabled and can enable it (required to keep the Gateway alive after logout).
- Migrates legacy on-disk state layouts (sessions, agentDir, provider auth dirs) into the current per-agent/per-account structure.
@@ -48,7 +48,6 @@ Doctor can migrate older on-disk layouts into the current structure:
- to `~/.clawdbot/credentials/whatsapp/<accountId>/...` (default account id: `default`)
These migrations are best-effort and idempotent; doctor will emit warnings when it leaves any legacy folders behind as backups.
The Gateway/CLI also auto-migrates the legacy sessions + agent dir on startup so history/auth/models land in the per-agent path without a manual doctor run. WhatsApp auth is intentionally only migrated via `clawdbot doctor`.
## Usage
@@ -56,20 +55,6 @@ The Gateway/CLI also auto-migrates the legacy sessions + agent dir on startup so
clawdbot doctor
```
### Headless / automation
```bash
clawdbot doctor --yes
```
Accept defaults without prompting (including restart/service/sandbox repair steps when applicable).
```bash
clawdbot doctor --non-interactive
```
Run without prompts and only apply safe migrations (config normalization + on-disk state moves). Skips restart/service/sandbox actions that require human confirmation.
If you want to review changes before writing, open the config file first:
```bash
@@ -77,6 +62,6 @@ cat ~/.clawdbot/clawdbot.json
```
## Legacy service migrations
Doctor checks for older Clawdis gateway services (launchd/systemd/schtasks). WSL2 installs use systemd.
Doctor checks for older Clawdis gateway services (launchd/systemd/schtasks).
If found, it offers to remove them and install the Clawdbot service using the current gateway port.
Remote mode skips the install step, and Nix mode only reports what it finds.
File renamed without changes.
+18 -44
View File
@@ -3,7 +3,7 @@ summary: "Frequently asked questions about Clawdbot setup, configuration, and us
---
# FAQ 🦞
Common questions from the community. For detailed configuration, see [Configuration](/gateway/configuration).
Common questions from the community. For detailed configuration, see [Configuration](https://docs.clawd.bot/configuration).
## Installation & Setup
@@ -28,11 +28,11 @@ Your **workspace** (AGENTS.md, memory files, skills) is separate — configured
### What platforms does Clawdbot run on?
**macOS and Linux** are the primary targets. On Windows, use **WSL2** (Ubuntu recommended).
**macOS and Linux** are the primary targets. Anywhere Node.js 22+ runs should work in theory.
- **macOS** — Fully supported, most tested
- **Linux** — Works great, common for VPS/server deployments
- **Windows (WSL2)** — WSL2 is strongly recommended; native Windows installs are untested and more problematic
- **Windows** — Should work but largely untested! You're in pioneer territory 🤠
Some features are platform-specific:
- **iMessage** — macOS only (uses `imsg` CLI)
@@ -49,21 +49,9 @@ Some features are platform-specific:
The gateway is just shuffling messages around. A Raspberry Pi 4 can run it. For the CLI, prefer the Node runtime (most stable):
```bash
clawdbot gateway
pnpm clawdbot gateway
```
### How do I install on Windows?
Use **WSL2** (Ubuntu recommended) and follow the Linux flow inside WSL. Quick start:
```powershell
wsl --install
```
Then open Ubuntu and run the normal Getting Started steps.
Full guide: [Windows (WSL2)](/platforms/windows)
### How do I install on Linux without Homebrew?
Build CLIs from source! Example for `gogcli`:
@@ -111,16 +99,6 @@ pnpm clawdbot doctor
It checks your config, skills status, and gateway health. It can also restart the gateway daemon if needed.
### How do I search the docs quickly?
Use the CLI docs search (live docs):
```bash
clawdbot docs "gateway lock"
```
The first run will fetch the helper CLIs if they are missing.
### Terminal onboarding vs macOS app?
**Use terminal onboarding** (`pnpm clawdbot onboard`) — it's more stable right now.
@@ -209,7 +187,7 @@ OAuth needs the callback to reach the machine running the CLI. Options:
5. **Start gateway:**
```bash
clawdbot gateway
pnpm clawdbot gateway
```
**Note:** WhatsApp may notice the IP change and require re-authentication. If so, run `pnpm clawdbot login` again. Stop the old instance before starting the new one to avoid conflicts.
@@ -243,7 +221,7 @@ Key considerations:
#!/bin/bash
npm install -g pnpm
cd /app
clawdbot gateway
pnpm clawdbot gateway
```
**Container command:**
@@ -260,19 +238,13 @@ Yes! The terminal QR code login works fine over SSH. For long-running operation:
- Use `pm2`, `systemd`, or a `launchd` plist to keep the gateway running.
- Consider Tailscale for secure remote access.
### I'm seeing `InvalidPnpmLockfile: failed to migrate lockfile: 'pnpm-lock.yaml'`
This can be ignored. This is simply a package manager warning when using PNPM and BUN.
It often shows up when switching between `pnpm install` and `bun install`. If installs/build/tests work, you can safely ignore it.
### bun binary vs Node runtime?
Clawdbot can run as:
- **bun binary (macOS app)** — Single executable, easy distribution, auto-restarts via launchd
- **Node runtime** (`clawdbot gateway`) — More stable for WhatsApp
- **Node runtime** (`pnpm clawdbot gateway`) — More stable for WhatsApp
If you see WebSocket errors like `ws.WebSocket 'upgrade' event is not implemented`, use Node instead of the bun binary. Bun's WebSocket implementation has edge cases that can break WhatsApp (Baileys) and can corrupt memory on reconnect. Baileys: https://github.com/WhiskeySockets/Baileys · Bun issue: https://github.com/oven-sh/bun/issues/5951
If you see WebSocket errors like `ws.WebSocket 'upgrade' event is not implemented`, use Node instead of the bun binary. Bun's WebSocket implementation has edge cases that can break WhatsApp (Baileys).
**For stability:** Use launchd (macOS) or the Clawdbot.app — they handle process supervision (auto-restart on crash).
@@ -329,7 +301,7 @@ Per-group activation can be changed by the owner:
- `/activation mention` — respond only when mentioned (default)
- `/activation always` — respond to all messages
See [Groups](/concepts/groups) for details.
See [Groups](https://docs.clawd.bot/groups) for details.
---
@@ -366,7 +338,7 @@ cat ~/.clawdbot/clawdbot.json | grep workspace
- **Telegram** — Via Bot API (grammY).
- **Discord** — Bot integration.
- **iMessage** — Via `imsg` CLI (macOS only).
- **Signal** — Via `signal-cli` (see [Signal](/providers/signal)).
- **Signal** — Via `signal-cli` (see [Signal](https://docs.clawd.bot/signal)).
- **WebChat** — Browser-based chat UI.
### Discord: Bot works in channels but not DMs?
@@ -516,10 +488,9 @@ Onboarding attempts to enable lingering; if it’s still off, run:
sudo loginctl enable-linger $USER
```
**macOS/WSL2**
**macOS/Windows**
Gateway daemons run in the user session by default. Keep the user logged in
(WSL2 services stop when the WSL VM shuts down).
Gateway daemons run in the user session by default. Keep the user logged in.
Headless/system services are not configured out of the box.
### Processes keep restarting after I kill them
@@ -533,6 +504,11 @@ The gateway runs under a supervisor that auto-restarts it. You need to stop the
- To inspect launchd state: `launchctl print gui/$UID | grep clawdbot`
**macOS (CLI launchd service, if installed)**
If you haven’t installed a supervisor yet, install one first:
```bash
clawdbot gateway install
# Or: clawdbot service install
```
```bash
clawdbot gateway stop
@@ -596,21 +572,19 @@ Quick reference (send these in chat):
|---------|--------|
| `/help` | Show available commands |
| `/status` | Health + session info |
| `/stop` | Abort the current run |
| `/new` or `/reset` | Reset the session |
| `/compact [notes]` | Compact session context |
| `/restart` | Restart Clawdbot |
| `/activation mention\|always` | Group activation (owner-only) |
| `/think <level>` | Set thinking level (off\|minimal\|low\|medium\|high) |
| `/verbose on\|off` | Toggle verbose mode |
| `/reasoning on\|off\|stream` | Toggle reasoning visibility (stream = Telegram draft only) |
| `/elevated on\|off` | Toggle elevated bash mode (approved senders only) |
| `/model <name>` | Switch AI model (see below) |
| `/queue <mode>` | Queue mode (see below) |
Slash commands are owner-only (gated by `whatsapp.allowFrom` and command authorization on other surfaces).
Commands are only recognized when the entire message is the command (slash required; no plain-text aliases).
Full list + config: [Slash commands](/tools/slash-commands)
Full list + config: https://docs.clawd.bot/slash-commands
### How do I switch models on the fly?
File renamed without changes.
+14 -9
View File
@@ -14,11 +14,11 @@ Last updated: 2025-12-09
## How to run (local)
```bash
clawdbot gateway --port 18789
pnpm clawdbot gateway --port 18789
# for full debug/trace logs in stdio:
clawdbot gateway --port 18789 --verbose
pnpm clawdbot gateway --port 18789 --verbose
# if the port is busy, terminate listeners then start:
clawdbot gateway --force
pnpm clawdbot gateway --force
# dev loop (auto-reload on TS changes):
pnpm gateway:watch
```
@@ -111,7 +111,7 @@ CLAWDBOT_CONFIG_PATH=~/.clawdbot/b.json CLAWDBOT_STATE_DIR=~/.clawdbot-b clawdbo
- `node.invoke` — invoke a command on a node (e.g. `canvas.*`, `camera.*`).
- `node.pair.*` — pairing lifecycle (`request`, `list`, `approve`, `reject`, `verify`).
See also: [`docs/presence.md`](/concepts/presence) for how presence is produced/deduped and why `instanceId` matters.
See also: [`docs/presence.md`](https://docs.clawd.bot/presence) for how presence is produced/deduped and why `instanceId` matters.
## Events
- `agent` — streamed tool/output events from the agent run (seq-tagged).
@@ -184,7 +184,7 @@ Enable lingering (required so the user service survives logout/idle):
```
sudo loginctl enable-linger youruser
```
Onboarding runs this on Linux/WSL2 (may prompt for sudo; writes `/var/lib/systemd/linger`).
Onboarding runs this on Linux (may prompt for sudo; writes `/var/lib/systemd/linger`).
Then enable the service:
```
systemctl --user enable --now clawdbot-gateway.service
@@ -199,9 +199,9 @@ sudo systemctl daemon-reload
sudo systemctl enable --now clawdbot-gateway.service
```
## Windows (WSL2)
Windows installs should use **WSL2** and follow the Linux systemd section above.
## Supervision (Windows scheduled task)
- Onboarding installs a Scheduled Task named `Clawdbot Gateway` (runs on user logon).
- Requires a logged-in user session; for headless setups use a system service or a task configured to run without a logged-in user (not shipped).
## Operational checks
- Liveness: open WS and send `req:connect` → expect `res` with `payload.type="hello-ok"` (with snapshot).
@@ -219,8 +219,13 @@ Windows installs should use **WSL2** and follow the Linux systemd section above.
- `clawdbot gateway send --to <num> --message "hi" [--media-url ...]` — send via Gateway (idempotent).
- `clawdbot gateway agent --message "hi" [--to ...]` — run an agent turn (waits for final by default).
- `clawdbot gateway call <method> --params '{"k":"v"}'` — raw method invoker for debugging.
- `clawdbot gateway stop|restart` — stop/restart the supervised gateway service (launchd/systemd).
- `clawdbot gateway install|uninstall` — install/remove the supervised gateway service (launchd/systemd/schtasks).
- `clawdbot gateway service-status` — report supervised gateway service status.
- `clawdbot gateway stop|restart` — stop/restart the supervised gateway service (launchd/systemd/schtasks).
- Gateway helper subcommands assume a running gateway on `--url`; they no longer auto-spawn one.
- Service aliases:
- `clawdbot service ...` — service controls (install/uninstall/status/stop/restart)
- `clawdbot daemon ...` — alias for `clawdbot service ...`
## Migration guidance
- Retire uses of `clawdbot gateway` and the legacy TCP control port.
@@ -17,29 +17,28 @@ Recommended path: use the **CLI onboarding wizard** (`clawdbot onboard`). It set
- workspace bootstrap + skills
- optional background daemon
If you want the deeper reference pages, jump to: [Wizard](/start/wizard), [Setup](/start/setup), [Pairing](/start/pairing), [Security](/gateway/security).
If you want the deeper reference pages, jump to: [Wizard](https://docs.clawd.bot/wizard), [Setup](https://docs.clawd.bot/setup), [Pairing](https://docs.clawd.bot/pairing), [Security](https://docs.clawd.bot/security).
## 0) Prereqs
- Node `>=22`
- `pnpm` (recommended) or `bun` (optional)
- `bun` (preferred) or `pnpm`
- Git
macOS: if you plan to build the apps, install Xcode / CLT. For the CLI + gateway only, Node is enough.
Windows: use **WSL2** (Ubuntu recommended). WSL2 is strongly recommended; native Windows is untested and more problematic. Install WSL2 first, then run the Linux steps inside WSL. See [Windows (WSL2)](/platforms/windows).
## 1) Check out from source
```bash
git clone https://github.com/clawdbot/clawdbot.git
cd clawdbot
pnpm install
bun install
```
Note: Bun is optional if you prefer running TypeScript directly:
Note: `pnpm` is also supported:
```bash
bun install
pnpm install
```
## 2) Build the Control UI (recommended)
@@ -47,9 +46,9 @@ bun install
The Gateway serves the browser dashboard (Control UI) when assets exist.
```bash
pnpm ui:install
pnpm ui:build
pnpm build
bun run ui:install
bun run ui:build
bun run build
```
If you skip UI build, the gateway still works — you just won’t get the dashboard.
@@ -57,17 +56,16 @@ If you skip UI build, the gateway still works — you just won’t get the dashb
## 3) Run the onboarding wizard
```bash
pnpm clawdbot onboard
bun run clawdbot onboard
```
What you’ll choose:
- **Local vs Remote** gateway
- **Auth**: Anthropic OAuth or OpenAI OAuth (recommended), API key (optional), or skip for now
- **Providers**: WhatsApp QR login, bot tokens, etc.
- **Daemon**: optional background install (launchd/systemd; WSL2 uses systemd)
- **Runtime**: Node (recommended; required for WhatsApp) or Bun (faster, but incompatible with WhatsApp)
- **Daemon**: optional background install (launchd/systemd/Task Scheduler)
Wizard doc: [Wizard](/start/wizard)
Wizard doc: https://docs.clawd.bot/wizard
### Auth: where it lives (important)
@@ -81,36 +79,28 @@ Headless/server tip: do OAuth on a normal machine first, then copy `oauth.json`
If the wizard didn’t start it for you:
```bash
# If you installed the CLI (npm/pnpm link --global):
clawdbot gateway --port 18789 --verbose
# From this repo:
node dist/entry.js gateway --port 18789 --verbose
bun run clawdbot gateway --port 18789 --verbose
```
Dashboard (local loopback): `http://127.0.0.1:18789/`
⚠️ **WhatsApp + Bun warning:** Baileys (WhatsApp Web library) uses a WebSocket
path that is currently incompatible with Bun and can cause memory corruption on
reconnect. If you use WhatsApp, run the Gateway with **Node** until this is
resolved. Baileys: https://github.com/WhiskeySockets/Baileys · Bun issue:
https://github.com/oven-sh/bun/issues/5951
## 5) Pair + connect your first chat surface
### WhatsApp (QR login)
```bash
pnpm clawdbot login
bun run clawdbot login
```
Scan via WhatsApp → Settings → Linked Devices.
WhatsApp doc: [WhatsApp](/providers/whatsapp)
WhatsApp doc: https://docs.clawd.bot/whatsapp
### Telegram / Discord / others
The wizard can write tokens/config for you. If you prefer manual config, start with:
- Telegram: [Telegram](/providers/telegram)
- Discord: [Discord](/providers/discord)
- Telegram: https://docs.clawd.bot/telegram
- Discord: https://docs.clawd.bot/discord
## 6) DM safety (pairing approvals)
@@ -119,25 +109,25 @@ Default posture: unknown DMs get a short code and messages are not processed unt
Approve:
```bash
pnpm clawdbot pairing list --provider telegram
pnpm clawdbot pairing approve --provider telegram <CODE>
bun run clawdbot pairing list --provider telegram
bun run clawdbot pairing approve --provider telegram <CODE>
```
Pairing doc: [Pairing](/start/pairing)
Pairing doc: https://docs.clawd.bot/pairing
## 7) Verify end-to-end
In a new terminal:
```bash
pnpm clawdbot health
pnpm clawdbot send --to +15555550123 --message "Hello from Clawdbot"
bun run clawdbot health
bun run clawdbot send --to +15555550123 --message "Hello from Clawdbot"
```
If `health` shows “no auth configured”, go back to the wizard and set OAuth/key auth — the agent won’t be able to respond without it.
## Next steps (optional, but great)
- macOS menu bar app + voice wake: [macOS app](/platforms/macos)
- iOS/Android nodes (Canvas/camera/voice): [Nodes](/nodes)
- Remote access (SSH tunnel / Tailscale Serve): [Remote access](/gateway/remote) and [Tailscale](/gateway/tailscale)
- macOS menu bar app + voice wake: https://docs.clawd.bot/macos
- iOS/Android nodes (Canvas/camera/voice): https://docs.clawd.bot/nodes
- Remote access (SSH tunnel / Tailscale Serve): https://docs.clawd.bot/remote and https://docs.clawd.bot/tailscale
@@ -11,12 +11,10 @@ Goal: Gmail watch -> Pub/Sub push -> `gog gmail watch serve` -> Clawdbot webhook
## Prereqs
- `gcloud` installed and logged in ([install guide](https://docs.cloud.google.com/sdk/docs/install-sdk)).
- `gog` (gogcli) installed and authorized for the Gmail account ([gogcli.sh](https://gogcli.sh/)).
- Clawdbot hooks enabled (see [`docs/webhook.md`](https://docs.clawd.bot/automation/webhook)).
- `tailscale` logged in ([tailscale.com](https://tailscale.com/)). Supported setup uses Tailscale Funnel for the public HTTPS endpoint.
Other tunnel services can work, but are DIY/unsupported and require manual wiring.
Right now, Tailscale is what we support.
- `gcloud` installed and logged in.
- `gog` (gogcli) installed and authorized for the Gmail account.
- Clawdbot hooks enabled (see [`docs/webhook.md`](https://docs.clawd.bot/webhook)).
- `tailscale` logged in if you want a public HTTPS endpoint via Funnel.
Example hook config (enable Gmail preset mapping):
@@ -32,7 +30,7 @@ Example hook config (enable Gmail preset mapping):
```
To customize payload handling, add `hooks.mappings` or a JS/TS transform module
under `hooks.transformsDir` (see [`docs/webhook.md`](https://docs.clawd.bot/automation/webhook)).
under `hooks.transformsDir` (see [`docs/webhook.md`](https://docs.clawd.bot/webhook)).
## Wizard (recommended)
@@ -137,10 +135,9 @@ Notes:
Recommended: `clawdbot hooks gmail run` wraps the same flow and auto-renews the watch.
## Expose the handler (dev, unsupported hack)
## Expose the handler (dev)
If you insist on a non-Tailscale tunnel, wire it manually and use the public URL in the push
subscription (unsupported, no guardrails):
For local testing, tunnel the handler and use the public URL in the push subscription:
```bash
cloudflared tunnel --url http://127.0.0.1:8788 --no-autoupdate
+1 -2
View File
@@ -5,7 +5,7 @@ read_when:
---
# grammY Integration (Telegram Bot API)
Updated: 2026-01-07
Updated: 2025-12-07
# Why grammY
- TS-first Bot API client with built-in long-poll + webhook helpers, middleware, error handling, rate limiter.
@@ -19,7 +19,6 @@ Updated: 2026-01-07
- **Webhook support:** `webhook-set.ts` wraps `setWebhook/deleteWebhook`; `webhook.ts` hosts the callback with health + graceful shutdown. Gateway enables webhook mode when `telegram.webhookUrl` is set (otherwise it long-polls).
- **Sessions:** direct chats collapse into the agent main session (`agent:<agentId>:<mainKey>`); groups use `agent:<agentId>:telegram:group:<chatId>`; replies route back to the same provider.
- **Config knobs:** `telegram.botToken`, `telegram.dmPolicy`, `telegram.groups` (allowlist + mention defaults), `telegram.allowFrom`, `telegram.groupAllowFrom`, `telegram.groupPolicy`, `telegram.mediaMaxMb`, `telegram.proxy`, `telegram.webhookSecret`, `telegram.webhookUrl`.
- **Draft streaming:** optional `telegram.streamMode` uses `sendMessageDraft` in private topic chats (Bot API 9.3+).
- **Tests:** grammy mocks cover DM + group mention gating and outbound send; more media/webhook fixtures still welcome.
Open questions
File renamed without changes.
+1 -3
View File
@@ -9,7 +9,6 @@ Clawdbot treats group chats consistently across surfaces: WhatsApp, Telegram, Di
## Session keys
- Group sessions use `agent:<agentId>:<provider>:group:<id>` session keys (rooms/channels use `agent:<agentId>:<provider>:channel:<id>`).
- Telegram forum topics add `:topic:<threadId>` to the group id so each topic has its own session.
- Direct chats use the main session (or per-sender if configured).
- Heartbeats are skipped for group sessions.
@@ -119,7 +118,6 @@ Group inbound payloads set:
- `GroupSubject` (if known)
- `GroupMembers` (if known)
- `WasMentioned` (mention gating result)
- Telegram forum topics also include `MessageThreadId` and `IsForum`.
The agent system prompt includes a group intro on the first turn of a new group session.
@@ -129,4 +127,4 @@ The agent system prompt includes a group intro on the first turn of a new group
- Group replies always go back to the same `chat_id`.
## WhatsApp specifics
See [`docs/group-messages.md`](/concepts/group-messages) for WhatsApp-only behavior (history injection, mention handling details).
See [`docs/group-messages.md`](https://docs.clawd.bot/group-messages) for WhatsApp-only behavior (history injection, mention handling details).
File renamed without changes.
@@ -8,28 +8,17 @@ read_when:
Heartbeat runs periodic agent turns in the **main session** so the model can
surface anything that needs attention without spamming the user.
## Defaults
- Interval: `30m` (set `agent.heartbeat.every` to change, `0m` disables).
- Prompt body (configurable via `agent.heartbeat.prompt`):
`Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`
- Heartbeat prompt text is sent **verbatim** as the user message. Clawdbot does
not append extra body text. The system prompt includes a Heartbeats section
and the run is flagged as a heartbeat internally.
## Prompt contract
- Heartbeat body defaults to: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.` (configurable via `agent.heartbeat.prompt`).
- If nothing needs attention, the model should reply `HEARTBEAT_OK`.
- During heartbeat runs, Clawdbot treats `HEARTBEAT_OK` as an ack when it appears at
the **start or end** of the reply. Clawdbot strips the token and discards the
reply if the remaining content is **≤ `ackMaxChars`** (default: 30).
- If `HEARTBEAT_OK` is in the **middle** of a reply, it is not treated specially.
- For alerts, do **not** include `HEARTBEAT_OK`; return only the alert text.
## Prompt overrides
- Overriding `agent.heartbeat.prompt` **replaces** the default body. Nothing is
merged for you.
- If you still want `HEARTBEAT.md` instructions, keep a line like
`Read HEARTBEAT.md if exists` in your custom prompt.
- `HEARTBEAT_OK` handling stays the same; changing the prompt won’t break acks.
- Heartbeat prompt text is sent **verbatim** as the user message. Clawdbot does
not append extra body text. The system prompt includes a Heartbeats section
and the run is flagged as a heartbeat internally.
### Stray `HEARTBEAT_OK` outside heartbeats
If the model accidentally includes `HEARTBEAT_OK` at the start or end of a
@@ -74,9 +63,9 @@ and final replies:
- `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery (default: 30).
## Cost awareness
Heartbeats run full agent turns. Shorter intervals burn more tokens. Be
intentional about `every`, keep `HEARTBEAT.md` tiny, and consider a cheaper
`model` or `target: "none"` if you only want internal state updates.
Heartbeats run full agent turns. Shorter intervals burn more tokens. If you
don’t need frequent checks, increase `every`, pick a cheaper `model`, or set
`target: "none"` to keep results internal.
## HEARTBEAT.md (optional)
If a `HEARTBEAT.md` file exists in the workspace, the default prompt tells the
@@ -96,8 +85,6 @@ bloat.
- Handle mundane tasks (triage inboxes, summarize queues, refresh notes).
- Nudge on open loops or reminders.
- Background monitoring (health checks, status polling, low-priority alerts).
- Scheduled routines (use [Cron jobs](/automation/cron-jobs) when you
need exact schedules or isolated runs).
## Wake hook
- The gateway exposes a heartbeat wake hook so cron/jobs/webhooks can request an
+150
View File
@@ -0,0 +1,150 @@
---
summary: "Hubs that link to every Clawdbot doc"
read_when:
- You want a complete map of the documentation
---
# Docs hubs
Use these hubs to discover every page, including deep dives and reference docs that don’t appear in the left nav.
## Start here
- [Index](https://docs.clawd.bot)
- [Getting Started](https://docs.clawd.bot/getting-started)
- [Onboarding](https://docs.clawd.bot/onboarding)
- [Wizard](https://docs.clawd.bot/wizard)
- [Setup](https://docs.clawd.bot/setup)
- [FAQ](https://docs.clawd.bot/faq)
- [Configuration](https://docs.clawd.bot/configuration)
- [Clawd (personal assistant)](https://docs.clawd.bot/clawd)
- [Lore](https://docs.clawd.bot/lore)
## Installation + distribution
- [Docker](https://docs.clawd.bot/docker)
- [Nix](https://docs.clawd.bot/nix)
## Core concepts
- [Architecture](https://docs.clawd.bot/architecture)
- [Agent runtime](https://docs.clawd.bot/agent)
- [Agent loop](https://docs.clawd.bot/agent-loop)
- [Multi-agent routing](https://docs.clawd.bot/multi-agent)
- [Sessions](https://docs.clawd.bot/session)
- [Sessions (alias)](https://docs.clawd.bot/sessions)
- [Session tools](https://docs.clawd.bot/session-tool)
- [Queue](https://docs.clawd.bot/queue)
- [Slash commands](https://docs.clawd.bot/slash-commands)
- [RPC adapters](https://docs.clawd.bot/rpc)
- [TypeBox schemas](https://docs.clawd.bot/typebox)
- [Presence](https://docs.clawd.bot/presence)
- [Discovery + transports](https://docs.clawd.bot/discovery)
- [Bonjour](https://docs.clawd.bot/bonjour)
- [Provider routing](https://docs.clawd.bot/provider-routing)
- [Groups](https://docs.clawd.bot/groups)
- [Group messages](https://docs.clawd.bot/group-messages)
## Providers + ingress
- [WhatsApp](https://docs.clawd.bot/whatsapp)
- [Telegram](https://docs.clawd.bot/telegram)
- [Telegram (grammY notes)](https://docs.clawd.bot/grammy)
- [Slack](https://docs.clawd.bot/slack)
- [Discord](https://docs.clawd.bot/discord)
- [Signal](https://docs.clawd.bot/signal)
- [iMessage](https://docs.clawd.bot/imessage)
- [WebChat](https://docs.clawd.bot/webchat)
- [Webhooks](https://docs.clawd.bot/webhook)
- [Gmail Pub/Sub](https://docs.clawd.bot/gmail-pubsub)
## Gateway + operations
- [Gateway runbook](https://docs.clawd.bot/gateway)
- [Gateway pairing](https://docs.clawd.bot/gateway/pairing)
- [Gateway lock](https://docs.clawd.bot/gateway-lock)
- [Background process](https://docs.clawd.bot/background-process)
- [Health](https://docs.clawd.bot/health)
- [Heartbeat](https://docs.clawd.bot/heartbeat)
- [Doctor](https://docs.clawd.bot/doctor)
- [Logging](https://docs.clawd.bot/logging)
- [Dashboard](https://docs.clawd.bot/dashboard)
- [Control UI](https://docs.clawd.bot/control-ui)
- [Remote access](https://docs.clawd.bot/remote)
- [Remote gateway README](https://docs.clawd.bot/remote-gateway-readme)
- [Tailscale](https://docs.clawd.bot/tailscale)
- [Security](https://docs.clawd.bot/security)
- [Troubleshooting](https://docs.clawd.bot/troubleshooting)
## Tools + automation
- [Tools surface](https://docs.clawd.bot/tools)
- [Bash tool](https://docs.clawd.bot/bash)
- [Elevated mode](https://docs.clawd.bot/elevated)
- [Cron + wakeups](https://docs.clawd.bot/cron)
- [Thinking + verbose](https://docs.clawd.bot/thinking)
- [Models](https://docs.clawd.bot/models)
- [Agent send CLI](https://docs.clawd.bot/agent-send)
- [Terminal UI](https://docs.clawd.bot/tui)
- [Browser control](https://docs.clawd.bot/browser)
- [Browser (Linux troubleshooting)](https://docs.clawd.bot/browser-linux-troubleshooting)
## Nodes, media, voice
- [Nodes overview](https://docs.clawd.bot/nodes)
- [Camera](https://docs.clawd.bot/camera)
- [Images](https://docs.clawd.bot/images)
- [Audio](https://docs.clawd.bot/audio)
- [Location command](https://docs.clawd.bot/location-command)
- [Voice wake](https://docs.clawd.bot/voicewake)
- [Talk mode](https://docs.clawd.bot/talk)
## Platforms
- [macOS app overview](https://docs.clawd.bot/macos)
- [macOS dev setup](https://docs.clawd.bot/mac/dev-setup)
- [macOS menu bar](https://docs.clawd.bot/mac/menu-bar)
- [macOS voice wake](https://docs.clawd.bot/mac/voicewake)
- [macOS voice overlay](https://docs.clawd.bot/mac/voice-overlay)
- [macOS WebChat](https://docs.clawd.bot/mac/webchat)
- [macOS Canvas](https://docs.clawd.bot/mac/canvas)
- [macOS child process](https://docs.clawd.bot/mac/child-process)
- [macOS health](https://docs.clawd.bot/mac/health)
- [macOS icon](https://docs.clawd.bot/mac/icon)
- [macOS logging](https://docs.clawd.bot/mac/logging)
- [macOS permissions](https://docs.clawd.bot/mac/permissions)
- [macOS remote](https://docs.clawd.bot/mac/remote)
- [macOS signing](https://docs.clawd.bot/mac/signing)
- [macOS release](https://docs.clawd.bot/mac/release)
- [macOS bun gateway](https://docs.clawd.bot/mac/bun)
- [macOS XPC](https://docs.clawd.bot/mac/xpc)
- [macOS skills](https://docs.clawd.bot/mac/skills)
- [macOS Peekaboo plan](https://docs.clawd.bot/mac/peekaboo)
- [iOS node](https://docs.clawd.bot/ios)
- [Android node](https://docs.clawd.bot/android)
- [Windows app](https://docs.clawd.bot/windows)
- [Linux app](https://docs.clawd.bot/linux)
- [Web surfaces](https://docs.clawd.bot/web)
## Workspace + templates
- [Skills](https://docs.clawd.bot/skills)
- [Skills config](https://docs.clawd.bot/skills-config)
- [Default AGENTS](https://docs.clawd.bot/AGENTS.default)
- [Templates: AGENTS](https://docs.clawd.bot/templates/AGENTS)
- [Templates: BOOTSTRAP](https://docs.clawd.bot/templates/BOOTSTRAP)
- [Templates: IDENTITY](https://docs.clawd.bot/templates/IDENTITY)
- [Templates: SOUL](https://docs.clawd.bot/templates/SOUL)
- [Templates: TOOLS](https://docs.clawd.bot/templates/TOOLS)
- [Templates: USER](https://docs.clawd.bot/templates/USER)
## Experiments + proposals
- [Onboarding config protocol](https://docs.clawd.bot/onboarding-config-protocol)
- [Research: memory](https://docs.clawd.bot/research/memory)
- [Proposal: model config](https://docs.clawd.bot/proposals/model-config)
## Testing + release
- [Testing](https://docs.clawd.bot/test)
- [Release checklist](https://docs.clawd.bot/RELEASING)
- [Device models](https://docs.clawd.bot/device-models)
File renamed without changes.
+94
View File
@@ -0,0 +1,94 @@
---
summary: "iMessage support via imsg (JSON-RPC over stdio), setup, and chat_id routing"
read_when:
- Setting up iMessage support
- Debugging iMessage send/receive
---
# iMessage (imsg)
Status: external CLI integration. No daemon.
## Model
- Clawdbot spawns `imsg rpc` as a child process.
- JSON-RPC runs over stdin/stdout (one JSON object per line).
- Gateway owns the process; no TCP port needed.
## Multi-account (Apple IDs)
iMessage “multi-account” in one Gateway process is not currently supported in a meaningful way:
- Messages accounts are owned by the signed-in macOS user session.
- `imsg` reads the local Messages DB and sends via that user’s configured services.
- There isn’t a robust “pick AppleID X as the sender” switch we can depend on.
### Practical approach: multiple gateways on multiple Macs/users
If you need two iMessage identities:
- Run one Gateway on each macOS user/machine that’s signed into the desired Apple ID.
- Connect to the desired Gateway remotely (Tailscale preferred; SSH tunnel is the universal fallback).
See:
- `docs/remote.md` (SSH tunnel to `127.0.0.1:18789`)
- `docs/discovery.md` (bridge vs SSH transport model)
### Could we do “iMessage over SSH” from a single Gateway?
Maybe, but it’s a new design:
- Outbound could theoretically pipe `imsg rpc` over SSH (stdio bridge).
- Inbound still needs a remote watcher (DB polling / event stream) and a transport back to the main Gateway.
That’s closer to “remote provider instances” (or “multi-gateway aggregation”) than a small config tweak.
## Requirements
- macOS with Messages signed in.
- Full Disk Access for Clawdbot + the `imsg` binary (Messages DB access).
- Automation permission for Messages when sending.
## Config
```json5
{
imessage: {
enabled: true,
cliPath: "imsg",
dbPath: "~/Library/Messages/chat.db",
dmPolicy: "pairing", // pairing | allowlist | open | disabled
allowFrom: ["+15555550123", "user@example.com", "chat_id:123"],
groupPolicy: "open",
groupAllowFrom: ["chat_id:123"],
includeAttachments: false,
mediaMaxMb: 16,
service: "auto",
region: "US"
}
}
```
Notes:
- `allowFrom` accepts handles (phone/email) or `chat_id:<id>` entries.
- Default: `imessage.dmPolicy="pairing"` — unknown DM senders get a pairing code (approve via `clawdbot pairing approve --provider imessage <code>`). `"open"` requires `allowFrom=["*"]`.
- `groupPolicy` controls group handling (`open|disabled|allowlist`).
- `groupAllowFrom` accepts the same entries as `allowFrom`.
- `service` defaults to `auto` (use `imessage` or `sms` to pin).
- `region` is only used for SMS targeting.
## Addressing / targets
Prefer `chat_id` for stable routing:
- `chat_id:123` (preferred)
- `chat_guid:...` (fallback)
- `chat_identifier:...` (fallback)
- direct handles: `imessage:+1555` / `sms:+1555` / `user@example.com`
List chats:
```
imsg chats --limit 20
```
## Group chat behavior
- Group messages set `ChatType=group`, `GroupSubject`, and `GroupMembers`.
- Group activation respects `imessage.groups."*".requireMention` and `routing.groupChat.mentionPatterns` (patterns are required to detect mentions on iMessage). When `imessage.groups` is set, it also acts as a group allowlist; include `"*"` to allow all groups.
- Replies go back to the same `chat_id` (group or direct).
## Troubleshooting
- `clawdbot gateway call providers.status --params '{"probe":true}'`
- Verify `imsg` is on PATH and has access to Messages DB.
+38 -50
View File
@@ -20,7 +20,7 @@ read_when:
<a href="https://github.com/clawdbot/clawdbot">GitHub</a> ·
<a href="https://github.com/clawdbot/clawdbot/releases">Releases</a> ·
<a href="https://docs.clawd.bot">Docs</a> ·
<a href="https://docs.clawd.bot/start/clawd">Clawd setup</a>
<a href="https://docs.clawd.bot/clawd">Clawd setup</a>
</p>
CLAWDBOT bridges WhatsApp (via WhatsApp Web / Baileys), Telegram (Bot API / grammY), Discord (Bot API / discord.js), and iMessage (imsg CLI) to coding agents like [Pi](https://github.com/badlogic/pi-mono).
@@ -28,18 +28,8 @@ It’s built for [Clawd](https://clawd.me), a space lobster who needed a TARDIS.
## Start here
- **New install from zero:** https://docs.clawd.bot/start/getting-started
- **Guided setup (recommended):** https://docs.clawd.bot/start/wizard (`clawdbot onboard`)
- **Open the dashboard (local Gateway):** http://127.0.0.1:18789/ (or http://localhost:18789/)
If the Gateway is running on the same computer, that link opens the browser Control UI
immediately. If it fails, start the Gateway first: `clawdbot gateway`.
## Dashboard (browser Control UI)
The dashboard is the browser Control UI for chat, config, nodes, sessions, and more.
Local default: http://127.0.0.1:18789/
Remote access: https://docs.clawd.bot/web and https://docs.clawd.bot/gateway/tailscale
- **New install from zero:** https://docs.clawd.bot/getting-started
- **Guided setup (recommended):** https://docs.clawd.bot/wizard (`clawdbot onboard`)
## How it works
@@ -70,8 +60,8 @@ Most operations flow through the **Gateway** (`clawdbot gateway`), a single long
- **Loopback-first**: Gateway WS defaults to `ws://127.0.0.1:18789`.
- For Tailnet access, run `clawdbot gateway --bind tailnet --token ...` (token is required for non-loopback binds).
- **Bridge for nodes**: optional LAN/tailnet-facing bridge on `tcp://0.0.0.0:18790` for paired nodes (Bonjour-discoverable).
- **Canvas host**: HTTP file server on `canvasHost.port` (default `18793`), serving `/__clawdbot__/canvas/` for node WebViews; see [`docs/configuration.md`](https://docs.clawd.bot/gateway/configuration) (`canvasHost`).
- **Remote use**: SSH tunnel or tailnet/VPN; see [`docs/remote.md`](https://docs.clawd.bot/gateway/remote) and [`docs/discovery.md`](https://docs.clawd.bot/gateway/discovery).
- **Canvas host**: HTTP file server on `canvasHost.port` (default `18793`), serving `/__clawdbot__/canvas/` for node WebViews; see [`docs/configuration.md`](https://docs.clawd.bot/configuration) (`canvasHost`).
- **Remote use**: SSH tunnel or tailnet/VPN; see [`docs/remote.md`](https://docs.clawd.bot/remote) and [`docs/discovery.md`](https://docs.clawd.bot/discovery).
## Features (high level)
@@ -145,47 +135,45 @@ Example:
## Docs
- Start here:
- [Docs hubs (all pages linked)](https://docs.clawd.bot/start/hubs)
- [FAQ](https://docs.clawd.bot/start/faq) ← *common questions answered*
- [Configuration](https://docs.clawd.bot/gateway/configuration)
- [Slash commands](https://docs.clawd.bot/tools/slash-commands)
- [Multi-agent routing](https://docs.clawd.bot/concepts/multi-agent)
- [Updating / rollback](https://docs.clawd.bot/install/updating)
- [Pairing (DM + nodes)](https://docs.clawd.bot/start/pairing)
- [Nix mode](https://docs.clawd.bot/install/nix)
- [Clawd personal assistant setup](https://docs.clawd.bot/start/clawd)
- [Skills](https://docs.clawd.bot/tools/skills)
- [Skills config](https://docs.clawd.bot/tools/skills-config)
- [Workspace templates](https://docs.clawd.bot/reference/templates/AGENTS)
- [RPC adapters](https://docs.clawd.bot/reference/rpc)
- [Docs hubs (all pages linked)](https://docs.clawd.bot/hubs)
- [FAQ](https://docs.clawd.bot/faq) ← *common questions answered*
- [Configuration](https://docs.clawd.bot/configuration)
- [Slash commands](https://docs.clawd.bot/slash-commands)
- [Multi-agent routing](https://docs.clawd.bot/multi-agent)
- [Updating / rollback](https://docs.clawd.bot/updating)
- [Pairing (DM + nodes)](https://docs.clawd.bot/pairing)
- [Nix mode](https://docs.clawd.bot/nix)
- [Clawd personal assistant setup](https://docs.clawd.bot/clawd)
- [Skills](https://docs.clawd.bot/skills)
- [Skills config](https://docs.clawd.bot/skills-config)
- [Workspace templates](https://docs.clawd.bot/templates/AGENTS)
- [RPC adapters](https://docs.clawd.bot/rpc)
- [Gateway runbook](https://docs.clawd.bot/gateway)
- [Nodes (iOS/Android)](https://docs.clawd.bot/nodes)
- [Web surfaces (Control UI)](https://docs.clawd.bot/web)
- [Discovery + transports](https://docs.clawd.bot/gateway/discovery)
- [Remote access](https://docs.clawd.bot/gateway/remote)
- [Discovery + transports](https://docs.clawd.bot/discovery)
- [Remote access](https://docs.clawd.bot/remote)
- Providers and UX:
- [WebChat](https://docs.clawd.bot/web/webchat)
- [Control UI (browser)](https://docs.clawd.bot/web/control-ui)
- [Telegram](https://docs.clawd.bot/providers/telegram)
- [Discord](https://docs.clawd.bot/providers/discord)
- [iMessage](https://docs.clawd.bot/providers/imessage)
- [Groups](https://docs.clawd.bot/concepts/groups)
- [WhatsApp group messages](https://docs.clawd.bot/concepts/group-messages)
- [Media: images](https://docs.clawd.bot/nodes/images)
- [Media: audio](https://docs.clawd.bot/nodes/audio)
- [WebChat](https://docs.clawd.bot/webchat)
- [Control UI (browser)](https://docs.clawd.bot/control-ui)
- [Telegram](https://docs.clawd.bot/telegram)
- [Discord](https://docs.clawd.bot/discord)
- [iMessage](https://docs.clawd.bot/imessage)
- [Groups](https://docs.clawd.bot/groups)
- [WhatsApp group messages](https://docs.clawd.bot/group-messages)
- [Media: images](https://docs.clawd.bot/images)
- [Media: audio](https://docs.clawd.bot/audio)
- Companion apps:
- [macOS app](https://docs.clawd.bot/platforms/macos)
- [iOS app](https://docs.clawd.bot/platforms/ios)
- [Android app](https://docs.clawd.bot/platforms/android)
- [Windows (WSL2)](https://docs.clawd.bot/platforms/windows)
- [Linux app](https://docs.clawd.bot/platforms/linux)
- [macOS app](https://docs.clawd.bot/macos)
- [iOS app](https://docs.clawd.bot/ios)
- [Android app](https://docs.clawd.bot/android)
- [Windows app](https://docs.clawd.bot/windows)
- [Linux app](https://docs.clawd.bot/linux)
- Ops and safety:
- [Sessions](https://docs.clawd.bot/concepts/session)
- [Cron jobs](https://docs.clawd.bot/automation/cron-jobs)
- [Webhooks](https://docs.clawd.bot/automation/webhook)
- [Gmail hooks (Pub/Sub)](https://docs.clawd.bot/automation/gmail-pubsub)
- [Security](https://docs.clawd.bot/gateway/security)
- [Troubleshooting](https://docs.clawd.bot/gateway/troubleshooting)
- [Sessions](https://docs.clawd.bot/session)
- [Cron + wakeups](https://docs.clawd.bot/cron)
- [Security](https://docs.clawd.bot/security)
- [Troubleshooting](https://docs.clawd.bot/troubleshooting)
## The name
+12 -12
View File
@@ -34,7 +34,7 @@ The Gateway WebSocket stays loopback-only (`ws://127.0.0.1:18789`). The iOS node
Bridge is enabled by default (disable via `CLAWDBOT_BRIDGE_ENABLED=0`).
```bash
clawdbot gateway --port 18789 --verbose
pnpm clawdbot gateway --port 18789 --verbose
```
Confirm in logs you see something like:
@@ -61,7 +61,7 @@ If browse works, but the iOS node can’t connect, try resolving one instance:
dns-sd -L "<instance name>" _clawdbot-bridge._tcp local.
```
More debugging notes: [`docs/bonjour.md`](/gateway/bonjour).
More debugging notes: [`docs/bonjour.md`](https://docs.clawd.bot/bonjour).
#### Tailnet (Vienna ⇄ London) discovery via unicast DNS-SD
@@ -70,7 +70,7 @@ If the iOS node and the gateway are on different networks but connected via Tail
1) Set up a DNS-SD zone (example `clawdbot.internal.`) on the gateway host and publish `_clawdbot-bridge._tcp` records.
2) Configure Tailscale split DNS for `clawdbot.internal` pointing at that DNS server.
Details and example CoreDNS config: [`docs/bonjour.md`](/gateway/bonjour).
Details and example CoreDNS config: [`docs/bonjour.md`](https://docs.clawd.bot/bonjour).
### 3) Connect from the iOS node app
@@ -102,7 +102,7 @@ clawdbot nodes approve <requestId>
After approval, the iOS node receives/stores the token and reconnects authenticated.
Pairing details: [`docs/gateway/pairing.md`](/gateway/pairing).
Pairing details: [`docs/gateway/pairing.md`](https://docs.clawd.bot/gateway/pairing).
### 5) Verify the node is connected
@@ -169,7 +169,7 @@ The response includes `{ format, base64 }` image data (default `format="jpeg"`;
- **iOS in background:** all `canvas.*` commands fail fast with `NODE_BACKGROUND_UNAVAILABLE` (bring the iOS node app to foreground).
- **Return to default scaffold:** `canvas.navigate` with `{"url":""}` or `{"url":"/"}` returns to the built-in scaffold page.
- **mDNS blocked:** some networks block multicast; use a different LAN or plan a tailnet-capable bridge (see [`docs/discovery.md`](/gateway/discovery)).
- **mDNS blocked:** some networks block multicast; use a different LAN or plan a tailnet-capable bridge (see [`docs/discovery.md`](https://docs.clawd.bot/discovery)).
- **Wrong node selector:** `--node` can be the node id (UUID), display name (e.g. `iOS Node`), IP, or an unambiguous prefix. If it’s ambiguous, the CLI will tell you.
- **Stale pairing / Keychain cleared:** if the pairing token is missing (or iOS Keychain was wiped), the node must pair again; approve a new pending request.
- **App reinstall but no reconnect:** the node restores `instanceId` + last bridge preference from Keychain; if it still comes up “unpaired”, verify Keychain persistence on your device/simulator and re-pair once.
@@ -195,8 +195,8 @@ Non-goals (v1):
### Current repo reality (constraints we respect)
- The Gateway WebSocket server binds to `127.0.0.1:18789` ([`src/gateway/server.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/gateway/server.ts)) with an optional `CLAWDBOT_GATEWAY_TOKEN`.
- The Gateway exposes a Canvas file server (`canvasHost`) on `canvasHost.port` (default `18793`), so nodes can `canvas.navigate` to `http://<lanHost>:18793/__clawdbot__/canvas/` and auto-reload on file changes ([`docs/configuration.md`](/gateway/configuration)).
- macOS “Canvas” is controlled via the Gateway node protocol (`canvas.*`), matching iOS/Android ([`docs/mac/canvas.md`](/platforms/mac/canvas)).
- The Gateway exposes a Canvas file server (`canvasHost`) on `canvasHost.port` (default `18793`), so nodes can `canvas.navigate` to `http://<lanHost>:18793/__clawdbot__/canvas/` and auto-reload on file changes ([`docs/configuration.md`](https://docs.clawd.bot/configuration)).
- macOS “Canvas” is controlled via the Gateway node protocol (`canvas.*`), matching iOS/Android ([`docs/mac/canvas.md`](https://docs.clawd.bot/mac/canvas)).
- Voice wake forwards via `GatewayChannel` to Gateway `agent` (mac app: `VoiceWakeForwarder` → `GatewayConnection.sendAgent`).
### Recommended topology (B): Gateway-owned Bridge + loopback Gateway
@@ -237,7 +237,7 @@ Desired behavior:
- If the Swift UI is present: show alert with Approve/Reject/Later.
- If the Swift UI is not present: `clawdbot` CLI can list pending requests and approve/reject.
See [`docs/gateway/pairing.md`](/gateway/pairing) for the API/events and storage.
See [`docs/gateway/pairing.md`](https://docs.clawd.bot/gateway/pairing) for the API/events and storage.
CLI (headless approvals):
- `clawdbot nodes pending`
@@ -366,7 +366,7 @@ open Clawdbot.xcodeproj
## Related docs
- [`docs/gateway.md`](/gateway) (gateway runbook)
- [`docs/gateway/pairing.md`](/gateway/pairing) (approval + storage)
- [`docs/bonjour.md`](/gateway/bonjour) (discovery debugging)
- [`docs/discovery.md`](/gateway/discovery) (LAN vs tailnet vs SSH)
- [`docs/gateway.md`](https://docs.clawd.bot/gateway) (gateway runbook)
- [`docs/gateway/pairing.md`](https://docs.clawd.bot/gateway/pairing) (approval + storage)
- [`docs/bonjour.md`](https://docs.clawd.bot/bonjour) (discovery debugging)
- [`docs/discovery.md`](https://docs.clawd.bot/discovery) (LAN vs tailnet vs SSH)
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
View File
File renamed without changes.
File renamed without changes.
@@ -10,7 +10,7 @@ read_when:
Status: draft spec · Date: 2025-12-12
Note: for iOS/Android nodes that should render agent-edited HTML/CSS/JS over the network, prefer the Gateway `canvasHost` (serves `~/clawd/canvas` over LAN/tailnet with live reload). A2UI is also **hosted by the Gateway** over HTTP. This doc focuses on the macOS in-app canvas panel. See [`docs/configuration.md`](/gateway/configuration).
Note: for iOS/Android nodes that should render agent-edited HTML/CSS/JS over the network, prefer the Gateway `canvasHost` (serves `~/clawd/canvas` over LAN/tailnet with live reload). A2UI is also **hosted by the Gateway** over HTTP. This doc focuses on the macOS in-app canvas panel. See [`docs/configuration.md`](https://docs.clawd.bot/configuration).
Clawdbot can embed an agent-controlled “visual workspace” panel (“Canvas”) inside the macOS app using `WKWebView`, served via a **custom URL scheme** (no loopback HTTP port required).
@@ -81,7 +81,7 @@ Canvas is exposed via the Gateway **node bridge**, so the agent can:
This should be modeled after `WebChatManager`/`WebChatSwiftUIWindowController` but targeting `clawdbot-canvas://…` URLs.
Related:
- For “invoke the agent again from UI” flows, prefer the macOS deep link scheme (`clawdbot://agent?...`) so *any* UI surface (Canvas, WebChat, native views) can trigger a new agent run. See [`docs/macos.md`](/platforms/macos).
- For “invoke the agent again from UI” flows, prefer the macOS deep link scheme (`clawdbot://agent?...`) so *any* UI surface (Canvas, WebChat, native views) can trigger a new agent run. See [`docs/macos.md`](https://docs.clawd.bot/macos).
## Agent commands (current)
File renamed without changes.
File renamed without changes.
@@ -25,4 +25,4 @@ How to see whether the WhatsApp Web/Baileys bridge is healthy from the menu bar
- Cache the last good snapshot and the last error separately to avoid flicker; show the timestamp of each.
## When in doubt
- You can still use the CLI flow in [`docs/health.md`](/gateway/health) (`clawdbot status`, `clawdbot status --deep`, `clawdbot health --json`) and tail `/tmp/clawdbot/clawdbot-*.log` for `web-heartbeat` / `web-reconnect`.
- You can still use the CLI flow in [`docs/health.md`](https://docs.clawd.bot/health) (`clawdbot status`, `clawdbot status --deep`, `clawdbot health --json`) and tail `/tmp/clawdbot/clawdbot-*.log` for `web-heartbeat` / `web-reconnect`.
File renamed without changes.
File renamed without changes.
@@ -9,7 +9,6 @@ read_when:
- We surface the current agent work state in the menu bar icon and in the first status row of the menu.
- Health status is hidden while work is active; it returns when all sessions are idle.
- The “Nodes” block in the menu lists **devices** only (gateway bridge nodes via `node.list`), not client/presence entries.
- A “Usage” section appears under Context when provider usage snapshots are available.
## State model
- Sessions: events arrive with `runId` (per-run) plus `sessionKey` in the payload. The “main” session is the key `main`; if absent, we fall back to the most recently updated session.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -9,7 +9,7 @@ This app is usually built from [`scripts/package-mac-app.sh`](https://github.com
- sets a stable debug bundle identifier: `com.clawdbot.mac.debug`
- writes the Info.plist with that bundle id (override via `BUNDLE_ID=...`)
- calls [`scripts/codesign-mac-app.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/codesign-mac-app.sh) to sign the main binary, bundled CLI, and app bundle so macOS treats each rebuild as the same signed bundle and keeps TCC permissions (notifications, accessibility, screen recording, mic, speech). For stable permissions, use a real signing identity; ad-hoc is opt-in and fragile (see [`docs/mac/permissions.md`](/platforms/mac/permissions)).
- calls [`scripts/codesign-mac-app.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/codesign-mac-app.sh) to sign the main binary, bundled CLI, and app bundle so macOS treats each rebuild as the same signed bundle and keeps TCC permissions (notifications, accessibility, screen recording, mic, speech). For stable permissions, use a real signing identity; ad-hoc is opt-in and fragile (see [`docs/mac/permissions.md`](https://docs.clawd.bot/mac/permissions)).
- uses `CODESIGN_TIMESTAMP=auto` by default; it enables trusted timestamps for Developer ID signatures. Set `CODESIGN_TIMESTAMP=off` to skip timestamping (offline debug builds).
- inject build metadata into Info.plist: `ClawdbotBuildTimestamp` (UTC) and `ClawdbotGitCommit` (short hash) so the About pane can show build, git, and debug/release channel.
- **Packaging requires Bun**: The embedded gateway relay is compiled using `bun`. Ensure it is installed (`curl -fsSL https://bun.sh/install | bash`).
@@ -26,7 +26,7 @@ SIGN_IDENTITY="-" scripts/package-mac-app.sh # explicit ad-hoc (same cave
```
### Ad-hoc Signing Note
When signing with `SIGN_IDENTITY="-"` (ad-hoc), the script automatically disables the **Hardened Runtime** (`--options runtime`). This is necessary to prevent crashes when the app attempts to load embedded frameworks (like Sparkle) that do not share the same Team ID. Ad-hoc signatures also break TCC permission persistence; see [`docs/mac/permissions.md`](/platforms/mac/permissions) for recovery steps.
When signing with `SIGN_IDENTITY="-"` (ad-hoc), the script automatically disables the **Hardened Runtime** (`--options runtime`). This is necessary to prevent crashes when the app attempts to load embedded frameworks (like Sparkle) that do not share the same Team ID. Ad-hoc signatures also break TCC permission persistence; see [`docs/mac/permissions.md`](https://docs.clawd.bot/mac/permissions) for recovery steps.
## Build metadata for About
File renamed without changes.
@@ -3,7 +3,7 @@ summary: "Voice overlay lifecycle when wake-word and push-to-talk overlap"
read_when:
- Adjusting voice overlay behavior
---
# Voice Overlay Lifecycle (macOS)
## Voice Overlay Lifecycle (macOS)
Audience: macOS app contributors. Goal: keep the voice overlay predictable when wake-word and push-to-talk overlap.
File renamed without changes.
File renamed without changes.
@@ -21,7 +21,7 @@ read_when:
- UI automation uses a separate UNIX socket named `bridge.sock` and the PeekabooBridge JSON protocol.
- Host preference order (client-side): Peekaboo.app → Claude.app → Clawdbot.app → local execution.
- Security: bridge hosts require TeamID `Y5PE65HELJ`; DEBUG-only same-UID escape hatch is guarded by `PEEKABOO_ALLOW_UNSIGNED_SOCKET_CLIENTS=1` (Peekaboo convention).
- See: [`docs/mac/peekaboo.md`](/platforms/mac/peekaboo) for the Clawdbot plan and naming.
- See: [`docs/mac/peekaboo.md`](https://docs.clawd.bot/mac/peekaboo) for the Clawdbot plan and naming.
### Mach/XPC (future direction)
- Still optional for internal app services, but **not required** for automation now that node.invoke is the surface.
+1 -1
View File
@@ -13,7 +13,7 @@ Author: steipete · Status: draft spec · Date: 2025-12-20
- Shows native notifications for Clawdbot/clawdbot events.
- Owns TCC prompts (Notifications, Accessibility, Screen Recording, Automation/AppleScript, Microphone, Speech Recognition).
- Runs (or connects to) the **Gateway** and exposes itself as a **node** so agents can reach macOS‑only features.
- Hosts **PeekabooBridge** for UI automation (consumed by `peekaboo`; see [`docs/mac/peekaboo.md`](/platforms/mac/peekaboo)).
- Hosts **PeekabooBridge** for UI automation (consumed by `peekaboo`; see [`docs/mac/peekaboo.md`](https://docs.clawd.bot/mac/peekaboo)).
- Installs a single CLI (`clawdbot`) by symlinking the bundled binary.
## High-level design
@@ -85,9 +85,9 @@ timeouts that exhausted profile rotation.
## Related config
See [`docs/configuration.md`](/gateway/configuration) for:
See [`docs/configuration.md`](https://docs.clawd.bot/configuration) for:
- `auth.profiles` / `auth.order`
- `agent.model.primary` / `agent.model.fallbacks`
- `agent.imageModel` routing
See [`docs/models.md`](/concepts/models) for the broader model selection and fallback overview.
See [`docs/models.md`](https://docs.clawd.bot/models) for the broader model selection and fallback overview.
+93
View File
@@ -0,0 +1,93 @@
---
summary: "Plan for models CLI: scan, list, aliases, fallbacks, status"
read_when:
- Adding or modifying models CLI (models list/set/scan/aliases/fallbacks)
- Changing model fallback behavior or selection UX
- Updating model scan probes (tools/images)
---
# Models CLI plan
See [`docs/model-failover.md`](https://docs.clawd.bot/model-failover) for how auth profiles rotate (OAuth vs API keys), cooldowns, and how that interacts with model fallbacks.
Goal: give clear model visibility + control (configured vs available), plus scan tooling
that prefers tool-call + image-capable models and maintains ordered fallbacks.
## Command tree (draft)
- `clawdbot models list`
- default: configured models only
- flags: `--all` (full catalog), `--local`, `--provider <name>`, `--json`, `--plain`
- `clawdbot models status`
- show default model + aliases + fallbacks + configured models
- `clawdbot models set <modelOrAlias>`
- writes `agent.model.primary` and ensures `agent.models` entry
- `clawdbot models set-image <modelOrAlias>`
- writes `agent.imageModel.primary` and ensures `agent.models` entry
- `clawdbot models aliases list|add|remove`
- writes `agent.models.*.alias`
- `clawdbot models fallbacks list|add|remove|clear`
- writes `agent.model.fallbacks`
- `clawdbot models image-fallbacks list|add|remove|clear`
- writes `agent.imageModel.fallbacks`
- `clawdbot models scan`
- OpenRouter :free scan; probe tool-call + image; interactive selection
## Config changes
- `agent.models` (configured model catalog + aliases).
- `agent.model.primary` + `agent.model.fallbacks`.
- `agent.imageModel.primary` + `agent.imageModel.fallbacks` (optional).
- `auth.profiles` + `auth.order` for per-provider auth failover.
## Scan behavior (models scan)
Input
- OpenRouter `/models` list (filter `:free`)
- Requires OpenRouter API key from auth profiles or `OPENROUTER_API_KEY`
- Optional filters: `--max-age-days`, `--min-params`, `--provider`, `--max-candidates`
- Probe controls: `--timeout`, `--concurrency`
Probes (direct pi-ai complete)
- Tool-call probe (required):
- Provide a dummy tool, verify tool call emitted.
- Image probe (preferred):
- Prompt includes 1x1 PNG; success if no "unsupported image" error.
Scoring/selection
- Prefer models passing tool + image for text/tool fallbacks.
- Prefer image-only models for image tool fallback (even if tool probe fails).
- Rank by: image ok, then lower tool latency, then larger context, then params.
Interactive selection (TTY)
- Multiselect list with per-model stats:
- model id, tool ok, image ok, median latency, context, inferred params.
- Pre-select top N (default 6).
- Non-TTY: auto-select; require `--yes`/`--no-input` to apply.
Output
- Writes `agent.model.fallbacks` ordered.
- Writes `agent.imageModel.fallbacks` ordered (image-capable models).
- Ensures `agent.models` entries exist for selected models.
- Optional `--set-default` to set `agent.model.primary`.
- Optional `--set-image` to set `agent.imageModel.primary`.
## Runtime fallback
- On model failure: try `agent.model.fallbacks` in order.
- Per-provider auth failover uses `auth.order` (or stored profile order) **before**
moving to the next model.
- Image routing uses `agent.imageModel` **only when configured** and the primary
model lacks image input.
- Persist last successful provider/model to session entry; auth profile success is global.
- See [`docs/model-failover.md`](https://docs.clawd.bot/model-failover) for auth profile rotation, cooldowns, and timeout handling.
## Tests
- Unit: scan selection ordering + probe classification.
- CLI: list/aliases/fallbacks add/remove + scan writes config.
- Status: shows last used model + fallbacks.
## Docs
- Update [`docs/configuration.md`](https://docs.clawd.bot/configuration) with `agent.models` + `agent.model` + `agent.imageModel`.
- Keep this doc current when CLI surface or scan logic changes.
@@ -28,16 +28,6 @@ If you do nothing, Clawdbot runs a single agent:
- Workspace defaults to `~/clawd` (or `~/clawd-<profile>` when `CLAWDBOT_PROFILE` is set).
- State defaults to `~/.clawdbot/agents/main/agent`.
## Agent helper
Use the agent wizard to add a new isolated agent:
```bash
clawdbot agents add work
```
Then add `routing.bindings` (or let the wizard do it) to route inbound messages.
## Multiple agents = multiple people, multiple personalities
With **multiple agents**, each `agentId` becomes a **fully isolated persona**:
@@ -83,12 +73,10 @@ multiple phone numbers without mixing sessions.
agents: {
home: {
name: "Home",
workspace: "~/clawd-home",
agentDir: "~/.clawdbot/agents/home/agent",
},
work: {
name: "Work",
workspace: "~/clawd-work",
agentDir: "~/.clawdbot/agents/work/agent",
},
+2 -2
View File
@@ -91,5 +91,5 @@ packaging and Nix builds (which do not rely on a full Xcode toolchain).
## Related
- [nix-clawdbot](https://github.com/clawdbot/nix-clawdbot) — full setup guide
- [Wizard](/start/wizard) — non-Nix CLI setup
- [Docker](/install/docker) — containerized setup
- [Wizard](https://docs.clawd.bot/wizard) — non-Nix CLI setup
- [Docker](https://docs.clawd.bot/docker) — containerized setup
+1 -1
View File
@@ -14,7 +14,7 @@ macOS can also run in **node mode**: the menubar app connects to the Gateway’s
## Pairing + status
Pairing is gateway-owned and approval-based. See [`docs/gateway/pairing.md`](/gateway/pairing) for the full flow.
Pairing is gateway-owned and approval-based. See [`docs/gateway/pairing.md`](https://docs.clawd.bot/gateway/pairing) for the full flow.
Quick CLI:
Loaded 100 of 353 files, more files were not shown because too many files have changed in this diff. Show more