Compare commits

...
Author SHA1 Message Date
Peter Steinberger 47e141f03d chore: fix lint in gateway cli test 2026-01-08 04:00:49 +01:00
Peter Steinberger 69d8e46b84 fix: harden skill install timeouts 2026-01-08 03:54:06 +01:00
Peter Steinberger f1a09febbf docs: add /landpr prompt 2026-01-08 00:07:42 +01:00
Peter Steinberger a1e7d321bc feat: add gateway service commands 2026-01-07 00:04:20 +01:00
Peter Steinberger fec7f37271 merge upstream/main 2026-01-06 23:09:01 +01:00
Peter Steinberger 86b56b2308 fix: harden gemini session reset 2026-01-06 23:06:01 +01:00
Peter Steinberger 7aa7fa79d0 feat: update heartbeat defaults 2026-01-06 21:54:42 +00:00
Peter Steinberger dba09058f5 fix(agents): default agent dir to multi-agent path 2026-01-06 21:54:42 +00:00
Peter Steinberger 7360abad8e docs: update multi-agent guide 2026-01-06 22:44:07 +01:00
Peter Steinberger 22f835a5f2 Merge branch 'integrate/pr-331' 2026-01-06 22:43:51 +01:00
Peter Steinberger 96164b5955 fix: improve socket error handling 2026-01-06 22:43:29 +01:00
Peter Steinberger 4198fcd7db docs: correct paths and setup guidance 2026-01-06 21:29:45 +00:00
Peter Steinberger a1f5cfcd08 docs: refresh clawtributors 2026-01-06 22:26:42 +01:00
Emanuel Stadler fb17a32283 feat: enhance error handling for socket connection errors
- Added `isError` property to `EmbeddedPiRunResult` and reply items to indicate error states.
- Updated error handling in `runReplyAgent` to provide more informative messages for specific socket connection errors.
2026-01-06 22:19:37 +01:00
Peter Steinberger de454fc385 docs: add macOS build toolchain troubleshooting 2026-01-06 22:18:08 +01:00
Peter Steinberger 6f4cd7485f docs: update FAQ auth paths + add clawtributor 2026-01-06 20:56:23 +00:00
Peter Steinberger ff5d6d15ba Merge pull request #324 from jamesgroat/fix/browser-profile-flag
fix(browser): add profile support to CLI flag and browser-tool, tabs routes
2026-01-06 20:55:35 +00:00
Peter Steinberger e99536d3d9 docs: add changelog entry for browser profile fix 2026-01-06 21:54:46 +01:00
Peter Steinberger b0bd7b946e fix(macos): fix swiftformat lint in A2UI handler 2026-01-06 21:54:46 +01:00
James Groat 9b6e2478f5 fix(browser): add profile param to tabs routes and browser-tool
- tabs.ts now uses getProfileContext like other routes
- browser-tool threads profile param through all actions
- add tests for profile query param on /tabs endpoints
- update docs with browser tool profile parameter
2026-01-06 21:54:46 +01:00
James Groat 40758b16a9 fix(browser-cli): rename --profile to --browser-profile to avoid conflict with global --profile flag 2026-01-06 21:54:46 +01:00
minghinmatthewlam 2dd6b3aeb2 fix: write auth profiles to multi-agent path during onboarding
- Onboarding now writes auth profiles under ~/.clawdbot/agents/main/agent so the gateway sees credentials on first start.
- Hardened onboarding test to ignore legacy env vars.

Thanks @minghinmatthewlam!
2026-01-06 20:53:18 +00:00
Peter Steinberger c7ffa28980 docs: update provider command refs 2026-01-06 20:45:54 +00:00
Peter Steinberger 39487998a3 docs: add slash commands guide 2026-01-06 20:45:54 +00:00
Peter Steinberger 84c8209158 fix(slack): clear assistant thread status after replies 2026-01-06 21:41:30 +01:00
Shadow 8ebc789d25 Slack: send assistant thread status while typing 2026-01-06 21:34:52 +01:00
Peter Steinberger 792ae99ffc docs: enforce PR merge expectations 2026-01-06 21:31:50 +01:00
Peter Steinberger 4845c615cb docs: link source references to GitHub 2026-01-06 20:25:08 +00:00
ShadowandPeter Steinberger 9b22e1f6e9 feat(commands): unify chat commands (#275)
* Chat commands: registry, access groups, Carbon

* Chat commands: clear native commands on disable

* fix(commands): align command surface typing

* docs(changelog): note commands registry (PR #275)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-01-06 20:17:56 +00:00
Peter Steinberger 1bf44bf30c feat(models): show auth overview 2026-01-06 20:07:04 +00:00
Peter Steinberger ea7836afad docs: add Start Here and getting started 2026-01-06 19:56:22 +00:00
Peter Steinberger 118c1e1042 fix: keep oauth profile stable 2026-01-06 19:43:28 +00:00
Peter Steinberger 1bc3461800 docs: refine PR merge strategy 2026-01-06 20:31:43 +01:00
Peter Steinberger 4cf02cc705 docs: prefer rebase + run gate before commits 2026-01-06 19:27:09 +00:00
Peter Steinberger 1a5c515ca8 docs: formalize PR review/landing workflow 2026-01-06 19:27:09 +00:00
Peter Steinberger fab37be7a0 fix(browser): sync lockfile for Playwright Bun patch (PR #307) 2026-01-06 19:27:09 +00:00
Azade 16ce78233f fix(browser): patch playwright-core for Bun WebSocket compatibility
Bun's WebSocket implementation doesn't fully support Playwright's CDP
connection because Playwright bundles its own 'ws' module. This causes
connectOverCDP to timeout.

The patch makes Playwright use the native 'ws' module when running
under Bun, which works with Bun's WebSocket shim.

Fixes browser snapshot/act timeouts after PR #278 (tsx → bun migration).

Ref: https://github.com/oven-sh/bun/issues/9911
2026-01-06 19:27:09 +00:00
Peter Steinberger b4e28c74b9 docs: update PR workflow and changelog for PR #310 2026-01-06 20:26:12 +01:00
Peter Steinberger 250debcc00 Merge PR #310 2026-01-06 20:25:47 +01:00
Peter Steinberger 67bda21811 fix: preserve markdown fences when chunking 2026-01-06 20:23:41 +01:00
Peter Steinberger afc42c7547 fix(ui): tighten focus mode spacing 2026-01-06 19:10:06 +00:00
Peter Steinberger 31dbc62bdd fix(telegram): prevent stuck typing after tool runs 2026-01-06 18:56:43 +00:00
Peter Steinberger 72ab9f3f42 docs(changelog): note Telegram typing fix (#322) 2026-01-06 18:56:43 +00:00
Peter Steinberger 369af5fc58 style(agents): format usage helper 2026-01-06 19:54:50 +01:00
Peter Steinberger d07e78855c fix(workspace): align clawd + bootstrap 2026-01-06 19:54:50 +01:00
Abhi bdf597eb95 fix(telegram): stop typing after tool results (#322)
Thanks @AbhisekBasu1.
2026-01-06 18:54:08 +00:00
Peter Steinberger 2f24ea492b fix: restore Anthropic token accounting 2026-01-06 18:52:01 +00:00
Peter Steinberger 672762bdd0 docs(security): explain allowFrom + group allowlists 2026-01-06 19:44:23 +01:00
Peter Steinberger 91f67f5bd7 build(protocol): regenerate protocol artifacts 2026-01-06 18:37:20 +00:00
Peter Steinberger a38bd4d3a2 docs(security): explain allowlists terminology 2026-01-06 19:35:40 +01:00
Peter Steinberger 9fb37cbf93 style: format whatsapp inbound allowlist 2026-01-06 18:33:37 +00:00
Peter Steinberger 730cc72388 docs: document multi-agent mode 2026-01-06 18:33:37 +00:00
Peter Steinberger dbfa316d19 feat: multi-agent routing + multi-account providers 2026-01-06 18:33:37 +00:00
Peter Steinberger 50d4b17417 docs: add pairing overview 2026-01-06 19:30:30 +01:00
Peter Steinberger 1bde6cffec docs: add updating guide 2026-01-06 19:24:33 +01:00
Peter Steinberger e6864346b8 docs(readme): make build step optional 2026-01-06 19:18:47 +01:00
Peter Steinberger 3260fde9cd docs(security): clarify DM policy coverage 2026-01-06 19:13:31 +01:00
Peter Steinberger 9a5cfa863e docs: remove deprecated control API 2026-01-06 19:04:47 +01:00
James Groat cdd0cb6089 fix(browser): add profile param to tabs routes and browser-tool
- tabs.ts now uses getProfileContext like other routes
- browser-tool threads profile param through all actions
- add tests for profile query param on /tabs endpoints
- update docs with browser tool profile parameter
2026-01-06 11:04:33 -07:00
Peter Steinberger 7d122b828e docs(security): add security@clawd.bot 2026-01-06 19:03:34 +01:00
Peter Steinberger 67240252f8 docs: make internal links clickable 2026-01-06 19:02:33 +01:00
DBH d79e34040f docs: remove .md extension from internal links (#319) 2026-01-06 11:58:39 -06:00
James Groat 6cebd26529 fix(browser-cli): rename --profile to --browser-profile to avoid conflict with global --profile flag 2026-01-06 10:47:25 -07:00
Peter Steinberger 30ea1e37f2 docs(readme): link docs site + security guide 2026-01-06 18:41:00 +01:00
Shadow b5f7431428 Changelog: note skipBootstrap 2026-01-06 11:25:27 -06:00
Peter Steinberger ee085ffd65 docs(docker): align sandbox allowlist 2026-01-06 18:25:00 +01:00
Peter Steinberger 8d9b2208d5 docs(security): explain sandboxing options 2026-01-06 18:23:53 +01:00
Peter Steinberger 94e300fde5 docs(security): document pairing + prompt injection 2026-01-06 18:13:12 +01:00
Peter Steinberger c47aff5244 fix(onboard): clarify DM policy keys 2026-01-06 18:09:21 +01:00
Peter Steinberger 6ea25b0354 docs(security): explain DM pairing 2026-01-06 18:09:21 +01:00
Peter Steinberger 4933905366 fix(onboard): configure DM policies 2026-01-06 18:09:21 +01:00
Onur 6cf3570c5b feat(agent): add skipBootstrap config to skip bootstrap file creation (#292) 2026-01-06 11:02:51 -06:00
Shadow 38aaa8563b Changelog: note README ui install 2026-01-06 10:59:26 -06:00
Hugo Baraúna 42af37aea9 docs: add missing ui:install step to README setup (#300) 2026-01-06 10:58:48 -06:00
Peter Steinberger b081f45b17 fix(onboard): explain DM pairing defaults 2026-01-06 17:58:06 +01:00
Muhammed Mukhthar CM 4bb53e19f9 fix(build): import tool-display.json instead of fs.readFileSync (#312) 2026-01-06 10:55:02 -06:00
Peter Steinberger 967cef80bc fix(security): lock down inbound DMs by default 2026-01-06 17:51:56 +01:00
Shadow 327ad3c9c7 Changelog: credit favicon PR 2026-01-06 10:49:45 -06:00
Jefferson Nunn 848f36b670 feat(ui): add favicon.ico from Mac app icon (#305) 2026-01-06 10:41:19 -06:00
Nimrod Gutman 62e590323a fix(macOS): keep gateway config sync local 2026-01-06 18:05:28 +02:00
Peter Steinberger 11b6fddcbf Merge pull request #283 from Oncomatic/jarvis/telegram-media-error-notify
fix(telegram): notify user when media exceeds size limit
2026-01-06 15:18:52 +00:00
Peter Steinberger 2a50eadcc1 fix(ui): self-heal ui builds 2026-01-06 16:03:04 +01:00
Peter Steinberger 3f10655e3f test: make test:coverage pass 2026-01-06 15:43:24 +01:00
Peter Steinberger 3ff17b70ea chore: changelog for #293 2026-01-06 15:32:06 +01:00
Palash Oswal b91012b697 fix(cli): don't force localhost gateway url in remote mode
Fixes remote gateway setup (remote mode) by not overriding url; adds regression tests. Thanks @oswalpalash.
2026-01-06 14:30:45 +00:00
Simon KellyandPeter Steinberger 5aa1ed2c96 fix(slack): use named import for @slack/bolt App class (#299)
* fix(slack): use named import for @slack/bolt App class

The default import `import bolt from '@slack/bolt'` followed by
`const { App } = bolt` doesn't work correctly in Bun due to ESM/CJS
interop issues. The default export comes through as a function rather
than the module object.

Switching to a named import `import { App } from '@slack/bolt'`
resolves the issue and allows the Slack provider to start successfully.

* fix(slack): align Bolt mock with named App export

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-01-06 14:22:14 +00:00
Lukáš Loukota c16510c6ea fix: install Bun in Dockerfile (#284)
Install Bun in Dockerfile so `pnpm build` can run Bun scripts inside Docker.

Thanks @loukotal.
2026-01-06 14:05:19 +00:00
VAC eadb923000 fix: auto-recover from Gemini session corruption
Detect the Gemini API error 'function call turn comes immediately after
a user turn or after a function response turn' which indicates corrupted
session history.

When detected:
- Delete the corrupted transcript file
- Remove the session entry from the store
- Return a user-friendly message asking them to retry

This prevents the error loop where every subsequent message fails with
the same error until manual intervention.

Fixes #296
2026-01-06 07:25:15 -05:00
Peter Steinberger dbac51e60f chore(protocol): regenerate GatewayModels.swift 2026-01-06 09:55:58 +01:00
Manuel Hettich aae5926db9 fix(telegram): notify user when media exceeds size limit
When a file exceeds mediaMaxMb, send a friendly error message
instead of silently dropping the upload.

---
🤖 Authored by Jarvis (AI assistant)
2026-01-06 08:45:07 +00:00
Peter Steinberger ddba2c6912 docs: point docs links to GitHub while docs down 2026-01-06 09:45:03 +01:00
Peter Steinberger 3693449d7e feat: sandbox session tool visibility 2026-01-06 08:40:30 +00:00
Peter Steinberger ef58399fcd docs(changelog): note auth rotation + configure order 2026-01-06 09:25:42 +01:00
Peter Steinberger 5926a98c52 fix(configure): don’t write auth.order by default 2026-01-06 09:25:36 +01:00
Peter Steinberger f2d353459f test(auth): stop prioritizing lastGood 2026-01-06 09:25:33 +01:00
Peter Steinberger ed2075ce69 test(gateway): deflake cron finished event wait 2026-01-06 09:25:31 +01:00
Muhammed Mukhthar CM 9e49c762e0 fix(auth): prioritize round-robin over lastGood for multi-account rotation (#281)
* fix(auth): prioritize round-robin over lastGood for multi-account rotation

When multiple OAuth accounts are configured, the round-robin rotation was
not working because lastGood was always prioritized, defeating the sort by
lastUsed.

Changes:
- Remove lastGood prioritization in resolveAuthProfileOrder
- Always apply orderProfilesByMode (sorts by lastUsed, oldest first)
- Only respect configuredOrder when explicitly set in config
- preferredProfile still takes priority for explicit user choice

Tested with 2 Google Antigravity accounts - verified alternating usage.

Follow-up to PR #269.

* style: fix formatting
2026-01-06 08:16:35 +00:00
Peter Steinberger cf1a1d107e fix: add OpenAI Codex OAuth to configure 2026-01-06 09:13:51 +01:00
Peter Steinberger 30b6c417c7 docs(changelog): note bun UI build 2026-01-06 09:13:51 +01:00
Muhammed Mukhthar CM 42d1c2448e fix(cron-tool): use generic object schema for job/patch to fix Claude via Antigravity (#280) 2026-01-06 02:13:09 -06:00
Peter Steinberger c27dd75135 build(control-ui): prefer bun for UI build 2026-01-06 09:08:25 +01:00
Peter Steinberger 5774b4f300 fix(control-ui): pad chat composer in focus mode 2026-01-06 08:59:05 +01:00
Peter Steinberger df6d545050 docs: update docs domain + link labels 2026-01-06 08:57:58 +01:00
Peter Steinberger a279bcfeb1 feat: add sessions_spawn sub-agent tool 2026-01-06 08:41:45 +01:00
Peter Steinberger 952657d55c feat(tui): add /elev alias 2026-01-06 08:41:04 +01:00
Peter Steinberger 882048d90b feat(control-ui): add chat focus mode 2026-01-06 08:16:21 +01:00
Peter Steinberger 173e9f103e docs: add changelog entry for bun migration (#278) 2026-01-06 08:15:20 +01:00
Peter Steinberger e03a5628e3 docs: prefer bun-first TypeScript execution 2026-01-06 08:15:03 +01:00
Ayaan Zaidi 7a48b908e4 refactor: replace tsx with bun for TypeScript execution (#278) 2026-01-06 07:14:08 +00:00
Peter Steinberger b472143882 chore: update terminal css 2026-01-06 07:58:09 +01:00
Peter Steinberger d14505ff78 Merge pull request #274 from kiranjd/fix/chat-scroll-to-bottom
fix(ui): scroll chat to bottom on initial load
2026-01-06 06:49:29 +00:00
Peter Steinberger 5b183b4fe3 fix(ui): scroll chat to bottom on initial load 2026-01-06 07:49:12 +01:00
Peter Steinberger dbb51006cd feat: unify group policy allowlists 2026-01-06 06:40:42 +00:00
Peter Steinberger 51e8bbd2a8 style: normalize type definitions 2026-01-06 07:21:10 +01:00
Peter Steinberger aa16b679ad fix: improve auth profile failover 2026-01-06 07:18:06 +01:00
Peter Steinberger a7b5753dc4 Merge pull request #269 from mukhtharcm/feat/multi-account-roundrobin
feat: Multi-account OAuth with round-robin rotation
2026-01-06 06:13:19 +00:00
Peter Steinberger b5c604b7b7 fix: require slash for control commands 2026-01-06 07:05:17 +01:00
Peter Steinberger 7d896b5f67 fix: doctor memory hint 2026-01-06 06:01:24 +00:00
Kiran Jd b584770055 Merge branch 'main' into fix/chat-scroll-to-bottom 2026-01-06 11:27:57 +05:30
Shadow f29efb9862 docs: add issue templates 2026-01-05 23:55:51 -06:00
kiranjd 511632f47c fix(ui): scroll chat to bottom on initial load
The chat view was starting at the top showing oldest messages instead of
scrolling to the bottom to show the latest messages (like WhatsApp).

Root causes:
1. scheduleChatScroll() was called without force flag in refreshActiveTab()
2. The scroll was targeting .chat-thread element which has overflow:visible
   and doesn't actually scroll - the window scrolls instead

Fixes:
- Pass force flag (!chatHasAutoScrolled) when loading chat tab
- Wait for Lit updateComplete before scrolling to ensure DOM is ready
- Scroll the window instead of the .chat-thread container
- Use behavior: 'instant' for immediate scroll without animation
2026-01-06 11:23:27 +05:30
Shadow 91cb2c02a7 fix: allow optional reply body 2026-01-05 23:47:33 -06:00
Shadow 4be6ec39dd docs: add recent contributors 2026-01-05 23:44:48 -06:00
Shadow 0204f45352 docs: add PR 272 changelog entry 2026-01-05 23:37:37 -06:00
Shadow 69f285c5ca chore: fixed CI 2026-01-05 23:36:48 -06:00
Ayaan Zaidi a79c100594 fix: targetDir symlink handling in postinstall script (#272) 2026-01-05 23:36:11 -06:00
Peter Steinberger b759cb6f37 feat(providers): normalize location parsing 2026-01-06 06:31:09 +01:00
Nacho Iacovino 255e77f530 feat(telegram): parse location and venue messages
- Add TelegramLocation, TelegramVenue, and TelegramMessageWithLocation types
- Add formatLocationMessage() to convert location/venue shares to text
- Add extractLocationData() for structured location access in ctxPayload
- Handle both raw location pins and venue shares (places with names)
- Include location in reply-to context for quoted messages

Location messages now appear as:
- [Location: lat, lon ±accuracy] for raw pins
- [Venue: Name - Address (lat, lon)] for places

ctxPayload includes LocationLat, LocationLon, LocationAccuracy,
VenueName, and VenueAddress fields for programmatic access.
2026-01-06 06:31:09 +01:00
Peter Steinberger 9ffea23f31 templates: Add memory maintenance during heartbeats
New section explaining how to periodically review daily memory files
and update MEMORY.md with distilled learnings. Like a human reviewing
their journal and updating their mental model.
2026-01-06 05:21:09 +00:00
Muhammed Mukhthar CM 18c7795ee0 feat: treat timeout as rate limit for profile rotation
Antigravity rate limits cause requests to hang indefinitely rather than
returning 429 errors. This change detects timeouts and treats them as
potential rate limits:

- Added timedOut flag to track timeout-triggered aborts
- Timeout now triggers profile cooldown + rotation
- Logs: "Profile X timed out (possible rate limit). Trying next account..."

This ensures automatic failover when Antigravity hangs due to rate limiting.
2026-01-06 05:20:01 +00:00
Iamadig 29a89ff9fa nano-banana: emit MEDIA token for generated images (#271) 2026-01-05 23:19:50 -06:00
Peter Steinberger 7d1fee70e7 templates: Add MEMORY.md long-term memory concept
- Updated session start to include MEMORY.md loading for main sessions
- Added 🧠 MEMORY.md section explaining:
  - Only load in main sessions (direct with human), not shared contexts
  - Security boundary: personal context shouldn't leak to strangers
  - Can freely read/edit/update in main sessions
  - Write significant events, thoughts, decisions, opinions
  - Curated memory vs raw daily logs

This gives new agents proper long-term memory that's secure and personal.
2026-01-06 05:19:24 +00:00
Muhammed Mukhthar CM ce6c7737c1 feat: add round-robin rotation and cooldown for auth profiles
Adds usage tracking to auth profiles for automatic rotation:

- ProfileUsageStats type with lastUsed, cooldownUntil, errorCount
- markAuthProfileUsed(): tracks successful usage, resets errors
- markAuthProfileCooldown(): applies exponential backoff (1/5/25/60min)
- isProfileInCooldown(): checks if profile should be skipped
- orderProfilesByMode(): now sorts by lastUsed (oldest first)

On auth/rate-limit failures, profiles are marked for cooldown before
rotation. On success, usage is recorded for round-robin ordering.

This enables automatic load distribution across multiple accounts
(e.g., Antigravity 5-hour rate limit windows).
2026-01-06 05:17:59 +00:00
Muhammed Mukhthar CM 06df6a955a feat: use email-based profile IDs for OAuth providers
Changes writeOAuthCredentials and applyAuthProfileConfig calls to use
the email from OAuth response as part of the profile ID instead of
hardcoded ":default".

This enables multiple accounts per provider - each login creates a
separate profile (e.g., google-antigravity:user@gmail.com) instead
of overwriting the same :default profile.

Affected files:
- src/commands/onboard-auth.ts (generic writeOAuthCredentials)
- src/commands/configure.ts (Antigravity flow)
- src/wizard/onboarding.ts (Antigravity flow)
2026-01-06 05:17:59 +00:00
Shadow 88cb13dc82 Auto-reply: fix typing stop race (#270) 2026-01-05 22:57:04 -06:00
Peter Steinberger 35a2140e48 fix: clean up poll merge 2026-01-06 04:51:05 +00:00
Peter SteinbergerandDBH 0b27964693 feat: unify poll support
Co-authored-by: DBH <5251425+dbhurley@users.noreply.github.com>
2026-01-06 04:51:05 +00:00
Sreekaran SrinathandShadow 1f4d9e83ff fix(ui): add anyOf/oneOf support in config form (#268)
* fix(ui): add anyOf/oneOf support in config form

- Handle literal unions as dropdowns with type preservation
- Handle primitive unions (string|number, boolean|string) as text inputs
- Unwrap single-variant optional types
- Fix enum handler to preserve types via index-based values
- Update normalizeUnion to support primitive unions in schema analysis
- Exclude allOf from union normalization (stays unsupported)

Fields like Thinking Default, Allow From, Memory now render properly
instead of showing 'unsupported schema node' errors.

* UI: fix enum placeholder collision

* Docs: update changelog for PR #268

---------

Co-authored-by: Shadow <hi@shadowing.dev>
2026-01-05 22:50:07 -06:00
AsleepandPeter Steinberger 8880128ebf Format messages so they work with Gemini API (#266)
* fix: Gemini stops working after one message in a session

* fix: small issue in test file

* test: cover google role-merge behavior

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-01-06 04:45:40 +00:00
DBH 2737e17c67 feat: Add WhatsApp poll support (#248)
Implements issue #123 - WhatsApp Poll Support

## Gateway Protocol
- Add `poll` RPC method with params: to, question, options (2-12), selectableCount

## ActiveWebListener
- Add `sendPoll(to, poll)` method to interface
- Implementation uses Baileys poll message type

## CLI Command
- `clawdbot poll --to <jid> -q <question> -o <opt1> -o <opt2> [-s count]`
- Supports --dry-run, --json, --verbose flags
- Validates 2-12 options

## Changes
- src/gateway/protocol/schema.ts: Add PollParamsSchema
- src/gateway/protocol/index.ts: Export validator and types
- src/web/active-listener.ts: Add sendPoll to interface
- src/web/inbound.ts: Implement sendPoll using Baileys
- src/web/outbound.ts: Add sendPollWhatsApp function
- src/gateway/server-methods/send.ts: Add poll handler
- src/commands/poll.ts: New CLI command
- src/cli/program.ts: Register poll command

Closes #123
2026-01-06 04:44:15 +00:00
Peter Steinberger ea6ee16461 chore: fix lint warnings 2026-01-06 05:41:24 +01:00
Peter Steinberger 77789cb9a8 fix: improve compaction queueing and oauth flows 2026-01-06 05:41:24 +01:00
9ab0b88ac6 feat(whatsapp,telegram): add groupPolicy config option (#216)
Co-authored-by: Marcus Neves <conhecendo.contato@gmail.com>
Co-authored-by: Shadow <hi@shadowing.dev>
2026-01-05 22:41:19 -06:00
Peter SteinbergerandKevin Kern f6d9d3ce67 docs: credit Kevin Kern for mention gating
Co-authored-by: Kevin Kern <hello@regenrek.at>
2026-01-06 04:22:02 +00:00
Peter Steinberger 53c9feb597 test: cover slack thread reply routing 2026-01-06 05:11:06 +01:00
Peter Steinberger e54865bbd2 Merge pull request #251 from scald/fix/slack-thread-replies
fix(slack): preserve thread context in auto-replies
2026-01-06 04:10:53 +00:00
Steve Caldwell 7034d4f807 fix(slack): preserve thread context in auto-replies
When replying to a message in a Slack thread, the response now stays
in the thread instead of going to the channel root.

Only threads replies when the incoming message was already in a thread
(has thread_ts). Top-level messages get top-level replies.

Fixes #250
2026-01-06 05:09:04 +01:00
Shadow 7b343f995c Changelog: add entries for PRs 220 and 261 2026-01-05 22:07:29 -06:00
Ayaan Zaidi bd735182b6 feat(telegram): support media groups (multi-image messages) (#220) 2026-01-05 22:04:33 -06:00
VACIncandVAC fb2513e265 fix(discord): Use channel ID for DMs instead of user ID (#261)
Co-authored-by: VAC <vac@vacs-mac-mini.localdomain>
2026-01-05 22:02:33 -06:00
Peter Steinberger 13eb9c9ee9 refactor: centralize reply dispatch 2026-01-06 04:55:00 +01:00
Peter Steinberger 319dd14e8e docs: clarify group allowlists in README 2026-01-06 04:47:41 +01:00
Peter Steinberger e14a0b3746 Merge pull request #265 from clawdbot/chore/pr-241-reopen
PR #241 reconciliation
2026-01-06 03:42:47 +00:00
Peter Steinberger 5946f4c341 test: extend typing idle coverage 2026-01-06 03:42:33 +00:00
Peter Steinberger 241a215528 chore: reconcile PR #241 2026-01-06 04:42:18 +01:00
kitze 97afd3a388 chore: credit @kitze for PR #241 2026-01-06 04:31:08 +01:00
Peter Steinberger 1a4f7d3388 feat: add ack reaction defaults 2026-01-06 03:28:47 +00:00
Peter Steinberger 58186aa56e test: cover typing idle gate 2026-01-06 03:28:47 +00:00
Peter Steinberger ca8f66f844 refactor: unify group allowlist policy 2026-01-06 04:27:51 +01:00
Ayaan Zaidi b1bb3ff6a6 feat: add reaction to acknowledge message in createTelegramBot 2026-01-06 03:21:56 +00:00
Peter Steinberger 9d656f4269 style: satisfy lint 2026-01-06 03:11:42 +00:00
Peter Steinberger d5f088978a fix: stop typing after dispatcher idle 2026-01-06 03:09:49 +00:00
Josh PalmerandPeter Steinberger cbc39bd005 use process PATH for bash tool (#202)
what: default bash PATH to process.env.PATH

why: ensure Nix-provided tools on PATH inside sessions

tests: not run

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-01-06 03:05:21 +00:00
Peter Steinberger 070f7db196 docs: thank @joshp123 for PR #202 2026-01-06 04:04:42 +01:00
Peter Steinberger 20a361a3cf refactor: centralize agent timeout defaults 2026-01-06 02:48:44 +00:00
Martin Schürrerandclawd@msch d83ca74c18 gateway: honor agent timeout for chat.send (#229)
Co-authored-by: clawd@msch <clawd@msch>
2026-01-06 02:45:02 +00:00
Peter Steinberger 9b5610aa45 style: format telegram bot test 2026-01-06 03:43:05 +01:00
Peter Steinberger 92ff3311ee chore: remove unused patch file 2026-01-06 03:41:56 +01:00
Peter Steinberger 3211fee063 docs: note legacy patch file 2026-01-06 03:41:56 +01:00
Peter Steinberger c1698b6975 docs: add bun install support 2026-01-06 03:41:56 +01:00
Peter Steinberger 9623bd7763 fix: route agent CLI via gateway 2026-01-06 03:41:56 +01:00
Ayaan Zaidi e5e8ed4aef chore: credit PR #226 contribution 2026-01-06 02:40:29 +00:00
Peter Steinberger 77ac45b90e docs: update contributors 2026-01-06 02:33:27 +00:00
Peter Steinberger 0398f684e7 fix: add gateway stop/restart commands 2026-01-06 03:25:32 +01:00
Peter Steinberger cc0ef4d012 fix(telegram): improve gif handling 2026-01-06 02:22:19 +00:00
Peter Steinberger 45c67a48af docs: thank mneves75 for cron hardening 2026-01-06 03:10:13 +01:00
Marcus NevesandPeter Steinberger 67e1452f4a Cron: normalize cron.add inputs + align channels (#256)
* fix: harden cron add and align channels

* fix: keep cron tool id params

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-01-06 02:09:48 +00:00
Peter Steinberger 00061b2fd3 fix: harden config form 2026-01-06 03:05:56 +01:00
Peter Steinberger 20705d1b37 fix: set codex oauth model default 2026-01-06 02:49:45 +01:00
Peter Steinberger 17db03ad55 test: ignore SIGPIPE in docker e2e 2026-01-06 02:49:45 +01:00
Peter Steinberger 28fad05e96 test: stabilize docker onboarding e2e 2026-01-06 02:49:45 +01:00
Peter Steinberger b6ac2d860d fix: resolve embedded api key lookup 2026-01-06 02:49:44 +01:00
Peter Steinberger b30bae89ed feat: track compaction count + verbose notice 2026-01-06 02:49:03 +01:00
Peter Steinberger 3c6dea3ef3 style: format gmail watcher test 2026-01-06 01:46:59 +00:00
Peter Steinberger 55b33b4e69 fix: stop gmail watcher restart on bind error 2026-01-06 01:40:15 +00:00
Peter Steinberger 11a5495919 docs: add group chat guidance 2026-01-06 01:40:02 +00:00
Peter Steinberger 87f4efda8d fix: restore auth fallback ordering 2026-01-06 01:38:15 +00:00
Peter Steinberger 6f541d6304 fix: improve discord permission errors 2026-01-06 01:38:15 +00:00
EchoandCash Williams 162f8e9bb7 fix(discord): convert readMessages timestamps to local time (#240)
Co-authored-by: Cash Williams <cashwilliams@gmail.com>
2026-01-05 19:37:05 -06:00
Peter Steinberger b6ae376076 fix: gate reset auth and infer whatsapp sender 2026-01-06 02:23:55 +01:00
Peter Steinberger b85248bd07 fix: patch qrcode-terminal import for Node 22 2026-01-06 02:23:55 +01:00
Peter Steinberger b56338171b feat: gate slash commands and add compact 2026-01-06 02:23:55 +01:00
Peter Steinberger 085c70a87b fix: prefer env keys unless profiles configured 2026-01-06 01:21:45 +00:00
Peter Steinberger 216a23ed08 fix: auto-migrate legacy config on CLI 2026-01-06 01:10:32 +00:00
Peter Steinberger e73573eaea fix: clean model config typing 2026-01-06 01:08:36 +00:00
Peter Steinberger b04c838c15 feat!: redesign model config + auth profiles 2026-01-06 00:56:58 +00:00
Peter Steinberger bd2e003171 docs: expand Slack scope notes 2026-01-06 01:54:06 +01:00
6fe250cb46 docs(slack): add missing scopes for DM replies (#235)
The manifest was missing scopes required for conversations.open API,
which is used to get DM channel IDs for replies.

Added scopes:
- im:write (required for DM replies)
- im:read (list DM conversations)
- mpim:write (reply to multi-person DMs)
- mpim:read (list MPDMs)
- groups:write (private channel interactions)
- groups:read (list private channels)

Without im:write, the example config (dm.enabled: true) cannot
actually reply to DMs - fails with missing_scope error.

Co-authored-by: Manuel Hettich <17690367+ManuelHettich@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-01-06 00:53:29 +00:00
Peter Steinberger f7074ea45f test: cover logging defaults 2026-01-06 01:39:42 +01:00
Peter Steinberger d813e14950 chore: update mention gating docs and tests 2026-01-06 01:38:36 +01:00
Peter Steinberger 811ec8b78b fix: unify mention gating across providers 2026-01-06 01:32:17 +01:00
Peter Steinberger 48d52d13f1 docs: clarify 1password tmux flow 2026-01-06 01:30:48 +01:00
Peter Steinberger df9005d64c fix(ui): handle slack config snapshot 2026-01-06 01:16:25 +01:00
Peter Steinberger 5356adba8f fix: keep Slack thread replies in thread 2026-01-06 01:09:25 +01:00
Peter Steinberger 291c6f3b60 test: cover WhatsApp DM senderE164 2026-01-06 00:55:41 +01:00
Xin a6a45f4b84 fix(whatsapp): populate senderE164 for direct chats to enable owner commands (#247) 2026-01-05 23:54:35 +00:00
Peter Steinberger a4fdfc2414 chore: fix redaction lint 2026-01-06 00:42:23 +01:00
Peter Steinberger 8be168b180 fix: redact sensitive tokens in tool summaries 2026-01-06 00:41:12 +01:00
Peter Steinberger 2ec9d75ac2 feat: add 1password skill 2026-01-06 00:26:58 +01:00
Peter Steinberger 20e00eb89b fix: normalize unknown prompt errors 2026-01-05 23:05:57 +00:00
Peter Steinberger ac3dedaa1b feat: standardize timestamps to UTC 2026-01-05 23:03:59 +00:00
Peter SteinbergerandJosh Lehman f790f3f3ba fix/heartbeat ok delivery filter (#246)
* cron: skip delivery for HEARTBEAT_OK responses

When an isolated cron job has deliver:true, skip message delivery if the
response is just HEARTBEAT_OK (or contains HEARTBEAT_OK at edges with
short remaining content <= 30 chars). This allows cron jobs to silently
ack when nothing to report but still deliver actual content when there
is something meaningful to say.

Media is still delivered even if text is HEARTBEAT_OK, since the
presence of media indicates there's something to share.

* fix(heartbeat): make ack padding configurable

* chore(deps): update to latest

---------

Co-authored-by: Josh Lehman <josh@martian.engineering>
2026-01-05 22:52:13 +00:00
Josh Lehman dae7f560a5 cron: skip delivery for HEARTBEAT_OK responses (#238)
When an isolated cron job has deliver:true, skip message delivery if the
response is just HEARTBEAT_OK (or contains HEARTBEAT_OK at edges with
short remaining content <= 30 chars). This allows cron jobs to silently
ack when nothing to report but still deliver actual content when there
is something meaningful to say.

Media is still delivered even if text is HEARTBEAT_OK, since the
presence of media indicates there's something to share.
2026-01-05 22:16:28 +00:00
Peter Steinberger 4c6302d0f4 docs: refine showcase page 2026-01-05 23:06:14 +01:00
Peter Steinberger 53bf8b7b80 fix: avoid duplicate missing auth label 2026-01-05 23:00:37 +01:00
Peter Steinberger e5058a4cf9 docs: add showcase page 2026-01-05 22:58:38 +01:00
CI d9cdf3b8ac fix(model): treat quota errors as rate limits 2026-01-05 21:34:08 +00:00
CI c627efce3e fix(model): retry with supported thinking level 2026-01-05 21:34:08 +00:00
CI 5622dfe86b fix: retry model fallback on rate limits 2026-01-05 21:34:08 +00:00
Peter Steinberger 7900d33701 docs: add README clarifiers 2026-01-05 22:32:02 +01:00
Peter Steinberger 29748864a4 docs: expand README doc links 2026-01-05 22:30:47 +01:00
Peter Steinberger d787316e65 docs: prune refactor notes + update README 2026-01-05 22:24:31 +01:00
Peter Steinberger b5c2c724dd docs: clarify sessions tools 2026-01-05 22:23:31 +01:00
Peter Steinberger 1b6c8178ae style: apply biome formatting 2026-01-05 21:21:53 +00:00
Peter Steinberger dbea8eb69e docs: clarify lingering onboarding notes 2026-01-05 21:20:05 +00:00
Tobias Bischoff de153a40d0 Onboard: auto-enable systemd lingering on Linux 2026-01-05 21:20:05 +00:00
Peter Steinberger 949ea38ef5 docs: clarify bun + browser enablement 2026-01-05 22:17:14 +01:00
Peter Steinberger 57abcba08a docs: add remote gateway and elevated notes 2026-01-05 22:15:26 +01:00
Peter Steinberger ab27b98f7b docs: fix front matter + workspace defaults 2026-01-05 22:13:21 +01:00
Peter Steinberger 1e9d7e0d79 docs: fix oauth path references 2026-01-05 21:53:37 +01:00
Peter Steinberger 872f30fee0 docs: clawtributors line 2026-01-05 21:47:56 +01:00
Peter Steinberger 055b497332 docs: add hubs index and clawdibuted 2026-01-05 21:46:52 +01:00
Peter Steinberger 60adfecdfa docs: sync platform docs + nav 2026-01-05 21:30:19 +01:00
Peter Steinberger 70f38400c0 docs: expand README platform + subsystem links 2026-01-05 21:02:02 +01:00
Peter Steinberger 14d7da6ec2 docs: unify app docs 2026-01-05 20:59:54 +01:00
Peter Steinberger 79e4354e5c docs: merge contributing into community 2026-01-05 20:38:20 +01:00
Peter Steinberger 6c33574160 docs: add community contributors 2026-01-05 20:34:39 +01:00
Peter Steinberger 2f9d85f4c7 docs: finalize model config decisions 2026-01-05 19:28:06 +00:00
Peter Steinberger cd12f34eba docs: refine model config decisions 2026-01-05 19:26:47 +00:00
Peter Steinberger d88c523ba4 docs: add model config proposal 2026-01-05 19:25:07 +00:00
Peter Steinberger 38e63cbe0e docs: refresh README + architecture links 2026-01-05 20:10:56 +01:00
Peter Steinberger c75b2a7067 refactor: unify reply dispatch across providers 2026-01-05 19:43:54 +01:00
Peter Steinberger bfe7f5f126 docs: add recommended source setup 2026-01-05 19:40:05 +01:00
Peter Steinberger cc790f2c84 docs(agent): annotate stream invariants 2026-01-05 18:10:03 +00:00
Peter Steinberger 86ad703f53 refactor(agent): extract block chunker + tool adapter 2026-01-05 18:05:40 +00:00
Peter Steinberger 7c89ce93b5 fix(agent): align tools + preserve indentation 2026-01-05 17:55:20 +00:00
Peter Steinberger 196eb86e38 fix(ui): animate reading indicator dots 2026-01-05 17:40:15 +00:00
Peter Steinberger ad6bec4612 fix: enable systemd lingering for gateway 2026-01-05 18:38:43 +01:00
Peter Steinberger 0fb30db819 test: expand fenced block chunking coverage 2026-01-05 18:38:43 +01:00
Peter Steinberger 22105c8496 fix(agent): finalize block chunking 2026-01-05 17:22:29 +00:00
Peter Steinberger b7e708c764 fix(chat): stabilize web UI tool runs 2026-01-05 17:22:29 +00:00
Peter Steinberger 86c404c48b chore: fix reply commands lint 2026-01-05 18:16:39 +01:00
Peter Steinberger f0abd619be chore: add model-usage skill 2026-01-05 18:16:29 +01:00
Julian Engel 110e2255c4 fix: pass custom tools via customTools parameter to pi-coding-agent SDK
The SDK's tools parameter only accepts built-in tools (read, bash, edit, write).
Custom clawdbot tools (browser, canvas, nodes, cron, etc.) were being filtered
out, causing 'Tool not found' errors at runtime.

Split tools into built-in and custom, passing them via the correct parameters.
2026-01-05 17:00:06 +00:00
Julian Engel ec26ad81be docs: add cross-references to Linux browser troubleshooting 2026-01-05 17:00:06 +00:00
Julian Engel 27a77454ae docs: add Linux browser troubleshooting guide
Covers:
- Snap Chromium issues on Ubuntu
- Solution 1: Install Google Chrome (recommended)
- Solution 2: attachOnly mode workaround
- Systemd service for auto-starting browser
- Config reference
2026-01-05 17:00:06 +00:00
Peter Steinberger 55e4e76d43 fix: preserve fenced markdown in block streaming 2026-01-05 17:53:53 +01:00
Peter Steinberger 234059811c feat(ui): add chat reading indicator 2026-01-05 16:16:34 +00:00
Peter Steinberger 7f3f73af1c fix: show model auth in status 2026-01-05 15:50:18 +01:00
Peter Steinberger bf6aad1965 fix(ci): format directive-handling 2026-01-05 14:34:55 +00:00
Django Navarro 977467066d fix(coding-agent): close PR template code block correctly
The outer fence (4 backticks) was closing prematurely after the bash
example, leaving the rest of the template (Feature intent through
Submitted by Razor) rendered as prose instead of inside the code block.

Fixed by moving the closing fence to the end of the full template.
2026-01-05 14:33:21 +00:00
Peter Steinberger 0c37f27a4a fix: show /model auth source 2026-01-05 14:14:26 +00:00
Peter Steinberger cffbe79077 fix: add /model list alias 2026-01-05 14:11:33 +00:00
Peter Steinberger bb959684fe fix(tui): support pi-tui 0.36 key exports 2026-01-05 13:59:50 +00:00
Peter Steinberger 8e8d07cbf4 fix(ci): satisfy formatter checks 2026-01-05 13:55:53 +00:00
Peter Steinberger 5f4936dce5 fix(wizard): type OAuth provider login 2026-01-05 13:55:46 +00:00
Peter Steinberger a9bcf88bfa refactor(tui): use key helper predicates 2026-01-05 13:55:43 +00:00
Peter Steinberger f24fe4e9cd fix(whatsapp): reconnect on crypto unhandled rejection 2026-01-05 13:55:37 +00:00
Peter Steinberger 7619534bc0 feat(groups): resolve requireMention for discord/slack 2026-01-05 13:55:32 +00:00
Peter Steinberger ce68d82dfa fix: widen /model key masking 2026-01-05 13:50:45 +00:00
Peter Steinberger 5163886694 fix: show auth in /model list 2026-01-05 13:49:25 +00:00
Peter Steinberger d9103b387a fix: sync gateway mode via gateway config 2026-01-05 06:39:37 +00:00
Peter Steinberger 724354b9f0 fix: make tool list dynamic in system prompt 2026-01-05 06:36:24 +00:00
Peter Steinberger 79561d07a0 fix: allow openai-codex in onboarding types 2026-01-05 07:33:33 +01:00
Peter Steinberger 30038f7d37 fix: custom connections sidebar 2026-01-05 07:25:13 +01:00
Peter Steinberger 5431a9c692 fix: clean status + help + mid alias 2026-01-05 07:24:51 +01:00
Peter Steinberger 5aebc07369 chore: remove stale a2ui bundle hash 2026-01-05 06:17:06 +00:00
Peter Steinberger 7c51efe8f8 fix: prefer gateway config in local mode 2026-01-05 06:16:48 +00:00
Peter Steinberger 1119f2003e fix: preserve JSON5 config parsing 2026-01-05 06:16:48 +00:00
Peter Steinberger 9be1a14a08 fix: resolve agent dir in onboarding 2026-01-05 07:12:13 +01:00
Peter Steinberger 17ef7b3b0e fix: status runtime + help 2026-01-05 07:07:17 +01:00
Peter Steinberger 0d0da2e297 fix: remove sidebar toggle toolbar item safely 2026-01-05 06:49:57 +01:00
Peter Steinberger 82c16a8bed fix: remove settings sidebar toggle 2026-01-05 06:48:49 +01:00
Peter Steinberger 2c0f3a2887 docs: update auth docs 2026-01-05 06:46:20 +01:00
Peter Steinberger bc74e7cd9b docs: default mac build arch to host 2026-01-05 06:45:23 +01:00
Peter Steinberger 1545ac0003 chore: update a2ui bundle hash 2026-01-05 06:39:08 +01:00
Peter Steinberger 160fd1d8b6 docs: clarify a2ui bundle hash handling 2026-01-05 06:39:03 +01:00
Peter Steinberger 4305472787 docs: document sandbox media staging 2026-01-05 06:37:12 +01:00
Peter Steinberger 545f52d7d5 fix: hide settings toolbar row 2026-01-05 06:36:34 +01:00
Peter Steinberger a40fd5219c docs: clarify unrecognized file handling 2026-01-05 06:36:30 +01:00
Peter Steinberger 48322f7174 docs: highlight oauth and any-os support 2026-01-05 06:35:43 +01:00
Peter Steinberger f3cb41511d feat: add openai codex oauth 2026-01-05 06:31:45 +01:00
Peter Steinberger bce62f8c0f chore: update pi dependencies 2026-01-05 06:19:35 +01:00
Peter Steinberger 995f5959af fix: stage sandbox media for inbound attachments 2026-01-05 06:18:11 +01:00
Peter Steinberger a7d33c06f9 refactor: align agent lifecycle 2026-01-05 05:55:02 +01:00
Peter Steinberger ce5fd84432 docs: note settings sidebar layout 2026-01-05 05:54:37 +01:00
Peter Steinberger a89204362e fix: use sidebar settings layout 2026-01-05 05:54:21 +01:00
Peter Steinberger bcdaba1d48 chore: format custom editor 2026-01-05 05:32:30 +01:00
Peter Steinberger 95d9160e27 fix: avoid settings toolbar overflow 2026-01-05 05:32:14 +01:00
Peter Steinberger 8a31a868c0 fix: honor tailnet bind for macOS gateway endpoint 2026-01-05 05:30:40 +01:00
Peter Steinberger 870473be85 chore: update deps 2026-01-05 05:27:58 +01:00
Peter Steinberger b593ccb122 chore: update appcast for 2026.1.5-3 2026-01-05 05:14:07 +01:00
Peter Steinberger 35a32b31bd docs: note notarize env vars 2026-01-05 04:26:05 +01:00
Peter Steinberger 5dbbad0452 chore: default mac packaging to notarize 2026-01-05 04:22:58 +01:00
Peter Steinberger 19affcac90 chore: update appcast for 2026.1.5-3 2026-01-05 04:00:08 +01:00
Peter Steinberger 92f95abdcf docs: link to hosted docs 2026-01-05 03:59:58 +01:00
Peter Steinberger 477fa49a30 fix: include missing dist dirs in npm pack 2026-01-05 03:56:57 +01:00
Peter Steinberger 36b96c2b28 chore: update appcast for 2026.1.5-2 2026-01-05 03:53:09 +01:00
Peter Steinberger 2eb78b8da7 fix: resolve qrcode ESM import for Node 25 2026-01-05 03:47:57 +01:00
Peter Steinberger 3110e37db4 chore: update appcast for 2026.1.5-1 2026-01-05 03:32:53 +01:00
Peter Steinberger 93bb0257f0 fix: include sessions in npm pack and update qrcode import 2026-01-05 03:28:25 +01:00
Peter Steinberger 197b8f7c3b chore: update appcast for 2026.1.5 2026-01-05 03:13:14 +01:00
Peter Steinberger deba1b6739 style: format daemon program args test 2026-01-05 02:54:08 +01:00
Peter Steinberger aab98a6d18 test: fix daemon program args fs mocks 2026-01-05 02:51:56 +01:00
Peter Steinberger 849a008f34 test: avoid max port in browser server tests 2026-01-05 02:50:48 +01:00
Peter Steinberger 8791e46cf3 fix: resolve npx gateway daemon install 2026-01-05 02:48:25 +01:00
Peter Steinberger b779029517 fix: hide duplicate doc titles 2026-01-05 02:45:14 +01:00
Peter Steinberger 9c039e8356 docs: consolidate 2026.1.5 changelog 2026-01-05 02:39:42 +01:00
Peter Steinberger 4aeba76741 docs: add ClawdHub mention 2026-01-05 02:34:02 +01:00
Peter Steinberger d92a9e351e style: fix linting order and formatting 2026-01-05 02:33:59 +01:00
Peter Steinberger a1acd7dae8 chore: add qrcode-terminal vendor module stubs 2026-01-05 02:33:55 +01:00
Peter Steinberger 67420e9a81 fix: allow group activation for allowFrom senders 2026-01-05 02:33:51 +01:00
Peter Steinberger e4335ea094 fix: bundle qr renderer in relay 2026-01-05 02:19:49 +01:00
Peter Steinberger 0c632f4855 fix: prefer tailnet IP for local gateway calls 2026-01-05 02:19:26 +01:00
Peter Steinberger a322075764 fix: use id for cron tool params 2026-01-05 02:15:11 +01:00
Peter Steinberger 359cb66e68 fix: allow wildcard control commands 2026-01-05 02:06:18 +01:00
Peter Steinberger 00370139a5 docs: clarify derived port mapping 2026-01-05 02:03:29 +01:00
Peter Steinberger 17422608b2 fix: gate /activation to owners in groups 2026-01-05 02:03:29 +01:00
Peter Steinberger f871563f37 chore: sync generated protocol swift 2026-01-05 00:54:54 +00:00
Peter Steinberger 85549ac3b6 fix: gate group activation by owner 2026-01-05 00:48:16 +00:00
Peter Steinberger 1bad96aa2b style: tidy auto-reply imports and formatting 2026-01-05 01:46:16 +01:00
Peter Steinberger b0dcdc4982 fix: avoid mixing ?? and || in discord monitor 2026-01-05 01:46:16 +01:00
Shadow 13d39b8fb1 Fix discord/slack monitor compile errors 2026-01-04 18:44:19 -06:00
Peter Steinberger 50d26d827e fix: avoid duplicate senderName in slack monitor 2026-01-05 00:43:31 +00:00
Peter Steinberger 3fc9acc105 Merge remote-tracking branch 'mcinteerj/fix/whatsapp-offline-read-receipts' 2026-01-05 01:38:24 +01:00
Peter Steinberger d58828ebd7 test: relax timeouts for slow runs 2026-01-05 01:36:30 +01:00
Peter Steinberger f90eea5195 docs: add changelog entry for WhatsApp offline read receipts 2026-01-05 01:36:30 +01:00
Jake 3f40f4ab54 style: fix lint issues 2026-01-05 01:36:30 +01:00
Jake 65a55b97e0 WhatsApp: mark offline/history messages as read 2026-01-05 01:36:29 +01:00
Peter Steinberger 852f947b44 fix: unify control command handling 2026-01-05 01:31:36 +01:00
Peter Steinberger 54ad1ead80 docs: document --dev/--profile 2026-01-05 01:27:13 +01:00
Peter Steinberger c6de1b1f7d feat: add --dev/--profile CLI profiles 2026-01-05 01:27:13 +01:00
Peter Steinberger f601dac30d style: tidy tool schema normalization 2026-01-05 01:27:13 +01:00
Peter Steinberger 2bbf2698cb fix(ui): render markdown in tool result cards 2026-01-05 01:27:13 +01:00
Peter Steinberger f6097bc6e3 fix(ui): avoid overlapping guild action buttons 2026-01-05 01:27:13 +01:00
Peter Steinberger 39e482414a chore: apply upstream autostash 2026-01-05 00:26:52 +00:00
Peter Steinberger d6933b074a fix: make control ui chat scroll page 2026-01-05 00:18:18 +00:00
Peter Steinberger bcdfe461d4 fix(ci): resolve lint and docs build failures 2026-01-05 00:17:14 +00:00
Peter Steinberger 16ce76307e docs(faq): align model ids with shorthands 2026-01-05 01:11:29 +01:00
Peter Steinberger c37b4c18e0 docs: document env loading + shell fallback 2026-01-05 01:11:29 +01:00
Peter Steinberger 2899a986a8 feat(config): add default model shorthands 2026-01-05 01:11:29 +01:00
Peter Steinberger 7a63b4995b feat: opt-in login shell env fallback 2026-01-05 01:11:29 +01:00
Peter Steinberger 7a36e6fcd9 fix(discord): avoid duplicate block replies 2026-01-05 01:11:29 +01:00
Peter Steinberger 77b19643e2 fix: load global .env fallback 2026-01-05 01:11:29 +01:00
Josh Palmer aa45f512f4 fix sessions dir from state env
what: use CLAWDBOT_STATE_DIR/CLAWDIS_STATE_DIR for session transcripts

why: isolate multi-instance gateways

tests: not run
2026-01-05 00:51:11 +01:00
Peter Steinberger 59dfe0337d docs(changelog): note OpenAI duplicate reply fix 2026-01-05 00:39:34 +01:00
Peter Steinberger 4963432777 fix(discord): avoid duplicate replies on repeated message_end 2026-01-05 00:35:42 +01:00
Josh PalmerandClaude Opus 4.5 a737bfaab4 docs: make nix-clawdbot link more prominent
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 00:24:33 +01:00
Josh PalmerandClaude Opus 4.5 67c89e00c5 docs: add Nix installation guide and navigation
- Expand docs/nix.md from runtime-only to full installation guide
- Reference nix-clawdbot as the recommended Nix setup path
- Add "Installation" section to docs.json navigation (wizard, nix, docker, setup)
- Add Nix link to README quick links

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 00:22:15 +01:00
Peter Steinberger 8f572ab361 docs: add WhatsApp Business tip for same-phone setup 2026-01-04 23:17:26 +00:00
Peter Steinberger 435edaf997 fix: OpenAI tool schema compatibility 2026-01-05 00:15:55 +01:00
Peter Steinberger c3c9dee65e docs(tools): document agent tool allow/deny 2026-01-05 00:05:35 +01:00
Jake 946b32c842 fix(whatsapp): suppress typing during heartbeats
- Prevent typing indicator during heartbeat runs
- Add regression tests

Co-authored-by: Jake <mcinteerj@gmail.com>
2026-01-04 23:03:36 +00:00
Peter Steinberger 4dd515b65f fix(tools): honor agent tool denylist without sandbox 2026-01-05 00:02:14 +01:00
Andranik SahakyanandPeter Steinberger d9a9f6db7d fix(mac): add Sendable conformance to generated Swift protocol structs (#195)
* fix(mac): add Sendable conformance to generated Swift protocol structs

* fix(mac): make generated protocol types Sendable

* chore(mac): drop redundant Sendable extensions

* docs(changelog): thank @andranik-sahakyan for Sendable fix

* chore(swiftformat): exclude generated protocol models

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-01-04 22:39:21 +00:00
Jake a6f7ab499f style: fix lint issues 2026-01-05 11:25:14 +13:00
Jake 3c7c819e9b WhatsApp: mark offline/history messages as read 2026-01-05 10:23:22 +13:00
Jake 58dd2e9514 fix: also suppress typing indicators in agent-runner during heartbeats 2026-01-05 10:23:22 +13:00
Jake 424b7fe493 fix: prevent typing indicator during heartbeat background tasks 2026-01-05 10:23:22 +13:00
Peter Steinberger 1657c5e3d2 fix: route system events per session 2026-01-04 22:11:04 +01:00
Peter Steinberger 2ceceb8c25 style(ts): normalize type-only imports 2026-01-04 21:56:16 +01:00
Peter Steinberger 39be40cd23 chore(release): bump to 2026.1.5 2026-01-04 21:54:04 +01:00
Peter Steinberger 0faa200924 fix(onboarding): auto-build Control UI assets 2026-01-04 21:53:23 +01:00
Peter Steinberger ff605194ef fix(ui): render markdown in chat 2026-01-04 21:51:26 +01:00
Peter Steinberger 78998dba9e feat: add image model config + tool 2026-01-04 19:35:49 +01:00
Peter Steinberger 0716a624a8 chore(lint): apply biome fixes 2026-01-04 19:08:22 +01:00
Peter Steinberger e005dcb8e7 fix(oauth): derive oauth.json from state dir 2026-01-04 19:08:13 +01:00
Peter Steinberger 3300fba57c docs(discord): add bot creation guide 2026-01-04 19:01:04 +01:00
Nachx639andClaude Opus 4.5 fa3a768a3a fix(macos): remove authorizedWhenInUse references (iOS-only API) (#165)
CLAuthorizationStatus.authorizedWhenInUse only exists on iOS. On macOS,
location services only support .authorizedAlways. This was causing
compilation warnings and potentially incorrect behavior.

Fixes:
- GeneralSettings.swift: Remove authorizedWhenInUse check
- PermissionManager.swift: Update ensureLocation and status methods
- MacNodeRuntime.swift: Update location permission check

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 17:58:01 +00:00
Peter Steinberger da4f3211b8 chore: refresh version references 2026-01-04 18:49:36 +01:00
Peter Steinberger d85f91d247 feat: guide control ui access without gui 2026-01-04 18:49:36 +01:00
Peter Steinberger 5dcf43d6ad test: cover macos location permission status 2026-01-04 18:49:36 +01:00
Shadow 50cecd8210 Discord: remove duplicate message ids 2026-01-04 11:36:18 -06:00
Onur Solmaz 7dc8ea815e docs: add macOS launchd instructions for stopping gateway
Unify the "Processes keep restarting" FAQ section to cover both macOS
(launchd) and Linux (systemd). Previously only covered Linux.

Also update the "Clean uninstall" section with macOS commands.
2026-01-04 17:29:39 +00:00
Peter Steinberger 2110cac5d6 fix(cli): add config alias and reduce probe noise 2026-01-04 17:23:34 +00:00
Peter Steinberger 9eee832282 chore: update protocol swift models 2026-01-04 18:16:36 +01:00
Peter Steinberger 5d17b84e8a test(gateway): allow webchat chat.send without node 2026-01-04 17:12:49 +00:00
Peter Steinberger 3fed0ac2e8 fix(ui): show chat send errors 2026-01-04 17:12:49 +00:00
Peter Steinberger 2694e59ba6 fix(gateway): allow Control UI chat without node 2026-01-04 17:12:49 +00:00
Peter Steinberger 266fd748d0 fix(ui): allow Control UI chat without node 2026-01-04 17:12:49 +00:00
Peter Steinberger 564cc9359d style: swiftformat gateway models 2026-01-04 18:12:33 +01:00
Peter Steinberger ff46f8ce58 chore: format models CLI 2026-01-04 18:11:41 +01:00
Peter Steinberger 8e5153ba10 docs(changelog): add android notification tap fix 2026-01-04 18:05:26 +01:00
Peter Steinberger e2c6a96cd3 test(android): cover notification tap intent 2026-01-04 18:05:26 +01:00
Manuel Jiménez TorresandClaude Opus 4.5 7200dabfb2 feat(android): open app when tapping foreground service notification
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 18:05:26 +01:00
Peter Steinberger d923dc56ec fix: update ClawdBot Swift references 2026-01-04 17:57:53 +01:00
Peter Steinberger 5eb6b779f5 fix: macOS Swift cleanup 2026-01-04 17:57:53 +01:00
Peter Steinberger 0928e3c866 docs: update changelog for models CLI 2026-01-04 17:57:53 +01:00
Peter Steinberger 734bb6b4fd feat: add models scan and fallbacks 2026-01-04 17:57:52 +01:00
Peter Steinberger a2ba7ddf90 docs: add models cli plan 2026-01-04 17:57:35 +01:00
Cash Williams 64e656af82 fix: default elevated level to 'off' when not allowed
When elevatedAllowed is false (e.g., for heartbeat surface which isn't
in any allowFrom list), the elevated level was incorrectly defaulting
to 'on', causing bash commands to fail with 'elevated is not available'.

Now defaults to 'off' when elevated isn't allowed, so bash works
normally without trying to use elevated mode.

Fixes: https://github.com/clawdbot/clawdbot/issues/181
2026-01-04 17:36:14 +01:00
Peter Steinberger a2d7632cf3 docs: add changelog entry for cron tool fix 2026-01-04 17:18:29 +01:00
Clawd 17665d1732 fix(cron): pass 'id' instead of 'jobId' to gateway
The cron tool was passing { jobId } to the gateway for update/remove/run/runs
actions, but the gateway protocol schema expects { id }. This caused validation
errors when trying to update or remove cron jobs via the tool.

Fixes the parameter name while keeping the external tool API unchanged (still
accepts 'jobId' from callers).
2026-01-04 17:18:29 +01:00
545 changed files with 38149 additions and 7631 deletions

No files matched your search

+28
View File
@@ -0,0 +1,28 @@
---
name: Bug report
about: Report a problem or unexpected behavior in Clawdbot.
title: "[Bug]: "
labels: bug
---
## Summary
What went wrong?
## Steps to reproduce
1.
2.
3.
## Expected behavior
What did you expect to happen?
## Actual behavior
What actually happened?
## Environment
- Clawdbot version:
- OS:
- Install method (pnpm/npx/docker/etc):
## Logs or screenshots
Paste relevant logs or add screenshots (redact secrets).
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: Onboarding
url: https://discord.gg/clawd
about: New to Clawdbot? Join Discord for setup guidance from Krill in #help.
- name: Support
url: https://discord.gg/clawd
about: Get help from Krill and the community on Discord in #help.
+18
View File
@@ -0,0 +1,18 @@
---
name: Feature request
about: Suggest an idea or improvement for Clawdbot.
title: "[Feature]: "
labels: enhancement
---
## Summary
Describe the problem you are trying to solve or the opportunity you see.
## Proposed solution
What would you like Clawdbot to do?
## Alternatives considered
Any other approaches you have considered?
## Additional context
Links, screenshots, or related issues.
+2 -13
View File
@@ -52,32 +52,21 @@ jobs:
exit 1
- name: Setup Node.js
if: matrix.runtime == 'node'
uses: actions/setup-node@v4
with:
node-version: 24
check-latest: true
- name: Setup Bun
if: matrix.runtime == 'bun'
uses: oven-sh/setup-bun@v2
with:
# bun.sh downloads currently fail with:
# "Failed to list releases from GitHub: 401" -> "Unexpected HTTP response: 400"
bun-download-url: "https://github.com/oven-sh/bun/releases/latest/download/bun-linux-x64.zip"
- name: Setup Node.js (tooling for bun)
if: matrix.runtime == 'bun'
uses: actions/setup-node@v4
with:
node-version: 24
check-latest: true
bun-version: latest
- name: Runtime versions
run: |
node -v
npm -v
if [ "${{ matrix.runtime }}" = "bun" ]; then bun -v; fi
bun -v
- name: Capture node path
run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV"
+2
View File
@@ -3,6 +3,8 @@ node_modules
dist
*.bun-build
pnpm-lock.yaml
bun.lock
bun.lockb
coverage
.pnpm-store
.worktrees/
+1 -1
View File
@@ -48,4 +48,4 @@
--allman false
# Exclusions
--exclude .build,.swiftpm,DerivedData,node_modules,dist,coverage,xcuserdata,apps/macos/Sources/ClawdisProtocol
--exclude .build,.swiftpm,DerivedData,node_modules,dist,coverage,xcuserdata,apps/macos/Sources/ClawdisProtocol,apps/macos/Sources/ClawdbotProtocol
+18 -2
View File
@@ -6,8 +6,12 @@
- Docs: `docs/` (images, queue, Pi config). Built output lives in `dist/`.
## Build, Test, and Development Commands
- Runtime baseline: Node **22+** (keep Node + Bun paths working).
- Install deps: `pnpm install`
- Run CLI in dev: `pnpm clawdbot ...` (tsx entry) or `pnpm dev` for `src/index.ts`.
- Also supported: `bun install` (keep `pnpm-lock.yaml` + Bun patching in sync when touching deps/patches).
- Prefer Bun for TypeScript execution (scripts, dev, tests): `bun <file.ts>` / `bunx <tool>`.
- Run CLI in dev: `pnpm clawdbot ...` (bun) or `pnpm dev`.
- Node remains supported for running built output (`dist/*`) and production installs.
- Type-check/build: `pnpm build` (tsc)
- Lint/format: `pnpm lint` (biome check), `pnpm format` (biome format)
- Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage`
@@ -30,6 +34,16 @@
- Follow concise, action-oriented commit messages (e.g., `CLI: add verbose flag to send`).
- Group related changes; avoid bundling unrelated refactors.
- PRs should summarize scope, note testing performed, and mention any user-facing changes or new flags.
- PR review flow: when given a PR link, review via `gh pr view`/`gh pr diff` and do **not** change branches.
- PR merge flow: create a temp branch from `main`, merge the PR branch into it (prefer squash unless commit history is important; use rebase/merge when it is). Always try to merge the PR unless it’s truly difficult, then use another approach. If we squash, add the PR author as a co-contributor. Apply fixes, add changelog entry (include PR # + thanks), run full gate before the final commit, commit, merge back to `main`, delete the temp branch, and end on `main`.
- When working on a PR: add a changelog entry with the PR number and thank the contributor.
- When working on an issue: reference the issue in the changelog entry.
- When merging a PR: leave a PR comment that explains exactly what we did and include the SHA hashes.
- When merging a PR from a new contributor: add their avatar to the README “Thanks to all clawtributors” thumbnail list.
### PR Workflow (Review vs Land)
- **Review mode (PR link only):** read `gh pr view/diff`; **do not** switch branches; **do not** change code.
- **Landing mode:** create an integration branch from `main`, bring in PR commits (**prefer rebase** for linear history; **merge allowed** when complexity/conflicts make it safer), apply fixes, add changelog (+ thanks + PR #), run full gate **locally before committing** (`pnpm lint && pnpm build && pnpm test`), commit, merge back to `main`, then `git switch main` (never stay on a topic branch after landing).
## Security & Configuration Tips
- Web provider stores creds at `~/.clawdbot/credentials/`; rerun `clawdbot login` if logged out.
@@ -45,13 +59,15 @@
- **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; regenerate via `pnpm canvas:a2ui:bundle` (or `scripts/bundle-a2ui.sh`) instead of manual conflict resolution.
- 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.
- 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 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.
+181
View File
@@ -2,6 +2,186 @@
**Why this looks different:** the project was renamed from **Clawdis → Clawdbot**. To make the transition clear, releases now use **date-based versions** (`YYYY.M.D`) and the changelog is **compressed** into milestone summaries. Full detail still lives in git history and the docs.
## Unreleased
### Breaking
- **SECURITY (update ASAP):** inbound DMs are now **locked down by default** on Telegram/WhatsApp/Signal/iMessage/Discord/Slack.
- Previously, if you didn’t configure an allowlist, your bot could be **open to anyone** (especially discoverable Telegram bots).
- 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 ...`).
- 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.
### Fixes
- 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.
- Build: import tool-display JSON as a module instead of runtime file reads. Thanks @mukhtharcm for PR #312.
- 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.
- Telegram: stop typing after tool results. Thanks @AbhisekBasu1 for PR #322.
- 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: flag error payloads and improve Bun socket error messaging. Thanks @emanuelst for PR #331.
- 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.
- 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`).
- 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.
- Typing indicators: stop typing once the reply dispatcher drains to prevent stuck typing across Discord/Telegram/WhatsApp.
- Typing indicators: fix a race that could keep the typing indicator stuck after quick replies. Thanks @thewilloftheshadow for PR #270.
- Google: merge consecutive messages to satisfy strict role alternation for Google provider models. Thanks @Asleep123 for PR #266.
- Postinstall: handle targetDir symlinks in the install script. Thanks @obviyus for PR #272.
- WhatsApp/Telegram: add groupPolicy handling for group messages and normalize allowFrom matching (tg/telegram prefixes). Thanks @mneves75.
- Auto-reply: add configurable ack reactions for inbound messages (default 👀 or `identity.emoji`) with scope controls. Thanks @obviyus for PR #178.
- Polls: unify WhatsApp + Discord poll sends via the gateway + CLI (`clawdbot poll`). (#123) — thanks @dbhurley
- Onboarding: resolve CLI entrypoint when running via `npx` so gateway daemon install works without a build step.
- Onboarding: when OpenAI Codex OAuth is used, default to `openai-codex/gpt-5.2` and warn if the selected model lacks auth.
- CLI: auto-migrate legacy config entries on command start (same behavior as gateway startup).
- Gateway: add `gateway stop|restart` helpers and surface launchd/systemd/schtasks stop hints when the gateway is already running.
- 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.
- 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.
- 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: 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.
- macOS: drop deprecated `afterMs` from agent wait params to match gateway schema.
- Auth: add OpenAI Codex OAuth support and migrate legacy oauth.json into auth.json.
- Model: `/model` list shows auth source (masked key or OAuth email) per provider.
- Model: `/model list` is an alias for `/model`.
- Model: `/model` output now includes auth source location (env/auth.json/models.json).
- 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.
- 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.
- Control UI: animate reading indicator dots (honors reduced-motion).
- Control UI: stabilize chat streaming during tool runs (no flicker/vanishing text; correct run scoping).
- Control UI: let config-form enums select empty-string values. Thanks @sreekaransrinath for PR #268.
- Control UI: scroll chat to bottom on initial load. Thanks @kiranjd for PR #274.
- Control UI: add Chat focus mode toggle to collapse header + sidebar.
- Control UI: tighten focus mode spacing (reduce top padding, add comfortable compose inset).
- Control UI: standardize UI build instructions on `bun run ui:*` (fallback supported).
- Status: show runtime (docker/direct) and move shortcuts to `/help`.
- Status: show model auth source (api-key/oauth).
- Status: fix zero token counters for Anthropic (Opus) sessions by normalizing usage fields and ignoring empty usage updates.
- Block streaming: avoid splitting Markdown fenced blocks and reopen fences when forced to split.
- 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.
- 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.
- Slack: keep auto-replies in the original thread when responding to thread messages. Thanks @scald for PR #251.
- Slack: send typing status updates via assistant threads. Thanks @thewilloftheshadow for PR #320.
- 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.
- 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.
- 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.
- Telegram: support media groups (multi-image messages). Thanks @obviyus for PR #220.
- Telegram/WhatsApp: parse shared locations (pins, places, live) and expose structured ctx fields. Thanks @nachoiacovino for PR #194.
- Auto-reply: block unauthorized `/reset` and infer WhatsApp senders from E.164 inputs.
- Auto-reply: reset corrupted Gemini sessions when function-call ordering breaks. Thanks @VACInc for PR #297.
- Auto-reply: track compaction count in session status; verbose mode announces auto-compactions.
- Telegram: notify users when inbound media exceeds size limits. Thanks @jarvis-medmatic for PR #283.
- Telegram: send GIF media as animations (auto-play) and improve filename sniffing.
- Bash tool: inherit gateway PATH so Nix-provided tools resolve during commands. Thanks @joshp123 for PR #202.
- Delivery chunking: keep Markdown fenced code blocks valid when splitting long replies (close + reopen fences).
- Auth: prefer OAuth profiles over API keys during round-robin selection (prevents OAuth “lost after one message” when both are configured).
- Models: extend `clawdbot models` status output with a masked auth overview (profiles, env sources, and OAuth counts).
### Maintenance
- 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.
- Deps: bump pi-* stack, Slack SDK, discord-api-types, file-type, zod, and Biome.
- Skills: add CodexBar model usage helper with macOS requirement metadata.
- Skills: add 1Password CLI skill with op examples.
- Lint: organize imports and wrap long lines in reply commands.
- Refactor: centralize group allowlist/mention policy across providers.
- Deps: update to latest across the repo.
## 2026.1.5-3
### Fixes
- NPM package: include missing runtime dist folders (slack/signal/imessage/tui/wizard/control-ui/daemon) to avoid `ERR_MODULE_NOT_FOUND` in Node 25 npx installs.
## 2026.1.5-2
### Fixes
- NPM package: include `dist/sessions` so `clawdbot agent` resolves session helpers in npx installs.
- Node 25: avoid unsupported directory import by targeting `qrcode-terminal/vendor/QRCode/*.js` modules.
## 2026.1.5-1
### Fixes
- NPM package: include `dist/sessions` so `clawdbot agent` resolves session helpers in npx installs.
- Node 25: avoid unsupported directory import by targeting `qrcode-terminal/vendor/QRCode/index.js`.
## 2026.1.5
### Highlights
- Models: add image-specific model config (`agent.imageModel` + fallbacks) and scan support.
- Agent tools: new `image` tool routed to the image model (when configured).
- Config: default model shorthands (`opus`, `sonnet`, `gpt`, `gpt-mini`, `gemini`, `gemini-flash`).
- Docs: document built-in model shorthands + precedence (user config wins).
- Bun: optional local install/build workflow without maintaining a Bun lockfile (see `docs/bun.md`).
### Fixes
- Control UI: render Markdown in tool result cards.
- Control UI: prevent overlapping action buttons in Discord guild rules on narrow layouts.
- Android: tapping the foreground service notification brings the app to the front. (#179) — thanks @Syhids
- Cron tool uses `id` for update/remove/run/runs (aligns with gateway params). (#180) — thanks @adamgall
- Control UI: chat view uses page scroll with sticky header/sidebar and fixed composer (no inner scroll frame).
- macOS: treat location permission as always-only to avoid iOS-only enums. (#165) — thanks @Nachx639
- macOS: make generated gateway protocol models `Sendable` for Swift 6 strict concurrency. (#195) — thanks @andranik-sahakyan
- macOS: bundle QR code renderer modules so DMG gateway boot doesn't crash on missing qrcode-terminal vendor files.
- macOS: parse JSON5 config safely to avoid wiping user settings when comments are present.
- WhatsApp: suppress typing indicator during heartbeat background tasks. (#190) — thanks @mcinteerj
- WhatsApp: mark offline history sync messages as read without auto-reply. (#193) — thanks @mcinteerj
- Discord: avoid duplicate replies when a provider emits late streaming `text_end` events (OpenAI/GPT).
- CLI: use tailnet IP for local gateway calls when bind is tailnet/auto (fixes #176).
- Env: load global `$CLAWDBOT_STATE_DIR/.env` (`~/.clawdbot/.env`) as a fallback after CWD `.env`.
- Env: optional login-shell env fallback (opt-in; imports expected keys without overriding existing env).
- Agent tools: OpenAI-compatible tool JSON Schemas (fix `browser`, normalize union schemas).
- Onboarding: when running from source, auto-build missing Control UI assets (`bun run ui:build`).
- Discord/Slack: route reaction + system notifications to the correct session (no main-session bleed).
- Agent tools: honor `agent.tools` allow/deny policy even when sandbox is off.
- Discord: avoid duplicate replies when OpenAI emits repeated `message_end` events.
- Commands: unify /status (inline) and command auth across providers; group bypass for authorized control commands; remove Discord /clawd slash handler.
- CLI: run `clawdbot agent` via the Gateway by default; use `--local` to force embedded mode.
## 2026.1.5
### Fixes
- Control UI: render Markdown in chat messages (sanitized).
## 2026.1.4
### Highlights
@@ -10,6 +190,7 @@
- Gateway quality-of-life: config hot reload, port config support, and Control UI base paths.
- Sandbox additions: per-session Docker sandbox with hardened limits + optional sandboxed Chromium.
- New node capability: `location.get` across macOS/iOS/Android (CLI + tools).
- Models CLI: scan OpenRouter free models (tools/images), manage aliases/fallbacks, and show last-used model in status.
### Breaking
- Tool names drop the `clawdbot_` prefix (`browser`, `canvas`, `nodes`, `cron`, `gateway`).
+4
View File
@@ -1,5 +1,9 @@
FROM node:22-bookworm
# Install Bun (required for build scripts)
RUN curl -fsSL https://bun.sh/install | bash
ENV PATH="/root/.bun/bin:${PATH}"
RUN corepack enable
WORKDIR /app
Submodule Peekaboo updated: 5c8eedd642...c1243a7978.
+304 -133
View File
@@ -16,185 +16,267 @@
</p>
**Clawdbot** is a *personal AI assistant* you run on your own devices.
It answers you on the surfaces you already use (WhatsApp, Telegram, Discord, iMessage, WebChat), can speak and listen on macOS/iOS, and can render a live Canvas you control. The Gateway is just the control plane — the product is the assistant.
It answers you on the providers you already use (WhatsApp, Telegram, Slack, Discord, Signal, iMessage, WebChat), can speak and listen on macOS/iOS/Android, and can render a live Canvas you control. The Gateway is just the control plane — the product is the assistant.
If you want a personal, single-user assistant that feels local, fast, and always-on, this is it.
Website: https://clawd.me · Docs: [`docs/index.md`](docs/index.md) · FAQ: [`docs/faq.md`](docs/faq.md) · Wizard: [`docs/wizard.md`](docs/wizard.md) · Docker (optional): [`docs/docker.md`](docs/docker.md) · Discord: https://discord.gg/clawd
[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, Windows, and Linux**.
Works with npm, pnpm, or bun.
New install? Start here: https://docs.clawd.bot/getting-started
Using Claude Pro/Max subscription? See `docs/onboarding.md` for the Anthropic OAuth setup.
**Subscriptions (OAuth):**
- **Anthropic** (Claude Pro/Max)
- **OpenAI** (ChatGPT/Codex)
Model note: while any model is supported, I strongly recommend **Anthropic Pro/Max (100/200) + Opus 4.5** for long‑context strength and better prompt‑injection resistance. See [Onboarding](https://docs.clawd.bot/onboarding).
## Models (selection + auth)
- Models config + CLI: https://docs.clawd.bot/models
- Auth profile rotation (OAuth vs API keys) + fallbacks: https://docs.clawd.bot/model-failover
## Recommended setup (from source)
Do **not** download prebuilt binaries. Run from source.
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
bun install
bun run ui:install
bun run ui:build
bun run build
bun run clawdbot onboard
```
Note: `bun run clawdbot ...` runs TypeScript directly. `bun run build` produces `dist/` for running via Node / the packaged `clawdbot` binary.
## Quick start (TL;DR)
Runtime: **Node ≥22**.
Full beginner guide (auth, pairing, providers): https://docs.clawd.bot/getting-started
```bash
bun run clawdbot onboard
bun run clawdbot gateway --port 18789 --verbose
# Dev loop (auto-reload on TS changes)
bun run gateway:watch
# Send a message
bun run clawdbot send --to +1234567890 --message "Hello from Clawdbot"
# Talk to the assistant (optionally deliver back to WhatsApp/Telegram/Slack/Discord)
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 `bun run clawdbot …` or `pnpm clawdbot …` (not global `clawdbot`).
## Security defaults (DM access)
Clawdbot connects to real messaging surfaces. Treat inbound DMs as **untrusted input**.
Full security guide: https://docs.clawd.bot/security
Default behavior on Telegram/WhatsApp/Signal/iMessage/Discord/Slack:
- **DM pairing** (`dmPolicy="pairing"` / `discord.dm.policy="pairing"` / `slack.dm.policy="pairing"`): unknown senders receive a short pairing code and the bot does not process their message.
- Approve with: `clawdbot pairing approve --provider <provider> <code>` (then the sender is added to a local allowlist store).
- Public inbound DMs require an explicit opt-in: set `dmPolicy="open"` and include `"*"` in the provider allowlist (`allowFrom` / `discord.dm.allowFrom` / `slack.dm.allowFrom`).
Run `clawdbot doctor` to surface risky/misconfigured DM policies.
## Highlights
- **Local-first Gateway** — single control plane for sessions, providers, tools, and events.
- **Multi-surface inbox** — WhatsApp, Telegram, Discord, iMessage, WebChat, macOS, iOS/Android.
- **Voice Wake + Talk Mode** — always-on speech for macOS/iOS/Android with ElevenLabs.
- **Live Canvas** — agent-driven visual workspace with A2UI.
- **First-class tools** — browser, canvas, nodes, cron, sessions, and Discord actions.
- **Companion apps** — macOS menu bar app + iOS/Android nodes.
- **Onboarding + skills** — wizard-driven setup with bundled/managed/workspace skills.
- **[Local-first Gateway](https://docs.clawd.bot/gateway)** — single control plane for sessions, providers, tools, and events.
- **[Multi-provider inbox](https://docs.clawd.bot/surface)** — WhatsApp, Telegram, Slack, Discord, Signal, iMessage, WebChat, macOS, iOS/Android.
- **[Multi-agent routing](https://docs.clawd.bot/configuration)** — route inbound providers/accounts/peers to isolated agents (workspaces + per-agent sessions).
- **[Voice Wake](https://docs.clawd.bot/voicewake) + [Talk Mode](https://docs.clawd.bot/talk)** — always-on speech for macOS/iOS/Android with ElevenLabs.
- **[Live Canvas](https://docs.clawd.bot/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.clawd.bot/mac/canvas#canvas-a2ui).
- **[First-class tools](https://docs.clawd.bot/tools)** — browser, canvas, nodes, cron, sessions, and Discord/Slack actions.
- **[Companion apps](https://docs.clawd.bot/macos)** — macOS menu bar app + iOS/Android [nodes](https://docs.clawd.bot/nodes).
- **[Onboarding](https://docs.clawd.bot/wizard) + [skills](https://docs.clawd.bot/skills)** — wizard-driven setup with bundled/managed/workspace skills.
## Everything we built so far
### Core platform
- Gateway WS control plane with sessions, presence, config, cron, webhooks, control UI, and Canvas host.
- CLI surface: gateway, agent, send, wizard, doctor/update, and TUI.
- Pi agent runtime in RPC mode with tool streaming and block streaming.
- Session model: `main` for direct chats, group isolation, activation modes, queue modes, reply-back.
- Media pipeline: images/audio/video, transcription hooks, size caps, temp file lifecycle.
- [Gateway WS control plane](https://docs.clawd.bot/gateway) with sessions, presence, config, cron, webhooks, [Control UI](https://docs.clawd.bot/web), and [Canvas host](https://docs.clawd.bot/mac/canvas#canvas-a2ui).
- [CLI surface](https://docs.clawd.bot/agent-send): gateway, agent, send, [wizard](https://docs.clawd.bot/wizard), and [doctor](https://docs.clawd.bot/doctor).
- [Pi agent runtime](https://docs.clawd.bot/agent) in RPC mode with tool streaming and block streaming.
- [Session model](https://docs.clawd.bot/session): `main` for direct chats, group isolation, activation modes, queue modes, reply-back. Group rules: [Groups](https://docs.clawd.bot/groups).
- [Media pipeline](https://docs.clawd.bot/images): images/audio/video, transcription hooks, size caps, temp file lifecycle. Audio details: [Audio](https://docs.clawd.bot/audio).
### Surfaces + providers
- WhatsApp (Baileys), Telegram (grammY), Discord (discord.js), Signal (signal-cli), iMessage (imsg), WebChat.
- Group mention gating, reply tags, per-surface chunking and routing.
### Providers
- [Providers](https://docs.clawd.bot/surface): [WhatsApp](https://docs.clawd.bot/whatsapp) (Baileys), [Telegram](https://docs.clawd.bot/telegram) (grammY), [Slack](https://docs.clawd.bot/slack) (Bolt), [Discord](https://docs.clawd.bot/discord) (discord.js), [Signal](https://docs.clawd.bot/signal) (signal-cli), [iMessage](https://docs.clawd.bot/imessage) (imsg), [WebChat](https://docs.clawd.bot/webchat).
- [Group routing](https://docs.clawd.bot/group-messages): mention gating, reply tags, per-provider chunking and routing. Provider rules: [Providers](https://docs.clawd.bot/surface).
### Apps + nodes
- macOS app: menu bar control plane, Voice Wake/PTT, Talk Mode overlay, WebChat, Debug tools, SSH remote gateway control.
- iOS node: Canvas, Voice Wake, Talk Mode, camera, screen recording, Bonjour pairing.
- Android node: Canvas, Talk Mode, camera, screen recording, optional SMS.
- macOS node mode: system.run/notify + canvas/camera exposure.
- [macOS app](https://docs.clawd.bot/macos): menu bar control plane, [Voice Wake](https://docs.clawd.bot/voicewake)/PTT, [Talk Mode](https://docs.clawd.bot/talk) overlay, [WebChat](https://docs.clawd.bot/webchat), debug tools, [remote gateway](https://docs.clawd.bot/remote) control.
- [iOS node](https://docs.clawd.bot/ios): [Canvas](https://docs.clawd.bot/mac/canvas), [Voice Wake](https://docs.clawd.bot/voicewake), [Talk Mode](https://docs.clawd.bot/talk), camera, screen recording, Bonjour pairing.
- [Android node](https://docs.clawd.bot/android): [Canvas](https://docs.clawd.bot/mac/canvas), [Talk Mode](https://docs.clawd.bot/talk), camera, screen recording, optional SMS.
- [macOS node mode](https://docs.clawd.bot/nodes): system.run/notify + canvas/camera exposure.
### Tools + automation
- Browser control: dedicated clawd Chrome/Chromium, snapshots, actions, uploads, profiles.
- Canvas: A2UI push/reset, eval, snapshot.
- Nodes: camera snap/clip, screen record, location.get, notifications.
- Cron + wakeups; webhooks; Gmail Pub/Sub triggers.
- Skills platform: bundled, managed, and workspace skills with install gating + UI.
- [Browser control](https://docs.clawd.bot/browser): dedicated clawd Chrome/Chromium, snapshots, actions, uploads, profiles.
- [Canvas](https://docs.clawd.bot/mac/canvas): [A2UI](https://docs.clawd.bot/mac/canvas#canvas-a2ui) push/reset, eval, snapshot.
- [Nodes](https://docs.clawd.bot/nodes): camera snap/clip, screen record, [location.get](https://docs.clawd.bot/location-command), notifications.
- [Cron + wakeups](https://docs.clawd.bot/cron); [webhooks](https://docs.clawd.bot/webhook); [Gmail Pub/Sub](https://docs.clawd.bot/gmail-pubsub).
- [Skills platform](https://docs.clawd.bot/skills): bundled, managed, and workspace skills with install gating + UI.
### Ops + packaging
- Control UI + WebChat served directly from the Gateway.
- Tailscale Serve/Funnel or SSH tunnels with token/password auth.
- Nix mode for declarative config; Docker-based installs.
- Health, doctor migrations, structured logging, release tooling.
- [Control UI](https://docs.clawd.bot/web) + [WebChat](https://docs.clawd.bot/webchat) served directly from the Gateway.
- [Tailscale Serve/Funnel](https://docs.clawd.bot/tailscale) or [SSH tunnels](https://docs.clawd.bot/remote) with token/password auth.
- [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).
## Changes since 2.0.0-beta5 (2026-01-03)
#### Gateway service (optional)
### Highlights
- Project rename completed: CLIs, paths, bundle IDs, env vars, and docs unified on Clawdbot.
- Agent-to-agent relay: `sessions_send` ping‑pong with `REPLY_SKIP` plus announce step with `ANNOUNCE_SKIP`.
- Gateway config hot reload, configurable port, and Control UI base-path support.
- Sandbox options: per-session Docker sandbox with hardened limits + optional sandboxed Chromium.
- New node capability: `location.get` across macOS/iOS/Android (CLI + tools).
Install a supervised service (launchd/systemd/Scheduled Task):
### Fixes
- Presence beacons keep node lists fresh; Instances view stays accurate.
- Block streaming + chunking reliability (Telegram/Discord ordering, fewer duplicates).
- WhatsApp GIF playback for MP4-based GIFs.
- Onboarding/Control UI basePath handling fixes + UI polish.
- Cleaner logging + clearer tool summaries.
```bash
clawdbot gateway install
clawdbot gateway service-status
```
### Breaking
- Tool names drop the `clawdbot_` prefix (`browser`, `canvas`, `nodes`, `cron`, `gateway`).
- Bash tool removed `stdinMode: "pty"` support (use tmux for real TTYs).
- Primary session key is fixed to `main` (or `global` for global scope).
## Project rename + changelog format
Clawdis → Clawdbot. The rename touched every surface, path, and bundle ID. To make that transition explicit, releases now use **date-based versions** (`YYYY.M.D`), and the changelog is compressed into milestone summaries instead of long semver trains. Full detail still lives in git history and the docs.
Aliases: `clawdbot service ...` or `clawdbot daemon ...`.
## How it works (short)
```
Your surfaces
│
▼
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / WebChat
│
▼
┌───────────────────────────────┐
│ Gateway │ ws://127.0.0.1:18789
│ (control plane) │ tcp://0.0.0.0:18790 (optional Bridge)
│ (control plane) │ bridge: tcp://0.0.0.0:18790
└──────────────┬────────────────┘
│
├─ Pi agent (RPC)
├─ CLI (clawdbot …)
├─ WebChat (browser)
├─ macOS app (Clawdbot.app)
└─ iOS node (Canvas + voice)
├─ WebChat UI
├─ macOS app
└─ iOS/Android nodes
```
## Quick start (from source)
## Key subsystems
Runtime: **Node ≥22** + **pnpm**.
- **[Gateway WebSocket network](https://docs.clawd.bot/architecture)** — single WS control plane for clients, tools, and events (plus ops: [Gateway runbook](https://docs.clawd.bot/gateway)).
- **[Tailscale exposure](https://docs.clawd.bot/tailscale)** — Serve/Funnel for the Gateway dashboard + WS (remote access: [Remote](https://docs.clawd.bot/remote)).
- **[Browser control](https://docs.clawd.bot/browser)** — clawd‑managed Chrome/Chromium with CDP control.
- **[Canvas + A2UI](https://docs.clawd.bot/mac/canvas)** — agent‑driven visual workspace (A2UI host: [Canvas/A2UI](https://docs.clawd.bot/mac/canvas#canvas-a2ui)).
- **[Voice Wake](https://docs.clawd.bot/voicewake) + [Talk Mode](https://docs.clawd.bot/talk)** — always‑on speech and continuous conversation.
- **[Nodes](https://docs.clawd.bot/nodes)** — Canvas, camera snap/clip, screen record, `location.get`, notifications, plus macOS‑only `system.run`/`system.notify`.
```bash
pnpm install
pnpm build
pnpm ui:build
## Tailscale access (Gateway dashboard)
# Recommended: run the onboarding wizard
pnpm clawdbot onboard
Clawdbot can auto-configure Tailscale **Serve** (tailnet-only) or **Funnel** (public) while the Gateway stays bound to loopback. Configure `gateway.tailscale.mode`:
# Link WhatsApp (stores creds in ~/.clawdbot/credentials)
pnpm clawdbot login
- `off`: no Tailscale automation (default).
- `serve`: tailnet-only HTTPS via `tailscale serve` (uses Tailscale identity headers by default).
- `funnel`: public HTTPS via `tailscale funnel` (requires shared password auth).
# Start the gateway
pnpm clawdbot gateway --port 18789 --verbose
Notes:
- `gateway.bind` must stay `loopback` when Serve/Funnel is enabled (Clawdbot enforces this).
- Serve can be forced to require a password by setting `gateway.auth.mode: "password"` or `gateway.auth.allowTailscale: false`.
- Funnel refuses to start unless `gateway.auth.mode: "password"` is set.
- Optional: `gateway.tailscale.resetOnExit` to undo Serve/Funnel on shutdown.
# Dev loop (auto-reload on TS changes)
pnpm gateway:watch
Details: [Tailscale guide](https://docs.clawd.bot/tailscale) · [Web surfaces](https://docs.clawd.bot/web)
# Send a message
pnpm clawdbot send --to +1234567890 --message "Hello from Clawdbot"
## Remote Gateway (Linux is great)
# Talk to the assistant (optionally deliver back to WhatsApp/Telegram/Discord)
pnpm clawdbot agent --message "Ship checklist" --thinking high
```
It’s perfectly fine to run the Gateway on a small Linux instance. Clients (macOS app, CLI, WebChat) can connect over **Tailscale Serve/Funnel** or **SSH tunnels**, and you can still pair device nodes (macOS/iOS/Android) to execute device‑local actions when needed.
If you run from source, prefer `pnpm clawdbot …` (not global `clawdbot`).
- **Gateway host** runs the bash tool and provider connections by default.
- **Device nodes** run device‑local actions (`system.run`, camera, screen recording, notifications) via `node.invoke`.
In short: bash runs where the Gateway lives; device actions run where the device lives.
Details: [Remote access](https://docs.clawd.bot/remote) · [Nodes](https://docs.clawd.bot/nodes) · [Security](https://docs.clawd.bot/security)
## macOS permissions via the Gateway protocol
The macOS app can run in **node mode** and advertises its capabilities + permission map over the Gateway WebSocket (`node.list` / `node.describe`). Clients can then execute local actions via `node.invoke`:
- `system.run` runs a local command and returns stdout/stderr/exit code; set `needsScreenRecording: true` to require screen-recording permission (otherwise you’ll get `PERMISSION_MISSING`).
- `system.notify` posts a user notification and fails if notifications are denied.
- `canvas.*`, `camera.*`, `screen.record`, and `location.get` are also routed via `node.invoke` and follow TCC permission status.
Elevated bash (host permissions) is separate from macOS TCC:
- Use `/elevated on|off` to toggle per‑session elevated access when enabled + allowlisted.
- Gateway persists the per‑session toggle via `sessions.patch` (WS method) alongside `thinkingLevel`, `verboseLevel`, `model`, `sendPolicy`, and `groupActivation`.
Details: [Nodes](https://docs.clawd.bot/nodes) · [macOS app](https://docs.clawd.bot/macos) · [Gateway protocol](https://docs.clawd.bot/architecture)
## Agent to Agent (sessions_* tools)
- Use these to coordinate work across sessions without jumping between chat surfaces.
- `sessions_list` — discover active sessions (agents) and their metadata.
- `sessions_history` — fetch transcript logs for a session.
- `sessions_send` — message another session; optional reply‑back ping‑pong + announce step (`REPLY_SKIP`, `ANNOUNCE_SKIP`).
Details: [Session tools](https://docs.clawd.bot/session-tool)
## Skills registry (ClawdHub)
ClawdHub is a minimal skill registry. With ClawdHub enabled, the agent can search for skills automatically and pull in new ones as needed.
https://ClawdHub.com
## Chat commands
Send these in WhatsApp/Telegram/WebChat (group commands are owner-only):
Send these in WhatsApp/Telegram/Slack/WebChat (group commands are owner-only):
- `/status` — health + session info (group shows activation mode)
- `/new` or `/reset` — reset the session
- `/compact` — compact session context (summary)
- `/think <level>` — off|minimal|low|medium|high
- `/verbose on|off`
- `/restart` — restart the gateway (owner-only in groups)
- `/activation mention|always` — group activation toggle (groups only)
## Architecture
## macOS app (optional)
### TypeScript Gateway (src/gateway/server.ts)
- **Single HTTP+WS server** on `ws://127.0.0.1:18789` (bind policy: loopback/lan/tailnet/auto). The first frame must be `connect`; AJV validates frames against TypeBox schemas (`src/gateway/protocol`).
- **Single source of truth** for sessions, providers, cron, voice wake, and presence. Methods cover `send`, `agent`, `chat.*`, `sessions.*`, `config.*`, `cron.*`, `voicewake.*`, `node.*`, `system-*`, `wake`.
- **Events + snapshot**: handshake returns a snapshot (presence/health) and declares event types; runtime events include `agent`, `chat`, `presence`, `tick`, `health`, `heartbeat`, `cron`, `node.pair.*`, `voicewake.changed`, `shutdown`.
- **Idempotency & safety**: `send`/`agent`/`chat.send` require idempotency keys with a TTL cache (5 min, cap 1000) to avoid double‑sends on reconnects; payload sizes are capped per connection.
- **Bridge for nodes**: optional TCP bridge (`src/infra/bridge/server.ts`) is newline‑delimited JSON frames (`hello`, pairing, RPC, `invoke`); node connect/disconnect is surfaced into presence.
- **Control UI + Canvas Host**: HTTP serves Control UI assets (default `/`, optional base path) and can host a live‑reload Canvas host for nodes (`src/canvas-host/server.ts`), injecting the A2UI postMessage bridge.
The Gateway alone delivers a great experience. All apps are optional and add extra features.
### iOS app (apps/ios)
- **Discovery + pairing**: Bonjour discovery via `BridgeDiscoveryModel` (NWBrowser). `BridgeConnectionController` auto‑connects using Keychain token or allows manual host/port.
- **Node runtime**: `BridgeSession` (actor) maintains the `NWConnection`, hello handshake, ping/pong, RPC requests, and `invoke` callbacks.
- **Capabilities + commands**: advertises `canvas`, `screen`, `camera`, `voiceWake` (settings‑driven) and executes `canvas.*`, `canvas.a2ui.*`, `camera.*`, `screen.record` (`NodeAppModel.handleInvoke`).
- **Canvas**: `WKWebView` with bundled Canvas scaffold + A2UI, JS eval, snapshot capture, and `clawdbot://` deep‑link interception (`ScreenController`).
- **Voice + deep links**: voice wake sends `voice.transcript` events; `clawdbot://agent` links emit `agent.request`. Voice wake triggers sync via `voicewake.get` + `voicewake.changed`.
If you plan to build/run companion apps, initialize submodules first:
## Companion apps
```bash
git submodule update --init --recursive
./scripts/restart-mac.sh
```
The **macOS app is critical**: it runs the menu‑bar control plane, owns local permissions (TCC), hosts Voice Wake, exposes WebChat/debug tools, and coordinates local/remote gateway mode. Most “assistant” UX lives here.
### macOS (Clawdbot.app)
### macOS (Clawdbot.app) (optional)
- Menu bar control for the Gateway and health.
- Voice Wake + push-to-talk overlay.
- WebChat + debug tools.
- Remote gateway control over SSH.
Build/run: `./scripts/restart-mac.sh` (packages + launches).
Note: signed builds required for macOS permissions to stick across rebuilds (see `docs/mac/permissions.md`).
### iOS node (internal)
### iOS node (optional)
- Pairs as a node via the Bridge.
- Voice trigger forwarding + Canvas surface.
- Controlled via `clawdbot nodes …`.
Runbook: `docs/ios/connect.md`.
Runbook: [iOS connect](https://docs.clawd.bot/ios).
### Android node (internal)
### Android node (optional)
- Pairs via the same Bridge + pairing flow as iOS.
- Exposes Canvas, Camera, and Screen capture commands.
- Runbook: `docs/android/connect.md`.
- Runbook: [Android connect](https://docs.clawd.bot/android).
## Agent workspace + skills
@@ -204,25 +286,36 @@ Runbook: `docs/ios/connect.md`.
## Configuration
Minimal `~/.clawdbot/clawdbot.json`:
Minimal `~/.clawdbot/clawdbot.json` (model + defaults):
```json5
{
whatsapp: {
allowFrom: ["+1234567890"]
agent: {
model: "anthropic/claude-opus-4-5"
}
}
```
### WhatsApp
[Full configuration reference (all keys + examples).](https://docs.clawd.bot/configuration)
## Security model (important)
- **Default:** tools run on the host for the **main** session, so the agent has full access when it’s just you.
- **Group/channel safety:** set `agent.sandbox.mode: "non-main"` to run **non‑main sessions** (groups/channels) inside per‑session Docker sandboxes; bash then runs in Docker for those sessions.
- **Sandbox defaults:** allowlist `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`; denylist `browser`, `canvas`, `nodes`, `cron`, `discord`, `gateway`.
Details: [Security guide](https://docs.clawd.bot/security) · [Docker + sandboxing](https://docs.clawd.bot/docker) · [Sandbox config](https://docs.clawd.bot/configuration)
### [WhatsApp](https://docs.clawd.bot/whatsapp)
- Link the device: `pnpm clawdbot login` (stores creds in `~/.clawdbot/credentials`).
- Allowlist who can talk to the assistant via `whatsapp.allowFrom`.
- If `whatsapp.groups` is set, it becomes a group allowlist; include `"*"` to allow all.
### Telegram
### [Telegram](https://docs.clawd.bot/telegram)
- Set `TELEGRAM_BOT_TOKEN` or `telegram.botToken` (env wins).
- Optional: set `telegram.groups` (with `telegram.groups."*".requireMention`), `telegram.allowFrom`, or `telegram.webhookUrl` as needed.
- Optional: set `telegram.groups` (with `telegram.groups."*".requireMention`); when set, it is a group allowlist (include `"*"` to allow all). Also `telegram.allowFrom` or `telegram.webhookUrl` as needed.
```json5
{
@@ -232,10 +325,14 @@ Minimal `~/.clawdbot/clawdbot.json`:
}
```
### Discord
### [Slack](https://docs.clawd.bot/slack)
- Set `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` (or `slack.botToken` + `slack.appToken`).
### [Discord](https://docs.clawd.bot/discord)
- Set `DISCORD_BOT_TOKEN` or `discord.token` (env wins).
- Optional: set `discord.slashCommand`, `discord.dm.allowFrom`, `discord.guilds`, or `discord.mediaMaxMb` as needed.
- Optional: set `commands.native`, `commands.text`, or `commands.useAccessGroups`, plus `discord.dm.allowFrom`, `discord.guilds`, or `discord.mediaMaxMb` as needed.
```json5
{
@@ -245,6 +342,19 @@ Minimal `~/.clawdbot/clawdbot.json`:
}
```
### [Signal](https://docs.clawd.bot/signal)
- Requires `signal-cli` and a `signal` config section.
### [iMessage](https://docs.clawd.bot/imessage)
- macOS only; Messages must be signed in.
- If `imessage.groups` is set, it becomes a group allowlist; include `"*"` to allow all.
### [WebChat](https://docs.clawd.bot/webchat)
- Uses the Gateway WebSocket; no separate WebChat port/config.
Browser control (optional):
```json5
@@ -259,39 +369,100 @@ Browser control (optional):
## Docs
- [`docs/index.md`](docs/index.md) (overview)
- [`docs/configuration.md`](docs/configuration.md)
- [`docs/group-messages.md`](docs/group-messages.md)
- [`docs/gateway.md`](docs/gateway.md)
- [`docs/web.md`](docs/web.md)
- [`docs/discovery.md`](docs/discovery.md)
- [`docs/agent.md`](docs/agent.md)
- [`docs/discord.md`](docs/discord.md)
- [`docs/wizard.md`](docs/wizard.md)
- Webhooks + external triggers: [`docs/webhook.md`](docs/webhook.md)
- Gmail hooks (email → wake): [`docs/gmail-pubsub.md`](docs/gmail-pubsub.md)
Use these when you’re past the onboarding flow and want the deeper reference.
- [Start with the docs index for navigation and “what’s where.”](https://docs.clawd.bot)
- [Read the architecture overview for the gateway + protocol model.](https://docs.clawd.bot/architecture)
- [Use the full configuration reference when you need every key and example.](https://docs.clawd.bot/configuration)
- [Run the Gateway by the book with the operational runbook.](https://docs.clawd.bot/gateway)
- [Learn how the Control UI/Web surfaces work and how to expose them safely.](https://docs.clawd.bot/web)
- [Understand remote access over SSH tunnels or tailnets.](https://docs.clawd.bot/remote)
- [Follow the onboarding wizard flow for a guided setup.](https://docs.clawd.bot/wizard)
- [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](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)
## Advanced docs (discovery + control)
- [Discovery + transports](https://docs.clawd.bot/discovery)
- [Bonjour/mDNS](https://docs.clawd.bot/bonjour)
- [Gateway pairing](https://docs.clawd.bot/gateway/pairing)
- [Remote gateway README](https://docs.clawd.bot/remote-gateway-readme)
- [Control UI](https://docs.clawd.bot/control-ui)
- [Dashboard](https://docs.clawd.bot/dashboard)
## Operations & troubleshooting
- [Health checks](https://docs.clawd.bot/health)
- [Gateway lock](https://docs.clawd.bot/gateway-lock)
- [Background process](https://docs.clawd.bot/background-process)
- [Browser troubleshooting (Linux)](https://docs.clawd.bot/browser-linux-troubleshooting)
- [Logging](https://docs.clawd.bot/logging)
## Deep dives
- [Agent loop](https://docs.clawd.bot/agent-loop)
- [Presence](https://docs.clawd.bot/presence)
- [TypeBox schemas](https://docs.clawd.bot/typebox)
- [RPC adapters](https://docs.clawd.bot/rpc)
- [Queue](https://docs.clawd.bot/queue)
## Workspace & 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)
## Platform internals
- [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)
- [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)
## Email hooks (Gmail)
[Gmail Pub/Sub wiring (gcloud + gogcli), hook tokens, and auto-watch behavior are documented here.](https://docs.clawd.bot/gmail-pubsub)
Gateway auto-starts the watcher when `hooks.enabled=true` and `hooks.gmail.account` is set; `clawdbot hooks gmail run` is the manual daemon wrapper if you don’t want auto-start.
```bash
clawdbot hooks gmail setup --account you@gmail.com
clawdbot hooks gmail run
```
- [`docs/security.md`](docs/security.md)
- [`docs/troubleshooting.md`](docs/troubleshooting.md)
- [`docs/ios/connect.md`](docs/ios/connect.md)
- [`docs/clawdbot-mac.md`](docs/clawdbot-mac.md)
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines, maintainers, and how to submit PRs.
AI/vibe-coded PRs welcome! 🤖
## Clawd
Clawdbot was built for **Clawd**, a space lobster AI assistant.
Clawdbot was built for **Clawd**, a space lobster AI assistant. 🦞
by Peter Steinberger and the community.
- https://clawd.me
- https://soul.md
- https://steipete.me
## Community
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines, maintainers, and how to submit PRs.
AI/vibe-coded PRs welcome! 🤖
Thanks to all clawtributors:
<p align="left">
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a> <a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a> <a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="Nachx639" title="Nachx639"/></a> <a href="https://github.com/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a>
<a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="mbelinky" title="mbelinky"/></a> <a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a> <a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="omniwired" title="omniwired"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="vsabavat" title="vsabavat"/></a>
<a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/conhecendocontato"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendocontato" title="conhecendocontato"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="djangonavarro220" title="djangonavarro220"/></a>
<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>
</p>
+41 -198
View File
@@ -1,212 +1,55 @@
<?xml version="1.0" standalone="yes"?>
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>Clawdbot</title>
<title>Clawdis</title>
<item>
<title>2.0.0-beta5</title>
<pubDate>Sat, 03 Jan 2026 07:15:16 +0100</pubDate>
<title>2026.1.5-3</title>
<pubDate>Mon, 05 Jan 2026 04:30:46 +0100</pubDate>
<link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
<sparkle:version>2765</sparkle:version>
<sparkle:shortVersionString>2.0.0-beta5</sparkle:shortVersionString>
<sparkle:version>3095</sparkle:version>
<sparkle:shortVersionString>2026.1.5-3</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>Clawdbot 2.0.0-beta5</h2>
<h3>Fixed</h3>
<ul>
<li>Media: preserve GIF animation when uploading to Discord/other providers (skip JPEG optimization for image/gif).</li>
<li>Agent runtime: update pi-mono dependencies to 0.31.1 (agent-core split).</li>
<li>Dependencies: bump to latest compatible versions (TypeBox, grammY, Zod, Rolldown, oxlint-tsgolint).</li>
<li>Tests: cover read tool image metadata + text output.</li>
<li>Tests: add queue mode coverage (collect/followup + directive parsing).</li>
</ul>
<h3>Breaking</h3>
<ul>
<li>Skills config schema moved under <code>skills.*</code>:</li>
</ul>
- <code>skillsLoad.extraDirs</code> → <code>skills.load.extraDirs</code>
- <code>skillsInstall.*</code> → <code>skills.install.*</code>
- per-skill config map moved to <code>skills.entries</code> (e.g. <code>skills.peekaboo.enabled</code> → <code>skills.entries.peekaboo.enabled</code>)
- new optional bundled allowlist: <code>skills.allowBundled</code> (only affects bundled skills)
<ul>
<li>Sessions: group keys now use <code>surface:group:<id></code> / <code>surface:channel:<id></code>; legacy <code>group:*</code> keys migrate on next message; <code>groupdm</code> keys are no longer recognized.</li>
<li>Discord: remove legacy <code>discord.allowFrom</code>, <code>discord.guildAllowFrom</code>, and <code>discord.requireMention</code>; use <code>discord.dm</code> + <code>discord.guilds</code>.</li>
<li>Providers: Discord/Telegram no longer auto-start from env tokens alone; add <code>discord: { enabled: true }</code> / <code>telegram: { enabled: true }</code> to your config when using <code>DISCORD_BOT_TOKEN</code> / <code>TELEGRAM_BOT_TOKEN</code>.</li>
<li>Config: remove <code>routing.allowFrom</code>; use <code>whatsapp.allowFrom</code> instead (run <code>clawdbot doctor</code> to migrate).</li>
<li>Config: remove <code>routing.groupChat.requireMention</code> + <code>telegram.requireMention</code>; use <code>whatsapp.groups</code>, <code>imessage.groups</code>, and <code>telegram.groups</code> defaults instead (run <code>clawdbot doctor</code> to migrate).</li>
</ul>
<h3>Features</h3>
<ul>
<li>Discord: expand <code>discord</code> tool actions (reactions, stickers, polls, threads, search, moderation gates) (#115) — thanks @thewilloftheshadow.</li>
<li>Discord/Telegram: add reply tags (<code>[[reply_to_current]]</code>, <code>[[reply_to:<id>]]</code>) with per-provider <code>replyToMode</code> (off|first|all) for native threaded replies.</li>
<li>Talk mode: continuous speech conversations (macOS/iOS/Android) with ElevenLabs TTS, reply directives, and optional interrupt-on-speech.</li>
<li>Auto-reply: expand queue modes (steer/followup/collect/steer-backlog) with debounce/cap/drop options and followup backlog handling.</li>
<li>UI: add optional <code>ui.seamColor</code> accent to tint the Talk Mode side bubble (macOS/iOS/Android).</li>
<li>Nix mode: opt-in declarative config + read-only settings UI when <code>CLAWDBOT_NIX_MODE=1</code> (thanks @joshp123 for the persistence — earned my trust; I'll merge these going forward).</li>
<li>CLI: add Google Antigravity OAuth auth option for Claude Opus 4.5/Gemini 3 (#88) — thanks @mukhtharcm.</li>
<li>Agent runtime: accept legacy <code>Z_AI_API_KEY</code> for Z.AI provider auth (maps to <code>ZAI_API_KEY</code>).</li>
<li>Groups: add per-group mention gating defaults/overrides for Telegram/WhatsApp/iMessage via <code>*.groups</code> with <code>"*"</code> defaults; Discord now supports <code>discord.guilds."*"</code> as a default.</li>
<li>Discord: add user-installed slash command handling with per-user sessions and auto-registration (#94) — thanks @thewilloftheshadow.</li>
<li>Discord: add DM enable/allowlist plus guild channel/user/guild allowlists with id/name matching.</li>
<li>Signal: add <code>signal-cli</code> JSON-RPC support for send/receive via the Signal provider.</li>
<li>iMessage: add imsg JSON-RPC integration (stdio), chat_id routing, and group chat support.</li>
<li>Chat UI: add recent-session dropdown switcher (main first) in macOS/iOS/Android + Control UI.</li>
<li>UI: add Discord/Signal/iMessage connection panels in macOS + Control UI (thanks @thewilloftheshadow).</li>
<li>Discord: allow agent-triggered reactions via <code>clawdbot_discord</code> when enabled, and surface message ids in context.</li>
<li>Discord: revamp guild routing config with per-guild/channel rules and slugged display names; add optional group DM support (default off).</li>
<li>Discord: remove legacy guild/channel ignore lists in favor of per-guild allowlists (and proposed per-guild ignore lists).</li>
<li>Skills: add Trello skill for board/list/card management (thanks @clawd).</li>
<li>Docker: add containerized gateway/CLI setup via Dockerfile, compose, and setup script (thanks @dan-dr).</li>
<li>Tests: add a Z.AI live test gate for smoke validation when keys are present.</li>
<li>macOS Debug: add app log verbosity and rolling file log toggle for swift-log-backed app logs.</li>
<li>CLI: add onboarding wizard (gateway + workspace + skills) with daemon installers and Anthropic/Minimax setup paths.</li>
<li>CLI: add ASCII banner header to wizard entry points.</li>
<li>CLI: add <code>configure</code>, <code>doctor</code>, and <code>update</code> wizards for ongoing setup, health checks, and modernization.</li>
<li>CLI: add Signal CLI auto-install from GitHub releases in the wizard and persist wizard run metadata in config.</li>
<li>CLI: add remote gateway client config (gateway.remote.*) with Bonjour-assisted discovery.</li>
<li>CLI: enhance <code>clawdbot tui</code> with model/session pickers, tool cards, and slash commands (local or remote).</li>
<li>Gateway: allow <code>sessions.patch</code> to set per-session model overrides (used by the TUI <code>/model</code> flow).</li>
<li>Skills: allow <code>bun</code> as a node manager for skill installs.</li>
<li>Skills: add <code>things-mac</code> (Things 3 CLI) for read/search plus add/update via URL scheme.</li>
<li>Skills: add Apple Notes + Reminders skills via memo CLI (thanks @tylerwince).</li>
<li>Tests: add a Docker-based onboarding E2E harness.</li>
<li>Tests: harden wizard E2E flows for reset, providers, skills, and remote non-interactive runs.</li>
<li>Browser tools: add remote CDP URL support, Linux launcher options (<code>executablePath</code>, <code>noSandbox</code>), and surface <code>cdpUrl</code> in status.</li>
<li>Skills: add tmux-first coding-agent skill + <code>requires.anyBins</code> gate for multi-CLI setup (thanks @sreekaransrinath).</li>
</ul>
<description><![CDATA[<h2>Clawdbot 2026.1.5-3</h2>
<h3>Fixes</h3>
<ul>
<li>Gog calendar: format date ranges as RFC 3339 with timezone to satisfy Google Calendar API (thanks @jayhickey).</li>
<li>macOS onboarding: add scrollable page gutter for overflowing content (#105) — thanks @thewilloftheshadow.</li>
<li>Chat UI: keep the chat scrolled to the latest message after switching sessions.</li>
<li>Chat UI: show rich session display names in Web Chat + SwiftUI + Android.</li>
<li>Auto-reply: stream completed reply blocks as soon as they finish (configurable default + break); skip empty tool-only blocks unless verbose.</li>
<li>Discord: avoid duplicate sends when block streaming is enabled (race with typing hook).</li>
<li>Providers: make outbound text chunk limits configurable via <code>*.textChunkLimit</code> (defaults remain 4000/Discord 2000).</li>
<li>CLI onboarding: persist gateway token in config so local CLI auth works; recommend auth Off unless you need multi-machine access.</li>
<li>Control UI: accept a <code>?token=</code> URL param to auto-fill Gateway auth; onboarding now opens the dashboard with token auth when configured.</li>
<li>Agent prompt: remove hardcoded user name in system prompt example.</li>
<li>Chat UI: add extra top padding before the first message bubble in Web Chat (macOS/iOS/Android).</li>
<li>Control UI: refine Web Chat session selector styling (chevron spacing + background).</li>
<li>WebChat: stream live updates for sessions even when runs start outside the chat UI.</li>
<li>Gateway CLI: read <code>CLAWDBOT_GATEWAY_PASSWORD</code> from environment in <code>callGateway()</code> — allows <code>doctor</code>/<code>health</code> commands to auth without explicit <code>--password</code> flag.</li>
<li>Gateway: add password auth support for remote gateway connections (thanks @jeffersonwarrior).</li>
<li>Auto-reply: strip stray leading/trailing <code>HEARTBEAT_OK</code> from normal replies; drop short (≤ 30 chars) heartbeat acks.</li>
<li>WhatsApp auto-reply: default to self-only when no config is present.</li>
<li>Logging: trim provider prefix duplication in Discord/Signal/Telegram runtime log lines.</li>
<li>Logging/Signal: treat signal-cli "Failed …" lines as errors in gateway logs.</li>
<li>Discord: include recent guild context when replying to mentions and add <code>discord.historyLimit</code> to tune how many messages are captured.</li>
<li>Discord: include author tag + id in group context <code>[from:]</code> lines for ping-ready replies (thanks @thewilloftheshadow).</li>
<li>Discord: include replied-to message context when a Discord message references another message (thanks @thewilloftheshadow).</li>
<li>Discord: preserve newlines when stripping reply tags from agent output.</li>
<li>Gateway: fix TypeScript build by aligning hook mapping <code>channel</code> types and removing a dead Group DM branch in Discord monitor.</li>
<li>Skills: switch imsg installer to brew tap formula.</li>
<li>Skills: gate macOS-only skills by OS and surface block reasons in the Skills UI.</li>
<li>Onboarding: show skill descriptions in the macOS setup flow and surface clearer Gateway/skills error messages.</li>
<li>Onboarding: auto-verify Claude OAuth tokens, show “verified” when detected working, and avoid re-auth prompts unless verification fails.</li>
<li>CLI onboarding: include exit code + a useful one-line summary when skill dependency installs fail.</li>
<li>CLI onboarding: explain Tailscale exposure options (Off/Serve/Funnel) and colorize provider status (linked/configured/needs setup).</li>
<li>CLI onboarding: add provider primers (WhatsApp/Telegram/Discord/Signal) incl. Discord bot token setup steps.</li>
<li>CLI onboarding: allow skipping the “install missing skill dependencies” selection without canceling the wizard.</li>
<li>CLI onboarding: always prompt for WhatsApp <code>whatsapp.allowFrom</code> and print (optionally open) the Control UI URL when done.</li>
<li>CLI onboarding: detect gateway reachability and annotate Local/Remote choices (helps pick the right mode).</li>
<li>macOS settings: colorize provider status subtitles to distinguish healthy vs degraded states.</li>
<li>macOS: keep config writes on the main actor to satisfy Swift concurrency rules.</li>
<li>macOS menu: show multi-line gateway error details, add an always-visible gateway row, avoid duplicate gateway status rows, suppress transient <code>cancelled</code> device refresh errors, and auto-recover the control channel on disconnect.</li>
<li>macOS menu: show session last-used timestamps in the list and add recent-message previews in session submenus.</li>
<li>macOS menu: tighten session row padding and time out session preview loading with cached fallback.</li>
<li>macOS: log health refresh failures and recovery to make gateway issues easier to diagnose.</li>
<li>macOS codesign: skip hardened runtime for ad-hoc signing and avoid empty options args (#70) — thanks @petter-b</li>
<li>macOS codesign: include camera entitlement so permission prompts work in the menu bar app.</li>
<li>Agent tools: bash tool supports real TTY via <code>stdinMode: "pty"</code> with node-pty, warning + fallback on load/start failure.</li>
<li>Agent tools: map <code>camera.snap</code> JPEG payloads to <code>image/jpeg</code> to avoid MIME mismatch errors.</li>
<li>Tests: cover <code>camera.snap</code> MIME mapping to prevent image/png vs image/jpeg mismatches.</li>
<li>macOS camera: wait for exposure/white balance to settle before capturing a snap to avoid dark images.</li>
<li>Camera snap: add <code>delayMs</code> parameter (default 2000ms on macOS) to improve exposure reliability.</li>
<li>Camera: add <code>camera.list</code> and optional <code>deviceId</code> selection for snaps/clips.</li>
<li>Tests: cover camera device selection params in CLI + agent tools.</li>
<li>macOS packaging: move rpath config into swift build for reliability (#69) — thanks @petter-b</li>
<li>macOS: prioritize main bundle for device resources to prevent crash (#73) — thanks @petter-b</li>
<li>macOS remote: route settings through gateway config and avoid local config reads in remote mode.</li>
<li>Telegram: align token resolution for cron/agent/CLI sends (env/config/tokenFile) to prevent isolated delivery failures (#76).</li>
<li>Telegram: honor per-group mention gating defaults/overrides via <code>telegram.groups</code> and <code>"*"</code> defaults (thanks @joshp123).</li>
<li>Chat UI: clear composer input immediately and allow clear while editing to prevent duplicate sends (#72) — thanks @hrdwdmrbl</li>
<li>Restart: use systemd on Linux (and report actual restart method) instead of always launchctl.</li>
<li>Gateway relay: detect Bun binaries via execPath to resolve packaged assets on macOS.</li>
<li>Cron: prevent <code>every</code> schedules without an anchor from firing in a tight loop (thanks @jamesgroat).</li>
<li>Docs: add manual OAuth setup for remote/headless deployments (#67) — thanks @wstock</li>
<li>Docs/agent tools: clarify that browser <code>wait</code> should be avoided by default and used only in exceptional cases.</li>
<li>Docs: clarify self-chat mode and group mention gating config (#111) — thanks @rafaelreis-r.</li>
<li>Browser tools: <code>upload</code> supports auto-click refs, direct <code>inputRef</code>/<code>element</code> file inputs, and emits input/change after <code>setFiles</code> so JS-heavy sites pick up attachments.</li>
<li>Browser tools: harden CDP readiness (HTTP + WS), retry CDP connects, and auto-restart the clawd browser when the socket handshake stalls.</li>
<li>Browser CLI: add <code>clawdbot browser reset-profile</code> to move the clawd profile to Trash when it gets wedged.</li>
<li>Signal: fix daemon startup race (wait for <code>/api/v1/check</code>) and normalize JSON-RPC <code>version</code> probe parsing.</li>
<li>Docs/Signal: clarify bot-number vs personal-account setup (self-chat loop protection) and add a quickstart config snippet.</li>
<li>Docs: refresh the CLI wizard guide and highlight onboarding in the README.</li>
<li>CLI: tighten onboarding prompt typing to keep bun builds green.</li>
<li>macOS: Voice Wake now fully tears down the Speech pipeline when disabled (cancel pending restarts, drop stale callbacks) to avoid high CPU in the background.</li>
<li>macOS menu: add a Talk Mode action alongside the Open Dashboard/Chat/Canvas entries.</li>
<li>macOS Debug: hide “Restart Gateway” when the app won’t start a local gateway (remote mode / attach-only).</li>
<li>macOS Debug: add an icon for the App Logging submenu.</li>
<li>macOS Talk Mode: orb overlay refresh, ElevenLabs request logging, API key status in settings, and auto-select first voice when none is configured.</li>
<li>macOS Talk Mode: add hard timeout around ElevenLabs TTS synthesis to avoid getting stuck “speaking” forever on hung requests.</li>
<li>macOS Talk Mode: avoid stuck playback when the audio player never starts (fail-fast + watchdog).</li>
<li>macOS Talk Mode: fix audio stop ordering so disabling Talk Mode always stops in-flight playback.</li>
<li>macOS Talk Mode: throttle audio-level updates (avoid per-buffer task creation) to reduce CPU/task churn.</li>
<li>macOS Talk Mode: increase overlay window size so wave rings don’t clip; close button is hover-only and closer to the orb.</li>
<li>WebChat: preserve chat run ordering per session so concurrent runs don’t strand the typing indicator.</li>
<li>Talk Mode: fall back to system TTS when ElevenLabs is unavailable, returns non-audio, or playback fails (macOS/iOS/Android).</li>
<li>Talk Mode: stream PCM on macOS/iOS for lower latency (incremental playback); Android continues MP3 streaming.</li>
<li>Talk Mode: validate ElevenLabs v3 stability and latency tier directives before sending requests.</li>
<li>iOS/Android Talk Mode: auto-select the first ElevenLabs voice when none is configured.</li>
<li>ElevenLabs: add retry/backoff for 429/5xx and include content-type in errors for debugging.</li>
<li>Talk Mode: align to the gateway’s main session key and fall back to history polling when chat events drop (prevents stuck “thinking” / missing messages).</li>
<li>Talk Mode: treat history timestamps as seconds or milliseconds to avoid stale assistant picks (macOS/iOS/Android).</li>
<li>Chat UI: clear streaming/tool bubbles when external runs finish, preventing duplicate assistant bubbles.</li>
<li>Chat UI: user bubbles use <code>ui.seamColor</code> (fallback to a calmer default blue).</li>
<li>Android Chat UI: use <code>onPrimary</code> for user bubble text to preserve contrast (thanks @Syhids).</li>
<li>Control UI: sync sidebar navigation with the URL for deep-linking, and auto-scroll chat to the latest message.</li>
<li>Control UI: disable Web Chat + Talk when no iOS/Android node is connected; refreshed Web Chat styling and keyboard send.</li>
<li>Control UI: keep chat pinned to the latest message while typing/sending and restore drafts on send failures.</li>
<li>Control UI: soften chat bubble text opacity for calmer readability.</li>
<li>macOS Web Chat: improve empty/error states, focus message field on open, keep pill/send inside the input field, and make the composer pill edge-to-edge with square top corners.</li>
<li>macOS: bundle Control UI assets into the app relay so the packaged app can serve them (thanks @mbelinky).</li>
<li>Talk Mode: wait for chat history to surface the assistant reply before starting TTS (macOS/iOS/Android).</li>
<li>iOS Talk Mode: fix chat completion wait to time out even if no events arrive (prevents “Thinking…” hangs).</li>
<li>iOS Talk Mode: keep recognition running during playback to support interrupt-on-speech.</li>
<li>iOS Talk Mode: preserve directive voice/model overrides across config reloads and add ElevenLabs request timeouts.</li>
<li>iOS/Android Talk Mode: explicitly <code>chat.subscribe</code> when Talk Mode is active, so completion events arrive even if the Chat UI isn’t open.</li>
<li>Chat UI: refresh history when another client finishes a run in the same session, so Talk Mode + Voice Wake transcripts appear consistently.</li>
<li>Gateway: <code>voice.transcript</code> now also maps agent bus output to <code>chat</code> events, ensuring chat UIs refresh for voice-triggered runs.</li>
<li>Gateway: auto-migrate legacy config on startup (non-Nix); Nix mode hard-fails with a clear error when legacy keys are present.</li>
<li>iOS/Android: show a centered Talk Mode orb overlay while Talk Mode is enabled.</li>
<li>Gateway config: inject <code>talk.apiKey</code> from <code>ELEVENLABS_API_KEY</code>/shell profile so nodes can fetch it on demand.</li>
<li>Canvas A2UI: tag requests with <code>platform=android|ios|macos</code> and boost Android canvas background contrast.</li>
<li>iOS/Android nodes: enable scrolling for loaded web pages in the Canvas WebView (default scaffold stays touch-first).</li>
<li>macOS menu: device list now uses <code>node.list</code> (devices only; no agent/tool presence entries).</li>
<li>macOS menu: device list now shows connected nodes only.</li>
<li>macOS menu: device rows now pack platform/version on the first line, and command lists wrap in submenus.</li>
<li>macOS menu: split device platform/version across first and second rows for better fit.</li>
<li>macOS Canvas: show remote control status in the debug overlay and log A2UI auto-nav decisions.</li>
<li>Canvas A2UI: polish the debug status HUD styling.</li>
<li>iOS node: fix ReplayKit screen recording crash caused by queue isolation assertions during capture.</li>
<li>iOS Talk Mode: avoid audio tap queue assertions when starting recognition.</li>
<li>macOS: use $HOME/Library/pnpm for SSH PATH exports (thanks @mbelinky).</li>
<li>macOS remote: harden SSH tunnel recovery/logging, honor <code>gateway.remote.url</code> port when forwarding, clarify gateway disconnect status, and add Debug menu tunnel reset.</li>
<li>iOS/Android nodes: bridge auto-connect refreshes stale tokens and settings now show richer bridge/device details.</li>
<li>macOS: bundle device model resources to prevent Instances crashes (thanks @mbelinky).</li>
<li>iOS/Android nodes: status pill now surfaces camera activity instead of overlay toasts.</li>
<li>iOS/Android/macOS nodes: camera snaps recompress to keep base64 payloads under 5 MB.</li>
<li>iOS/Android nodes: status pill now surfaces pairing, screen recording, voice wake, and foreground-required states.</li>
<li>iOS/Android nodes: avoid duplicating “Gateway reconnecting…” when the bridge is already connecting.</li>
<li>iOS/Android nodes: Talk Mode now lives on a side bubble (with an iOS toggle to hide it), and Android settings no longer show the Talk Mode switch.</li>
<li>macOS menu: top status line now shows pending node pairing approvals (incl. repairs).</li>
<li>CLI: avoid spurious gateway close errors after successful request/response cycles.</li>
<li>Agent runtime: clamp tool-result images to the 5MB Anthropic limit to avoid hard request rejections.</li>
<li>Agent runtime: write v2 session headers so Pi session branching stays in the Clawdbot sessions dir.</li>
<li>Tests: add Swift Testing coverage for camera errors and Kotest coverage for Android bridge endpoints.</li>
<li>NPM package: include missing runtime dist folders (slack/signal/imessage/tui/wizard/control-ui/daemon) to avoid <code>ERR_MODULE_NOT_FOUND</code> in Node 25 npx installs.</li>
</ul>
<p><a href="https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2.0.0-beta5/Clawdbot-2.0.0-beta5.zip" length="145432870" type="application/octet-stream" sparkle:edSignature="qKPcmSx2pAaIYz9NqFp0TY63KrcDlpctUHnNpRs6Q60qQqBWtQycLIhhvhxmGnHupaiEXJfspb/Ad9RgODIzAw=="/>
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.5-3/Clawdbot-2026.1.5-3.zip" length="160800596" type="application/octet-stream" sparkle:edSignature="P8U3nvIFpbGmRItT/NGPmJ/i370OMVvDHYQL/znYsLI0MrbGfXgMGEvR5A0uwW+cJevlX/hrJLiY51zo4rAMBg=="/>
</item>
<item>
<title>2026.1.5-3</title>
<pubDate>Mon, 05 Jan 2026 03:57:59 +0100</pubDate>
<link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
<sparkle:version>3091</sparkle:version>
<sparkle:shortVersionString>2026.1.5-3</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>Clawdbot 2026.1.5-3</h2>
<h3>Fixes</h3>
<ul>
<li>NPM package: include missing runtime dist folders (slack/signal/imessage/tui/wizard/control-ui/daemon) to avoid <code>ERR_MODULE_NOT_FOUND</code> in Node 25 npx installs.</li>
</ul>
<p><a href="https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.5-3/Clawdbot-2026.1.5-3.zip" length="160797048" type="application/octet-stream" sparkle:edSignature="5KYFg0SW7liwLxLJbfzd2KsAxbX06gMH0rH/W3a4V0p4N48hjz4AsSrfFLdGZSnW+6XaJjC3MN6Ynh+l7kffDQ=="/>
</item>
<item>
<title>2026.1.5-2</title>
<pubDate>Mon, 05 Jan 2026 03:51:30 +0100</pubDate>
<link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
<sparkle:version>3089</sparkle:version>
<sparkle:shortVersionString>2026.1.5-2</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>Clawdbot 2026.1.5-2</h2>
<h3>Fixes</h3>
<ul>
<li>NPM package: include <code>dist/sessions</code> so <code>clawdbot agent</code> resolves session helpers in npx installs.</li>
<li>Node 25: avoid unsupported directory import by targeting <code>qrcode-terminal/vendor/QRCode/*.js</code> modules.</li>
</ul>
<p><a href="https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.5-2/Clawdbot-2026.1.5-2.zip" length="150250417" type="application/octet-stream" sparkle:edSignature="ntHNmwyHrv6cPk6NAKOT3AUkwdt5ZadrGU6mJK4GmVxi44uIMT3ZXluvnqK9SxXQwA0H0dXjiGMS/cg8NbgqDA=="/>
</item>
</channel>
</rss>
+7 -2
View File
@@ -48,6 +48,10 @@ android {
lint {
disable += setOf("IconLauncherShape")
}
testOptions {
unitTests.isIncludeAndroidResources = true
}
}
kotlin {
@@ -64,7 +68,7 @@ dependencies {
implementation("androidx.core:core-ktx:1.17.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
implementation("androidx.activity:activity-compose:1.12.2")
implementation("androidx.webkit:webkit:1.14.0")
implementation("androidx.webkit:webkit:1.15.0")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview")
@@ -96,7 +100,8 @@ dependencies {
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
testImplementation("io.kotest:kotest-runner-junit5-jvm:6.0.7")
testImplementation("io.kotest:kotest-assertions-core-jvm:6.0.7")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.13.3")
testImplementation("org.robolectric:robolectric:4.16")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:6.0.1")
}
tasks.withType<Test>().configureEach {
@@ -12,7 +12,7 @@
"element",
"node",
"nodeId",
"jobId",
"id",
"requestId",
"to",
"channelId",
@@ -136,10 +136,10 @@
"label": "add",
"detailKeys": ["job.name", "job.id", "job.schedule", "job.cron"]
},
"update": { "label": "update", "detailKeys": ["jobId"] },
"remove": { "label": "remove", "detailKeys": ["jobId"] },
"run": { "label": "run", "detailKeys": ["jobId"] },
"runs": { "label": "runs", "detailKeys": ["jobId"] },
"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"] }
}
},
@@ -98,14 +98,31 @@ class NodeForegroundService : Service() {
}
private fun buildNotification(title: String, text: String): Notification {
val launchIntent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val launchPending =
PendingIntent.getActivity(
this,
1,
launchIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val stopIntent = Intent(this, NodeForegroundService::class.java).setAction(ACTION_STOP)
val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
val stopPending = PendingIntent.getService(this, 2, stopIntent, flags)
val stopPending =
PendingIntent.getService(
this,
2,
stopIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(title)
.setContentText(text)
.setContentIntent(launchPending)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
@@ -515,7 +515,6 @@ class NodeRuntime(context: Context) {
}
private fun hasBackgroundLocationPermission(): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return true
return (
ContextCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_BACKGROUND_LOCATION) ==
PackageManager.PERMISSION_GRANTED
@@ -1,10 +1,12 @@
package com.clawdbot.android.node
import android.annotation.SuppressLint
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationManager
import android.os.CancellationSignal
import androidx.core.content.ContextCompat
import java.time.Instant
import java.time.format.DateTimeFormatter
import kotlinx.coroutines.Dispatchers
@@ -64,6 +66,15 @@ class LocationCaptureManager(private val context: Context) {
providers: List<String>,
maxAgeMs: Long?,
): Location? {
val fineOk =
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
val coarseOk =
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
if (!fineOk && !coarseOk) {
throw IllegalStateException("LOCATION_PERMISSION_REQUIRED: grant Location permission")
}
val now = System.currentTimeMillis()
val candidates =
providers.mapNotNull { provider -> manager.getLastKnownLocation(provider) }
@@ -72,12 +83,20 @@ class LocationCaptureManager(private val context: Context) {
return freshest
}
@SuppressLint("MissingPermission")
private suspend fun requestCurrent(
manager: LocationManager,
providers: List<String>,
timeoutMs: Long,
): Location {
val fineOk =
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
val coarseOk =
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
if (!fineOk && !coarseOk) {
throw IllegalStateException("LOCATION_PERMISSION_REQUIRED: grant Location permission")
}
val resolved =
providers.firstOrNull { manager.isProviderEnabled(it) }
?: throw IllegalStateException("LOCATION_UNAVAILABLE: no providers available")
@@ -133,7 +133,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
if (requestedMode != null) {
viewModel.setLocationMode(requestedMode)
if (requestedMode == LocationMode.Always && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (requestedMode == LocationMode.Always) {
val backgroundOk =
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_BACKGROUND_LOCATION) ==
PackageManager.PERMISSION_GRANTED
@@ -191,7 +191,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
PackageManager.PERMISSION_GRANTED
if (fineOk || coarseOk) {
viewModel.setLocationMode(targetMode)
if (targetMode == LocationMode.Always && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (targetMode == LocationMode.Always) {
val backgroundOk =
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_BACKGROUND_LOCATION) ==
PackageManager.PERMISSION_GRANTED
@@ -28,9 +28,9 @@ import androidx.compose.ui.unit.dp
fun StatusPill(
bridge: BridgeState,
voiceEnabled: Boolean,
activity: StatusActivity? = null,
onClick: () -> Unit,
modifier: Modifier = Modifier,
activity: StatusActivity? = null,
) {
Surface(
onClick = onClick,
@@ -0,0 +1,43 @@
package com.clawdbot.android
import android.app.Notification
import android.content.Intent
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class NodeForegroundServiceTest {
@Test
fun buildNotificationSetsLaunchIntent() {
val service = Robolectric.buildService(NodeForegroundService::class.java).get()
val notification = buildNotification(service)
val pendingIntent = notification.contentIntent
assertNotNull(pendingIntent)
val savedIntent = Shadows.shadowOf(pendingIntent).savedIntent
assertNotNull(savedIntent)
assertEquals(MainActivity::class.java.name, savedIntent.component?.className)
val expectedFlags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
assertEquals(expectedFlags, savedIntent.flags and expectedFlags)
}
private fun buildNotification(service: NodeForegroundService): Notification {
val method =
NodeForegroundService::class.java.getDeclaredMethod(
"buildNotification",
String::class.java,
String::class.java,
)
method.isAccessible = true
return method.invoke(service, "Title", "Text") as Notification
}
}
@@ -55,7 +55,7 @@ final class ScreenRecordService: @unchecked Sendable {
outPath: outPath)
let state = CaptureState()
let recordQueue = DispatchQueue(label: "com.clawdis.screenrecord")
let recordQueue = DispatchQueue(label: "com.clawdbot.screenrecord")
try await self.startCapture(state: state, config: config, recordQueue: recordQueue)
try await Task.sleep(nanoseconds: UInt64(config.durationMs) * 1_000_000)
+42 -31
View File
@@ -9,7 +9,7 @@ import SwiftUI
final class AppState {
private let isPreview: Bool
private var isInitializing = true
private nonisolated var configWatcher: ConfigFileWatcher?
private var configWatcher: ConfigFileWatcher?
private var suppressVoiceWakeGlobalSync = false
private var voiceWakeGlobalSyncTask: Task<Void, Never>?
@@ -321,6 +321,7 @@ final class AppState {
}
}
@MainActor
deinit {
self.configWatcher?.stop()
}
@@ -400,11 +401,9 @@ final class AppState {
private func syncGatewayConfigIfNeeded() {
guard !self.isPreview, !self.isInitializing else { return }
var root = ClawdbotConfigFile.loadDict()
var gateway = root["gateway"] as? [String: Any] ?? [:]
var changed = false
let desiredMode: String? = switch self.connectionMode {
let connectionMode = self.connectionMode
let remoteTarget = self.remoteTarget
let desiredMode: String? = switch connectionMode {
case .local:
"local"
case .remote:
@@ -412,37 +411,49 @@ final class AppState {
case .unconfigured:
nil
}
let remoteHost = connectionMode == .remote
? CommandResolver.parseSSHTarget(remoteTarget)?.host
: nil
let currentMode = (gateway["mode"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
if let desiredMode {
if currentMode != desiredMode {
gateway["mode"] = desiredMode
Task { @MainActor in
// Keep app-only connection settings local to avoid overwriting remote gateway config.
var root = ClawdbotConfigFile.loadDict()
var gateway = root["gateway"] as? [String: Any] ?? [:]
var changed = false
let currentMode = (gateway["mode"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
if let desiredMode {
if currentMode != desiredMode {
gateway["mode"] = desiredMode
changed = true
}
} else if currentMode != nil {
gateway.removeValue(forKey: "mode")
changed = true
}
} else if currentMode != nil {
gateway.removeValue(forKey: "mode")
changed = true
}
if self.connectionMode == .remote,
let host = CommandResolver.parseSSHTarget(self.remoteTarget)?.host
{
var remote = gateway["remote"] as? [String: Any] ?? [:]
let existingUrl = (remote["url"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let parsedExisting = existingUrl.isEmpty ? nil : URL(string: existingUrl)
let scheme = parsedExisting?.scheme?.isEmpty == false ? parsedExisting?.scheme : "ws"
let port = parsedExisting?.port ?? 18789
let desiredUrl = "\(scheme ?? "ws")://\(host):\(port)"
if existingUrl != desiredUrl {
remote["url"] = desiredUrl
gateway["remote"] = remote
changed = true
if connectionMode == .remote, let host = remoteHost {
var remote = gateway["remote"] as? [String: Any] ?? [:]
let existingUrl = (remote["url"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let parsedExisting = existingUrl.isEmpty ? nil : URL(string: existingUrl)
let scheme = parsedExisting?.scheme?.isEmpty == false ? parsedExisting?.scheme : "ws"
let port = parsedExisting?.port ?? 18789
let desiredUrl = "\(scheme ?? "ws")://\(host):\(port)"
if existingUrl != desiredUrl {
remote["url"] = desiredUrl
gateway["remote"] = remote
changed = true
}
}
}
guard changed else { return }
root["gateway"] = gateway
ClawdbotConfigFile.saveDict(root)
guard changed else { return }
if gateway.isEmpty {
root.removeValue(forKey: "gateway")
} else {
root["gateway"] = gateway
}
ClawdbotConfigFile.saveDict(root)
}
}
func triggerVoiceEars(ttl: TimeInterval? = 5) {
@@ -187,7 +187,7 @@ actor BridgeServer {
thinking: "low",
deliver: false,
to: nil,
channel: .last))
provider: .last))
case "agent.request":
guard let json = evt.payloadJSON, let data = json.data(using: .utf8) else {
@@ -205,7 +205,7 @@ actor BridgeServer {
?? "node-\(nodeId)"
let thinking = link.thinking?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty
let to = link.to?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty
let channel = GatewayAgentChannel(raw: link.channel)
let provider = GatewayAgentProvider(raw: link.channel)
_ = await GatewayConnection.shared.sendAgent(GatewayAgentInvocation(
message: message,
@@ -213,7 +213,7 @@ actor BridgeServer {
thinking: thinking,
deliver: link.deliver,
to: to,
channel: channel))
provider: provider))
default:
break
@@ -79,14 +79,15 @@ final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
GatewayProcessManager.shared.setActive(true)
}
let result = await GatewayConnection.shared.sendAgent(GatewayAgentInvocation(
message: text,
sessionKey: self.sessionKey,
thinking: "low",
deliver: false,
to: nil,
channel: .last,
idempotencyKey: actionId))
let result = await GatewayConnection.shared.sendAgent(
GatewayAgentInvocation(
message: text,
sessionKey: self.sessionKey,
thinking: "low",
deliver: false,
to: nil,
provider: .last,
idempotencyKey: actionId))
await MainActor.run {
guard let webView else { return }
@@ -20,7 +20,7 @@ enum ClawdbotConfigFile {
guard FileManager.default.fileExists(atPath: url.path) else { return [:] }
do {
let data = try Data(contentsOf: url)
guard let root = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
guard let root = self.parseConfigData(data) else {
self.logger.warning("config JSON root invalid")
return [:]
}
@@ -122,4 +122,19 @@ enum ClawdbotConfigFile {
}
return nil
}
private static func parseConfigData(_ data: Data) -> [String: Any]? {
if let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
return root
}
let decoder = JSONDecoder()
if #available(macOS 12.0, *) {
decoder.allowsJSON5 = true
}
if let decoded = try? decoder.decode([String: AnyCodable].self, from: data) {
self.logger.notice("config parsed with JSON5 decoder")
return decoded.mapValues { $0.foundationValue }
}
return nil
}
}
+11 -4
View File
@@ -34,11 +34,14 @@ enum ConfigStore {
if let override = overrides.loadRemote {
return await override()
}
return await self.loadFromGateway()
return await self.loadFromGateway() ?? [:]
}
if let override = overrides.loadLocal {
return override()
}
if let gateway = await self.loadFromGateway() {
return gateway
}
return ClawdbotConfigFile.loadDict()
}
@@ -55,13 +58,17 @@ enum ConfigStore {
if let override = overrides.saveLocal {
override(root)
} else {
ClawdbotConfigFile.saveDict(root)
do {
try await self.saveToGateway(root)
} catch {
ClawdbotConfigFile.saveDict(root)
}
}
}
}
@MainActor
private static func loadFromGateway() async -> [String: Any] {
private static func loadFromGateway() async -> [String: Any]? {
do {
let snap: ConfigSnapshot = try await GatewayConnection.shared.requestDecoded(
method: .configGet,
@@ -69,7 +76,7 @@ enum ConfigStore {
timeoutMs: 8000)
return snap.config?.mapValues { $0.foundationValue } ?? [:]
} catch {
return [:]
return nil
}
}
@@ -2,11 +2,11 @@ import SwiftUI
extension ConnectionsSettings {
var body: some View {
NavigationSplitView {
HStack(spacing: 0) {
self.sidebar
} detail: {
self.detail
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.onAppear {
self.store.start()
self.ensureSelection()
@@ -18,27 +18,30 @@ extension ConnectionsSettings {
}
private var sidebar: some View {
List(selection: self.$selectedProvider) {
if !self.enabledProviders.isEmpty {
Section("Configured") {
ScrollView {
LazyVStack(alignment: .leading, spacing: 8) {
if !self.enabledProviders.isEmpty {
self.sidebarSectionHeader("Configured")
ForEach(self.enabledProviders) { provider in
self.sidebarRow(provider)
.tag(provider)
}
}
}
if !self.availableProviders.isEmpty {
Section("Available") {
if !self.availableProviders.isEmpty {
self.sidebarSectionHeader("Available")
ForEach(self.availableProviders) { provider in
self.sidebarRow(provider)
.tag(provider)
}
}
}
.padding(.vertical, 10)
.padding(.horizontal, 10)
}
.listStyle(.sidebar)
.frame(minWidth: 210, idealWidth: 230, maxWidth: 260)
.frame(minWidth: 220, idealWidth: 240, maxWidth: 280, maxHeight: .infinity, alignment: .topLeading)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(nsColor: .windowBackgroundColor)))
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
}
private var detail: some View {
@@ -49,7 +52,7 @@ extension ConnectionsSettings {
self.emptyDetail
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.frame(minWidth: 460, maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
private var emptyDetail: some View {
@@ -79,18 +82,41 @@ extension ConnectionsSettings {
}
private func sidebarRow(_ provider: ConnectionProvider) -> some View {
HStack(spacing: 8) {
Circle()
.fill(self.providerTint(provider))
.frame(width: 8, height: 8)
VStack(alignment: .leading, spacing: 2) {
Text(provider.title)
Text(self.providerSummary(provider))
.font(.caption)
.foregroundStyle(.secondary)
let isSelected = self.selectedProvider == provider
return Button {
self.selectedProvider = provider
} label: {
HStack(spacing: 8) {
Circle()
.fill(self.providerTint(provider))
.frame(width: 8, height: 8)
VStack(alignment: .leading, spacing: 2) {
Text(provider.title)
Text(self.providerSummary(provider))
.font(.caption)
.foregroundStyle(.secondary)
}
}
.padding(.vertical, 4)
.padding(.horizontal, 6)
.frame(maxWidth: .infinity, alignment: .leading)
.background(isSelected ? Color.accentColor.opacity(0.18) : Color.clear)
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
.background(Color.clear) // ensure full-width hit test area
.contentShape(Rectangle())
}
.padding(.vertical, 4)
.frame(maxWidth: .infinity, alignment: .leading)
.buttonStyle(.plain)
.contentShape(Rectangle())
}
private func sidebarSectionHeader(_ title: String) -> some View {
Text(title)
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
.textCase(.uppercase)
.padding(.horizontal, 4)
.padding(.top, 2)
}
private func detailHeader(for provider: ConnectionProvider) -> some View {
@@ -33,13 +33,13 @@ extension CronJobEditor {
case let .systemEvent(text):
self.payloadKind = .systemEvent
self.systemEventText = text
case let .agentTurn(message, thinking, timeoutSeconds, deliver, channel, to, bestEffortDeliver):
case let .agentTurn(message, thinking, timeoutSeconds, deliver, provider, to, bestEffortDeliver):
self.payloadKind = .agentTurn
self.agentMessage = message
self.thinking = thinking ?? ""
self.timeoutSeconds = timeoutSeconds.map(String.init) ?? ""
self.deliver = deliver ?? false
self.channel = GatewayAgentChannel(raw: channel)
self.provider = GatewayAgentProvider(raw: provider)
self.to = to ?? ""
self.bestEffortDeliver = bestEffortDeliver ?? false
}
@@ -166,7 +166,7 @@ extension CronJobEditor {
if let n = Int(self.timeoutSeconds), n > 0 { payload["timeoutSeconds"] = n }
payload["deliver"] = self.deliver
if self.deliver {
payload["channel"] = self.channel.rawValue
payload["provider"] = self.provider.rawValue
let to = self.to.trimmingCharacters(in: .whitespacesAndNewlines)
if !to.isEmpty { payload["to"] = to }
payload["bestEffortDeliver"] = self.bestEffortDeliver
@@ -13,7 +13,7 @@ extension CronJobEditor {
self.payloadKind = .agentTurn
self.agentMessage = "Run diagnostic"
self.deliver = true
self.channel = .last
self.provider = .last
self.to = "+15551230000"
self.thinking = "low"
self.timeoutSeconds = "90"
@@ -17,7 +17,7 @@ struct CronJobEditor: View {
static let scheduleKindNote =
"“At” runs once, “Every” repeats with a duration, “Cron” uses a 5-field Unix expression."
static let isolatedPayloadNote =
"Isolated jobs always run an agent turn. The result can be delivered to a surface, "
"Isolated jobs always run an agent turn. The result can be delivered to a provider, "
+ "and a short summary is posted back to your main chat."
static let mainPayloadNote =
"System events are injected into the current main session. Agent turns require an isolated session target."
@@ -42,7 +42,7 @@ struct CronJobEditor: View {
@State var systemEventText: String = ""
@State var agentMessage: String = ""
@State var deliver: Bool = false
@State var channel: GatewayAgentChannel = .last
@State var provider: GatewayAgentProvider = .last
@State var to: String = ""
@State var thinking: String = ""
@State var timeoutSeconds: String = ""
@@ -309,7 +309,7 @@ struct CronJobEditor: View {
}
GridRow {
self.gridLabel("Deliver")
Toggle("Deliver result to a surface", isOn: self.$deliver)
Toggle("Deliver result to a provider", isOn: self.$deliver)
.toggleStyle(.switch)
}
}
@@ -317,12 +317,15 @@ struct CronJobEditor: View {
if self.deliver {
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) {
GridRow {
self.gridLabel("Channel")
Picker("", selection: self.$channel) {
Text("last").tag(GatewayAgentChannel.last)
Text("whatsapp").tag(GatewayAgentChannel.whatsapp)
Text("telegram").tag(GatewayAgentChannel.telegram)
Text("discord").tag(GatewayAgentChannel.discord)
self.gridLabel("Provider")
Picker("", selection: self.$provider) {
Text("last").tag(GatewayAgentProvider.last)
Text("whatsapp").tag(GatewayAgentProvider.whatsapp)
Text("telegram").tag(GatewayAgentProvider.telegram)
Text("discord").tag(GatewayAgentProvider.discord)
Text("slack").tag(GatewayAgentProvider.slack)
Text("signal").tag(GatewayAgentProvider.signal)
Text("imessage").tag(GatewayAgentProvider.imessage)
}
.labelsHidden()
.pickerStyle(.segmented)
+5 -5
View File
@@ -74,12 +74,12 @@ enum CronPayload: Codable, Equatable {
thinking: String?,
timeoutSeconds: Int?,
deliver: Bool?,
channel: String?,
provider: String?,
to: String?,
bestEffortDeliver: Bool?)
enum CodingKeys: String, CodingKey {
case kind, text, message, thinking, timeoutSeconds, deliver, channel, to, bestEffortDeliver
case kind, text, message, thinking, timeoutSeconds, deliver, provider, to, bestEffortDeliver
}
var kind: String {
@@ -101,7 +101,7 @@ enum CronPayload: Codable, Equatable {
thinking: container.decodeIfPresent(String.self, forKey: .thinking),
timeoutSeconds: container.decodeIfPresent(Int.self, forKey: .timeoutSeconds),
deliver: container.decodeIfPresent(Bool.self, forKey: .deliver),
channel: container.decodeIfPresent(String.self, forKey: .channel),
provider: container.decodeIfPresent(String.self, forKey: .provider),
to: container.decodeIfPresent(String.self, forKey: .to),
bestEffortDeliver: container.decodeIfPresent(Bool.self, forKey: .bestEffortDeliver))
default:
@@ -118,12 +118,12 @@ enum CronPayload: Codable, Equatable {
switch self {
case let .systemEvent(text):
try container.encode(text, forKey: .text)
case let .agentTurn(message, thinking, timeoutSeconds, deliver, channel, to, bestEffortDeliver):
case let .agentTurn(message, thinking, timeoutSeconds, deliver, provider, to, bestEffortDeliver):
try container.encode(message, forKey: .message)
try container.encodeIfPresent(thinking, forKey: .thinking)
try container.encodeIfPresent(timeoutSeconds, forKey: .timeoutSeconds)
try container.encodeIfPresent(deliver, forKey: .deliver)
try container.encodeIfPresent(channel, forKey: .channel)
try container.encodeIfPresent(provider, forKey: .provider)
try container.encodeIfPresent(to, forKey: .to)
try container.encodeIfPresent(bestEffortDeliver, forKey: .bestEffortDeliver)
}
@@ -206,7 +206,7 @@ extension CronSettings {
Text(text)
.font(.callout)
.textSelection(.enabled)
case let .agentTurn(message, thinking, timeoutSeconds, deliver, channel, to, _):
case let .agentTurn(message, thinking, timeoutSeconds, deliver, provider, to, _):
VStack(alignment: .leading, spacing: 4) {
Text(message)
.font(.callout)
@@ -216,7 +216,7 @@ extension CronSettings {
if let timeoutSeconds { StatusPill(text: "\(timeoutSeconds)s", tint: .secondary) }
if deliver ?? false {
StatusPill(text: "deliver", tint: .secondary)
if let channel, !channel.isEmpty { StatusPill(text: channel, tint: .secondary) }
if let provider, !provider.isEmpty { StatusPill(text: provider, tint: .secondary) }
if let to, !to.isEmpty { StatusPill(text: to, tint: .secondary) }
}
}
@@ -20,7 +20,7 @@ struct CronSettings_Previews: PreviewProvider {
thinking: "low",
timeoutSeconds: 600,
deliver: true,
channel: "last",
provider: "last",
to: nil,
bestEffortDeliver: true),
isolation: CronIsolation(postToMainPrefix: "Cron"),
@@ -72,7 +72,7 @@ extension CronSettings {
thinking: "low",
timeoutSeconds: 120,
deliver: true,
channel: "whatsapp",
provider: "whatsapp",
to: "+15551234567",
bestEffortDeliver: true),
isolation: CronIsolation(postToMainPrefix: "[cron] "),
+3 -3
View File
@@ -59,7 +59,7 @@ final class DeepLinkHandler {
}
do {
let channel = GatewayAgentChannel(raw: link.channel)
let provider = GatewayAgentProvider(raw: link.channel)
let explicitSessionKey = link.sessionKey?
.trimmingCharacters(in: .whitespacesAndNewlines)
.nonEmpty
@@ -72,9 +72,9 @@ final class DeepLinkHandler {
message: messagePreview,
sessionKey: resolvedSessionKey,
thinking: link.thinking?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty,
deliver: channel.shouldDeliver(link.deliver),
deliver: provider.shouldDeliver(link.deliver),
to: link.to?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty,
channel: channel,
provider: provider,
timeoutSeconds: link.timeoutSeconds,
idempotencyKey: UUID().uuidString)
@@ -5,16 +5,19 @@ import OSLog
private let gatewayConnectionLogger = Logger(subsystem: "com.clawdbot", category: "gateway.connection")
enum GatewayAgentChannel: String, Codable, CaseIterable, Sendable {
enum GatewayAgentProvider: String, Codable, CaseIterable, Sendable {
case last
case whatsapp
case telegram
case discord
case slack
case signal
case imessage
case webchat
init(raw: String?) {
let normalized = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
self = GatewayAgentChannel(rawValue: normalized) ?? .last
self = GatewayAgentProvider(rawValue: normalized) ?? .last
}
var isDeliverable: Bool { self != .webchat }
@@ -28,7 +31,7 @@ struct GatewayAgentInvocation: Sendable {
var thinking: String?
var deliver: Bool = false
var to: String?
var channel: GatewayAgentChannel = .last
var provider: GatewayAgentProvider = .last
var timeoutSeconds: Int?
var idempotencyKey: String = UUID().uuidString
}
@@ -365,7 +368,7 @@ extension GatewayConnection {
"thinking": AnyCodable(invocation.thinking ?? "default"),
"deliver": AnyCodable(invocation.deliver),
"to": AnyCodable(invocation.to ?? ""),
"channel": AnyCodable(invocation.channel.rawValue),
"provider": AnyCodable(invocation.provider.rawValue),
"idempotencyKey": AnyCodable(invocation.idempotencyKey),
]
if let timeout = invocation.timeoutSeconds {
@@ -386,7 +389,7 @@ extension GatewayConnection {
sessionKey: String,
deliver: Bool,
to: String?,
channel: GatewayAgentChannel = .last,
provider: GatewayAgentProvider = .last,
timeoutSeconds: Int? = nil,
idempotencyKey: String = UUID().uuidString) async -> (ok: Bool, error: String?)
{
@@ -396,7 +399,7 @@ extension GatewayConnection {
thinking: thinking,
deliver: deliver,
to: to,
channel: channel,
provider: provider,
timeoutSeconds: timeoutSeconds,
idempotencyKey: idempotencyKey))
}
@@ -13,12 +13,14 @@ enum GatewayEndpointState: Sendable, Equatable {
/// - The endpoint store owns observation + explicit "ensure tunnel" actions.
actor GatewayEndpointStore {
static let shared = GatewayEndpointStore()
private static let supportedBindModes: Set<String> = ["loopback", "tailnet", "lan", "auto"]
struct Deps: Sendable {
let mode: @Sendable () async -> AppState.ConnectionMode
let token: @Sendable () -> String?
let password: @Sendable () -> String?
let localPort: @Sendable () -> Int
let localHost: @Sendable () async -> String
let remotePortIfRunning: @Sendable () async -> UInt16?
let ensureRemoteTunnel: @Sendable () async throws -> UInt16
@@ -33,6 +35,16 @@ actor GatewayEndpointStore {
env: ProcessInfo.processInfo.environment)
},
localPort: { GatewayEnvironment.gatewayPort() },
localHost: {
let root = ClawdbotConfigFile.loadDict()
let bind = GatewayEndpointStore.resolveGatewayBindMode(
root: root,
env: ProcessInfo.processInfo.environment)
let tailscaleIP = await MainActor.run { TailscaleService.shared.tailscaleIP }
return GatewayEndpointStore.resolveLocalGatewayHost(
bindMode: bind,
tailscaleIP: tailscaleIP)
},
remotePortIfRunning: { await RemoteTunnelManager.shared.controlTunnelPortIfRunning() },
ensureRemoteTunnel: { try await RemoteTunnelManager.shared.ensureControlTunnel() })
}
@@ -89,13 +101,17 @@ actor GatewayEndpointStore {
}
let port = deps.localPort()
let bind = GatewayEndpointStore.resolveGatewayBindMode(
root: ClawdbotConfigFile.loadDict(),
env: ProcessInfo.processInfo.environment)
let host = GatewayEndpointStore.resolveLocalGatewayHost(bindMode: bind, tailscaleIP: nil)
let token = deps.token()
let password = deps.password()
switch initialMode {
case .local:
self.state = .ready(
mode: .local,
url: URL(string: "ws://127.0.0.1:\(port)")!,
url: URL(string: "ws://\(host):\(port)")!,
token: token,
password: password)
case .remote:
@@ -129,9 +145,10 @@ actor GatewayEndpointStore {
switch mode {
case .local:
let port = self.deps.localPort()
let host = await self.deps.localHost()
self.setState(.ready(
mode: .local,
url: URL(string: "ws://127.0.0.1:\(port)")!,
url: URL(string: "ws://\(host):\(port)")!,
token: token,
password: password))
case .remote:
@@ -218,6 +235,39 @@ actor GatewayEndpointStore {
"endpoint unavailable mode=\(modeDesc, privacy: .public) reason=\(reason, privacy: .public)")
}
}
private static func resolveGatewayBindMode(
root: [String: Any],
env: [String: String]) -> String?
{
if let envBind = env["CLAWDBOT_GATEWAY_BIND"] {
let trimmed = envBind.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if self.supportedBindModes.contains(trimmed) {
return trimmed
}
}
if let gateway = root["gateway"] as? [String: Any],
let bind = gateway["bind"] as? String
{
let trimmed = bind.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if self.supportedBindModes.contains(trimmed) {
return trimmed
}
}
return nil
}
private static func resolveLocalGatewayHost(
bindMode: String?,
tailscaleIP: String?) -> String
{
switch bindMode {
case "tailnet", "auto":
tailscaleIP ?? "127.0.0.1"
default:
"127.0.0.1"
}
}
}
#if DEBUG
@@ -229,5 +279,19 @@ extension GatewayEndpointStore {
{
self.resolveGatewayPassword(isRemote: isRemote, root: root, env: env)
}
static func _testResolveGatewayBindMode(
root: [String: Any],
env: [String: String]) -> String?
{
self.resolveGatewayBindMode(root: root, env: env)
}
static func _testResolveLocalGatewayHost(
bindMode: String?,
tailscaleIP: String?) -> String
{
self.resolveLocalGatewayHost(bindMode: bindMode, tailscaleIP: tailscaleIP)
}
}
#endif
@@ -1,6 +0,0 @@
import ClawdbotProtocol
// The generated gateway protocol models are value types, but they don't currently declare Sendable.
// We use them across actors via GatewayConnection's event stream, so mark them as unchecked.
extension HelloOk: @unchecked Sendable {}
extension EventFrame: @unchecked Sendable {}
@@ -151,6 +151,7 @@ struct GeneralSettings: View {
private func requestLocationAuthorization(mode: ClawdbotLocationMode) async -> Bool {
guard mode != .off else { return true }
let status = CLLocationManager().authorizationStatus
// Note: macOS only supports authorizedAlways, not authorizedWhenInUse (iOS only)
if status == .authorizedAlways {
return true
}
@@ -11,9 +11,6 @@ final class MacNodeLocationService: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
private var locationContinuation: CheckedContinuation<CLLocation, Swift.Error>?
private struct UncheckedSendable<T>: @unchecked Sendable {
let value: T
}
override init() {
super.init()
@@ -63,7 +60,7 @@ final class MacNodeLocationService: NSObject, CLLocationManagerDelegate {
}
}
private func withTimeout<T>(
private func withTimeout<T: Sendable>(
timeoutMs: Int,
operation: @escaping () async throws -> T) async throws -> T
{
@@ -71,15 +68,38 @@ final class MacNodeLocationService: NSObject, CLLocationManagerDelegate {
return try await operation()
}
return try await withThrowingTaskGroup(of: UncheckedSendable<T>.self) { group in
group.addTask { try await UncheckedSendable(value: operation()) }
group.addTask {
try await Task.sleep(nanoseconds: UInt64(timeoutMs) * 1_000_000)
throw Error.timeout
return try await withCheckedThrowingContinuation { continuation in
var didFinish = false
func finish(returning value: T) {
guard !didFinish else { return }
didFinish = true
continuation.resume(returning: value)
}
func finish(throwing error: Swift.Error) {
guard !didFinish else { return }
didFinish = true
continuation.resume(throwing: error)
}
let timeoutItem = DispatchWorkItem {
finish(throwing: Error.timeout)
}
DispatchQueue.main.asyncAfter(
deadline: .now() + .milliseconds(timeoutMs),
execute: timeoutItem)
Task { @MainActor in
do {
let value = try await operation()
timeoutItem.cancel()
finish(returning: value)
} catch {
timeoutItem.cancel()
finish(throwing: error)
}
}
let result = try await group.next()!
group.cancelAll()
return result.value
}
}
@@ -86,7 +86,7 @@ extension OnboardingView {
var navigationBar: some View {
let wizardLockIndex = self.wizardPageOrderIndex
HStack(spacing: 20) {
return HStack(spacing: 20) {
ZStack(alignment: .leading) {
Button(action: {}, label: {
Label("Back", systemImage: "chevron.left").labelStyle(.iconOnly)
@@ -1,3 +1,4 @@
import ClawdbotProtocol
import Observation
import SwiftUI
@@ -232,44 +232,52 @@ struct OnboardingWizardStepView: View {
private var selectOptions: some View {
VStack(alignment: .leading, spacing: 8) {
ForEach(self.optionItems) { item in
Button {
self.selectedIndex = item.index
} label: {
HStack(alignment: .top, spacing: 8) {
Image(systemName: self.selectedIndex == item.index ? "largecircle.fill.circle" : "circle")
.foregroundStyle(.accent)
VStack(alignment: .leading, spacing: 2) {
Text(item.option.label)
.foregroundStyle(.primary)
if let hint = item.option.hint, !hint.isEmpty {
Text(hint)
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
.buttonStyle(.plain)
ForEach(self.optionItems, id: \.index) { item in
self.selectOptionRow(item)
}
}
}
private var multiselectOptions: some View {
VStack(alignment: .leading, spacing: 8) {
ForEach(self.optionItems) { item in
Toggle(isOn: self.bindingForOption(item)) {
VStack(alignment: .leading, spacing: 2) {
Text(item.option.label)
if let hint = item.option.hint, !hint.isEmpty {
Text(hint)
.font(.caption)
.foregroundStyle(.secondary)
}
ForEach(self.optionItems, id: \.index) { item in
self.multiselectOptionRow(item)
}
}
}
private func selectOptionRow(_ item: WizardOptionItem) -> some View {
Button {
self.selectedIndex = item.index
} label: {
HStack(alignment: .top, spacing: 8) {
Image(systemName: self.selectedIndex == item.index ? "largecircle.fill.circle" : "circle")
.foregroundStyle(Color.accentColor)
VStack(alignment: .leading, spacing: 2) {
Text(item.option.label)
.foregroundStyle(.primary)
if let hint = item.option.hint, !hint.isEmpty {
Text(hint)
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
.buttonStyle(.plain)
}
private func multiselectOptionRow(_ item: WizardOptionItem) -> some View {
Toggle(isOn: self.bindingForOption(item)) {
VStack(alignment: .leading, spacing: 2) {
Text(item.option.label)
if let hint = item.option.hint, !hint.isEmpty {
Text(hint)
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
private func bindingForOption(_ item: WizardOptionItem) -> Binding<Bool> {
@@ -140,6 +140,7 @@ enum PermissionManager {
private static func ensureLocation(interactive: Bool) async -> Bool {
let status = CLLocationManager().authorizationStatus
switch status {
// Note: macOS only supports authorizedAlways, not authorizedWhenInUse (iOS only)
case .authorizedAlways:
return true
case .notDetermined:
@@ -201,6 +202,7 @@ enum PermissionManager {
case .location:
let status = CLLocationManager().authorizationStatus
// Note: macOS only supports authorizedAlways
results[cap] = status == .authorizedAlways
}
}
@@ -9,7 +9,7 @@ struct GatewaySessionDefaultsRecord: Codable {
struct GatewaySessionEntryRecord: Codable {
let key: String
let displayName: String?
let surface: String?
let provider: String?
let subject: String?
let room: String?
let space: String?
@@ -71,7 +71,7 @@ struct SessionRow: Identifiable {
let key: String
let kind: SessionKind
let displayName: String?
let surface: String?
let provider: String?
let subject: String?
let room: String?
let space: String?
@@ -141,7 +141,7 @@ extension SessionRow {
key: "user@example.com",
kind: .direct,
displayName: nil,
surface: nil,
provider: nil,
subject: nil,
room: nil,
space: nil,
@@ -158,7 +158,7 @@ extension SessionRow {
key: "discord:channel:release-squad",
kind: .group,
displayName: "discord:#release-squad",
surface: "discord",
provider: "discord",
subject: nil,
room: "#release-squad",
space: nil,
@@ -175,7 +175,7 @@ extension SessionRow {
key: "global",
kind: .global,
displayName: nil,
surface: nil,
provider: nil,
subject: nil,
room: nil,
space: nil,
@@ -298,7 +298,7 @@ enum SessionLoader {
key: entry.key,
kind: SessionKind.from(key: entry.key),
displayName: entry.displayName,
surface: entry.surface,
provider: entry.provider,
subject: entry.subject,
room: entry.room,
space: entry.space,
@@ -194,6 +194,22 @@ enum SettingsTab: CaseIterable {
case .about: "About"
}
}
var systemImage: String {
switch self {
case .general: "gearshape"
case .connections: "link"
case .skills: "sparkles"
case .sessions: "clock.arrow.circlepath"
case .cron: "calendar"
case .config: "slider.horizontal.3"
case .instances: "network"
case .voiceWake: "waveform.circle"
case .permissions: "lock.shield"
case .debug: "ant"
case .about: "info.circle"
}
}
}
@MainActor
@@ -588,7 +588,9 @@ actor TalkModeRuntime {
let stream = client.streamSynthesize(voiceId: voiceId, request: request)
guard self.isCurrent(input.generation) else { return }
if self.interruptOnSpeech, ! await self.prepareForPlayback(generation: input.generation) { return }
if self.interruptOnSpeech {
guard await self.prepareForPlayback(generation: input.generation) else { return }
}
await MainActor.run { TalkModeController.shared.updatePhase(.speaking) }
self.phase = .speaking
@@ -643,7 +645,9 @@ actor TalkModeRuntime {
private func playSystemVoice(input: TalkPlaybackInput) async throws {
self.ttsLogger.info("talk system voice start chars=\(input.cleanedText.count, privacy: .public)")
if self.interruptOnSpeech, ! await self.prepareForPlayback(generation: input.generation) { return }
if self.interruptOnSpeech {
guard await self.prepareForPlayback(generation: input.generation) else { return }
}
await MainActor.run { TalkModeController.shared.updatePhase(.speaking) }
self.phase = .speaking
await TalkSystemSpeechSynthesizer.shared.stop()
@@ -727,7 +731,6 @@ actor TalkModeRuntime {
}
extension TalkModeRuntime {
// MARK: - Audio playback (MainActor helpers)
@MainActor
@@ -37,7 +37,7 @@ enum VoiceWakeForwarder {
var thinking: String = "low"
var deliver: Bool = true
var to: String?
var channel: GatewayAgentChannel = .last
var provider: GatewayAgentProvider = .last
}
@discardableResult
@@ -46,14 +46,14 @@ enum VoiceWakeForwarder {
options: ForwardOptions = ForwardOptions()) async -> Result<Void, VoiceWakeForwardError>
{
let payload = Self.prefixedTranscript(transcript)
let deliver = options.channel.shouldDeliver(options.deliver)
let deliver = options.provider.shouldDeliver(options.deliver)
let result = await GatewayConnection.shared.sendAgent(GatewayAgentInvocation(
message: payload,
sessionKey: options.sessionKey,
thinking: options.thinking,
deliver: deliver,
to: options.to,
channel: options.channel))
provider: options.provider))
if result.ok {
self.logger.info("voice wake forward ok")
@@ -3,14 +3,14 @@ import Foundation
public let GATEWAY_PROTOCOL_VERSION = 2
public enum ErrorCode: String, Codable {
public enum ErrorCode: String, Codable, Sendable {
case notLinked = "NOT_LINKED"
case agentTimeout = "AGENT_TIMEOUT"
case invalidRequest = "INVALID_REQUEST"
case unavailable = "UNAVAILABLE"
}
public struct ConnectParams: Codable {
public struct ConnectParams: Codable, Sendable {
public let minprotocol: Int
public let maxprotocol: Int
public let client: [String: AnyCodable]
@@ -47,7 +47,7 @@ public struct ConnectParams: Codable {
}
}
public struct HelloOk: Codable {
public struct HelloOk: Codable, Sendable {
public let type: String
public let _protocol: Int
public let server: [String: AnyCodable]
@@ -84,7 +84,7 @@ public struct HelloOk: Codable {
}
}
public struct RequestFrame: Codable {
public struct RequestFrame: Codable, Sendable {
public let type: String
public let id: String
public let method: String
@@ -109,7 +109,7 @@ public struct RequestFrame: Codable {
}
}
public struct ResponseFrame: Codable {
public struct ResponseFrame: Codable, Sendable {
public let type: String
public let id: String
public let ok: Bool
@@ -138,7 +138,7 @@ public struct ResponseFrame: Codable {
}
}
public struct EventFrame: Codable {
public struct EventFrame: Codable, Sendable {
public let type: String
public let event: String
public let payload: AnyCodable?
@@ -167,7 +167,7 @@ public struct EventFrame: Codable {
}
}
public struct PresenceEntry: Codable {
public struct PresenceEntry: Codable, Sendable {
public let host: String?
public let ip: String?
public let version: String?
@@ -228,7 +228,7 @@ public struct PresenceEntry: Codable {
}
}
public struct StateVersion: Codable {
public struct StateVersion: Codable, Sendable {
public let presence: Int
public let health: Int
@@ -245,7 +245,7 @@ public struct StateVersion: Codable {
}
}
public struct Snapshot: Codable {
public struct Snapshot: Codable, Sendable {
public let presence: [PresenceEntry]
public let health: AnyCodable
public let stateversion: StateVersion
@@ -278,7 +278,7 @@ public struct Snapshot: Codable {
}
}
public struct ErrorShape: Codable {
public struct ErrorShape: Codable, Sendable {
public let code: String
public let message: String
public let details: AnyCodable?
@@ -307,7 +307,7 @@ public struct ErrorShape: Codable {
}
}
public struct AgentEvent: Codable {
public struct AgentEvent: Codable, Sendable {
public let runid: String
public let seq: Int
public let stream: String
@@ -336,12 +336,13 @@ public struct AgentEvent: Codable {
}
}
public struct SendParams: Codable {
public struct SendParams: Codable, Sendable {
public let to: String
public let message: String
public let mediaurl: String?
public let gifplayback: Bool?
public let provider: String?
public let accountid: String?
public let idempotencykey: String
public init(
@@ -350,6 +351,7 @@ public struct SendParams: Codable {
mediaurl: String?,
gifplayback: Bool?,
provider: String?,
accountid: String?,
idempotencykey: String
) {
self.to = to
@@ -357,6 +359,7 @@ public struct SendParams: Codable {
self.mediaurl = mediaurl
self.gifplayback = gifplayback
self.provider = provider
self.accountid = accountid
self.idempotencykey = idempotencykey
}
private enum CodingKeys: String, CodingKey {
@@ -365,18 +368,60 @@ public struct SendParams: Codable {
case mediaurl = "mediaUrl"
case gifplayback = "gifPlayback"
case provider
case accountid = "accountId"
case idempotencykey = "idempotencyKey"
}
}
public struct AgentParams: Codable {
public struct PollParams: Codable, Sendable {
public let to: String
public let question: String
public let options: [String]
public let maxselections: Int?
public let durationhours: Int?
public let provider: String?
public let accountid: String?
public let idempotencykey: String
public init(
to: String,
question: String,
options: [String],
maxselections: Int?,
durationhours: Int?,
provider: String?,
accountid: String?,
idempotencykey: String
) {
self.to = to
self.question = question
self.options = options
self.maxselections = maxselections
self.durationhours = durationhours
self.provider = provider
self.accountid = accountid
self.idempotencykey = idempotencykey
}
private enum CodingKeys: String, CodingKey {
case to
case question
case options
case maxselections = "maxSelections"
case durationhours = "durationHours"
case provider
case accountid = "accountId"
case idempotencykey = "idempotencyKey"
}
}
public struct AgentParams: Codable, Sendable {
public let message: String
public let to: String?
public let sessionid: String?
public let sessionkey: String?
public let thinking: String?
public let deliver: Bool?
public let channel: String?
public let provider: String?
public let timeout: Int?
public let lane: String?
public let extrasystemprompt: String?
@@ -389,7 +434,7 @@ public struct AgentParams: Codable {
sessionkey: String?,
thinking: String?,
deliver: Bool?,
channel: String?,
provider: String?,
timeout: Int?,
lane: String?,
extrasystemprompt: String?,
@@ -401,7 +446,7 @@ public struct AgentParams: Codable {
self.sessionkey = sessionkey
self.thinking = thinking
self.deliver = deliver
self.channel = channel
self.provider = provider
self.timeout = timeout
self.lane = lane
self.extrasystemprompt = extrasystemprompt
@@ -414,7 +459,7 @@ public struct AgentParams: Codable {
case sessionkey = "sessionKey"
case thinking
case deliver
case channel
case provider
case timeout
case lane
case extrasystemprompt = "extraSystemPrompt"
@@ -422,28 +467,24 @@ public struct AgentParams: Codable {
}
}
public struct AgentWaitParams: Codable {
public struct AgentWaitParams: Codable, Sendable {
public let runid: String
public let afterms: Int?
public let timeoutms: Int?
public init(
runid: String,
afterms: Int?,
timeoutms: Int?
) {
self.runid = runid
self.afterms = afterms
self.timeoutms = timeoutms
}
private enum CodingKeys: String, CodingKey {
case runid = "runId"
case afterms = "afterMs"
case timeoutms = "timeoutMs"
}
}
public struct WakeParams: Codable {
public struct WakeParams: Codable, Sendable {
public let mode: AnyCodable
public let text: String
@@ -460,7 +501,7 @@ public struct WakeParams: Codable {
}
}
public struct NodePairRequestParams: Codable {
public struct NodePairRequestParams: Codable, Sendable {
public let nodeid: String
public let displayname: String?
public let platform: String?
@@ -509,10 +550,10 @@ public struct NodePairRequestParams: Codable {
}
}
public struct NodePairListParams: Codable {
public struct NodePairListParams: Codable, Sendable {
}
public struct NodePairApproveParams: Codable {
public struct NodePairApproveParams: Codable, Sendable {
public let requestid: String
public init(
@@ -525,7 +566,7 @@ public struct NodePairApproveParams: Codable {
}
}
public struct NodePairRejectParams: Codable {
public struct NodePairRejectParams: Codable, Sendable {
public let requestid: String
public init(
@@ -538,7 +579,7 @@ public struct NodePairRejectParams: Codable {
}
}
public struct NodePairVerifyParams: Codable {
public struct NodePairVerifyParams: Codable, Sendable {
public let nodeid: String
public let token: String
@@ -555,7 +596,7 @@ public struct NodePairVerifyParams: Codable {
}
}
public struct NodeRenameParams: Codable {
public struct NodeRenameParams: Codable, Sendable {
public let nodeid: String
public let displayname: String
@@ -572,10 +613,10 @@ public struct NodeRenameParams: Codable {
}
}
public struct NodeListParams: Codable {
public struct NodeListParams: Codable, Sendable {
}
public struct NodeDescribeParams: Codable {
public struct NodeDescribeParams: Codable, Sendable {
public let nodeid: String
public init(
@@ -588,7 +629,7 @@ public struct NodeDescribeParams: Codable {
}
}
public struct NodeInvokeParams: Codable {
public struct NodeInvokeParams: Codable, Sendable {
public let nodeid: String
public let command: String
public let params: AnyCodable?
@@ -617,37 +658,42 @@ public struct NodeInvokeParams: Codable {
}
}
public struct SessionsListParams: Codable {
public struct SessionsListParams: Codable, Sendable {
public let limit: Int?
public let activeminutes: Int?
public let includeglobal: Bool?
public let includeunknown: Bool?
public let spawnedby: String?
public init(
limit: Int?,
activeminutes: Int?,
includeglobal: Bool?,
includeunknown: Bool?
includeunknown: Bool?,
spawnedby: String?
) {
self.limit = limit
self.activeminutes = activeminutes
self.includeglobal = includeglobal
self.includeunknown = includeunknown
self.spawnedby = spawnedby
}
private enum CodingKeys: String, CodingKey {
case limit
case activeminutes = "activeMinutes"
case includeglobal = "includeGlobal"
case includeunknown = "includeUnknown"
case spawnedby = "spawnedBy"
}
}
public struct SessionsPatchParams: Codable {
public struct SessionsPatchParams: Codable, Sendable {
public let key: String
public let thinkinglevel: AnyCodable?
public let verboselevel: AnyCodable?
public let elevatedlevel: AnyCodable?
public let model: AnyCodable?
public let spawnedby: AnyCodable?
public let sendpolicy: AnyCodable?
public let groupactivation: AnyCodable?
@@ -657,6 +703,7 @@ public struct SessionsPatchParams: Codable {
verboselevel: AnyCodable?,
elevatedlevel: AnyCodable?,
model: AnyCodable?,
spawnedby: AnyCodable?,
sendpolicy: AnyCodable?,
groupactivation: AnyCodable?
) {
@@ -665,6 +712,7 @@ public struct SessionsPatchParams: Codable {
self.verboselevel = verboselevel
self.elevatedlevel = elevatedlevel
self.model = model
self.spawnedby = spawnedby
self.sendpolicy = sendpolicy
self.groupactivation = groupactivation
}
@@ -674,12 +722,13 @@ public struct SessionsPatchParams: Codable {
case verboselevel = "verboseLevel"
case elevatedlevel = "elevatedLevel"
case model
case spawnedby = "spawnedBy"
case sendpolicy = "sendPolicy"
case groupactivation = "groupActivation"
}
}
public struct SessionsResetParams: Codable {
public struct SessionsResetParams: Codable, Sendable {
public let key: String
public init(
@@ -692,7 +741,7 @@ public struct SessionsResetParams: Codable {
}
}
public struct SessionsDeleteParams: Codable {
public struct SessionsDeleteParams: Codable, Sendable {
public let key: String
public let deletetranscript: Bool?
@@ -709,7 +758,7 @@ public struct SessionsDeleteParams: Codable {
}
}
public struct SessionsCompactParams: Codable {
public struct SessionsCompactParams: Codable, Sendable {
public let key: String
public let maxlines: Int?
@@ -726,10 +775,10 @@ public struct SessionsCompactParams: Codable {
}
}
public struct ConfigGetParams: Codable {
public struct ConfigGetParams: Codable, Sendable {
}
public struct ConfigSetParams: Codable {
public struct ConfigSetParams: Codable, Sendable {
public let raw: String
public init(
@@ -742,10 +791,10 @@ public struct ConfigSetParams: Codable {
}
}
public struct ConfigSchemaParams: Codable {
public struct ConfigSchemaParams: Codable, Sendable {
}
public struct ConfigSchemaResponse: Codable {
public struct ConfigSchemaResponse: Codable, Sendable {
public let schema: AnyCodable
public let uihints: [String: AnyCodable]
public let version: String
@@ -770,7 +819,7 @@ public struct ConfigSchemaResponse: Codable {
}
}
public struct WizardStartParams: Codable {
public struct WizardStartParams: Codable, Sendable {
public let mode: AnyCodable?
public let workspace: String?
@@ -787,7 +836,7 @@ public struct WizardStartParams: Codable {
}
}
public struct WizardNextParams: Codable {
public struct WizardNextParams: Codable, Sendable {
public let sessionid: String
public let answer: [String: AnyCodable]?
@@ -804,7 +853,7 @@ public struct WizardNextParams: Codable {
}
}
public struct WizardCancelParams: Codable {
public struct WizardCancelParams: Codable, Sendable {
public let sessionid: String
public init(
@@ -817,7 +866,7 @@ public struct WizardCancelParams: Codable {
}
}
public struct WizardStatusParams: Codable {
public struct WizardStatusParams: Codable, Sendable {
public let sessionid: String
public init(
@@ -830,7 +879,7 @@ public struct WizardStatusParams: Codable {
}
}
public struct WizardStep: Codable {
public struct WizardStep: Codable, Sendable {
public let id: String
public let type: AnyCodable
public let title: String?
@@ -875,7 +924,7 @@ public struct WizardStep: Codable {
}
}
public struct WizardNextResult: Codable {
public struct WizardNextResult: Codable, Sendable {
public let done: Bool
public let step: [String: AnyCodable]?
public let status: AnyCodable?
@@ -900,7 +949,7 @@ public struct WizardNextResult: Codable {
}
}
public struct WizardStartResult: Codable {
public struct WizardStartResult: Codable, Sendable {
public let sessionid: String
public let done: Bool
public let step: [String: AnyCodable]?
@@ -929,7 +978,7 @@ public struct WizardStartResult: Codable {
}
}
public struct WizardStatusResult: Codable {
public struct WizardStatusResult: Codable, Sendable {
public let status: AnyCodable
public let error: String?
@@ -946,7 +995,7 @@ public struct WizardStatusResult: Codable {
}
}
public struct TalkModeParams: Codable {
public struct TalkModeParams: Codable, Sendable {
public let enabled: Bool
public let phase: String?
@@ -963,7 +1012,7 @@ public struct TalkModeParams: Codable {
}
}
public struct ProvidersStatusParams: Codable {
public struct ProvidersStatusParams: Codable, Sendable {
public let probe: Bool?
public let timeoutms: Int?
@@ -980,41 +1029,49 @@ public struct ProvidersStatusParams: Codable {
}
}
public struct WebLoginStartParams: Codable {
public struct WebLoginStartParams: Codable, Sendable {
public let force: Bool?
public let timeoutms: Int?
public let verbose: Bool?
public let accountid: String?
public init(
force: Bool?,
timeoutms: Int?,
verbose: Bool?
verbose: Bool?,
accountid: String?
) {
self.force = force
self.timeoutms = timeoutms
self.verbose = verbose
self.accountid = accountid
}
private enum CodingKeys: String, CodingKey {
case force
case timeoutms = "timeoutMs"
case verbose
case accountid = "accountId"
}
}
public struct WebLoginWaitParams: Codable {
public struct WebLoginWaitParams: Codable, Sendable {
public let timeoutms: Int?
public let accountid: String?
public init(
timeoutms: Int?
timeoutms: Int?,
accountid: String?
) {
self.timeoutms = timeoutms
self.accountid = accountid
}
private enum CodingKeys: String, CodingKey {
case timeoutms = "timeoutMs"
case accountid = "accountId"
}
}
public struct ModelChoice: Codable {
public struct ModelChoice: Codable, Sendable {
public let id: String
public let name: String
public let provider: String
@@ -1043,10 +1100,10 @@ public struct ModelChoice: Codable {
}
}
public struct ModelsListParams: Codable {
public struct ModelsListParams: Codable, Sendable {
}
public struct ModelsListResult: Codable {
public struct ModelsListResult: Codable, Sendable {
public let models: [ModelChoice]
public init(
@@ -1059,10 +1116,10 @@ public struct ModelsListResult: Codable {
}
}
public struct SkillsStatusParams: Codable {
public struct SkillsStatusParams: Codable, Sendable {
}
public struct SkillsInstallParams: Codable {
public struct SkillsInstallParams: Codable, Sendable {
public let name: String
public let installid: String
public let timeoutms: Int?
@@ -1083,7 +1140,7 @@ public struct SkillsInstallParams: Codable {
}
}
public struct SkillsUpdateParams: Codable {
public struct SkillsUpdateParams: Codable, Sendable {
public let skillkey: String
public let enabled: Bool?
public let apikey: String?
@@ -1108,7 +1165,7 @@ public struct SkillsUpdateParams: Codable {
}
}
public struct CronJob: Codable {
public struct CronJob: Codable, Sendable {
public let id: String
public let name: String
public let description: String?
@@ -1165,7 +1222,7 @@ public struct CronJob: Codable {
}
}
public struct CronListParams: Codable {
public struct CronListParams: Codable, Sendable {
public let includedisabled: Bool?
public init(
@@ -1178,10 +1235,10 @@ public struct CronListParams: Codable {
}
}
public struct CronStatusParams: Codable {
public struct CronStatusParams: Codable, Sendable {
}
public struct CronAddParams: Codable {
public struct CronAddParams: Codable, Sendable {
public let name: String
public let description: String?
public let enabled: Bool?
@@ -1222,7 +1279,7 @@ public struct CronAddParams: Codable {
}
}
public struct CronUpdateParams: Codable {
public struct CronUpdateParams: Codable, Sendable {
public let id: String
public let patch: [String: AnyCodable]
@@ -1239,7 +1296,7 @@ public struct CronUpdateParams: Codable {
}
}
public struct CronRemoveParams: Codable {
public struct CronRemoveParams: Codable, Sendable {
public let id: String
public init(
@@ -1252,7 +1309,7 @@ public struct CronRemoveParams: Codable {
}
}
public struct CronRunParams: Codable {
public struct CronRunParams: Codable, Sendable {
public let id: String
public let mode: AnyCodable?
@@ -1269,7 +1326,7 @@ public struct CronRunParams: Codable {
}
}
public struct CronRunsParams: Codable {
public struct CronRunsParams: Codable, Sendable {
public let id: String
public let limit: Int?
@@ -1286,7 +1343,7 @@ public struct CronRunsParams: Codable {
}
}
public struct CronRunLogEntry: Codable {
public struct CronRunLogEntry: Codable, Sendable {
public let ts: Int
public let jobid: String
public let action: String
@@ -1331,7 +1388,7 @@ public struct CronRunLogEntry: Codable {
}
}
public struct ChatHistoryParams: Codable {
public struct ChatHistoryParams: Codable, Sendable {
public let sessionkey: String
public let limit: Int?
@@ -1348,7 +1405,7 @@ public struct ChatHistoryParams: Codable {
}
}
public struct ChatSendParams: Codable {
public struct ChatSendParams: Codable, Sendable {
public let sessionkey: String
public let message: String
public let thinking: String?
@@ -1385,7 +1442,7 @@ public struct ChatSendParams: Codable {
}
}
public struct ChatAbortParams: Codable {
public struct ChatAbortParams: Codable, Sendable {
public let sessionkey: String
public let runid: String
@@ -1402,7 +1459,7 @@ public struct ChatAbortParams: Codable {
}
}
public struct ChatEvent: Codable {
public struct ChatEvent: Codable, Sendable {
public let runid: String
public let sessionkey: String
public let seq: Int
@@ -1443,7 +1500,7 @@ public struct ChatEvent: Codable {
}
}
public struct TickEvent: Codable {
public struct TickEvent: Codable, Sendable {
public let ts: Int
public init(
@@ -1456,7 +1513,7 @@ public struct TickEvent: Codable {
}
}
public struct ShutdownEvent: Codable {
public struct ShutdownEvent: Codable, Sendable {
public let reason: String
public let restartexpectedms: Int?
@@ -1473,7 +1530,7 @@ public struct ShutdownEvent: Codable {
}
}
public enum GatewayFrame: Codable {
public enum GatewayFrame: Codable, Sendable {
case req(RequestFrame)
case res(ResponseFrame)
case event(EventFrame)
@@ -1,156 +1,36 @@
import Foundation
import Testing
@testable import Clawdbot
@Suite struct GatewayEndpointStoreTests {
private final class ModeBox: @unchecked Sendable {
private let lock = NSLock()
private var value: AppState.ConnectionMode
init(_ initial: AppState.ConnectionMode) {
self.value = initial
}
func get() -> AppState.ConnectionMode {
self.lock.lock()
defer { self.lock.unlock() }
return self.value
}
func set(_ next: AppState.ConnectionMode) {
self.lock.lock()
defer { self.lock.unlock() }
self.value = next
}
@Suite(.serialized)
struct GatewayEndpointStoreTests {
@Test func resolvesLocalHostFromBindModes() {
#expect(GatewayEndpointStore._testResolveLocalGatewayHost(
bindMode: "loopback",
tailscaleIP: "100.64.0.10") == "127.0.0.1")
#expect(GatewayEndpointStore._testResolveLocalGatewayHost(
bindMode: "lan",
tailscaleIP: "100.64.0.10") == "127.0.0.1")
#expect(GatewayEndpointStore._testResolveLocalGatewayHost(
bindMode: "tailnet",
tailscaleIP: "100.64.0.10") == "100.64.0.10")
#expect(GatewayEndpointStore._testResolveLocalGatewayHost(
bindMode: "tailnet",
tailscaleIP: nil) == "127.0.0.1")
#expect(GatewayEndpointStore._testResolveLocalGatewayHost(
bindMode: "auto",
tailscaleIP: "100.64.0.10") == "100.64.0.10")
#expect(GatewayEndpointStore._testResolveLocalGatewayHost(
bindMode: "auto",
tailscaleIP: nil) == "127.0.0.1")
}
@Test func localRefreshResolvesToLocalhostPort() async throws {
let mode = ModeBox(.local)
let store = GatewayEndpointStore(deps: .init(
mode: { mode.get() },
token: { "t" },
password: { nil },
localPort: { 1234 },
remotePortIfRunning: { nil },
ensureRemoteTunnel: { 18789 }))
await store.refresh()
let cfg = try await store.requireConfig()
#expect(cfg.url.absoluteString == "ws://127.0.0.1:1234")
#expect(cfg.token == "t")
}
@Test func remoteWithoutTunnelRecoversByEnsuringTunnel() async throws {
let mode = ModeBox(.remote)
let store = GatewayEndpointStore(deps: .init(
mode: { mode.get() },
token: { nil },
password: { nil },
localPort: { 18789 },
remotePortIfRunning: { nil },
ensureRemoteTunnel: { 18789 }))
let cfg = try await store.requireConfig()
#expect(cfg.url.absoluteString == "ws://127.0.0.1:18789")
#expect(cfg.token == nil)
}
@Test func ensureRemoteTunnelPublishesReadyState() async throws {
let mode = ModeBox(.remote)
let store = GatewayEndpointStore(deps: .init(
mode: { mode.get() },
token: { "tok" },
password: { "pw" },
localPort: { 1 },
remotePortIfRunning: { 5555 },
ensureRemoteTunnel: { 5555 }))
let stream = await store.subscribe(bufferingNewest: 10)
var iterator = stream.makeAsyncIterator()
_ = await iterator.next() // initial
_ = try await store.ensureRemoteControlTunnel()
let next = await iterator.next()
guard case let .ready(mode, url, token, password) = next else {
Issue.record("expected .ready after ensure, got \(String(describing: next))")
return
}
#expect(mode == .remote)
#expect(url.absoluteString == "ws://127.0.0.1:5555")
#expect(token == "tok")
#expect(password == "pw")
}
@Test func resolvesGatewayPasswordByMode() {
let root: [String: Any] = [
"gateway": [
"auth": ["password": " local "],
"remote": ["password": " remote "],
],
]
let env: [String: String] = [:]
#expect(GatewayEndpointStore._testResolveGatewayPassword(
isRemote: false,
@Test func resolvesBindModeFromEnvOrConfig() {
let root: [String: Any] = ["gateway": ["bind": "tailnet"]]
#expect(GatewayEndpointStore._testResolveGatewayBindMode(
root: root,
env: env) == "local")
#expect(GatewayEndpointStore._testResolveGatewayPassword(
isRemote: true,
env: [:]) == "tailnet")
#expect(GatewayEndpointStore._testResolveGatewayBindMode(
root: root,
env: env) == "remote")
}
@Test func gatewayPasswordEnvOverridesConfig() {
let root: [String: Any] = [
"gateway": [
"auth": ["password": "local"],
"remote": ["password": "remote"],
],
]
let env = ["CLAWDBOT_GATEWAY_PASSWORD": " env "]
#expect(GatewayEndpointStore._testResolveGatewayPassword(
isRemote: false,
root: root,
env: env) == "env")
#expect(GatewayEndpointStore._testResolveGatewayPassword(
isRemote: true,
root: root,
env: env) == "env")
}
@Test func gatewayPasswordIgnoresWhitespaceValues() {
let root: [String: Any] = [
"gateway": [
"auth": ["password": " "],
"remote": ["password": "\n\t"],
],
]
let env = ["CLAWDBOT_GATEWAY_PASSWORD": " "]
#expect(GatewayEndpointStore._testResolveGatewayPassword(
isRemote: false,
root: root,
env: env) == nil)
#expect(GatewayEndpointStore._testResolveGatewayPassword(
isRemote: true,
root: root,
env: env) == nil)
}
@Test func unconfiguredModeRejectsConfig() async {
let mode = ModeBox(.unconfigured)
let store = GatewayEndpointStore(deps: .init(
mode: { mode.get() },
token: { nil },
password: { nil },
localPort: { 18789 },
remotePortIfRunning: { nil },
ensureRemoteTunnel: { 18789 }))
await #expect(throws: Error.self) {
_ = try await store.requireConfig()
}
env: ["CLAWDBOT_GATEWAY_BIND": "lan"]) == "lan")
}
}
@@ -1,5 +1,6 @@
import Testing
import ClawdbotIPC
import CoreLocation
@testable import Clawdbot
@Suite(.serialized)
@@ -22,4 +23,16 @@ struct PermissionManagerTests {
let ensured = await PermissionManager.ensure(caps, interactive: false)
#expect(ensured.keys.count == caps.count)
}
@Test func locationStatusMatchesAuthorizationAlways() async {
let status = CLLocationManager().authorizationStatus
let results = await PermissionManager.status([.location])
#expect(results[.location] == (status == .authorizedAlways))
}
@Test func ensureLocationNonInteractiveMatchesAuthorizationAlways() async {
let status = CLLocationManager().authorizationStatus
let ensured = await PermissionManager.ensure([.location], interactive: false)
#expect(ensured[.location] == (status == .authorizedAlways))
}
}
@@ -12,7 +12,7 @@
"element",
"node",
"nodeId",
"jobId",
"id",
"requestId",
"to",
"channelId",
@@ -136,10 +136,10 @@
"label": "add",
"detailKeys": ["job.name", "job.id", "job.schedule", "job.cron"]
},
"update": { "label": "update", "detailKeys": ["jobId"] },
"remove": { "label": "remove", "detailKeys": ["jobId"] },
"run": { "label": "run", "detailKeys": ["jobId"] },
"runs": { "label": "runs", "detailKeys": ["jobId"] },
"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"] }
}
},
+8 -8
View File
@@ -8,26 +8,26 @@ read_when:
## First run (recommended)
Clawdbot uses a dedicated workspace directory for the agent. Default: `~/.clawdbot/workspace`.
Clawdbot uses a dedicated workspace directory for the agent. Default: `~/clawd` (configurable via `agent.workspace`).
1) Create the workspace (if it doesn’t already exist):
```bash
mkdir -p ~/.clawdbot/workspace
mkdir -p ~/clawd
```
2) Copy the default workspace templates into the workspace:
```bash
cp docs/templates/AGENTS.md ~/.clawdbot/workspace/AGENTS.md
cp docs/templates/SOUL.md ~/.clawdbot/workspace/SOUL.md
cp docs/templates/TOOLS.md ~/.clawdbot/workspace/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/AGENTS.default.md ~/.clawdbot/workspace/AGENTS.md
cp docs/AGENTS.default.md ~/clawd/AGENTS.md
```
4) Optional: choose a different workspace by setting `agent.workspace` (supports `~`):
@@ -73,7 +73,7 @@ cp docs/AGENTS.default.md ~/.clawdbot/workspace/AGENTS.md
If you treat this workspace as Clawd’s “memory”, make it a git repo (ideally private) so `AGENTS.md` and your memory files are backed up.
```bash
cd ~/.clawdbot/workspace
cd ~/clawd
git init
git add AGENTS.md
git commit -m "Add Clawd workspace"
@@ -83,7 +83,7 @@ git commit -m "Add Clawd workspace"
## What Clawdbot Does
- Runs WhatsApp gateway + Pi coding agent so the assistant can read/write chats, fetch context, and run skills via the host Mac.
- macOS app manages permissions (screen recording, notifications, microphone) and exposes the `clawdbot` CLI via its bundled binary.
- Direct chats collapse into the shared `main` session by default; groups stay isolated as `surface:group:<id>` (rooms: `surface:channel:<id>`); heartbeats keep background tasks alive.
- Direct chats collapse into the agent's `main` session by default; groups stay isolated as `agent:<agentId>:<provider>:group:<id>` (rooms/channels: `agent:<agentId>:<provider>:channel:<id>`); heartbeats keep background tasks alive.
## Core Skills (enable in Settings → Skills)
- **mcporter** — Tool server runtime/CLI for managing external skill backends.
+6 -6
View File
@@ -12,12 +12,12 @@ Use `pnpm` (Node 22+) from the repo root. Keep the working tree clean before tag
1) **Version & metadata**
- [ ] Bump `package.json` version (e.g., `1.1.0`).
- [ ] Update CLI/version strings: `src/cli/program.ts` and the Baileys user agent in `src/provider-web.ts`.
- [ ] Confirm package metadata (name, description, repository, keywords, license) and `bin` map points to `dist/index.js` for `clawdbot`.
- [ ] Update CLI/version strings: [`src/cli/program.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/cli/program.ts) and the Baileys user agent in [`src/provider-web.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/provider-web.ts).
- [ ] Confirm package metadata (name, description, repository, keywords, license) and `bin` map points to [`dist/entry.js`](https://github.com/clawdbot/clawdbot/blob/main/dist/entry.js) for `clawdbot`.
- [ ] If dependencies changed, run `pnpm install` so `pnpm-lock.yaml` is current.
2) **Build & artifacts**
- [ ] If A2UI inputs changed, run `pnpm canvas:a2ui:bundle` and commit any updated `src/canvas-host/a2ui/a2ui.bundle.js`.
- [ ] If A2UI inputs changed, run `pnpm canvas:a2ui:bundle` and commit any updated [`src/canvas-host/a2ui/a2ui.bundle.js`](https://github.com/clawdbot/clawdbot/blob/main/src/canvas-host/a2ui/a2ui.bundle.js).
- [ ] `pnpm run build` (regenerates `dist/`).
- [ ] Optional: `npm pack --pack-destination /tmp` after the build; inspect the tarball contents and keep it handy for the GitHub release (do **not** commit it).
@@ -34,11 +34,11 @@ Use `pnpm` (Node 22+) from the repo root. Keep the working tree clean before tag
5) **macOS app (Sparkle)**
- [ ] Build + sign the macOS app, then zip it for distribution.
- [ ] Generate the Sparkle appcast (HTML notes via `scripts/make_appcast.sh`) and update `appcast.xml`.
- [ ] 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` 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`).
- 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.
+6 -4
View File
@@ -1,5 +1,5 @@
title: "CLAWDBOT Docs"
description: "A TypeScript/Node gateway + macOS/iOS companions for WhatsApp (web) and Telegram (bot)."
description: "A TypeScript/Node gateway + macOS/iOS/Android companions for WhatsApp (web) and Telegram (bot)."
markdown: kramdown
highlighter: rouge
@@ -35,9 +35,11 @@ nav:
- title: "WebChat"
url: "/webchat.html"
- title: "macOS App"
url: "/clawdbot-mac.html"
- title: "iOS Node"
url: "/ios/connect.html"
url: "/macos.html"
- title: "iOS App"
url: "/ios.html"
- title: "Android App"
url: "/android.html"
- title: "Telegram"
url: "/telegram.html"
- title: "Security"
+61
View File
@@ -0,0 +1,61 @@
---
summary: "Agent loop lifecycle, streams, and wait semantics"
read_when:
- You need an exact walkthrough of the agent loop or lifecycle events
---
# Agent Loop (Clawdis)
Short, exact flow of one agent run. Source of truth: current code in `src/`.
## Entry points
- Gateway RPC: `agent` and `agent.wait` in [`src/gateway/server-methods/agent.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/gateway/server-methods/agent.ts).
- CLI: `agentCommand` in [`src/commands/agent.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/commands/agent.ts).
## High-level flow
1) `agent` RPC validates params, resolves session (sessionKey/sessionId), persists session metadata, returns `{ runId, acceptedAt }` immediately.
2) `agentCommand` runs the agent:
- resolves model + thinking/verbose defaults
- loads skills snapshot
- calls `runEmbeddedPiAgent` (pi-agent-core runtime)
- emits **lifecycle end/error** if the embedded loop does not emit one
3) `runEmbeddedPiAgent`:
- builds `AgentSession` and subscribes to pi events
- streams assistant deltas + tool events
- enforces timeout -> aborts run if exceeded
- returns payloads + usage metadata
4) `subscribeEmbeddedPiSession` bridges pi-agent-core events to Clawdis `agent` stream:
- tool events => `stream: "tool"`
- assistant deltas => `stream: "assistant"`
- lifecycle events => `stream: "lifecycle"` (`phase: "start" | "end" | "error"`)
5) `agent.wait` uses `waitForAgentJob`:
- waits for **lifecycle end/error** for `runId`
- returns `{ status: ok|error|timeout, startedAt, endedAt, error? }`
## Event streams (today)
- `lifecycle`: emitted by `subscribeEmbeddedPiSession` (and as a fallback by `agentCommand`)
- `assistant`: streamed deltas from pi-agent-core
- `tool`: streamed tool events from pi-agent-core
## Chat provider handling
- `createAgentEventHandler` in [`src/gateway/server-chat.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/gateway/server-chat.ts):
- buffers assistant deltas
- emits chat `delta` messages
- emits chat `final` when **lifecycle end/error** arrives
## Timeouts
- `agent.wait` default: 30s (just the wait). `timeoutMs` param overrides.
- Agent runtime: `agent.timeoutSeconds` default 600s; enforced in `runEmbeddedPiAgent` abort timer.
## Where things can end early
- Agent timeout (abort)
- AbortSignal (cancel)
- Gateway disconnect or RPC timeout
- `agent.wait` timeout (wait-only, does not stop agent)
## Files
- [`src/gateway/server-methods/agent.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/gateway/server-methods/agent.ts)
- [`src/gateway/server-methods/agent-job.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/gateway/server-methods/agent-job.ts)
- [`src/commands/agent.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/commands/agent.ts)
- [`src/agents/pi-embedded-runner.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/agents/pi-embedded-runner.ts)
- [`src/agents/pi-embedded-subscribe.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/agents/pi-embedded-subscribe.ts)
- [`src/gateway/server-chat.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/gateway/server-chat.ts)
+14 -8
View File
@@ -3,20 +3,19 @@ summary: "Agent runtime (embedded p-mono), workspace contract, and session boots
read_when:
- Changing agent runtime, workspace bootstrap, or session behavior
---
<!-- {% raw %} -->
# Agent Runtime 🤖
CLAWDBOT runs a single embedded agent runtime derived from **p-mono** (internal name: **p**).
## Workspace (required)
You must set an agent home directory via `agent.workspace`. CLAWDBOT uses this as the agent’s **only** working directory (`cwd`) for tools and context.
CLAWDBOT uses a single agent workspace directory (`agent.workspace`) as the agent’s **only** working directory (`cwd`) for tools and context.
Recommended: use `clawdbot setup` to create `~/.clawdbot/clawdbot.json` if missing and initialize the workspace files.
If `agent.sandbox` is enabled, non-main sessions can override this with
per-session workspaces under `agent.sandbox.workspaceRoot` (see
`docs/configuration.md`).
[`docs/configuration.md`](https://docs.clawd.bot/configuration)).
## Bootstrap files (injected)
@@ -32,6 +31,14 @@ On the first turn of a new session, CLAWDBOT injects the contents of these files
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.
To disable bootstrap file creation entirely (for pre-seeded workspaces), set:
```json5
{ agent: { skipBootstrap: true } }
```
## Built-in tools (internal)
p’s embedded core tools (read/bash/edit/write and related internals) are defined in code and always available. `TOOLS.md` does **not** control which tools exist; it’s guidance for how *you* want them used.
@@ -43,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`).
Skills can be gated by config/env (see `skills` in [`docs/configuration.md`](https://docs.clawd.bot/configuration)).
## p-mono integration
@@ -67,7 +74,7 @@ Apply these notes **only** when the user is Peter Steinberger at steipete.
## Sessions
Session transcripts are stored as JSONL at:
- `~/.clawdbot/sessions/<SessionId>.jsonl`
- `~/.clawdbot/agents/<agentId>/sessions/<SessionId>.jsonl`
The session ID is stable and chosen by CLAWDBOT.
Legacy Pi/Tau session folders are **not** read.
@@ -82,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` 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.
@@ -100,5 +107,4 @@ At minimum, set:
---
*Next: [Group Chats](./group-messages.md)* 🦞
<!-- {% endraw %} -->
*Next: [Group Chats](https://docs.clawd.bot/group-messages)* 🦞
+18 -16
View File
@@ -1,18 +1,20 @@
---
summary: "Runbook: connect/pair the Android node to a Clawdbot Gateway and use Canvas/Chat/Camera"
summary: "Android app (node): connection runbook + Canvas/Chat/Camera"
read_when:
- Pairing or reconnecting the Android node
- Debugging Android bridge discovery or auth
- Verifying chat history parity across clients
---
# Android Node Connection Runbook
# Android App (Node)
## Connection Runbook
Android node app ⇄ (mDNS/NSD + TCP bridge) ⇄ **Gateway bridge** ⇄ (loopback WS) ⇄ **Gateway**
The Gateway WebSocket stays loopback-only (`ws://127.0.0.1:18789`). Android talks to the LAN-facing **bridge** (default `tcp://0.0.0.0:18790`) and uses Gateway-owned pairing.
## Prerequisites
### Prerequisites
- You can run the Gateway on the “master” machine.
- Android device/emulator can reach the gateway bridge:
@@ -21,7 +23,7 @@ The Gateway WebSocket stays loopback-only (`ws://127.0.0.1:18789`). Android talk
- Manual bridge host/port (fallback)
- You can run the CLI (`clawdbot`) on the gateway machine (or via SSH).
## 1) Start the Gateway (with bridge enabled)
### 1) Start the Gateway (with bridge enabled)
Bridge is enabled by default (disable via `CLAWDBOT_BRIDGE_ENABLED=0`).
@@ -37,7 +39,7 @@ For tailnet-only setups (recommended for Vienna ⇄ London), bind the bridge to
- Set `bridge.bind: "tailnet"` in `~/.clawdbot/clawdbot.json` on the gateway host.
- Restart the Gateway / macOS menubar app.
## 2) Verify discovery (optional)
### 2) Verify discovery (optional)
From the gateway machine:
@@ -45,18 +47,18 @@ From the gateway machine:
dns-sd -B _clawdbot-bridge._tcp local.
```
More debugging notes: `docs/bonjour.md`.
More debugging notes: [`docs/bonjour.md`](https://docs.clawd.bot/bonjour).
### Tailnet (Vienna ⇄ London) discovery via unicast DNS-SD
#### Tailnet (Vienna ⇄ London) discovery via unicast DNS-SD
Android NSD/mDNS discovery won’t cross networks. If your Android node and the gateway are on different networks but connected via Tailscale, use Wide-Area Bonjour / unicast DNS-SD instead:
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`.
Details and example CoreDNS config: [`docs/bonjour.md`](https://docs.clawd.bot/bonjour).
## 3) Connect from Android
### 3) Connect from Android
In the Android app:
@@ -69,7 +71,7 @@ After the first successful pairing, Android auto-reconnects on launch:
- Manual endpoint (if enabled), otherwise
- The last discovered bridge (best-effort).
## 4) Approve pairing (CLI)
### 4) Approve pairing (CLI)
On the gateway machine:
@@ -78,9 +80,9 @@ clawdbot nodes pending
clawdbot nodes approve <requestId>
```
Pairing details: `docs/gateway/pairing.md`.
Pairing details: [`docs/gateway/pairing.md`](https://docs.clawd.bot/gateway/pairing).
## 5) Verify the node is connected
### 5) Verify the node is connected
- Via nodes status:
```bash
@@ -91,7 +93,7 @@ Pairing details: `docs/gateway/pairing.md`.
clawdbot gateway call node.list --params "{}"
```
## 6) Chat + history
### 6) Chat + history
The Android node’s Chat sheet uses the gateway’s **primary session key** (`main`), so history and replies are shared with WebChat and other clients:
@@ -99,9 +101,9 @@ The Android node’s Chat sheet uses the gateway’s **primary session key** (`m
- Send: `chat.send`
- Push updates (best-effort): `chat.subscribe` → `event:"chat"`
## 7) Canvas + camera
### 7) Canvas + camera
### Gateway Canvas Host (recommended for web content)
#### Gateway Canvas Host (recommended for web content)
If you want the node to show real HTML/CSS/JS that the agent can edit on disk, point the node at the Gateway canvas host.
@@ -128,4 +130,4 @@ Camera commands (foreground only; permission-gated):
- `camera.snap` (jpg)
- `camera.clip` (mp4)
See `docs/camera.md` for parameters and CLI helpers.
See [`docs/camera.md`](https://docs.clawd.bot/camera) for parameters and CLI helpers.
+32 -7
View File
@@ -1,27 +1,52 @@
---
summary: "Target WebSocket gateway architecture, components, and client flows"
summary: "WebSocket gateway architecture, components, and client flows"
read_when:
- Working on gateway protocol, clients, or transports
---
# Gateway Architecture (target state)
# Gateway Architecture
Last updated: 2025-12-09
Last updated: 2026-01-05
## Overview
- A single long-lived **Gateway** process owns all messaging surfaces (WhatsApp via Baileys, Telegram via grammY, Discord via discord.js) and the control/event plane.
- All clients (macOS app, CLI, web UI, automations) connect to the Gateway over one transport: **WebSocket on 127.0.0.1:18789** (tunnel or VPN for remote).
- A single long-lived **Gateway** process owns all messaging surfaces (WhatsApp via Baileys, Telegram via grammY, Slack via Bolt, Discord via discord.js, Signal via signal-cli, iMessage via imsg, WebChat) and the control/event plane.
- All clients (macOS app, CLI, web UI, automations) connect to the Gateway over one transport: **WebSocket on the configured bind host** (default `127.0.0.1:18789`; tunnel or VPN for remote).
- One Gateway per host; it is the only place that is allowed to open a WhatsApp session. All sends/agent runs go through it.
- By default: the Gateway exposes a Canvas host on `canvasHost.port` (default `18793`), serving `~/clawd/canvas` at `/__clawdbot__/canvas/` with live-reload; disable via `canvasHost.enabled=false` or `CLAWDBOT_SKIP_CANVAS_HOST=1`.
## Implementation snapshot (current code)
### TypeScript Gateway ([`src/gateway/server.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/gateway/server.ts))
- Single HTTP + WebSocket server (default `18789`); bind policy `loopback|lan|tailnet|auto`. Refuses non-loopback binds without auth; Tailscale serve/funnel requires loopback.
- Handshake: first frame must be a `connect` request; AJV validates request + params against TypeBox schemas; protocol negotiated via `minProtocol`/`maxProtocol`.
- `hello-ok` includes snapshot (presence/health/stateVersion/uptime/configPath/stateDir), features (methods/events), policy (max payload/buffer/tick), and `canvasHostUrl` when available.
- Events emitted: `agent`, `chat`, `presence`, `tick`, `health`, `heartbeat`, `cron`, `talk.mode`, `node.pair.requested`, `node.pair.resolved`, `voicewake.changed`, `shutdown`.
- Idempotency keys are required for `send`, `agent`, `chat.send`, and node invokes; the dedupe cache avoids double-sends on reconnects. Payload sizes are capped per connection.
- Optional node bridge ([`src/infra/bridge/server.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/infra/bridge/server.ts)): TCP JSONL frames (`hello`, `pair-request`, `req/res`, `event`, `invoke`, `ping`). Node connect/disconnect updates presence and flows into the session bus.
- Control UI + Canvas host: HTTP serves Control UI (base path configurable) and can host the A2UI canvas via [`src/canvas-host/server.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/canvas-host/server.ts) (live reload). Canvas host URL is advertised to nodes + clients.
### iOS node (`apps/ios`)
- Discovery + pairing: `BridgeDiscoveryModel` uses `NWBrowser` Bonjour discovery and reads TXT fields for LAN/tailnet host hints plus gateway/bridge/canvas ports.
- Auto-connect: `BridgeConnectionController` uses stored `node.instanceId` + Keychain token; supports manual host/port; performs `pair-and-hello`.
- Bridge runtime: `BridgeSession` actor owns an `NWConnection`, JSONL frames, `hello`/`hello-ok`, ping/pong, `req/res`, server `event`s, and `invoke` callbacks; stores `canvasHostUrl`.
- Commands: `NodeAppModel` executes `canvas.*`, `canvas.a2ui.*`, `camera.*`, `screen.record`, `location.get`. Canvas/camera/screen are blocked when backgrounded.
- Canvas + actions: `WKWebView` with A2UI action bridge; accepts actions from local-network or trusted file URLs; intercepts `clawdbot://` deep links and forwards `agent.request` to the bridge.
- Voice/talk: voice wake sends `voice.transcript` events and syncs triggers via `voicewake.get` + `voicewake.changed`; Talk Mode attaches to the bridge.
### Android node (`apps/android`)
- Discovery + pairing: `BridgeDiscovery` uses mDNS/NSD to find `_clawdbot-bridge._tcp`, with manual host/port fallback.
- Auto-connect: `NodeRuntime` restores a stored token, performs `pair-and-hello`, and reconnects to the last discovered or manual bridge.
- Bridge runtime: `BridgeSession` owns the TCP JSONL session (`hello`/`hello-ok`, ping/pong, `req/res`, `event`, `invoke`); stores `canvasHostUrl`.
- Commands: `NodeRuntime` executes `canvas.*`, `canvas.a2ui.*`, `camera.*`, and chat/session events; foreground-only for canvas/camera.
## Components and flows
- **Gateway (daemon)**
- Maintains Baileys/Telegram/Discord connections.
- Maintains WhatsApp (Baileys), Telegram (grammY), Slack (Bolt), Discord (discord.js), Signal (signal-cli), and iMessage (imsg) connections.
- Exposes a typed WS API (req/resp + server push events).
- Validates every inbound frame against JSON Schema; rejects anything before a mandatory `connect`.
- **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/clawdbot-mac.md`).
- 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**
+4 -1
View File
@@ -3,6 +3,10 @@
line-height: 1.7;
}
.mdx-content > h1:first-of-type {
display: none;
}
.markdown h1,
.markdown h2,
.markdown h3,
@@ -127,4 +131,3 @@
font-family: var(--font-pixel);
letter-spacing: 0.06em;
}
+35 -62
View File
@@ -29,40 +29,40 @@ html[data-theme="auto"] {
}
html[data-theme="dark"] {
--bg0: #06141f;
--bg1: #031019;
--panel: #061a16;
--panel2: #071f19;
--text: #d6f6ea;
--muted: #95c9b9;
--faint: #66a391;
--link: #79ffd0;
--link2: #ff775f;
--bg0: #0b1a22;
--bg1: #0a1720;
--panel: #0e231f;
--panel2: #102a24;
--text: #c9eadc;
--muted: #8ab8aa;
--faint: #699b8d;
--link: #6fe8c7;
--link2: #ff7b63;
--accent: #ff4f40;
--accent2: #67ff9b;
--frame-border: #b7ffe6;
--code-bg: #04110d;
--code-fg: #dcfff1;
--code-accent: #67ff9b;
--accent2: #5fdfa2;
--frame-border: #6fbfa8;
--code-bg: #091814;
--code-fg: #d7f5e8;
--code-accent: #5fdfa2;
}
@media (prefers-color-scheme: dark) {
html[data-theme="auto"] {
--bg0: #06141f;
--bg1: #031019;
--panel: #061a16;
--panel2: #071f19;
--text: #d6f6ea;
--muted: #95c9b9;
--faint: #66a391;
--link: #79ffd0;
--link2: #ff775f;
--bg0: #0b1a22;
--bg1: #0a1720;
--panel: #0e231f;
--panel2: #102a24;
--text: #c9eadc;
--muted: #8ab8aa;
--faint: #699b8d;
--link: #6fe8c7;
--link2: #ff7b63;
--accent: #ff4f40;
--accent2: #67ff9b;
--frame-border: #b7ffe6;
--code-bg: #04110d;
--code-fg: #dcfff1;
--code-accent: #67ff9b;
--accent2: #5fdfa2;
--frame-border: #6fbfa8;
--code-bg: #091814;
--code-fg: #d7f5e8;
--code-accent: #5fdfa2;
}
}
@@ -70,11 +70,14 @@ html[data-theme="dark"] {
box-sizing: border-box;
}
html,
body {
html {
height: 100%;
}
body {
min-height: 100%;
}
body {
margin: 0;
font-family: var(--font-body);
@@ -87,39 +90,9 @@ body {
overflow-x: hidden;
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
opacity: 0.45;
background-image:
linear-gradient(to right, color-mix(in oklab, var(--text) 10%, transparent) 1px, transparent 1px),
linear-gradient(to bottom, color-mix(in oklab, var(--text) 10%, transparent) 1px, transparent 1px);
background-size: 28px 28px;
mix-blend-mode: overlay;
}
body::before,
body::after {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background: repeating-linear-gradient(
to bottom,
rgba(0, 0, 0, var(--scanline-opacity)),
rgba(0, 0, 0, var(--scanline-opacity)) 1px,
transparent 1px,
transparent var(--scanline-size)
);
opacity: 0.8;
mix-blend-mode: multiply;
}
@media (prefers-reduced-motion: reduce) {
body::after {
display: none;
}
display: none;
}
.skip-link {
-2
View File
@@ -3,7 +3,6 @@ summary: "How inbound audio/voice notes are downloaded, transcribed, and injecte
read_when:
- Changing audio transcription or media handling
---
<!-- {% raw %} -->
# Audio / Voice Notes — 2025-12-05
## What works
@@ -47,4 +46,3 @@ Requires `OPENAI_API_KEY` in env and `openai` CLI installed:
## Gotchas
- Ensure your CLI exits 0 and prints plain text; JSON needs to be massaged via `jq -r .text`.
- Keep timeouts reasonable (`timeoutSeconds`, default 45s) to avoid blocking the reply queue.
<!-- {% endraw %} -->
+6 -6
View File
@@ -69,7 +69,7 @@ The bridge port (default `18790`) is a plain TCP service. By default it binds to
For a tailnet-only setup, bind it to the Tailscale IP instead:
- Set `bridge.bind: "tailnet"` in `~/.clawdbot/clawdbot.json`.
- Restart the Gateway (or restart the macOS menubar app via `./scripts/restart-mac.sh` on that machine).
- Restart the Gateway (or restart the macOS menubar app via [`./scripts/restart-mac.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/restart-mac.sh) on that machine).
This keeps the bridge reachable only from devices on your tailnet (while still listening on loopback for local/SSH port-forwards).
@@ -77,8 +77,8 @@ This keeps the bridge reachable only from devices on your tailnet (while still l
Only the **Node Gateway** (`clawd` / `clawdbot gateway`) advertises Bonjour beacons.
- Implementation: `src/infra/bonjour.ts`
- Gateway wiring: `src/gateway/server.ts`
- Implementation: [`src/infra/bonjour.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/infra/bonjour.ts)
- Gateway wiring: [`src/gateway/server.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/gateway/server.ts)
## Service types
@@ -136,7 +136,7 @@ The log includes browser state transitions (`ready`, `waiting`, `failed`, `cance
- **Sleep / interface churn**: macOS may temporarily drop mDNS results when switching networks; retry.
- **Browse works but resolve fails (iOS “NoSuchRecord”)**: make sure the advertiser publishes a valid SRV target hostname.
- Implementation detail: `@homebridge/ciao` defaults `hostname` to the *service instance name* when `hostname` is omitted. If your instance name contains spaces/parentheses, some resolvers can fail to resolve the implied A/AAAA record.
- Fix: set an explicit DNS-safe `hostname` (single label; no `.local`) in `src/infra/bonjour.ts`.
- Fix: set an explicit DNS-safe `hostname` (single label; no `.local`) in [`src/infra/bonjour.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/infra/bonjour.ts).
## Escaped instance names (`\\032`)
Bonjour/DNS-SD often escapes bytes in service instance names as decimal `\\DDD` sequences (e.g. spaces become `\\032`).
@@ -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`
- Node pairing + approvals: `docs/gateway/pairing.md`
- 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)
+114
View File
@@ -0,0 +1,114 @@
---
summary: "Fix Chrome/Chromium CDP startup issues for Clawdbot browser control on Linux"
read_when: "Browser control fails on Linux, especially with snap Chromium"
---
# Browser Troubleshooting (Linux)
## Problem: "Failed to start Chrome CDP on port 18800"
Clawdbot's browser control server fails to launch Chrome/Chromium with the error:
```
{"error":"Error: Failed to start Chrome CDP on port 18800 for profile \"clawd\"."}
```
### Root Cause
On Ubuntu (and many Linux distros), the default Chromium installation is a **snap package**. Snap's AppArmor confinement interferes with how Clawdbot spawns and monitors the browser process.
The `apt install chromium` command installs a stub package that redirects to snap:
```
Note, selecting 'chromium-browser' instead of 'chromium'
chromium-browser is already the newest version (2:1snap1-0ubuntu2).
```
This is NOT a real browser — it's just a wrapper.
### Solution 1: Install Google Chrome (Recommended)
Install the official Google Chrome `.deb` package, which is not sandboxed by snap:
```bash
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo dpkg -i google-chrome-stable_current_amd64.deb
sudo apt --fix-broken install -y # if there are dependency errors
```
Then update your Clawdbot config (`~/.clawdbot/clawdbot.json`):
```json
{
"browser": {
"enabled": true,
"executablePath": "/usr/bin/google-chrome-stable",
"headless": true,
"noSandbox": true
}
}
```
### Solution 2: Use Snap Chromium with Attach-Only Mode
If you must use snap Chromium, configure Clawdbot to attach to a manually-started browser:
1. Update config:
```json
{
"browser": {
"enabled": true,
"attachOnly": true,
"headless": true,
"noSandbox": true
}
}
```
2. Start Chromium manually:
```bash
chromium-browser --headless --no-sandbox --disable-gpu \
--remote-debugging-port=18800 \
--user-data-dir=$HOME/.clawdbot/browser/clawd/user-data \
about:blank &
```
3. Optionally create a systemd user service to auto-start Chrome:
```ini
# ~/.config/systemd/user/clawd-browser.service
[Unit]
Description=Clawd Browser (Chrome CDP)
After=network.target
[Service]
ExecStart=/snap/bin/chromium --headless --no-sandbox --disable-gpu --remote-debugging-port=18800 --user-data-dir=%h/.clawdbot/browser/clawd/user-data about:blank
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
```
Enable with: `systemctl --user enable --now clawd-browser.service`
### Verifying the Browser Works
Check status:
```bash
curl -s http://127.0.0.1:18791/ | jq '{running, pid, chosenBrowser}'
```
Test browsing:
```bash
curl -s -X POST http://127.0.0.1:18791/start
curl -s http://127.0.0.1:18791/tabs
```
### Config Reference
| Option | Description | Default |
|--------|-------------|---------|
| `browser.enabled` | Enable browser control | `true` |
| `browser.executablePath` | Path to Chrome/Chromium binary | auto-detected |
| `browser.headless` | Run without GUI | `false` |
| `browser.noSandbox` | Add `--no-sandbox` flag (needed for some Linux setups) | `false` |
| `browser.attachOnly` | Don't launch browser, only attach to existing | `false` |
| `browser.cdpPort` | Chrome DevTools Protocol port | `18800` |
+21 -1
View File
@@ -189,10 +189,26 @@ All existing endpoints accept optional `?profile=<name>` query parameter:
- `GET /?profile=work` — status for work profile
- `POST /start?profile=work` — start work profile browser
- `GET /tabs?profile=work` — list tabs for work profile
- `POST /tabs/open?profile=work` — open tab in work profile
- etc.
When `profile` is omitted, uses `browser.defaultProfile` (defaults to "clawd").
### Agent browser tool
The `browser` tool accepts an optional `profile` parameter for all actions:
```json
{
"action": "open",
"targetUrl": "https://example.com",
"profile": "work"
}
```
This routes the operation to the specified profile's browser instance. Omitting
`profile` uses the default profile.
### Profile naming rules
- Lowercase alphanumeric characters and hyphens only
@@ -221,7 +237,7 @@ The agent should not assume tabs are ephemeral. It should:
## CLI quick reference (one example each)
All commands accept `--profile <name>` to target a specific profile (default: `clawd`).
All commands accept `--browser-profile <name>` to target a specific profile (default: `clawd`).
Profile management:
- `clawdbot browser profiles`
@@ -287,3 +303,7 @@ Notes:
- Sharing the user's logged-in Chrome sessions automatically.
- General-purpose web scraping; this is primarily for "close-the-loop" verification
and interaction.
## Troubleshooting
For Linux-specific issues (especially Ubuntu with snap Chromium), see [browser-linux-troubleshooting](https://docs.clawd.bot/browser-linux-troubleshooting).
+68
View File
@@ -0,0 +1,68 @@
---
summary: "Bun workflow (preferred): installs, patches, and gotchas vs pnpm"
read_when:
- You want the fastest local dev loop (bun + watch)
- You hit Bun install/patch/lifecycle script issues
---
# Bun
Goal: run this repo with **Bun** (preferred) without losing pnpm patch behavior.
## Status
- 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
Default:
```sh
bun install
```
Note: `bun.lock`/`bun.lockb` are gitignored, so there’s no repo churn either way. If you want *no lockfile writes*:
```sh
bun install --no-save
```
## Build / Test (Bun)
```sh
bun run build
bun run vitest run
```
## pnpm patchedDependencies under Bun
pnpm supports `package.json#pnpm.patchedDependencies` and records it in `pnpm-lock.yaml`.
Bun does not support pnpm patches, so we apply them in `postinstall` when Bun is detected:
- [`scripts/postinstall.js`](https://github.com/clawdbot/clawdbot/blob/main/scripts/postinstall.js) runs only for Bun installs and applies every entry from `package.json#pnpm.patchedDependencies` into `node_modules/...` using `git apply` (idempotent).
To add a new patch that works in both pnpm + Bun:
1. Add an entry to `package.json#pnpm.patchedDependencies`
2. Add the patch file under `patches/`
3. Run `pnpm install` (updates `pnpm-lock.yaml` patch hash)
## Bun lifecycle scripts (blocked by default)
Bun may block dependency lifecycle scripts unless explicitly trusted (`bun pm untrusted` / `bun pm trust`).
For this repo, the commonly blocked scripts are not required:
- `@whiskeysockets/baileys` `preinstall`: checks Node major >= 20 (we run Node 22+).
- `protobufjs` `postinstall`: emits warnings about incompatible version schemes (no build artifacts).
If you hit a real runtime issue that requires these scripts, trust them explicitly:
```sh
bun pm trust @whiskeysockets/baileys protobufjs
```
## Caveats
- Some scripts still hardcode pnpm (e.g. `docs:build`, `ui:*`, `protocol:check`). Run those via pnpm for now.
+34 -14
View File
@@ -4,7 +4,6 @@ read_when:
- Onboarding a new assistant instance
- Reviewing safety/permission implications
---
<!-- {% raw %} -->
# Building a personal assistant with CLAWDBOT (Clawd-style)
CLAWDBOT is a WhatsApp + Telegram + Discord gateway for **Pi** agents. This guide is the “personal assistant” setup: one dedicated WhatsApp number that behaves like your always-on agent.
@@ -19,7 +18,7 @@ You’re putting an agent in a position to:
Start conservative:
- Always set `whatsapp.allowFrom` (never run open-to-the-world on your personal Mac).
- Use a dedicated WhatsApp number for the assistant.
- Keep heartbeats disabled until you trust the setup (omit `agent.heartbeat` or set `agent.heartbeat.every: "0m"`).
- Heartbeats now default to every 30 minutes. Disable until you trust the setup by setting `agent.heartbeat.every: "0m"`.
## Prerequisites
@@ -70,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
@@ -82,11 +86,11 @@ clawdbot gateway --port 18789
Now message the assistant number from your allowlisted phone.
## Give the agent a workspace (AGENTS.md)
## Give the agent a workspace (AGENTS)
Clawd reads operating instructions and “memory” from its workspace directory.
By default, Clawdbot uses `~/clawd` as the agent workspace, and will create it (plus starter `AGENTS.md`, `SOUL.md`, `TOOLS.md`) automatically on setup/first agent run.
By default, Clawdbot uses `~/clawd` as the agent workspace, and will create it (plus starter `AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`) automatically on setup/first agent run. `BOOTSTRAP.md` is only created when the workspace is brand new (it should not come back after you delete it).
Tip: treat this folder like Clawd’s “memory” and make it a git repo (ideally private) so your `AGENTS.md` + memory files are backed up.
@@ -104,6 +108,16 @@ Optional: choose a different workspace with `agent.workspace` (supports `~`).
}
```
If you already ship your own workspace files from a repo, you can disable bootstrap file creation entirely:
```json5
{
agent: {
skipBootstrap: true
}
}
```
## The config that turns it into “an assistant”
CLAWDBOT defaults to a good assistant setup, but you’ll usually want to tune:
@@ -145,15 +159,18 @@ Example:
## Sessions and memory
- Session files: `~/.clawdbot/sessions/{{SessionId}}.jsonl`
- Session metadata (token usage, last route, etc): `~/.clawdbot/sessions/sessions.json` (legacy: `~/.clawdbot/sessions.json`)
- Session files: `~/.clawdbot/agents/<agentId>/sessions/{{SessionId}}.jsonl`
- Session metadata (token usage, last route, etc): `~/.clawdbot/agents/<agentId>/sessions/sessions.json` (legacy: `~/.clawdbot/sessions/sessions.json`)
- `/new` or `/reset` starts a fresh session for that chat (configurable via `resetTriggers`). If sent alone, the agent replies with a short hello to confirm the reset.
- `/compact [instructions]` compacts the session context and reports the remaining context budget.
## Heartbeats (proactive mode)
When `agent.heartbeat.every` is set to a positive interval, CLAWDBOT periodically runs a heartbeat prompt (default: `HEARTBEAT`).
By default, CLAWDBOT runs a heartbeat every 30 minutes with the prompt:
`Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`
Set `agent.heartbeat.every: "0m"` to disable.
- If the agent replies with `HEARTBEAT_OK` (exact token), CLAWDBOT suppresses outbound delivery for that heartbeat.
- If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agent.heartbeat.ackMaxChars`), CLAWDBOT suppresses outbound delivery for that heartbeat.
```json5
{
@@ -191,9 +208,12 @@ Logs live under `/tmp/clawdbot/` (default: `clawdbot-YYYY-MM-DD.log`).
## Next steps
- WebChat: [WebChat](./webchat.md)
- Gateway ops: [Gateway runbook](./gateway.md)
- Cron + wakeups: [Cron + wakeups](./cron.md)
- macOS menu bar companion: [Clawdbot macOS app](./clawdbot-mac.md)
- Security: [Security](./security.md)
<!-- {% endraw %} -->
- 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)
+397 -68
View File
@@ -3,14 +3,13 @@ summary: "All configuration options for ~/.clawdbot/clawdbot.json with examples"
read_when:
- Adding or modifying config fields
---
<!-- {% raw %} -->
# Configuration 🔧
CLAWDBOT reads an optional **JSON5** config from `~/.clawdbot/clawdbot.json` (comments + trailing commas allowed).
If the file is missing, CLAWDBOT uses safe-ish defaults (embedded Pi agent + per-sender sessions + workspace `~/clawd`). You usually only need a config to:
- restrict who can trigger the bot (`whatsapp.allowFrom`, `telegram.allowFrom`, etc.)
- control group mention behavior (`whatsapp.groups`, `telegram.groups`, `discord.guilds`, `routing.groupChat`)
- control group allowlists + mention behavior (`whatsapp.groups`, `telegram.groups`, `discord.guilds`, `routing.groupChat`)
- customize message prefixes (`messages`)
- set the agent's workspace (`agent.workspace`)
- tune the embedded agent (`agent`) and session behavior (`session`)
@@ -60,13 +59,83 @@ To prevent the bot from responding to WhatsApp @-mentions in groups (only respon
## Common options
### Env vars + `.env`
CLAWDBOT reads env vars from the parent process (shell, launchd/systemd, CI, etc.).
Additionally, it loads:
- `.env` from the current working directory (if present)
- a global fallback `.env` from `~/.clawdbot/.env` (aka `$CLAWDBOT_STATE_DIR/.env`)
Neither `.env` file overrides existing env vars.
### `env.shellEnv` (optional)
Opt-in convenience: if enabled and none of the expected keys are set yet, CLAWDBOT runs your login shell and imports only the missing expected keys (never overrides).
This effectively sources your shell profile.
```json5
{
env: {
shellEnv: {
enabled: true,
timeoutMs: 15000
}
}
}
```
Env var equivalent:
- `CLAWDBOT_LOAD_SHELL_ENV=1`
- `CLAWDBOT_SHELL_ENV_TIMEOUT_MS=15000`
### Auth storage (OAuth + API keys)
Clawdbot stores **per-agent** auth profiles (OAuth + API keys) in:
- `<agentDir>/auth-profiles.json` (default: `~/.clawdbot/agents/<agentId>/agent/auth-profiles.json`)
Legacy OAuth imports:
- `~/.clawdbot/credentials/oauth.json` (or `$CLAWDBOT_STATE_DIR/credentials/oauth.json`)
The embedded Pi agent maintains a runtime cache at:
- `<agentDir>/auth.json` (managed automatically; don’t edit manually)
Legacy agent dir (pre multi-agent):
- `~/.clawdbot/agent/*` (migrated by `clawdbot doctor` into `~/.clawdbot/agents/<defaultAgentId>/agent/*`)
Overrides:
- OAuth dir (legacy import only): `CLAWDBOT_OAUTH_DIR`
- Agent dir (default agent root override): `CLAWDBOT_AGENT_DIR` (preferred), `PI_CODING_AGENT_DIR` (legacy)
On first use, Clawdbot imports `oauth.json` entries into `auth-profiles.json`.
### `auth`
Optional metadata for auth profiles. This does **not** store secrets; it maps
profile IDs to a provider + mode (and optional email) and defines the provider
rotation order used for failover.
```json5
{
auth: {
profiles: {
"anthropic:default": { provider: "anthropic", mode: "oauth", email: "me@example.com" },
"anthropic:work": { provider: "anthropic", mode: "api_key" }
},
order: {
anthropic: ["anthropic:default", "anthropic:work"]
}
}
}
```
### `identity`
Optional agent identity used for defaults and UX. This is written by the macOS onboarding assistant.
If set, CLAWDBOT derives defaults (only when you haven’t set them explicitly):
- `messages.responsePrefix` from `identity.emoji`
- `routing.groupChat.mentionPatterns` from `identity.name` (so “@Samantha” works in groups)
- `messages.ackReaction` from `identity.emoji` (falls back to 👀)
- `routing.groupChat.mentionPatterns` from `identity.name` (so “@Samantha” works in groups across Telegram/Slack/Discord/iMessage/WhatsApp)
```json5
{
@@ -82,7 +151,7 @@ Metadata written by CLI wizards (`onboard`, `configure`, `doctor`, `update`).
{
wizard: {
lastRunAt: "2026-01-01T00:00:00.000Z",
lastRunVersion: "2.0.0-beta5",
lastRunVersion: "2026.1.4",
lastRunCommit: "abc1234",
lastRunCommand: "configure",
lastRunMode: "local"
@@ -97,6 +166,9 @@ Metadata written by CLI wizards (`onboard`, `configure`, `doctor`, `update`).
- Console output can be tuned separately via:
- `logging.consoleLevel` (defaults to `info`, bumps to `debug` when `--verbose`)
- `logging.consoleStyle` (`pretty` | `compact` | `json`)
- Tool summaries can be redacted to avoid leaking secrets:
- `logging.redactSensitive` (`off` | `tools`, default: `tools`)
- `logging.redactPatterns` (array of regex strings; overrides defaults)
```json5
{
@@ -104,25 +176,68 @@ Metadata written by CLI wizards (`onboard`, `configure`, `doctor`, `update`).
level: "info",
file: "/tmp/clawdbot/clawdbot.log",
consoleLevel: "info",
consoleStyle: "pretty"
consoleStyle: "pretty",
redactSensitive: "tools",
redactPatterns: [
// Example: override defaults with your own rules.
"\\bTOKEN\\b\\s*[=:]\\s*([\"']?)([^\\s\"']+)\\1",
"/\\bsk-[A-Za-z0-9_-]{8,}\\b/gi"
]
}
}
```
### `whatsapp.dmPolicy`
Controls how WhatsApp direct chats (DMs) are handled:
- `"pairing"` (default): unknown senders get a pairing code; owner must approve
- `"allowlist"`: only allow senders in `whatsapp.allowFrom` (or paired allow store)
- `"open"`: allow all inbound DMs (**requires** `whatsapp.allowFrom` to include `"*"`)
- `"disabled"`: ignore all inbound DMs
Pairing approvals:
- `clawdbot pairing list --provider whatsapp`
- `clawdbot pairing approve --provider whatsapp <code>`
### `whatsapp.allowFrom`
Allowlist of E.164 phone numbers that may trigger WhatsApp auto-replies (DMs only).
If empty, the default allowlist is your own WhatsApp number (self-chat mode).
Allowlist of E.164 phone numbers that may trigger WhatsApp auto-replies (**DMs only**).
If empty and `whatsapp.dmPolicy="pairing"`, unknown senders will receive a pairing code.
For groups, use `whatsapp.groupPolicy` + `whatsapp.groupAllowFrom`.
```json5
{
whatsapp: {
dmPolicy: "pairing", // pairing | allowlist | open | disabled
allowFrom: ["+15555550123", "+447700900123"],
textChunkLimit: 4000 // optional outbound chunk size (chars)
}
}
```
### `whatsapp.accounts` (multi-account)
Run multiple WhatsApp accounts in one gateway:
```json5
{
whatsapp: {
accounts: {
default: {}, // optional; keeps the default id stable
personal: {},
biz: {
// Optional override. Default: ~/.clawdbot/credentials/whatsapp/biz
// authDir: "~/.clawdbot/credentials/whatsapp/biz",
}
}
}
}
```
Notes:
- Outbound commands default to account `default` if present; otherwise the first configured account id (sorted).
- The legacy single-account Baileys auth dir is migrated by `clawdbot doctor` into `whatsapp/default`.
### `routing.groupChat`
Group messages default to **require mention** (either metadata mention or regex patterns). Applies to WhatsApp, Telegram, Discord, and iMessage group chats.
@@ -130,6 +245,7 @@ Group messages default to **require mention** (either metadata mention or regex
**Mention types:**
- **Metadata mentions**: Native platform @-mentions (e.g., WhatsApp tap-to-mention). Ignored in WhatsApp self-chat mode (see `whatsapp.allowFrom`).
- **Text patterns**: Regex patterns defined in `mentionPatterns`. Always checked regardless of self-chat mode.
- Mention gating is enforced only when mention detection is possible (native mentions or at least one `mentionPattern`).
```json5
{
@@ -142,7 +258,7 @@ Group messages default to **require mention** (either metadata mention or regex
}
```
Mention gating defaults live per provider (`whatsapp.groups`, `telegram.groups`, `imessage.groups`, `discord.guilds`).
Mention gating defaults live per provider (`whatsapp.groups`, `telegram.groups`, `imessage.groups`, `discord.guilds`). When `*.groups` is set, it also acts as a group allowlist; include `"*"` to allow all groups.
To respond **only** to specific text triggers (ignoring native @-mentions):
```json5
@@ -161,6 +277,114 @@ To respond **only** to specific text triggers (ignoring native @-mentions):
}
```
### Group policy (per provider)
Use `*.groupPolicy` to control whether group/room messages are accepted at all:
```json5
{
whatsapp: {
groupPolicy: "allowlist",
groupAllowFrom: ["+15551234567"]
},
telegram: {
groupPolicy: "allowlist",
groupAllowFrom: ["tg:123456789", "@alice"]
},
signal: {
groupPolicy: "allowlist",
groupAllowFrom: ["+15551234567"]
},
imessage: {
groupPolicy: "allowlist",
groupAllowFrom: ["chat_id:123"]
},
discord: {
groupPolicy: "allowlist",
guilds: {
"GUILD_ID": {
channels: { help: { allow: true } }
}
}
},
slack: {
groupPolicy: "allowlist",
channels: { "#general": { allow: true } }
}
}
```
Notes:
- `"open"` (default): groups bypass allowlists; mention-gating still applies.
- `"disabled"`: block all group/room messages.
- `"allowlist"`: only allow groups/rooms that match the configured allowlist.
- WhatsApp/Telegram/Signal/iMessage use `groupAllowFrom` (fallback: explicit `allowFrom`).
- Discord/Slack use channel allowlists (`discord.guilds.*.channels`, `slack.channels`).
- Group DMs (Discord/Slack) are still controlled by `dm.groupEnabled` + `dm.groupChannels`.
### Multi-agent routing (`routing.agents` + `routing.bindings`)
Run multiple isolated agents (separate workspace, `agentDir`, sessions) inside one Gateway. Inbound messages are routed to an agent via bindings.
- `routing.defaultAgentId`: fallback when no binding matches (default: `main`).
- `routing.agents.<agentId>`: per-agent overrides.
- `workspace`: default `~/clawd-<agentId>` (for `main`, falls back to legacy `agent.workspace`).
- `agentDir`: default `~/.clawdbot/agents/<agentId>/agent`.
- `routing.bindings[]`: routes inbound messages to an `agentId`.
- `match.provider` (required)
- `match.accountId` (optional; `*` = any account; omitted = default account)
- `match.peer` (optional; `{ kind: dm|group|channel, id }`)
- `match.guildId` / `match.teamId` (optional; provider-specific)
Deterministic match order:
1) `match.peer`
2) `match.guildId`
3) `match.teamId`
4) `match.accountId` (exact, no peer/guild/team)
5) `match.accountId: "*"` (provider-wide, no peer/guild/team)
6) `routing.defaultAgentId`
Within each match tier, the first matching entry in `routing.bindings` wins.
Example: two WhatsApp accounts → two agents:
```json5
{
routing: {
defaultAgentId: "home",
agents: {
home: { workspace: "~/clawd-home" },
work: { workspace: "~/clawd-work" },
},
bindings: [
{ agentId: "home", match: { provider: "whatsapp", accountId: "personal" } },
{ agentId: "work", match: { provider: "whatsapp", accountId: "biz" } },
],
},
whatsapp: {
accounts: {
personal: {},
biz: {},
}
}
}
```
### `routing.agentToAgent` (optional)
Agent-to-agent messaging is opt-in:
```json5
{
routing: {
agentToAgent: {
enabled: false,
allow: ["home", "work"]
}
}
}
```
### `routing.queue`
Controls how inbound messages behave when an agent run is already active.
@@ -173,7 +397,7 @@ Controls how inbound messages behave when an agent run is already active.
debounceMs: 1000,
cap: 20,
drop: "summarize", // old | new | summarize
bySurface: {
byProvider: {
whatsapp: "collect",
telegram: "collect",
discord: "collect",
@@ -185,6 +409,27 @@ Controls how inbound messages behave when an agent run is already active.
}
```
### `commands` (chat command handling)
Controls how chat commands are enabled across connectors.
```json5
{
commands: {
native: false, // register native commands when supported
text: true, // parse slash commands in chat messages
useAccessGroups: true // enforce access-group allowlists/policies for commands
}
}
```
Notes:
- Text commands must be sent as a **standalone** message and use the leading `/` (no plain-text aliases).
- `commands.text: false` disables parsing chat messages for commands.
- `commands.native: true` registers native commands on supported connectors (Discord/Slack/Telegram). Platforms without native commands still rely on text commands.
- `commands.native: false` skips native registration; Discord/Telegram clear previously registered commands on startup. Slack commands are managed in the Slack app.
- `commands.useAccessGroups: false` allows commands to bypass access-group allowlists/policies.
### `web` (WhatsApp web provider)
WhatsApp runs through the gateway’s web provider. It starts automatically when a linked session exists.
@@ -216,8 +461,9 @@ Set `telegram.enabled: false` to disable automatic startup.
telegram: {
enabled: true,
botToken: "your-bot-token",
requireMention: true,
allowFrom: ["123456789"],
dmPolicy: "pairing", // pairing | allowlist | open | disabled
allowFrom: ["tg:123456789"], // optional; "open" requires ["*"]
groups: { "*": { requireMention: true } },
mediaMaxMb: 5,
proxy: "socks5://localhost:9050",
webhookUrl: "https://example.com/telegram-webhook",
@@ -255,15 +501,10 @@ Configure the Discord bot by setting the bot token and optional gating:
moderation: false
},
replyToMode: "off", // off | first | all
slashCommand: { // user-installed app slash commands
enabled: true,
name: "clawd",
sessionPrefix: "discord:slash",
ephemeral: true
},
dm: {
enabled: true, // disable all DMs when false
allowFrom: ["1234567890", "steipete"], // optional DM allowlist (ids or names)
policy: "pairing", // pairing | allowlist | open | disabled
allowFrom: ["1234567890", "steipete"], // optional DM allowlist ("open" requires ["*"])
groupEnabled: false, // enable group DMs
groupChannels: ["clawd-dm"] // optional group DM allowlist
},
@@ -304,7 +545,8 @@ Slack runs in Socket Mode and requires both a bot token and app token:
appToken: "xapp-...",
dm: {
enabled: true,
allowFrom: ["U123", "U456", "*"],
policy: "pairing", // pairing | allowlist | open | disabled
allowFrom: ["U123", "U456", "*"], // optional; "open" requires ["*"]
groupEnabled: false,
groupChannels: ["G123"]
},
@@ -359,6 +601,7 @@ Clawdbot spawns `imsg rpc` (JSON-RPC over stdio). No daemon or port required.
enabled: true,
cliPath: "imsg",
dbPath: "~/Library/Messages/chat.db",
dmPolicy: "pairing", // pairing | allowlist | open | disabled
allowFrom: ["+15555550123", "user@example.com", "chat_id:123"],
includeAttachments: false,
mediaMaxMb: 16,
@@ -388,20 +631,57 @@ Default: `~/clawd`.
If `agent.sandbox` is enabled, non-main sessions can override this with their
own per-session workspaces under `agent.sandbox.workspaceRoot`.
### `agent.skipBootstrap`
Disables automatic creation of the workspace bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, and `BOOTSTRAP.md`).
Use this for pre-seeded deployments where your workspace files come from a repo.
```json5
{
agent: { skipBootstrap: true }
}
```
### `agent.userTimezone`
Sets the user’s timezone for **system prompt context** (not for timestamps in
message envelopes). If unset, Clawdbot uses the host timezone at runtime.
```json5
{
agent: { userTimezone: "America/Chicago" }
}
```
### `messages`
Controls inbound/outbound prefixes and timestamps.
Controls inbound/outbound prefixes and optional ack reactions.
```json5
{
messages: {
messagePrefix: "[clawdbot]",
responsePrefix: "🦞",
timestampPrefix: "Europe/London"
ackReaction: "👀",
ackReactionScope: "group-mentions"
}
}
```
`responsePrefix` is applied to **all outbound replies** (tool summaries, block
streaming, final replies) across providers unless already present.
`ackReaction` sends a best-effort emoji reaction to acknowledge inbound messages
on providers that support reactions (Slack/Discord/Telegram). Defaults to the
configured `identity.emoji` when set, otherwise `"👀"`. Set it to `""` to disable.
`ackReactionScope` controls when reactions fire:
- `group-mentions` (default): only when a group/room requires mentions **and** the bot was mentioned
- `group-all`: all group/room messages
- `direct`: direct messages only
- `all`: all messages
### `talk`
Defaults for Talk mode (macOS/iOS/Android). Voice IDs fall back to `ELEVENLABS_VOICE_ID` or `SAG_VOICE_ID` when unset.
@@ -427,21 +707,42 @@ Defaults for Talk mode (macOS/iOS/Android). Voice IDs fall back to `ELEVENLABS_V
### `agent`
Controls the embedded agent runtime (model/thinking/verbose/timeouts).
`allowedModels` lets `/model` list/filter and enforce a per-session allowlist
(omit to show the full catalog).
`modelAliases` adds short names for `/model` (alias -> provider/model).
`agent.models` defines the configured model catalog (and acts as the allowlist for `/model`).
`agent.model.primary` sets the default model; `agent.model.fallbacks` are global failovers.
`agent.imageModel` is optional and is **only used if the primary model lacks image input**.
Clawdbot also ships a few built-in alias shorthands. Defaults only apply when the model
is already present in `agent.models`:
- `opus` -> `anthropic/claude-opus-4-5`
- `sonnet` -> `anthropic/claude-sonnet-4-5`
- `gpt` -> `openai/gpt-5.2`
- `gpt-mini` -> `openai/gpt-5-mini`
- `gemini` -> `google/gemini-3-pro-preview`
- `gemini-flash` -> `google/gemini-3-flash-preview`
If you configure the same alias name (case-insensitive) yourself, your value wins (defaults never override).
```json5
{
agent: {
model: "anthropic/claude-opus-4-5",
allowedModels: [
"anthropic/claude-opus-4-5",
"anthropic/claude-sonnet-4-1"
],
modelAliases: {
Opus: "anthropic/claude-opus-4-5",
Sonnet: "anthropic/claude-sonnet-4-1"
models: {
"anthropic/claude-opus-4-5": { alias: "Opus" },
"anthropic/claude-sonnet-4-1": { alias: "Sonnet" },
"openrouter/deepseek/deepseek-r1:free": {}
},
model: {
primary: "anthropic/claude-opus-4-5",
fallbacks: [
"openrouter/deepseek/deepseek-r1:free",
"openrouter/meta-llama/llama-3.3-70b-instruct:free"
]
},
imageModel: {
primary: "openrouter/qwen/qwen-2.5-vl-72b-instruct:free",
fallbacks: [
"openrouter/google/gemini-2.0-flash-vision:free"
]
},
thinkingDefault: "low",
verboseDefault: "off",
@@ -477,29 +778,47 @@ Block streaming:
}
```
`agent.model` should be set as `provider/model` (e.g. `anthropic/claude-opus-4-5`).
If `modelAliases` is configured, you may also use the alias key (e.g. `Opus`).
`agent.model.primary` should be set as `provider/model` (e.g. `anthropic/claude-opus-4-5`).
Aliases come from `agent.models.*.alias` (e.g. `Opus`).
If you omit the provider, CLAWDBOT currently assumes `anthropic` as a temporary
deprecation fallback.
Z.AI models are available as `zai/<model>` (e.g. `zai/glm-4.7`) and require
`ZAI_API_KEY` (or legacy `Z_AI_API_KEY`) in the environment.
`agent.heartbeat` configures periodic heartbeat runs:
- `every`: duration string (`ms`, `s`, `m`, `h`); default unit minutes. Omit or set
`0m` to disable.
- `every`: duration string (`ms`, `s`, `m`, `h`); default unit minutes. Default:
`30m`. Set `0m` to disable.
- `model`: optional override model for heartbeat runs (`provider/model`).
- `target`: optional delivery channel (`last`, `whatsapp`, `telegram`, `discord`, `imessage`, `none`). Default: `last`.
- `to`: optional recipient override (E.164 for WhatsApp, chat id for Telegram).
- `prompt`: optional override for the heartbeat body (default: `HEARTBEAT`).
- `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.`).
- `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery (default: 30).
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.tools` configures a global tool allow/deny policy (deny wins).
This is applied even when the Docker sandbox is **off**.
Example (disable browser/canvas everywhere):
```json5
{
agent: {
tools: {
deny: ["browser", "canvas"]
}
}
}
```
`agent.elevated` controls elevated (host) bash access:
- `enabled`: allow elevated mode (default true)
- `allowFrom`: per-surface allowlists (empty = disabled)
- `allowFrom`: per-provider allowlists (empty = disabled)
- `whatsapp`: E.164 numbers
- `telegram`: chat ids or usernames
- `discord`: user ids or usernames (falls back to `discord.dm.allowFrom` if omitted)
@@ -536,7 +855,7 @@ Defaults (if enabled):
- Debian bookworm-slim based image
- workspace per session under `~/.clawdbot/sandboxes`
- auto-prune: idle > 24h OR age > 7d
- tools: allow only `bash`, `process`, `read`, `write`, `edit` (deny wins)
- 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`
@@ -582,7 +901,7 @@ Defaults (if enabled):
enableNoVnc: true
},
tools: {
allow: ["bash", "process", "read", "write", "edit"],
allow: ["bash", "process", "read", "write", "edit", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn"],
deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"]
},
prune: {
@@ -617,19 +936,24 @@ URL is injected per session.
Clawdbot uses the **pi-coding-agent** model catalog. You can add custom providers
(LiteLLM, local OpenAI-compatible servers, Anthropic proxies, etc.) by writing
`~/.clawdbot/agent/models.json` or by defining the same schema inside your
`~/.clawdbot/agents/<agentId>/agent/models.json` or by defining the same schema inside your
Clawdbot config under `models.providers`.
When `models.providers` is present, Clawdbot writes/merges a `models.json` into
`~/.clawdbot/agent/` on startup:
`~/.clawdbot/agents/<agentId>/agent/` on startup:
- default behavior: **merge** (keeps existing providers, overrides on name)
- set `models.mode: "replace"` to overwrite the file contents
Select the model via `agent.model` (provider/model).
Select the model via `agent.model.primary` (provider/model).
```json5
{
agent: { model: "custom-proxy/llama-3.1-8b" },
agent: {
model: { primary: "custom-proxy/llama-3.1-8b" },
models: {
"custom-proxy/llama-3.1-8b": {}
}
},
models: {
mode: "merge",
providers: {
@@ -662,14 +986,10 @@ via **LM Studio** using the **Responses API**.
```json5
{
agent: {
model: "Minimax",
allowedModels: [
"anthropic/claude-opus-4-5",
"lmstudio/minimax-m2.1-gs32"
],
modelAliases: {
Opus: "anthropic/claude-opus-4-5",
Minimax: "lmstudio/minimax-m2.1-gs32"
model: { primary: "lmstudio/minimax-m2.1-gs32" },
models: {
"anthropic/claude-opus-4-5": { alias: "Opus" },
"lmstudio/minimax-m2.1-gs32": { alias: "Minimax" }
}
},
models: {
@@ -706,7 +1026,7 @@ Notes:
`google-generative-ai`
- Use `authHeader: true` + `headers` for custom auth needs.
- Override the agent config root with `CLAWDBOT_AGENT_DIR` (or `PI_CODING_AGENT_DIR`)
if you want `models.json` stored elsewhere.
if you want `models.json` stored elsewhere (default: `~/.clawdbot/agents/main/agent`).
### `session`
@@ -718,15 +1038,18 @@ Controls session scoping, idle expiry, reset triggers, and where the session sto
scope: "per-sender",
idleMinutes: 60,
resetTriggers: ["/new", "/reset"],
store: "~/.clawdbot/sessions/sessions.json",
// mainKey is ignored; primary key is fixed to "main"
// Default is already per-agent under ~/.clawdbot/agents/<agentId>/sessions/sessions.json
// You can override with {agentId} templating:
store: "~/.clawdbot/agents/{agentId}/sessions/sessions.json",
// Direct chats collapse to agent:<agentId>:<mainKey> (default: "main").
mainKey: "main",
agentToAgent: {
// Max ping-pong reply turns between requester/target (0–5).
maxPingPongTurns: 5
},
sendPolicy: {
rules: [
{ action: "deny", match: { surface: "discord", chatType: "group" } }
{ action: "deny", match: { provider: "discord", chatType: "group" } }
],
default: "allow"
}
@@ -735,9 +1058,10 @@ Controls session scoping, idle expiry, reset triggers, and where the session sto
```
Fields:
- `mainKey`: direct-chat bucket key (default: `"main"`). Useful when you want to “rename” the primary DM thread without changing `agentId`.
- `agentToAgent.maxPingPongTurns`: max reply-back turns between requester/target (0–5, default 5).
- `sendPolicy.default`: `allow` or `deny` fallback when no rule matches.
- `sendPolicy.rules[]`: match by `surface` (provider), `chatType` (`direct|group|room`), or `keyPrefix` (e.g. `cron:`). First deny wins; otherwise allow.
- `sendPolicy.rules[]`: match by `provider`, `chatType` (`direct|group|room`), or `keyPrefix` (e.g. `cron:`). First deny wins; otherwise allow.
### `skills` (skills config)
@@ -951,10 +1275,16 @@ Requires full Gateway restart:
To run multiple gateways on one host, isolate per-instance state + config and use unique ports:
- `CLAWDBOT_CONFIG_PATH` (per-instance config)
- `CLAWDBOT_STATE_DIR` (sessions/creds/logs)
- `CLAWDBOT_STATE_DIR` (sessions/creds)
- `agent.workspace` (memories)
- `gateway.port` (unique per instance)
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`](https://docs.clawd.bot/gateway) for the derived port mapping (gateway/bridge/browser/canvas).
Example:
```bash
CLAWDBOT_CONFIG_PATH=~/.clawdbot/a.json \
@@ -964,7 +1294,7 @@ clawdbot gateway --port 19001
### `hooks` (Gateway webhooks)
Enable a simple HTTP webhook surface on the Gateway HTTP server.
Enable a simple HTTP webhook endpoint on the Gateway HTTP server.
Defaults:
- enabled: `false`
@@ -1001,7 +1331,7 @@ Requests must include the hook token:
Endpoints:
- `POST /hooks/wake` → `{ text, mode?: "now"|"next-heartbeat" }`
- `POST /hooks/agent` → `{ message, name?, sessionKey?, wakeMode?, deliver?, channel?, to?, thinking?, timeoutSeconds? }`
- `POST /hooks/agent` → `{ message, name?, sessionKey?, wakeMode?, deliver?, provider?, to?, thinking?, timeoutSeconds? }`
- `POST /hooks/<name>` → resolved via `hooks.mappings`
`/hooks/agent` always posts a summary into the main session (and can optionally trigger an immediate heartbeat via `wakeMode: "now"`).
@@ -1131,7 +1461,7 @@ Template placeholders are expanded in `routing.transcribeAudio.command` (and any
|----------|-------------|
| `{{Body}}` | Full inbound message body |
| `{{BodyStripped}}` | Body with group mentions stripped (best default for agents) |
| `{{From}}` | Sender identifier (E.164 for WhatsApp; may differ per surface) |
| `{{From}}` | Sender identifier (E.164 for WhatsApp; may differ per provider) |
| `{{To}}` | Destination identifier |
| `{{MessageSid}}` | Provider message id (when available) |
| `{{SessionId}}` | Current session UUID |
@@ -1145,11 +1475,11 @@ Template placeholders are expanded in `routing.transcribeAudio.command` (and any
| `{{GroupMembers}}` | Group members preview (best effort) |
| `{{SenderName}}` | Sender display name (best effort) |
| `{{SenderE164}}` | Sender phone number (best effort) |
| `{{Surface}}` | Surface hint (whatsapp|telegram|discord|imessage|webchat|…) |
| `{{Provider}}` | Provider hint (whatsapp|telegram|discord|imessage|webchat|…) |
## Cron (Gateway scheduler)
Cron is a Gateway-owned scheduler for wakeups and scheduled jobs. See [Cron + wakeups](./cron.md) for the full RFC 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
{
@@ -1162,5 +1492,4 @@ Cron is a Gateway-owned scheduler for wakeups and scheduled jobs. See [Cron + wa
---
*Next: [Agent Runtime](./agent.md)* 🦞
<!-- {% endraw %} -->
*Next: [Agent Runtime](https://docs.clawd.bot/agent)* 🦞
-49
View File
@@ -1,49 +0,0 @@
---
summary: "Deprecated newline-delimited control channel API (pre-gateway)"
read_when:
- Maintaining legacy control channel support
---
# Control channel API (newline-delimited JSON)
**Deprecated (historical):** superseded by the WebSocket Gateway protocol (`clawdbot gateway`, see `docs/architecture.md` and `docs/gateway.md`).
Current builds use a WebSocket server on `ws://127.0.0.1:18789` and do **not** expose this TCP control channel.
Legacy endpoint (if present in an older build): `127.0.0.1:18789` (TCP, localhost only), typically reached via SSH port forward in remote mode.
## Frame format
Each line is a JSON object. Two shapes exist:
- **Request**: `{ "type": "request", "id": "<uuid>", "method": "health" | "status" | "last-heartbeat" | "set-heartbeats" | "ping", "params"?: { ... } }`
- **Response**: `{ "type": "response", "id": "<same id>", "ok": true, "payload"?: { ... } }` or `{ "type": "response", "id": "<same id>", "ok": false, "error": "message" }`
- **Event**: `{ "type": "event", "event": "heartbeat" | "gateway-status" | "log", "payload": { ... } }`
## Methods
- `ping`: sanity check. Payload: `{ pong: true, ts }`.
- `health`: returns the gateway health snapshot (same shape as `clawdbot health --json`).
- `status`: shorter summary (linked/authAge/heartbeatSeconds, session counts).
- `last-heartbeat`: returns the most recent heartbeat event the gateway has seen.
- `set-heartbeats { enabled: boolean }`: toggle heartbeat scheduling.
## Events
- `heartbeat` payload:
```json
{
"ts": 1765224052664,
"status": "sent" | "ok-empty" | "ok-token" | "skipped" | "failed",
"to": "+15551234567",
"preview": "Heartbeat OK",
"hasMedia": false,
"durationMs": 1025,
"reason": "<error text>" // only on failed/skipped
}
```
- `gateway-status` payload: `{ "state": "starting" | "running" | "restarting" | "failed" | "stopped", "pid"?: number, "reason"?: string }`
- `log` payload: arbitrary log line; optional, can be disabled.
## Suggested client flow
1) Connect (or reconnect) → send `ping`.
2) Send `health` and `last-heartbeat` to populate UI.
3) Listen for `event` frames; update UI in real time.
4) For user toggles, send `set-heartbeats` and await response.
## Backward compatibility
- If the control channel is unavailable: that’s expected on modern builds. Use the Gateway WS protocol instead.
+5 -5
View File
@@ -63,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`).
+21 -10
View File
@@ -14,9 +14,9 @@ Last updated: 2025-12-13
## Context
Clawdbot already has:
- A **gateway heartbeat runner** that runs the agent with `HEARTBEAT` and suppresses `HEARTBEAT_OK` (`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`).
- A WebSocket **Gateway** daemon that is intended to be always-on (`docs/gateway.md`).
- 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
@@ -75,7 +75,7 @@ Each job is a JSON object with stable keys (unknown keys ignored for forward com
- 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,"channel"?: "last"|"whatsapp"|"telegram"|"discord"|"signal"|"imessage","to"?:string,"timeoutSeconds"?:number}`
- `{"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)
@@ -173,14 +173,14 @@ When due:
- 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 channel/to.
- 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`) to avoid collisions.
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.
@@ -198,7 +198,7 @@ We need a way for the Gateway (or the scheduler) to request an immediate heartbe
Design:
- `startHeartbeatRunner` owns the real heartbeat execution and installs a wake handler.
- Wake hook lives in `src/infra/heartbeat-wake.ts`:
- 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.
@@ -216,6 +216,17 @@ Retention:
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):
@@ -264,7 +275,7 @@ Add a `cron` command group (all commands should also support `--json` where sens
- `--wake now|next-heartbeat`
- payload flags (choose one):
- `--system-event "<text>"`
- `--message "<agent message>" [--deliver] [--channel last|whatsapp|telegram|discord|signal|imessage] [--to <dest>]`
- `--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>`
@@ -302,7 +313,7 @@ clawdbot cron add \
--wake now \
--message "Daily check: scan calendar + inbox; deliver only if urgent." \
--deliver \
--channel last
--provider last
```
### Run weekly (every Wednesday)
@@ -317,7 +328,7 @@ clawdbot cron add \
--wake now \
--message "Weekly: summarize status and remind me of goals." \
--deliver \
--channel last
--provider last
```
### “Next heartbeat”
+4 -4
View File
@@ -9,9 +9,9 @@ The Gateway dashboard is the browser Control UI served at `/` by default
(override with `gateway.controlUi.basePath`).
Key references:
- `docs/control-ui.md` for usage and UI capabilities.
- `docs/tailscale.md` for Serve/Funnel automation.
- `docs/web.md` for bind modes and security notes.
- [`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`.
(token or password). See `gateway.auth` in [`docs/configuration.md`](https://docs.clawd.bot/configuration).
+131 -23
View File
@@ -1,7 +1,7 @@
---
summary: "Discord bot support status, capabilities, and configuration"
read_when:
- Working on Discord surface features
- Working on Discord provider features
---
# Discord (Bot API)
@@ -11,9 +11,9 @@ Status: ready for DM and guild text channels via the official Discord bot gatewa
## Goals
- Talk to Clawdbot via Discord DMs or guild channels.
- Share the same `main` session used by WhatsApp/Telegram/WebChat; guild channels stay isolated as `discord:group:<channelId>` (display names use `discord:<guildSlug>#<channelSlug>`).
- Direct chats collapse into the agent's main session (default `agent:main:main`); guild channels stay isolated as `agent:<agentId>:discord:channel:<channelId>` (display names use `discord:<guildSlug>#<channelSlug>`).
- Group DMs are ignored by default; enable via `discord.dm.groupEnabled` and optionally restrict by `discord.dm.groupChannels`.
- Keep routing deterministic: replies always go back to the surface they arrived on.
- Keep routing deterministic: replies always go back to the provider they arrived on.
## How it works
1. Create a Discord application → Bot, enable the intents you need (DMs + guild messages + message content), and grab the bot token.
@@ -23,18 +23,128 @@ 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. Optional DM control: set `discord.dm.enabled = false` to ignore all DMs, or `discord.dm.allowFrom` to allow specific users (ids or names). Use `discord.dm.groupEnabled` + `discord.dm.groupChannels` to allow group DMs.
8. Optional guild rules: set `discord.guilds` keyed by guild id (preferred) or slug, with per-channel rules.
9. Optional slash commands: enable `discord.slashCommand` to accept user-installed app commands (ephemeral replies). Slash invocations respect the same DM/guild allowlists.
10. 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.
11. Reactions: the agent can trigger reactions via the `discord` tool (gated by `discord.actions.*`).
- The `discord` tool is only exposed when the current surface is Discord.
12. Slash commands use isolated session keys (`${sessionPrefix}:${userId}`) rather than the shared `main` session.
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: 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.*`).
- 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.
Note: Discord does not provide a simple username → id lookup without extra guild context, so prefer ids or `<@id>` mentions for DM delivery targets.
Note: Slugs are lowercase with spaces replaced by `-`. Channel names are slugged without the leading `#`.
Note: Guild context `[from:]` lines include `author.tag` + `id` to make ping-ready replies easy.
## How to create your own bot
This is the “Discord Developer Portal” setup for running Clawdbot in a server (guild) channel like `#help`.
### 1) Create the Discord app + bot user
1. Discord Developer Portal → **Applications** → **New Application**
2. In your app:
- **Bot** → **Add Bot**
- Copy the **Bot Token** (this is what you put in `DISCORD_BOT_TOKEN`)
### 2) Enable the gateway intents Clawdbot needs
Discord blocks “privileged intents” unless you explicitly enable them.
In **Bot** → **Privileged Gateway Intents**, enable:
- **Message Content Intent** (required to read message text in most guilds; without it you’ll see “Used disallowed intents” or the bot will connect but not react to messages)
- **Server Members Intent** (recommended; required for some member/user lookups and allowlist matching in guilds)
You usually do **not** need **Presence Intent**.
### 3) Generate an invite URL (OAuth2 URL Generator)
In your app: **OAuth2** → **URL Generator**
**Scopes**
- ✅ `bot`
- ✅ `applications.commands` (required for native commands)
**Bot Permissions** (minimal baseline)
- ✅ View Channels
- ✅ Send Messages
- ✅ Read Message History
- ✅ Embed Links
- ✅ Attach Files
- ✅ Add Reactions (optional but recommended)
- ✅ Use External Emojis / Stickers (optional; only if you want them)
Avoid **Administrator** unless you’re debugging and fully trust the bot.
Copy the generated URL, open it, pick your server, and install the bot.
### 4) Get the ids (guild/user/channel)
Discord uses numeric ids everywhere; Clawdbot config prefers ids.
1. Discord (desktop/web) → **User Settings** → **Advanced** → enable **Developer Mode**
2. Right-click:
- Server name → **Copy Server ID** (guild id)
- Channel (e.g. `#help`) → **Copy Channel ID**
- Your user → **Copy User ID**
### 5) Configure Clawdbot
#### Token
Set the bot token via env var (recommended on servers):
- `DISCORD_BOT_TOKEN=...`
Or via config:
```json5
{
discord: {
enabled: true,
token: "YOUR_BOT_TOKEN"
}
}
```
#### Allowlist + channel routing
Example “single server, only allow me, only allow #help”:
```json5
{
discord: {
enabled: true,
dm: { enabled: false },
guilds: {
"YOUR_GUILD_ID": {
users: ["YOUR_USER_ID"],
requireMention: true,
channels: {
help: { allow: true, requireMention: true }
}
}
}
}
}
```
Notes:
- `requireMention: true` means the bot only replies when mentioned (recommended for shared channels).
- `routing.groupChat.mentionPatterns` also count as mentions for guild messages.
- If `channels` is present, any channel not listed is denied by default.
### 6) Verify it works
1. Start the gateway.
2. In your server channel, send: `@Krill hello` (or whatever your bot name is).
3. If nothing happens: check **Troubleshooting** below.
### Troubleshooting
- **“Used disallowed intents”**: enable **Message Content Intent** (and likely **Server Members Intent**) in the Developer Portal, then restart the gateway.
- **Bot connects but never replies in a guild channel**:
- Missing **Message Content Intent**, or
- The bot lacks channel permissions (View/Send/Read History), or
- Your config requires mentions and you didn’t mention it, or
- Your guild/channel allowlist denies the channel/user.
- **DMs don’t work**: `discord.dm.enabled=false`, `discord.dm.policy="disabled"`, or you haven’t been approved yet (`discord.dm.policy="pairing"`).
## Capabilities & limits
- DMs and guild text channels (threads are treated as separate channels; voice not supported).
- Typing indicators sent best-effort; message chunking honors Discord’s 2k character limit.
@@ -50,6 +160,7 @@ Note: Guild context `[from:]` lines include `author.tag` + `id` to make ping-rea
discord: {
enabled: true,
token: "abc.123",
groupPolicy: "open",
mediaMaxMb: 8,
actions: {
reactions: true,
@@ -69,14 +180,9 @@ Note: Guild context `[from:]` lines include `author.tag` + `id` to make ping-rea
moderation: false
},
replyToMode: "off",
slashCommand: {
enabled: true,
name: "clawd",
sessionPrefix: "discord:slash",
ephemeral: true
},
dm: {
enabled: true,
policy: "pairing", // pairing | allowlist | open | disabled
allowFrom: ["123456789012345678", "steipete"],
groupEnabled: false,
groupChannels: ["clawd-dm"]
@@ -98,10 +204,15 @@ Note: Guild context `[from:]` lines include `author.tag` + `id` to make ping-rea
}
```
Ack reactions are controlled globally via `messages.ackReaction` +
`messages.ackReactionScope`.
- `dm.enabled`: set `false` to ignore all DMs (default `true`).
- `dm.allowFrom`: DM allowlist (user ids or names). Omit or set to `["*"]` to allow any DM sender.
- `dm.policy`: DM access control (`pairing` recommended). `"open"` requires `dm.allowFrom=["*"]`.
- `dm.allowFrom`: DM allowlist (user ids or names). Used by `dm.policy="allowlist"` and for `dm.policy="open"` validation.
- `dm.groupEnabled`: enable group DMs (default `false`).
- `dm.groupChannels`: optional allowlist for group DM channel ids or slugs.
- `groupPolicy`: controls guild channel handling (`open|disabled|allowlist`); `allowlist` requires channel allowlists.
- `guilds`: per-guild rules keyed by guild id (preferred) or slug.
- `guilds."*"`: default per-guild settings applied when no explicit entry exists.
- `guilds.<id>.slug`: optional friendly slug used for display names.
@@ -109,7 +220,6 @@ Note: Guild context `[from:]` lines include `author.tag` + `id` to make ping-rea
- `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`).
- `slashCommand`: optional config for user-installed slash commands (ephemeral responses).
- `mediaMaxMb`: clamp inbound media saved to disk.
- `historyLimit`: number of recent guild messages to include as context when replying to a mention (default 20, `0` disables).
- `actions`: per-action tool gates; omit to allow all (set `false` to disable).
@@ -163,11 +273,9 @@ Allowlist matching notes:
- Use `*` to allow any sender/channel.
- When `guilds.<id>.channels` is present, channels not listed are denied by default.
Slash command notes:
- Register a chat input command in Discord with at least one string option (e.g., `prompt`).
- The first non-empty string option is treated as the prompt.
- Slash commands honor the same allowlists as DMs/guild messages (`discord.dm.allowFrom`, `discord.guilds`, per-channel rules).
- Clawdbot will auto-register `/clawd` (or the configured name) if it doesn't already exist.
Native command notes:
- The registered commands mirror Clawdbot’s chat commands.
- Native commands honor the same allowlists as DMs/guild messages (`discord.dm.allowFrom`, `discord.guilds`, per-channel rules).
## Tool actions
The agent can call `discord` with actions like:
+3 -3
View File
@@ -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`.
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`.
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`).
- 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)
+11 -4
View File
@@ -59,10 +59,16 @@ docker compose exec clawdbot-gateway node dist/index.js health --token "$CLAWDBO
scripts/e2e/onboard-docker.sh
```
### QR import smoke test (Docker)
```bash
pnpm test:docker:qr
```
### Notes
- Gateway bind defaults to `lan` for container use.
- The gateway container is the source of truth for sessions (`~/.clawdbot/sessions`).
- The gateway container is the source of truth for sessions (`~/.clawdbot/agents/<agentId>/sessions/`).
## Per-session Agent Sandbox (host gateway + Docker tools)
@@ -73,6 +79,7 @@ container. The gateway stays on your host, but the tool execution is isolated:
- 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 sandbox workspace (`media/inbound/*`) so tools can read it
### Default behavior
@@ -81,7 +88,7 @@ container. The gateway stays on your host, but the tool execution is isolated:
- 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`
- Default allow: `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`
- Default deny: `browser`, `canvas`, `nodes`, `cron`, `discord`, `gateway`
### Enable sandboxing
@@ -117,7 +124,7 @@ container. The gateway stays on your host, but the tool execution is isolated:
extraHosts: ["internal.service:10.0.0.5"]
},
tools: {
allow: ["bash", "process", "read", "write", "edit"],
allow: ["bash", "process", "read", "write", "edit", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn"],
deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"]
},
prune: {
@@ -245,7 +252,7 @@ Example:
## Troubleshooting
- Image missing: build with `scripts/sandbox-setup.sh` or set `agent.sandbox.docker.image`.
- Image missing: build with [`scripts/sandbox-setup.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/sandbox-setup.sh) or set `agent.sandbox.docker.image`.
- Container not running: it will auto-create per session on demand.
- Permission errors in sandbox: set `docker.user` to a UID:GID that matches your
mounted workspace ownership (or chown the workspace folder).
+19 -6
View File
@@ -23,13 +23,25 @@
"navigation": {
"groups": [
{
"group": "Getting Started",
"group": "Start Here",
"pages": [
"index",
"onboarding",
"getting-started",
"wizard",
"index",
"setup",
"pairing",
"faq",
"clawd",
"faq"
"showcase",
"hubs",
"onboarding"
]
},
{
"group": "Install Options",
"pages": [
"nix",
"docker"
]
},
{
@@ -62,8 +74,9 @@
"mac/dev-setup",
"mac/menu-bar",
"mac/voicewake",
"ios/connect",
"android/connect",
"macos",
"ios",
"android",
"webchat",
"web"
]
+20
View File
@@ -15,6 +15,8 @@ read_when:
- 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/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.
## Legacy config file migration
If `~/.clawdis/clawdis.json` exists and `~/.clawdbot/clawdbot.json` does not, doctor will migrate the file and normalize old paths/image names.
@@ -26,8 +28,26 @@ Doctor will:
- Show the migration it applied.
- Rewrite `~/.clawdbot/clawdbot.json` with the updated schema.
The Gateway also auto-runs doctor migrations on startup when it detects a legacy
config format, so stale configs are repaired without manual intervention.
Current migrations:
- `routing.allowFrom` → `whatsapp.allowFrom`
- `agent.model`/`allowedModels`/`modelAliases`/`modelFallbacks`/`imageModelFallbacks`
→ `agent.models` + `agent.model.primary/fallbacks` + `agent.imageModel.primary/fallbacks`
## Legacy state migrations (disk layout)
Doctor can migrate older on-disk layouts into the current structure:
- Sessions store + transcripts:
- from `~/.clawdbot/sessions/` to `~/.clawdbot/agents/<agentId>/sessions/`
- Agent dir:
- from `~/.clawdbot/agent/` to `~/.clawdbot/agents/<agentId>/agent/`
- WhatsApp auth state (Baileys):
- from legacy `~/.clawdbot/credentials/*.json` (except `oauth.json`)
- 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.
## Usage
+1 -1
View File
@@ -22,7 +22,7 @@ read_when:
## Availability + allowlists
- Feature gate: `agent.elevated.enabled` (default can be off via config even if the code supports it).
- Sender allowlist: `agent.elevated.allowFrom` with per-surface allowlists (e.g. `discord`, `whatsapp`).
- Sender allowlist: `agent.elevated.allowFrom` with per-provider allowlists (e.g. `discord`, `whatsapp`).
- Both must pass; otherwise elevated is treated as unavailable.
- Discord fallback: if `agent.elevated.allowFrom.discord` is omitted, the `discord.dm.allowFrom` list is used as a fallback. Set `agent.elevated.allowFrom.discord` (even `[]`) to override.
+163 -69
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.md](./configuration.md).
Common questions from the community. For detailed configuration, see [Configuration](https://docs.clawd.bot/configuration).
## Installation & Setup
@@ -14,9 +14,15 @@ Everything lives under `~/.clawdbot/`:
| Path | Purpose |
|------|---------|
| `~/.clawdbot/clawdbot.json` | Main config (JSON5) |
| `~/.clawdbot/credentials/` | WhatsApp/Telegram auth tokens |
| `~/.clawdbot/sessions/` | Conversation history & state |
| `~/.clawdbot/sessions/sessions.json` | Session metadata |
| `~/.clawdbot/credentials/oauth.json` | Legacy OAuth import (copied into auth profiles on first use) |
| `~/.clawdbot/agents/<agentId>/agent/auth-profiles.json` | Auth profiles (OAuth + API keys) |
| `~/.clawdbot/agents/<agentId>/agent/auth.json` | Runtime auth cache (managed automatically) |
| `~/.clawdbot/credentials/` | Provider auth state (e.g. `whatsapp/<accountId>/creds.json`) |
| `~/.clawdbot/agents/` | Per-agent state (agentDir + sessions) |
| `~/.clawdbot/agents/<agentId>/sessions/` | Conversation history & state (per agent) |
| `~/.clawdbot/agents/<agentId>/sessions/sessions.json` | Session metadata (per agent) |
Legacy single-agent path: `~/.clawdbot/agent/*` (migrated by `clawdbot doctor`).
Your **workspace** (AGENTS.md, memory files, skills) is separate — configured via `agent.workspace` in your config (default: `~/clawd`).
@@ -34,16 +40,16 @@ Some features are platform-specific:
### What are the minimum system requirements?
**Basically nothing!** The gateway is very lightweight — all heavy compute happens on Anthropic's servers.
**Basically nothing!** The gateway is very lightweight — heavy compute happens on your model provider’s servers (Anthropic/OpenAI/etc.).
- **RAM:** 512MB-1GB is enough (community member runs on 1GB VPS!)
- **CPU:** 1 core is fine for personal use
- **Disk:** ~500MB for Clawdbot + deps, plus space for logs/media
The gateway is just shuffling messages around. A Raspberry Pi 4 can run it. You can also use **Bun** instead of Node for even lower memory footprint:
The gateway is just shuffling messages around. A Raspberry Pi 4 can run it. For the CLI, prefer the Node runtime (most stable):
```bash
bun clawdbot gateway
pnpm clawdbot gateway
```
### How do I install on Linux without Homebrew?
@@ -76,7 +82,7 @@ This creates `~/.clawdbot/clawdbot.json` with your API keys, workspace path, and
cp -r ~/.clawdbot ~/.clawdbot-backup
# Remove config and credentials
rm -rf ~/.clawdbot
trash ~/.clawdbot
# Re-run onboarding
pnpm clawdbot onboard
@@ -105,30 +111,48 @@ The macOS app onboarding is still being polished and can have quirks (e.g., What
### OAuth vs API key — what's the difference?
- **OAuth** — Uses your Claude Pro/Max subscription ($20-100/mo flat). No per-token charges. ✅ Recommended!
- **API key** — Pay-per-token via console.anthropic.com. Can get expensive fast.
- **OAuth** — Uses your **subscription** (Anthropic Claude Pro/Max or OpenAI ChatGPT/Codex). No per‑token charges. ✅ Recommended!
- **API key** — Pay‑per‑token via the provider’s API billing. Can get expensive fast.
They're **separate billing**! An API key does NOT use your subscription.
**For OAuth:** During onboarding, pick "Anthropic OAuth", log in to your Claude account, paste the code back. Or just run:
**For OAuth:** During onboarding, pick **Anthropic OAuth** or **OpenAI Codex OAuth**, log in, paste the code/URL when prompted. Or just run:
```bash
pnpm clawdbot login
```
**If OAuth fails** (headless/container): Do OAuth on a normal machine, then copy `~/.clawdbot/` to your server. The auth is just a JSON file.
**If OAuth fails** (headless/container): Do OAuth on a normal machine, then copy `~/.clawdbot/agents/<agentId>/agent/auth-profiles.json` (and `auth.json` if present) to your server. Legacy installs can still import `~/.clawdbot/credentials/oauth.json` on first use.
### How are env vars loaded?
CLAWDBOT reads env vars from the parent process (shell, launchd/systemd, CI, etc.). It also loads `.env` files:
- `.env` in the current working directory
- global fallback: `~/.clawdbot/.env` (aka `$CLAWDBOT_STATE_DIR/.env`)
Neither `.env` file overrides existing env vars.
Optional convenience: import missing expected keys from your login shell env (sources your shell profile):
```json5
{
env: { shellEnv: { enabled: true, timeoutMs: 15000 } }
}
```
Or set `CLAWDBOT_LOAD_SHELL_ENV=1` (timeout: `CLAWDBOT_SHELL_ENV_TIMEOUT_MS=15000`).
### Does enterprise OAuth work?
**Not currently.** Enterprise accounts use SSO which requires a different auth flow that pi-coding-agent doesn't support yet.
**Not currently.** Enterprise accounts use SSO which requires a different auth flow that Clawdbot’s OAuth login doesn’t support yet.
**Workaround:** Ask your enterprise admin to provision an API key via the Anthropic console, then use that with `ANTHROPIC_API_KEY`.
**Workaround:** Ask your enterprise admin to provision an API key (Anthropic or OpenAI) and use it via `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`.
### OAuth callback not working (containers/headless)?
OAuth needs the callback to reach the machine running the CLI. Options:
1. **Copy auth manually** — Run OAuth on your laptop, copy `~/.clawdbot/credentials/` to the container.
1. **Copy auth manually** — Run OAuth on your laptop, copy `~/.clawdbot/agents/<agentId>/agent/auth-profiles.json` (and `auth.json` if present) to the container. Legacy flow: copy `~/.clawdbot/credentials/oauth.json` to trigger import.
2. **SSH tunnel** — `ssh -L 18789:localhost:18789 user@server`
3. **Tailscale** — Put both machines on your tailnet.
@@ -170,7 +194,15 @@ OAuth needs the callback to reach the machine running the CLI. Options:
### Can I run Clawdbot in Docker?
There's no official Docker setup yet, but it works. Key considerations:
Yes — Docker is optional but supported. Recommended: run the setup script:
```bash
./docker-setup.sh
```
It builds the image, runs onboarding + login, and starts Docker Compose. For manual steps and sandbox notes, see `docs/docker.md`.
Key considerations:
- **WhatsApp login:** QR code works in terminal — no display needed.
- **Persistence:** Mount `~/.clawdbot/` and your workspace as volumes.
@@ -197,7 +229,7 @@ pnpm clawdbot gateway
bash /app/start.sh
```
Docker support is on the roadmap — PRs welcome!
For more detail, see `docs/docker.md`.
### Can I run Clawdbot headless on a VPS?
@@ -209,7 +241,7 @@ Yes! The terminal QR code login works fine over SSH. For long-running operation:
### bun binary vs Node runtime?
Clawdbot can run as:
- **bun binary** — Single executable, easy distribution, auto-restarts via launchd
- **bun binary (macOS app)** — Single executable, easy distribution, auto-restarts via launchd
- **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).
@@ -269,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.md](./groups.md) for details.
See [Groups](https://docs.clawd.bot/groups) for details.
---
@@ -277,11 +309,11 @@ See [groups.md](./groups.md) for details.
### How much context can Clawdbot handle?
Claude Opus has a 200k token context window, and Clawdbot uses **autocompaction** — older conversation gets summarized to stay under the limit.
Context window depends on the model. Clawdbot uses **autocompaction** — older conversation gets summarized to stay under the limit.
Practical tips:
- Keep `AGENTS.md` focused, not bloated.
- Use `/new` to reset the session when context gets stale.
- Use `/compact` to shrink older context or `/new` to reset when it gets stale.
- For large memory/notes collections, use search tools like `qmd` rather than loading everything.
### Where are my memory files?
@@ -306,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.md](./signal.md)).
- **Signal** — Via `signal-cli` (see [Signal](https://docs.clawd.bot/signal)).
- **WebChat** — Browser-based chat UI.
### Discord: Bot works in channels but not DMs?
@@ -344,7 +376,7 @@ If you send an image but your Clawd doesn't "see" it, check these:
Not all models support images! Check `agent.model` in your config:
- ✅ Vision: `claude-opus-4-5`, `claude-sonnet-4-5`, `claude-haiku-4-5`, `gpt-5.2`, `gpt-4o`, `gemini-pro`
- ✅ Vision (examples): `anthropic/claude-opus-4-5`, `anthropic/claude-sonnet-4-5`, `anthropic/claude-haiku-4-5`, `openai/gpt-5.2`, `openai/gpt-4o`, `google/gemini-3-pro-preview`, `google/gemini-3-flash-preview`
- ❌ No vision: Most local LLMs (Llama, Mistral), older models, text-only configs
**2. Is media being downloaded?**
@@ -446,39 +478,87 @@ cd ~/path/to/clawdbot
codex --full-auto "debug why clawdbot gateway won't start"
```
### Processes keep restarting after I kill them (Linux)
### Gateway stops after I log out (Linux)
Something is supervising them. Check:
Linux installs use a systemd **user** service. By default, systemd stops user
services on logout/idle, which kills the Gateway.
Onboarding attempts to enable lingering; if it’s still off, run:
```bash
# systemd?
systemctl list-units | grep -i clawdbot
sudo systemctl stop clawdbot
# pm2?
pm2 list
pm2 delete all
sudo loginctl enable-linger $USER
```
Stop the supervisor first, then the processes.
**macOS/Windows**
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
The gateway runs under a supervisor that auto-restarts it. You need to stop the supervisor, not just kill the process.
**macOS (Clawdbot.app)**
- Quit the menu bar app to stop the gateway.
- For debugging, restart via the app (or `scripts/restart-mac.sh` when working in the repo).
- 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
clawdbot gateway restart
```
**Linux (systemd)**
```bash
# Check if running
systemctl list-units | grep -i clawdbot
# Stop and disable
clawdbot gateway stop
systemctl --user disable --now clawdbot-gateway.service
# Or just restart
clawdbot gateway restart
```
**pm2 (if used)**
```bash
pm2 list
pm2 delete clawdbot
```
### Clean uninstall (start fresh)
```bash
# Stop processes
# macOS: stop launchd service
launchctl disable gui/$UID/com.clawdbot.gateway
launchctl bootout gui/$UID/com.clawdbot.gateway 2>/dev/null
# Linux: stop systemd user service
systemctl --user disable --now clawdbot-gateway.service
# Linux (system-wide unit, if installed)
sudo systemctl disable --now clawdbot-gateway.service
# Kill any remaining processes
pkill -f "clawdbot"
# If using systemd
sudo systemctl stop clawdbot
sudo systemctl disable clawdbot
# Remove data
rm -rf ~/.clawdbot
trash ~/.clawdbot
# Remove repo and re-clone
rm -rf ~/clawdbot
git clone https://github.com/clawdbot/clawdbot.git
cd clawdbot && pnpm install && pnpm build
# Remove repo and re-clone (adjust path if you cloned elsewhere)
trash ~/Projects/clawdbot
git clone https://github.com/clawdbot/clawdbot.git ~/Projects/clawdbot
cd ~/Projects/clawdbot && pnpm install && pnpm build
pnpm clawdbot onboard
```
@@ -490,14 +570,21 @@ Quick reference (send these in chat):
| Command | Action |
|---------|--------|
| `/help` | Show available commands |
| `/status` | Health + session info |
| `/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 |
| `/elevated on\|off` | Toggle elevated bash mode (approved senders only) |
| `/activation mention\|always` | Group activation (owner-only) |
| `/model <name>` | Switch AI model (see below) |
| `/queue instant\|batch\|serial` | Message queuing mode |
| `/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: https://docs.clawd.bot/slash-commands
### How do I switch models on the fly?
@@ -507,23 +594,27 @@ Use `/model` to switch without restarting:
/model sonnet
/model haiku
/model opus
/model gpt
/model gpt-mini
/model gemini
/model gemini-flash
```
**Setup:** Configure allowed models and aliases in `clawdbot.json`:
List available models with `/model`, `/model list`, or `/model status`.
Clawdbot ships a few default model shorthands (you can override them in config):
`opus`, `sonnet`, `gpt`, `gpt-mini`, `gemini`, `gemini-flash`.
**Setup:** Configure models and aliases in `clawdbot.json`:
```json
{
"agent": {
"model": "anthropic/claude-opus-4-5-20251022",
"allowedModels": [
"anthropic/claude-opus-4-5-20251022",
"anthropic/claude-sonnet-4-5-20251022",
"anthropic/claude-haiku-4-5-20251001"
],
"modelAliases": {
"opus": "anthropic/claude-opus-4-5-20251022",
"sonnet": "anthropic/claude-sonnet-4-5-20251022",
"haiku": "anthropic/claude-haiku-4-5-20251001"
"model": { "primary": "anthropic/claude-opus-4-5" },
"models": {
"anthropic/claude-opus-4-5": { "alias": "opus" },
"anthropic/claude-sonnet-4-5": { "alias": "sonnet" },
"anthropic/claude-haiku-4-5": { "alias": "haiku" }
}
}
}
@@ -539,7 +630,8 @@ If you don't want to use Anthropic directly, you can use alternative providers:
```json5
{
agent: {
model: "openrouter/anthropic/claude-sonnet-4",
model: { primary: "openrouter/anthropic/claude-sonnet-4-5" },
models: { "openrouter/anthropic/claude-sonnet-4-5": {} },
env: { OPENROUTER_API_KEY: "sk-or-..." }
}
}
@@ -549,7 +641,8 @@ If you don't want to use Anthropic directly, you can use alternative providers:
```json5
{
agent: {
model: "zai/glm-4.7",
model: { primary: "zai/glm-4.7" },
models: { "zai/glm-4.7": {} },
env: { ZAI_API_KEY: "..." }
}
}
@@ -569,11 +662,8 @@ If you get weird errors after switching models, try `/think off` and `/new` to r
### How do I stop/cancel a running task?
Send `/stop` to immediately abort the current agent run. Other stop words also work:
- `/stop`
- `/abort`
- `/esc`
- `/exit`
Send one of these **as a standalone message** (no slash): `stop`, `abort`, `esc`, `wait`, `exit`.
These are abort triggers, not slash commands.
For background processes (like Codex), use:
```
@@ -582,8 +672,10 @@ process action:kill sessionId:XXX
You can also configure `routing.queue.mode` to control how new messages interact with running tasks:
- `steer` — New messages redirect the current task
- `interrupt` — Kills current run, starts fresh
- `collect` — Queues messages for after
- `followup` — Run messages one at a time
- `collect` — Batch messages, reply once after things settle
- `steer-backlog` — Steer now, process backlog afterward
- `interrupt` — Abort current run, start fresh
### Does Codex CLI use my ChatGPT Pro subscription or API credits?
@@ -606,11 +698,13 @@ If you have a ChatGPT subscription, use browser auth to avoid API charges!
Use `/queue` to control how messages sent in quick succession are handled:
- **`/queue instant`** — New messages interrupt/steer the current response
- **`/queue batch`** — Messages queue up, processed after current turn
- **`/queue serial`** — One at a time, in order
- **`/queue steer`** — New messages steer the current response
- **`/queue collect`** — Batch messages, reply once after things settle
- **`/queue followup`** — One at a time, in order
- **`/queue steer-backlog`** — Steer now, process backlog afterward
- **`/queue interrupt`** — Abort current run, start fresh
If you tend to send multiple short messages, `/queue instant` feels most natural.
If you tend to send multiple short messages, `/queue steer` feels most natural.
---
+66 -8
View File
@@ -49,6 +49,34 @@ pnpm gateway:watch
Supported if you isolate state + config and use unique ports.
### Dev profile (`--dev`)
Fast path: run a fully-isolated dev instance (config/state/workspace) without touching your primary setup.
```bash
clawdbot --dev setup
clawdbot --dev gateway --allow-unconfigured
# then target the dev instance:
clawdbot --dev status
clawdbot --dev health
```
Defaults (can be overridden via env/flags/config):
- `CLAWDBOT_STATE_DIR=~/.clawdbot-dev`
- `CLAWDBOT_CONFIG_PATH=~/.clawdbot-dev/clawdbot.json`
- `CLAWDBOT_GATEWAY_PORT=19001` (Gateway WS + HTTP)
- `bridge.port=19002` (derived: `gateway.port+1`)
- `browser.controlUrl=http://127.0.0.1:19003` (derived: `gateway.port+2`)
- `canvasHost.port=19005` (derived: `gateway.port+4`)
- `agent.workspace` default becomes `~/clawd-dev` when you run `setup`/`onboard` under `--dev`.
Derived ports (rules of thumb):
- Base port = `gateway.port` (or `CLAWDBOT_GATEWAY_PORT` / `--port`)
- `bridge.port = base + 1` (or `CLAWDBOT_BRIDGE_PORT` / config override)
- `browser.controlUrl port = base + 2` (or `CLAWDBOT_BROWSER_CONTROL_URL` / config override)
- `canvasHost.port = base + 4` (or `CLAWDBOT_CANVAS_HOST_PORT` / config override)
- Browser profile CDP ports auto-allocate from `browser.controlPort + 9 .. + 108` (persisted per profile).
Checklist per instance:
- unique `gateway.port`
- unique `CLAWDBOT_CONFIG_PATH`
@@ -83,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` 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).
@@ -99,7 +127,7 @@ See also: `docs/presence.md` for how presence is produced/deduped and why `insta
## Typing and validation
- Server validates every inbound frame with AJV against JSON Schema emitted from the protocol definitions.
- Clients (TS/Swift) consume generated types (TS directly; Swift via the repo’s generator).
- Types live in `src/gateway/protocol/*.ts`; regenerate schemas/models with `pnpm protocol:gen` (writes `dist/protocol.schema.json`) and `pnpm protocol:gen:swift` (writes `apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift`).
- Types live in [`src/gateway/protocol/*.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/gateway/protocol/*.ts); regenerate schemas/models with `pnpm protocol:gen` (writes [`dist/protocol.schema.json`](https://github.com/clawdbot/clawdbot/blob/main/dist/protocol.schema.json)) and `pnpm protocol:gen:swift` (writes [`apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift`](https://github.com/clawdbot/clawdbot/blob/main/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift)).
## Connection snapshot
- `hello-ok` includes a `snapshot` with `presence`, `health`, `stateVersion`, and `uptimeMs` plus `policy {maxPayload,maxBufferedBytes,tickIntervalMs}` so clients can render immediately without extra requests.
@@ -127,11 +155,15 @@ See also: `docs/presence.md` for how presence is produced/deduped and why `insta
- KeepAlive: true
- StandardOut/Err: file paths or `syslog`
- On failure, launchd restarts; fatal misconfig should keep exiting so the operator notices.
- LaunchAgents are per-user and require a logged-in session; for headless setups use a custom LaunchDaemon (not shipped).
Bundled mac app:
- Clawdbot.app can bundle a bun-compiled gateway binary and install a per-user LaunchAgent labeled `com.clawdbot.gateway`.
- To stop it cleanly, use `clawdbot gateway stop` (or `launchctl bootout gui/$UID/com.clawdbot.gateway`).
- To restart, use `clawdbot gateway restart` (or `launchctl kickstart -k gui/$UID/com.clawdbot.gateway`).
## Supervision (systemd example)
## Supervision (systemd user unit)
Create `~/.config/systemd/user/clawdbot-gateway.service`:
```
[Unit]
Description=Clawdbot Gateway
@@ -140,16 +172,36 @@ Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/clawdbot gateway --port 18789
Restart=on-failure
Restart=always
RestartSec=5
User=clawdbot
Environment=CLAWDBOT_GATEWAY_TOKEN=
WorkingDirectory=/home/clawdbot
WorkingDirectory=/home/youruser
[Install]
WantedBy=multi-user.target
WantedBy=default.target
```
Enable with `systemctl enable --now clawdbot-gateway.service`.
Enable lingering (required so the user service survives logout/idle):
```
sudo loginctl enable-linger youruser
```
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
```
**Alternative (system service)** - for always-on or multi-user servers, you can
install a systemd **system** unit instead of a user unit (no lingering needed).
Create `/etc/systemd/system/clawdbot-gateway.service` (copy the unit above,
switch `WantedBy=multi-user.target`, set `User=` + `WorkingDirectory=`), then:
```
sudo systemctl daemon-reload
sudo systemctl enable --now clawdbot-gateway.service
```
## 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).
@@ -167,7 +219,13 @@ Enable with `systemctl enable --now clawdbot-gateway.service`.
- `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 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.
+5 -5
View File
@@ -20,7 +20,7 @@ This enables:
- **Bridge**: direct transport endpoint owned by the gateway. The bridge does not decide membership.
## API surface (gateway protocol)
These are conceptual method names; wire them into `src/gateway/protocol/schema.ts` and regenerate Swift types.
These are conceptual method names; wire them into [`src/gateway/protocol/schema.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/gateway/protocol/schema.ts) and regenerate Swift types.
### Events
- `node.pair.requested`
@@ -78,10 +78,10 @@ Optional interactive helper:
- `clawdbot nodes watch` (subscribe to `node.pair.requested` and prompt in-place)
Implementation pointers:
- CLI commands: `src/cli/nodes-cli.ts`
- Gateway handlers + events: `src/gateway/server.ts`
- Pairing store: `src/infra/node-pairing.ts` (under `~/.clawdbot/nodes/`)
- Optional macOS UI prompt (frontend only): `apps/macos/Sources/Clawdbot/NodePairingApprovalPrompter.swift`
- CLI commands: [`src/cli/nodes-cli.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/cli/nodes-cli.ts)
- Gateway handlers + events: [`src/gateway/server.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/gateway/server.ts) + [`src/gateway/server-methods/nodes.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/gateway/server-methods/nodes.ts)
- Pairing store: [`src/infra/node-pairing.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/infra/node-pairing.ts) (under `~/.clawdbot/nodes/`)
- Optional macOS UI prompt (frontend only): [`apps/macos/Sources/Clawdbot/NodePairingApprovalPrompter.swift`](https://github.com/clawdbot/clawdbot/blob/main/apps/macos/Sources/Clawdbot/NodePairingApprovalPrompter.swift)
- Push-first: listens to `node.pair.requested`/`node.pair.resolved`, does a `node.pair.list` on startup/reconnect,
and only runs a slow safety poll while a request is pending/visible.
+133
View File
@@ -0,0 +1,133 @@
---
summary: "Beginner guide: from repo checkout to first message (wizard, auth, providers, pairing)"
read_when:
- First time setup from zero
- You want the fastest path from checkout → onboarding → first message
---
# Getting Started
Goal: go from **zero** → **first working chat** (with sane defaults) as quickly as possible.
Recommended path: use the **CLI onboarding wizard** (`clawdbot onboard`). It sets up:
- model/auth (OAuth recommended)
- gateway settings
- providers (WhatsApp/Telegram/Discord/…)
- pairing defaults (secure DMs)
- workspace bootstrap + skills
- optional background daemon
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`
- `bun` (preferred) or `pnpm`
- Git
macOS: if you plan to build the apps, install Xcode / CLT. For the CLI + gateway only, Node is enough.
## 1) Check out from source
```bash
git clone https://github.com/clawdbot/clawdbot.git
cd clawdbot
bun install
```
Note: `pnpm` is also supported:
```bash
pnpm install
```
## 2) Build the Control UI (recommended)
The Gateway serves the browser dashboard (Control UI) when assets exist.
```bash
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.
## 3) Run the onboarding wizard
```bash
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/Task Scheduler)
Wizard doc: https://docs.clawd.bot/wizard
### Auth: where it lives (important)
- OAuth credentials (legacy import): `~/.clawdbot/credentials/oauth.json`
- Auth profiles (OAuth + API keys): `~/.clawdbot/agents/<agentId>/agent/auth-profiles.json`
Headless/server tip: do OAuth on a normal machine first, then copy `oauth.json` to the gateway host.
## 4) Start the Gateway
If the wizard didn’t start it for you:
```bash
bun run clawdbot gateway --port 18789 --verbose
```
Dashboard (local loopback): `http://127.0.0.1:18789/`
## 5) Pair + connect your first chat surface
### WhatsApp (QR login)
```bash
bun run clawdbot login
```
Scan via WhatsApp → Settings → Linked Devices.
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: https://docs.clawd.bot/telegram
- Discord: https://docs.clawd.bot/discord
## 6) DM safety (pairing approvals)
Default posture: unknown DMs get a short code and messages are not processed until approved.
Approve:
```bash
bun run clawdbot pairing list --provider telegram
bun run clawdbot pairing approve --provider telegram <CODE>
```
Pairing doc: https://docs.clawd.bot/pairing
## 7) Verify end-to-end
In a new terminal:
```bash
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: 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
+2 -2
View File
@@ -13,7 +13,7 @@ Goal: Gmail watch -> Pub/Sub push -> `gog gmail watch serve` -> Clawdbot webhook
- `gcloud` installed and logged in.
- `gog` (gogcli) installed and authorized for the Gmail account.
- Clawdbot hooks enabled (see `docs/webhook.md`).
- 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):
@@ -30,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`).
under `hooks.transformsDir` (see [`docs/webhook.md`](https://docs.clawd.bot/webhook)).
## Wizard (recommended)
+2 -2
View File
@@ -17,8 +17,8 @@ Updated: 2025-12-07
- **Gateway:** `monitorTelegramProvider` builds a grammY `Bot`, wires mention/allowlist gating, media download via `getFile`/`download`, and delivers replies with `sendMessage/sendPhoto/sendVideo/sendAudio/sendDocument`. Supports long-poll or webhook via `webhookCallback`.
- **Proxy:** optional `telegram.proxy` uses `undici.ProxyAgent` through grammY’s `client.baseFetch`.
- **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 map to `main`; groups map to `telegram:group:<chatId>`; replies route back to the same surface.
- **Config knobs:** `telegram.botToken`, `telegram.groups`, `telegram.allowFrom`, `telegram.mediaMaxMb`, `telegram.proxy`, `telegram.webhookSecret`, `telegram.webhookUrl`.
- **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`.
- **Tests:** grammy mocks cover DM + group mention gating and outbound send; more media/webhook fixtures still welcome.
Open questions
+9 -7
View File
@@ -1,5 +1,5 @@
---
summary: "Behavior and config for WhatsApp group message handling"
summary: "Behavior and config for WhatsApp group message handling (mentionPatterns are shared across surfaces)"
read_when:
- Changing group message rules or mentions
---
@@ -7,10 +7,12 @@ read_when:
Goal: let Clawd sit in WhatsApp groups, wake up only when pinged, and keep that thread separate from the personal DM session.
Note: `routing.groupChat.mentionPatterns` is now used by Telegram/Discord/Slack/iMessage as well; this doc focuses on WhatsApp-specific behavior.
## What’s implemented (2025-12-03)
- Activation modes: `mention` (default) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, regex patterns, or the bot’s E.164 anywhere in the text). `always` wakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the silent token `NO_REPLY`. Defaults can be set in config (`whatsapp.groups`) and overridden per group via `/activation`.
- Group allowlist bypass: we still enforce `whatsapp.allowFrom` on the participant at inbox ingest, but group JIDs themselves no longer block replies.
- Per-group sessions: session keys look like `whatsapp:group:<jid>` so commands such as `/verbose on` or `/think:high` are scoped to that group; personal DM state is untouched. Heartbeats are skipped for group threads.
- Activation modes: `mention` (default) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, regex patterns, or the bot’s E.164 anywhere in the text). `always` wakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the silent token `NO_REPLY`. Defaults can be set in config (`whatsapp.groups`) and overridden per group via `/activation`. When `whatsapp.groups` is set, it also acts as a group allowlist (include `"*"` to allow all).
- Group policy: `whatsapp.groupPolicy` controls whether group messages are accepted (`open|disabled|allowlist`). `allowlist` uses `whatsapp.groupAllowFrom` (fallback: explicit `whatsapp.allowFrom`).
- Per-group sessions: session keys look like `whatsapp:group:<jid>` so commands such as `/verbose on` or `/think high` (sent as standalone messages) are scoped to that group; personal DM state is untouched. Heartbeats are skipped for group threads.
- Context injection: last N (default 50) group messages are prefixed under `[Chat messages since your last reply - for context]`, with the triggering line under `[Current message - respond to this]`.
- Sender surfacing: every group batch now ends with `[from: Sender Name (+E164)]` so Pi knows who is speaking.
- Ephemeral/view-once: we unwrap those before extracting text/mentions, so pings inside them still trigger.
@@ -50,13 +52,13 @@ Use the group chat command:
- `/activation mention`
- `/activation always`
Only the owner number (from `whatsapp.allowFrom`, defaulting to the bot’s own E.164 when unset) can change this. `/status` in the group shows the current activation mode.
Only the owner number (from `whatsapp.allowFrom`, or the bot’s own E.164 when unset) can change this. Send `/status` as a standalone message in the group to see the current activation mode.
## How to use
1) Add Clawd UK (`+447700900123`) to the group.
2) Say `@clawd …` (or `@clawd uk`, `@clawdbot`, or include the number). Anyone in the group can trigger it.
3) The agent prompt will include recent group context plus the trailing `[from: …]` marker so it can address the right person.
4) Session-level directives (`/verbose on`, `/think:high`, `/new` or `/reset`) apply only to that group’s session; your personal DM session remains independent.
4) Session-level directives (`/verbose on`, `/think high`, `/new` or `/reset`, `/compact`) apply only to that group’s session; send them as standalone messages so they register. Your personal DM session remains independent.
## Testing / verification
- Automated: `pnpm test -- src/web/auto-reply.test.ts --runInBand` (covers mention gating, history injection, sender suffix).
@@ -68,4 +70,4 @@ Only the owner number (from `whatsapp.allowFrom`, defaulting to the bot’s own
## Known considerations
- Heartbeats are intentionally skipped for groups to avoid noisy broadcasts.
- Echo suppression uses the combined batch string; if you send identical text twice without mentions, only the first will get a response.
- Session store entries will appear as `whatsapp:group:<jid>` in the session store (`~/.clawdbot/sessions/sessions.json` by default); a missing entry just means the group hasn’t triggered a run yet.
- Session store entries will appear as `agent:<agentId>:whatsapp:group:<jid>` in the session store (`~/.clawdbot/agents/<agentId>/sessions/sessions.json` by default); a missing entry just means the group hasn’t triggered a run yet.
+58 -6
View File
@@ -1,21 +1,69 @@
---
summary: "Group chat behavior across surfaces (WhatsApp/Telegram/Discord/iMessage)"
summary: "Group chat behavior across surfaces (WhatsApp/Telegram/Discord/Slack/Signal/iMessage)"
read_when:
- Changing group chat behavior or mention gating
---
# Groups
Clawdbot treats group chats consistently across surfaces: WhatsApp, Telegram, Discord, iMessage.
Clawdbot treats group chats consistently across surfaces: WhatsApp, Telegram, Discord, Slack, Signal, iMessage.
## Session keys
- Group sessions use `surface:group:<id>` session keys (rooms/channels use `surface:channel:<id>`).
- Group sessions use `agent:<agentId>:<provider>:group:<id>` session keys (rooms/channels use `agent:<agentId>:<provider>:channel:<id>`).
- Direct chats use the main session (or per-sender if configured).
- Heartbeats are skipped for group sessions.
## Display labels
- UI labels use `displayName` when available, formatted as `surface:<token>`.
- UI labels use `displayName` when available, formatted as `<provider>:<token>`.
- `#room` is reserved for rooms/channels; group chats use `g-<slug>` (lowercase, spaces -> `-`, keep `#@+._-`).
## Group policy
Control how group/room messages are handled per provider:
```json5
{
whatsapp: {
groupPolicy: "disabled", // "open" | "disabled" | "allowlist"
groupAllowFrom: ["+15551234567"]
},
telegram: {
groupPolicy: "disabled",
groupAllowFrom: ["123456789", "@username"]
},
signal: {
groupPolicy: "disabled",
groupAllowFrom: ["+15551234567"]
},
imessage: {
groupPolicy: "disabled",
groupAllowFrom: ["chat_id:123"]
},
discord: {
groupPolicy: "allowlist",
guilds: {
"GUILD_ID": { channels: { help: { allow: true } } }
}
},
slack: {
groupPolicy: "allowlist",
channels: { "#general": { allow: true } }
}
}
```
| Policy | Behavior |
|--------|----------|
| `"open"` | Default. Groups bypass allowlists; mention-gating still applies. |
| `"disabled"` | Block all group messages entirely. |
| `"allowlist"` | Only allow groups/rooms that match the configured allowlist. |
Notes:
- `groupPolicy` is separate from mention-gating (which requires @mentions).
- WhatsApp/Telegram/Signal/iMessage: use `groupAllowFrom` (fallback: explicit `allowFrom`).
- Discord: allowlist uses `discord.guilds.<id>.channels`.
- Slack: allowlist uses `slack.channels`.
- Group DMs are controlled separately (`discord.dm.*`, `slack.dm.*`).
- Telegram allowlist can match user IDs (`"123456789"`, `"telegram:123456789"`, `"tg:123456789"`) or usernames (`"@alice"` or `"alice"`); prefixes are case-insensitive.
## Mention gating (default)
Group messages require a mention unless overridden per group. Defaults live per subsystem under `*.groups."*"`.
@@ -51,14 +99,18 @@ Group messages require a mention unless overridden per group. Defaults live per
Notes:
- `mentionPatterns` are case-insensitive regexes.
- Surfaces that provide explicit mentions still pass; patterns are a fallback.
- Mention gating is only enforced when mention detection is possible (native mentions or `mentionPatterns` are configured).
- Discord defaults live in `discord.guilds."*"` (overridable per guild/channel).
## Group allowlists
When `whatsapp.groups`, `telegram.groups`, or `imessage.groups` is configured, the keys act as a group allowlist. Use `"*"` to allow all groups while still setting default mention behavior.
## Activation (owner-only)
Group owners can toggle per-group activation:
- `/activation mention`
- `/activation always`
Owner is determined by `whatsapp.allowFrom` (or the bot’s self E.164 when unset). Other surfaces currently ignore `/activation`.
Owner is determined by `whatsapp.allowFrom` (or the bot’s self E.164 when unset). Send the command as a standalone message. Other surfaces currently ignore `/activation`.
## Context fields
Group inbound payloads set:
@@ -75,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` 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).
+4 -4
View File
@@ -11,18 +11,18 @@ Short guide to verify the WhatsApp Web / Baileys stack without guessing.
- `clawdbot status` — local summary: whether creds exist, auth age, session store path + recent sessions.
- `clawdbot status --deep` — also probes the running Gateway (WhatsApp connect + Telegram + Discord APIs).
- `clawdbot health --json` — asks the running Gateway for a full health snapshot (WS-only; no direct Baileys socket).
- Send `/status` in WhatsApp/WebChat to get a status reply without invoking the agent.
- Send `/status` as a standalone message in WhatsApp/WebChat to get a status reply without invoking the agent.
- Logs: tail `/tmp/clawdbot/clawdbot-*.log` and filter for `web-heartbeat`, `web-reconnect`, `web-auto-reply`, `web-inbound`.
## Deep diagnostics
- Creds on disk: `ls -l ~/.clawdbot/credentials/creds.json` (mtime should be recent).
- Session store: `ls -l ~/.clawdbot/sessions/sessions.json` (legacy: `~/.clawdbot/sessions.json`; path can be overridden in config). Count and recent recipients are surfaced via `status`.
- Creds on disk: `ls -l ~/.clawdbot/credentials/whatsapp/<accountId>/creds.json` (mtime should be recent).
- Session store: `ls -l ~/.clawdbot/agents/<agentId>/sessions/sessions.json` (path can be overridden in config). Count and recent recipients are surfaced via `status`.
- Relink flow: `clawdbot logout && clawdbot login --verbose` when status codes 409–515 or `loggedOut` appear in logs. (Note: the QR login flow auto-restarts once for status 515 after pairing.)
## When something fails
- `logged out` or status 409–515 → relink with `clawdbot logout` then `clawdbot login`.
- Gateway unreachable → start it: `clawdbot gateway --port 18789` (use `--force` if the port is busy).
- No inbound messages → confirm linked phone is online and the sender is allowed (`whatsapp.allowFrom`); for group chats, ensure mention rules match (`routing.groupChat.mentionPatterns` and `whatsapp.groups`).
- No inbound messages → confirm linked phone is online and the sender is allowed (`whatsapp.allowFrom`); for group chats, ensure allowlist + mention rules match (`whatsapp.groups`, `routing.groupChat.mentionPatterns`).
## Dedicated "health" command
`clawdbot health --json` asks the running Gateway for its health snapshot (no direct Baileys socket from the CLI). It reports linked creds, auth age, Baileys connect result/status code, session-store summary, and a probe duration. It exits non-zero if the Gateway is unreachable or the probe fails/timeouts. Use `--timeout <ms>` to override the 10s default.
+41 -12
View File
@@ -9,45 +9,68 @@ Heartbeat runs periodic agent turns in the **main session** so the model can
surface anything that needs attention without spamming the user.
## Prompt contract
- Heartbeat body defaults to `HEARTBEAT` (configurable via `agent.heartbeat.prompt`).
- If nothing needs attention, the model should reply **exactly** `HEARTBEAT_OK`.
- 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 **≤ 30 characters**.
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.
- 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
normal (non-heartbeat) reply, Clawdbot strips the token and logs a verbose
message. If the reply is only `HEARTBEAT_OK`, it is dropped.
### Outbound normalization (all providers)
For **all providers** (WhatsApp/Web, Telegram, Slack, Discord, Signal, iMessage),
Clawdbot applies the same filtering to tool summaries, streaming block replies,
and final replies:
- drop payloads that are only `HEARTBEAT_OK` with no media
- strip `HEARTBEAT_OK` at the edges when mixed with other text
## Config
```json5
{
agent: {
heartbeat: {
every: "30m", // duration string: ms|s|m|h (0m disables)
every: "30m", // default: 30m (0m disables)
model: "anthropic/claude-opus-4-5",
target: "last", // last | whatsapp | telegram | none
to: "+15551234567", // optional override for whatsapp/telegram
prompt: "HEARTBEAT" // optional override
target: "last", // last | whatsapp | telegram | discord | slack | signal | imessage | none
to: "+15551234567", // optional provider-specific override (e.g. E.164 or chat id)
prompt: "Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.",
ackMaxChars: 30 // max chars allowed after HEARTBEAT_OK
}
}
}
```
### Fields
- `every`: heartbeat interval (duration string; default unit minutes). Omit or set
to `0m` to disable.
- `every`: heartbeat interval (duration string; default unit minutes). Default:
`30m`. Set to `0m` to disable.
- `model`: optional model override for heartbeat runs (`provider/model`).
- `target`: where heartbeat output is delivered.
- `last` (default): send to the last used external channel.
- `whatsapp` / `telegram`: force the channel (optionally set `to`).
- `last` (default): send to the last used external provider.
- `whatsapp` / `telegram` / `discord` / `slack` / `signal` / `imessage`: force the provider (optionally set `to`).
- `none`: do not deliver externally; output stays in the session (WebChat-visible).
- `to`: optional recipient override (E.164 for WhatsApp, chat id for Telegram).
- `prompt`: optional override for the heartbeat body (default: `HEARTBEAT`).
- `prompt`: optional override for the heartbeat body (default shown above). Safe to
change; heartbeat acks are still keyed off `HEARTBEAT_OK`.
- `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery (default: 30).
## Cost awareness
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
agent to read it. Keep it tiny (short checklist or reminders) to avoid prompt
bloat.
## Behavior
- Runs in the main session (`main`, or `global` when scope is global).
@@ -57,6 +80,12 @@ message. If the reply is only `HEARTBEAT_OK`, it is dropped.
- If `target` resolves to no external destination (no last route or `none`), the
heartbeat still runs but no outbound message is sent.
## Ideas for use
- Check up on the user (light, respectful pings during daytime).
- Handle mundane tasks (triage inboxes, summarize queues, refresh notes).
- Nudge on open loops or reminders.
- Background monitoring (health checks, status polling, low-priority alerts).
## Wake hook
- The gateway exposes a heartbeat wake hook so cron/jobs/webhooks can request an
immediate run (`requestHeartbeatNow`).
+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)
+1 -2
View File
@@ -3,7 +3,6 @@ summary: "Image and media handling rules for send, gateway, and agent replies"
read_when:
- Modifying media pipeline or attachments
---
<!-- {% raw %} -->
# Image & Media Support — 2025-12-05
CLAWDBOT is now **web-only** (Baileys). This document captures the current media handling rules for send, gateway, and agent replies.
@@ -38,6 +37,7 @@ CLAWDBOT is now **web-only** (Baileys). This document captures the current media
- When inbound web messages include media, CLAWDBOT downloads to a temp file and exposes templating variables:
- `{{MediaUrl}}` pseudo-URL for the inbound media.
- `{{MediaPath}}` local temp path written before running the command.
- When a per-session Docker sandbox is enabled, inbound media is copied into the sandbox workspace and `MediaPath`/`MediaUrl` are rewritten to a relative path like `media/inbound/<filename>`.
- Audio transcription (if configured) runs before templating and can replace `Body` with the transcript.
## Limits & Errors
@@ -49,4 +49,3 @@ CLAWDBOT is now **web-only** (Baileys). This document captures the current media
- Cover send + reply flows for image/audio/document cases.
- Validate recompression for images (size bound) and voice-note flag for audio.
- Ensure multi-media replies fan out as sequential sends.
<!-- {% endraw %} -->
+32 -1
View File
@@ -13,6 +13,31 @@ Status: external CLI integration. No daemon.
- 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).
@@ -26,7 +51,10 @@ Status: external CLI integration. No daemon.
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",
@@ -37,6 +65,9 @@ Status: external CLI integration. No daemon.
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.
@@ -55,7 +86,7 @@ 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`.
- 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
+57 -36
View File
@@ -3,46 +3,53 @@ summary: "Top-level overview of Clawdbot, features, and purpose"
read_when:
- Introducing Clawdbot to newcomers
---
<!-- {% raw %} -->
# CLAWDBOT 🦞
> *"EXFOLIATE! EXFOLIATE!"* — A space lobster, probably
<p align="center">
<img src="whatsapp-clawd.jpg" alt="CLAWDBOT" width="420">
<img src="whatsapp-clawd.jpg" alt="CLAWDBOT" width="420" />
</p>
<p align="center">
<strong>WhatsApp + Telegram + Discord + iMessage gateway for AI agents (Pi).</strong><br>
<strong>Any OS + WhatsApp/Telegram/Discord/iMessage gateway for AI agents (Pi).</strong><br />
Send a message, get an agent response — from your pocket.
</p>
<p align="center">
<a href="https://github.com/clawdbot/clawdbot">GitHub</a> ·
<a href="https://github.com/clawdbot/clawdbot/releases">Releases</a> ·
<a href="./clawd.md">Clawd setup</a>
<a href="https://docs.clawd.bot">Docs</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).
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/getting-started
- **Guided setup (recommended):** https://docs.clawd.bot/wizard (`clawdbot onboard`)
## How it works
```
WhatsApp / Telegram / Discord
│
▼
┌──────────────────────────┐
┌───────────────────────────┐
│ Gateway │ ws://127.0.0.1:18789 (loopback-only)
│ (single source) │ tcp://0.0.0.0:18790 (Bridge)
│ │ http://<gateway-host>:18793/__clawdbot__/canvas/ (Canvas host)
│ │ http://<gateway-host>:18793
│ │ /__clawdbot__/canvas/ (Canvas host)
└───────────┬───────────────┘
│
├─ Pi agent (RPC)
├─ CLI (clawdbot …)
├─ Chat UI (SwiftUI)
├─ macOS app (Clawdbot.app)
└─ iOS node via Bridge + pairing
├─ iOS node via Bridge + pairing
└─ Android node via Bridge + pairing
```
Most operations flow through the **Gateway** (`clawdbot gateway`), a single long-running process that owns provider connections and the WebSocket control plane.
@@ -53,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` (`canvasHost`).
- **Remote use**: SSH tunnel or tailnet/VPN; see `docs/remote.md` and `docs/discovery.md`.
- **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)
@@ -63,12 +70,15 @@ Most operations flow through the **Gateway** (`clawdbot gateway`), a single long
- 🎮 **Discord Bot** — DMs + guild channels via discord.js
- 💬 **iMessage** — Local imsg CLI integration (macOS)
- 🤖 **Agent bridge** — Pi (RPC mode) with tool streaming
- 🧠 **Multi-agent routing** — Route provider accounts/peers to isolated agents (workspace + per-agent sessions)
- 🔐 **Subscription auth** — Anthropic (Claude Pro/Max) + OpenAI (ChatGPT/Codex) via OAuth
- 💬 **Sessions** — Direct chats collapse into shared `main` (default); groups are isolated
- 👥 **Group Chat Support** — Mention-based by default; owner can toggle `/activation always|mention`
- 📎 **Media Support** — Send and receive images, audio, documents
- 🎤 **Voice notes** — Optional transcription hook
- 🖥️ **WebChat + macOS app** — Local UI + menu bar companion for ops and voice wake
- 📱 **iOS node** — Pairs as a node and exposes a Canvas surface
- 📱 **Android node** — Pairs as a node and exposes Canvas + Chat + Camera
Note: legacy Claude/Codex/Gemini/Opencode paths have been removed; Pi is the only coding-agent path.
@@ -125,34 +135,45 @@ Example:
## Docs
- Start here:
- [FAQ](./faq.md) ← *common questions answered*
- [Configuration](./configuration.md)
- [Nix mode](./nix.md)
- [Clawd personal assistant setup](./clawd.md)
- [Skills](./skills.md)
- [Skills config](./skills-config.md)
- [Workspace templates](./templates/AGENTS.md)
- [RPC adapters](./rpc.md)
- [Gateway runbook](./gateway.md)
- [Nodes (iOS/Android)](./nodes.md)
- [Web surfaces (Control UI)](./web.md)
- [Discovery + transports](./discovery.md)
- [Remote access](./remote.md)
- [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/discovery)
- [Remote access](https://docs.clawd.bot/remote)
- Providers and UX:
- [WebChat](./webchat.md)
- [Control UI (browser)](./control-ui.md)
- [Telegram](./telegram.md)
- [Discord](./discord.md)
- [iMessage](./imessage.md)
- [Groups](./groups.md)
- [WhatsApp group messages](./group-messages.md)
- [Media: images](./images.md)
- [Media: audio](./audio.md)
- [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/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](./session.md)
- [Cron + wakeups](./cron.md)
- [Security](./security.md)
- [Troubleshooting](./troubleshooting.md)
- [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
@@ -161,7 +182,6 @@ Example:
---
*"We're all just playing with our own prompts."* — an AI, probably high on tokens
<!-- {% endraw %} -->
## Credits
@@ -172,6 +192,7 @@ Example:
## Core Contributors
- **Maxim Vovshin** (@Hyaxia, 36747317+Hyaxia@users.noreply.github.com) — Blogwatcher skill
- **Nacho Iacovino** (@nachoiacovino, nacho.iacovino@gmail.com) — Location parsing (Telegram + WhatsApp)
## License
+191 -55
View File
@@ -1,17 +1,182 @@
---
summary: "Plan for an iOS voice + canvas node that connects via a secure Bonjour-discovered macOS bridge"
summary: "iOS app (node): architecture + connection runbook"
read_when:
- Pairing or reconnecting the iOS node
- Debugging iOS bridge discovery or auth
- Sending screen/canvas commands to iOS
- Designing iOS node + gateway integration
- Extending the Gateway protocol for node/canvas commands
- Implementing Bonjour pairing or transport security
---
# iOS Node (internal) — Voice Trigger + Canvas
# iOS App (Node)
Status: prototype implemented (internal) · Date: 2025-12-13
Runbook (how to connect/pair + drive Canvas): `docs/ios/connect.md`
## Connection Runbook
## Goals
This is the practical “how do I connect the iOS node” guide:
**iOS app** ⇄ (Bonjour + TCP bridge) ⇄ **Gateway bridge** ⇄ (loopback WS) ⇄ **Gateway**
The Gateway WebSocket stays loopback-only (`ws://127.0.0.1:18789`). The iOS node talks to the LAN-facing **bridge** (default `tcp://0.0.0.0:18790`) and uses Gateway-owned pairing.
### Prerequisites
- You can run the Gateway on the “master” machine.
- iOS node app can reach the gateway bridge:
- Same LAN with Bonjour/mDNS, **or**
- Same Tailscale tailnet using Wide-Area Bonjour / unicast DNS-SD (see below), **or**
- Manual bridge host/port (fallback)
- You can run the CLI (`clawdbot`) on the gateway machine (or via SSH).
### 1) Start the Gateway (with bridge enabled)
Bridge is enabled by default (disable via `CLAWDBOT_BRIDGE_ENABLED=0`).
```bash
pnpm clawdbot gateway --port 18789 --verbose
```
Confirm in logs you see something like:
- `bridge listening on tcp://0.0.0.0:18790 (node)`
For tailnet-only setups (recommended for Vienna ⇄ London), bind the bridge to the gateway machine’s Tailscale IP instead:
- Set `bridge.bind: "tailnet"` in `~/.clawdbot/clawdbot.json` on the gateway host.
- Restart the Gateway / macOS menubar app.
### 2) Verify Bonjour discovery (optional but recommended)
From the gateway machine:
```bash
dns-sd -B _clawdbot-bridge._tcp local.
```
You should see your gateway advertising `_clawdbot-bridge._tcp`.
If browse works, but the iOS node can’t connect, try resolving one instance:
```bash
dns-sd -L "<instance name>" _clawdbot-bridge._tcp local.
```
More debugging notes: [`docs/bonjour.md`](https://docs.clawd.bot/bonjour).
#### Tailnet (Vienna ⇄ London) discovery via unicast DNS-SD
If the iOS node and the gateway are on different networks but connected via Tailscale, multicast mDNS won’t cross the boundary. Use Wide-Area Bonjour / unicast DNS-SD instead:
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`](https://docs.clawd.bot/bonjour).
### 3) Connect from the iOS node app
In the iOS node app:
- Pick the discovered bridge (or hit refresh).
- If not paired yet, it will initiate pairing automatically.
- After the first successful pairing, it will auto-reconnect **strictly to the last discovered gateway** on launch (including after reinstall), as long as the iOS Keychain entry is still present.
#### Connection indicator (always visible)
The Settings tab icon shows a small status dot:
- **Green**: connected to the bridge
- **Yellow**: connecting (subtle pulse)
- **Red**: not connected / error
### 4) Approve pairing (CLI)
On the gateway machine:
```bash
clawdbot nodes pending
```
Approve the request:
```bash
clawdbot nodes approve <requestId>
```
After approval, the iOS node receives/stores the token and reconnects authenticated.
Pairing details: [`docs/gateway/pairing.md`](https://docs.clawd.bot/gateway/pairing).
### 5) Verify the node is connected
- In the macOS app: **Instances** tab should show something like `iOS Node (...)` with a green “Active” presence dot shortly after connect.
- Via nodes status (paired + connected):
```bash
clawdbot nodes status
```
- Via Gateway (paired + connected):
```bash
clawdbot gateway call node.list --params "{}"
```
- Via Gateway presence (legacy-ish, still useful):
```bash
clawdbot gateway call system-presence --params "{}"
```
Look for the node `instanceId` (often a UUID).
### 6) Drive the iOS Canvas (draw / snapshot)
The iOS node runs a WKWebView “Canvas” scaffold which exposes:
- `window.__clawdbot.canvas`
- `window.__clawdbot.ctx` (2D context)
- `window.__clawdbot.setStatus(title, subtitle)`
#### Gateway Canvas Host (recommended for web content)
If you want the node to show real HTML/CSS/JS that the agent can edit on disk, point it at the Gateway canvas host.
Note: nodes always use the standalone canvas host on `canvasHost.port` (default `18793`), bound to the bridge interface.
1) Create `~/clawd/canvas/index.html` on the gateway host.
2) Navigate the node to it (LAN):
```bash
clawdbot nodes invoke --node "iOS Node" --command canvas.navigate --params '{"url":"http://<gateway-hostname>.local:18793/__clawdbot__/canvas/"}'
```
Notes:
- The server injects a live-reload client into HTML and reloads on file changes.
- A2UI is hosted on the same canvas host at `http://<gateway-host>:18793/__clawdbot__/a2ui/`.
- Tailnet (optional): if both devices are on Tailscale, use a MagicDNS name or tailnet IP instead of `.local`, e.g. `http://<gateway-magicdns>:18793/__clawdbot__/canvas/`.
- iOS may require App Transport Security allowances to load plain `http://` URLs; if it fails to load, prefer HTTPS or adjust the iOS app’s ATS config.
#### Draw with `canvas.eval`
```bash
clawdbot nodes invoke --node "iOS Node" --command canvas.eval --params "$(cat <<'JSON'
{"javaScript":"(() => { const {ctx,setStatus} = window.__clawdbot; setStatus('Drawing','…'); ctx.clearRect(0,0,innerWidth,innerHeight); ctx.lineWidth=6; ctx.strokeStyle='#ff2d55'; ctx.beginPath(); ctx.moveTo(40,40); ctx.lineTo(innerWidth-40, innerHeight-40); ctx.stroke(); setStatus(null,null); return 'ok'; })()"}
JSON
)"
```
#### Snapshot with `canvas.snapshot`
```bash
clawdbot nodes invoke --node 192.168.0.88 --command canvas.snapshot --params '{"maxWidth":900}'
```
The response includes `{ format, base64 }` image data (default `format="jpeg"`; pass `{"format":"png"}` when you specifically need lossless PNG).
### Common gotchas
- **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`](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.
## Design + Architecture
### Goals
- Build an **iOS app** that acts as a **remote node** for Clawdbot:
- **Voice trigger** (wake-word / always-listening intent) that forwards transcripts to the Gateway `agent` method.
- **Canvas** surface that the agent can control: navigate, draw/render, evaluate JS, snapshot.
@@ -28,13 +193,13 @@ Non-goals (v1):
- Supporting arbitrary third-party “plugins” on iOS.
- Perfect App Store compliance; this is **internal-only** initially.
## Current repo reality (constraints we respect)
- The Gateway WebSocket server binds to `127.0.0.1:18789` (`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`).
- macOS “Canvas” is controlled via the Gateway node protocol (`canvas.*`), matching iOS/Android (`docs/mac/canvas.md`).
### 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`](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
### Recommended topology (B): Gateway-owned Bridge + loopback Gateway
Keep the Node gateway loopback-only; expose a dedicated **gateway-owned bridge** to the LAN/tailnet.
**iOS App** ⇄ (TLS + pairing) ⇄ **Bridge (in gateway)** ⇄ (loopback) ⇄ **Gateway WS** (`ws://127.0.0.1:18789`)
@@ -44,12 +209,12 @@ Why:
- Centralizes auth, rate limiting, and allowlisting in the bridge.
- Lets us unify “canvas node” semantics across mac + iOS without exposing raw gateway methods.
## Security plan (internal, but still robust)
### Transport
### Security plan (internal, but still robust)
#### Transport
- **Current (v0):** bridge is a LAN-facing **TCP** listener with token-based auth after pairing.
- **Next:** wrap the bridge in **TLS** and prefer key-pinned or mTLS-like auth after pairing.
### Pairing
#### Pairing
- Bonjour discovery shows a candidate “Clawdbot Bridge” on the LAN.
- First connection:
1) iOS generates a keypair (Secure Enclave if available).
@@ -62,7 +227,7 @@ Why:
- Subsequent connections:
- The bridge requires the paired identity. Unpaired clients get a structured “not paired” error and no access.
#### Gateway-owned pairing (Option B details)
##### Gateway-owned pairing (Option B details)
Pairing decisions must be owned by the Gateway (`clawd` / Node) so nodes can be approved without the macOS app running.
Key idea:
@@ -72,14 +237,14 @@ 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` 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`
- `clawdbot nodes approve <requestId>`
- `clawdbot nodes reject <requestId>`
### Authorization / scope control (bridge-side ACL)
#### Authorization / scope control (bridge-side ACL)
The bridge must not be a raw proxy to every gateway method.
- Allow by default:
@@ -93,16 +258,16 @@ The bridge must not be a raw proxy to every gateway method.
- voice forwards per minute
- snapshot frequency / payload size
## Protocol unification: add “node/canvas” to Gateway protocol
### Principle
### Protocol unification: add “node/canvas” to Gateway protocol
#### Principle
Unify mac Canvas + iOS Canvas under a single conceptual surface:
- The agent talks to the Gateway using a stable method set (typed protocol).
- The Gateway routes node-targeted requests to:
- local mac Canvas implementation, or
- remote iOS node via the bridge
### Minimal protocol additions (v1)
Add to `src/gateway/protocol/schema.ts` (and regenerate Swift models):
#### Minimal protocol additions (v1)
Add to [`src/gateway/protocol/schema.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/gateway/protocol/schema.ts) (and regenerate Swift models):
**Identity**
- Node identity comes from `connect.params.client.instanceId` (stable), and `connect.params.client.mode = "node"` (or `"ios-node"`).
@@ -117,7 +282,7 @@ Add to `src/gateway/protocol/schema.ts` (and regenerate Swift models):
- `node.event` → async node status/errors
- e.g. background/foreground transitions, voice availability, canvas availability
### Node command set (canvas)
#### Node command set (canvas)
These are values for `node.invoke.command`:
- `canvas.present` / `canvas.hide`
- `canvas.navigate` with `{ url }` (loads a URL; use `""` or `"/"` to return to the default scaffold)
@@ -133,7 +298,7 @@ Result pattern:
- Request is a standard `req/res` with `ok` / `error`.
- Long operations (loads, streaming drawing, etc.) may also emit `node.event` progress.
#### Current (implemented)
##### Current (implemented)
As of 2025-12-13, the Gateway supports `node.invoke` for bridge-connected nodes.
Example: draw a diagonal line on the iOS Canvas:
@@ -199,38 +364,9 @@ open Clawdbot.xcodeproj
- Credentials:
- Keychain (paired identity + bridge trust anchor)
### macOS
- Keep current Canvas root (already implemented):
- `~/Library/Application Support/Clawdbot/canvas/<session>/...`
- Bridge state:
- No local pairing store (pairing is gateway-owned).
- Any local bridge-only state should remain private under Application Support.
## Related docs
### Gateway (node)
- Pairing (source of truth):
- `~/.clawdbot/nodes/paired.json`
- `~/.clawdbot/nodes/pending.json` (or `pending/*.json` for auditability)
## Rollout plan (phased)
1) **Bridge discovery + pairing (mac + iOS)**
- Bonjour browse + resolve
- Approve prompt on mac
- Persist pairing in Keychain/App Support
2) **Voice-only node**
- iOS voice wake toggle
- Forward transcript to Gateway `agent` via bridge
- Presence beacons via `system-event` (or node.event)
3) **Protocol additions for nodes**
- Add `node.list` / `node.invoke` / `node.event` to Gateway
- Implement bridge routing + ACLs
4) **iOS canvas**
- WKWebView canvas surface
- `canvas.navigate/eval/snapshot`
- Background fast-fail for `canvas.*`
5) **Unify mac Canvas under the same node.invoke**
- Keep existing implementation, but expose it through the unified protocol path so the agent uses one API.
## Open questions
- Should `connect.params.client.mode` be `"node"` with `platform="ios ..."` or a distinct mode `"ios-node"`? (Presence filtering currently excludes `"cli"` only.)
- Do we want a “permissions” model per node (voice only vs voice+canvas) at pairing time?
- Should loading arbitrary websites via `canvas.navigate` allow any https URL, or enforce an allowlist to reduce risk?
- [`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)
-177
View File
@@ -1,177 +0,0 @@
---
summary: "Runbook: connect/pair the iOS node to a Clawdbot Gateway and drive its Canvas"
read_when:
- Pairing or reconnecting the iOS node
- Debugging iOS bridge discovery or auth
- Sending screen/canvas commands to iOS
---
# iOS Node Connection Runbook
This is the practical “how do I connect the iOS node” guide:
**iOS app** ⇄ (Bonjour + TCP bridge) ⇄ **Gateway bridge** ⇄ (loopback WS) ⇄ **Gateway**
The Gateway WebSocket stays loopback-only (`ws://127.0.0.1:18789`). The iOS node talks to the LAN-facing **bridge** (default `tcp://0.0.0.0:18790`) and uses Gateway-owned pairing.
## Prerequisites
- You can run the Gateway on the “master” machine.
- iOS node app can reach the gateway bridge:
- Same LAN with Bonjour/mDNS, **or**
- Same Tailscale tailnet using Wide-Area Bonjour / unicast DNS-SD (see below), **or**
- Manual bridge host/port (fallback)
- You can run the CLI (`clawdbot`) on the gateway machine (or via SSH).
## 1) Start the Gateway (with bridge enabled)
Bridge is enabled by default (disable via `CLAWDBOT_BRIDGE_ENABLED=0`).
```bash
pnpm clawdbot gateway --port 18789 --verbose
```
Confirm in logs you see something like:
- `bridge listening on tcp://0.0.0.0:18790 (node)`
For tailnet-only setups (recommended for Vienna ⇄ London), bind the bridge to the gateway machine’s Tailscale IP instead:
- Set `bridge.bind: "tailnet"` in `~/.clawdbot/clawdbot.json` on the gateway host.
- Restart the Gateway / macOS menubar app.
## 2) Verify Bonjour discovery (optional but recommended)
From the gateway machine:
```bash
dns-sd -B _clawdbot-bridge._tcp local.
```
You should see your gateway advertising `_clawdbot-bridge._tcp`.
If browse works, but the iOS node can’t connect, try resolving one instance:
```bash
dns-sd -L "<instance name>" _clawdbot-bridge._tcp local.
```
More debugging notes: `docs/bonjour.md`.
### Tailnet (Vienna ⇄ London) discovery via unicast DNS-SD
If the iOS node and the gateway are on different networks but connected via Tailscale, multicast mDNS won’t cross the boundary. Use Wide-Area Bonjour / unicast DNS-SD instead:
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`.
## 3) Connect from the iOS node app
In the iOS node app:
- Pick the discovered bridge (or hit refresh).
- If not paired yet, it will initiate pairing automatically.
- After the first successful pairing, it will auto-reconnect **strictly to the last discovered gateway** on launch (including after reinstall), as long as the iOS Keychain entry is still present.
### Connection indicator (always visible)
The Settings tab icon shows a small status dot:
- **Green**: connected to the bridge
- **Yellow**: connecting (subtle pulse)
- **Red**: not connected / error
## 4) Approve pairing (CLI)
On the gateway machine:
```bash
clawdbot nodes pending
```
Approve the request:
```bash
clawdbot nodes approve <requestId>
```
After approval, the iOS node receives/stores the token and reconnects authenticated.
Pairing details: `docs/gateway/pairing.md`.
## 5) Verify the node is connected
- In the macOS app: **Instances** tab should show something like `iOS Node (...)` with a green “Active” presence dot shortly after connect.
- Via nodes status (paired + connected):
```bash
clawdbot nodes status
```
- Via Gateway (paired + connected):
```bash
clawdbot gateway call node.list --params "{}"
```
- Via Gateway presence (legacy-ish, still useful):
```bash
clawdbot gateway call system-presence --params "{}"
```
Look for the node `instanceId` (often a UUID).
## 6) Drive the iOS Canvas (draw / snapshot)
The iOS node runs a WKWebView “Canvas” scaffold which exposes:
- `window.__clawdbot.canvas`
- `window.__clawdbot.ctx` (2D context)
- `window.__clawdbot.setStatus(title, subtitle)`
### Gateway Canvas Host (recommended for web content)
If you want the node to show real HTML/CSS/JS that the agent can edit on disk, point it at the Gateway canvas host.
Note: nodes always use the standalone canvas host on `canvasHost.port` (default `18793`), bound to the bridge interface.
1) Create `~/clawd/canvas/index.html` on the gateway host.
2) Navigate the node to it (LAN):
```bash
clawdbot nodes invoke --node "iOS Node" --command canvas.navigate --params '{"url":"http://<gateway-hostname>.local:18793/__clawdbot__/canvas/"}'
```
Notes:
- The server injects a live-reload client into HTML and reloads on file changes.
- A2UI is hosted on the same canvas host at `http://<gateway-host>:18793/__clawdbot__/a2ui/`.
- Tailnet (optional): if both devices are on Tailscale, use a MagicDNS name or tailnet IP instead of `.local`, e.g. `http://<gateway-magicdns>:18793/__clawdbot__/canvas/`.
- iOS may require App Transport Security allowances to load plain `http://` URLs; if it fails to load, prefer HTTPS or adjust the iOS app’s ATS config.
### Draw with `canvas.eval`
```bash
clawdbot nodes invoke --node "iOS Node" --command canvas.eval --params "$(cat <<'JSON'
{"javaScript":"(() => { const {ctx,setStatus} = window.__clawdbot; setStatus('Drawing','…'); ctx.clearRect(0,0,innerWidth,innerHeight); ctx.lineWidth=6; ctx.strokeStyle='#ff2d55'; ctx.beginPath(); ctx.moveTo(40,40); ctx.lineTo(innerWidth-40, innerHeight-40); ctx.stroke(); setStatus(null,null); return 'ok'; })()"}
JSON
)"
```
### Snapshot with `canvas.snapshot`
```bash
clawdbot nodes invoke --node 192.168.0.88 --command canvas.snapshot --params '{"maxWidth":900}'
```
The response includes `{ format, base64 }` image data (default `format="jpeg"`; pass `{"format":"png"}` when you specifically need lossless PNG).
## Common gotchas
- **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`).
- **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.
## Related docs
- `docs/ios/spec.md` (design + architecture)
- `docs/gateway.md` (gateway runbook)
- `docs/gateway/pairing.md` (approval + storage)
- `docs/bonjour.md` (discovery debugging)
- `docs/discovery.md` (LAN vs tailnet vs SSH)
+11
View File
@@ -0,0 +1,11 @@
---
summary: "Linux app status + contribution call"
read_when:
- Looking for Linux companion app status
- Planning platform coverage or contributions
---
# Linux App
Clawdbot core is fully supported on Linux. The core is written in TypeScript, so it runs anywhere Node runs.
We do not have a Linux companion app yet. It is planned, and we would love contributions to make it happen.
+46
View File
@@ -0,0 +1,46 @@
---
summary: "Inbound provider location parsing (Telegram + WhatsApp) and context fields"
read_when:
- Adding or modifying provider location parsing
- Using location context fields in agent prompts or tools
---
# Provider location parsing
Clawdbot normalizes shared locations from chat providers into:
- human-readable text appended to the inbound body, and
- structured fields in the auto-reply context payload.
Currently supported:
- **Telegram** (location pins + venues + live locations)
- **WhatsApp** (locationMessage + liveLocationMessage)
## Text formatting
Locations are rendered as friendly lines without brackets:
- Pin:
- `📍 48.858844, 2.294351 ±12m`
- Named place:
- `📍 Eiffel Tower — Champ de Mars, Paris (48.858844, 2.294351 ±12m)`
- Live share:
- `🛰 Live location: 48.858844, 2.294351 ±12m`
If the provider includes a caption/comment, it is appended on the next line:
```
📍 48.858844, 2.294351 ±12m
Meet here
```
## Context fields
When a location is present, these fields are added to `ctx`:
- `LocationLat` (number)
- `LocationLon` (number)
- `LocationAccuracy` (number, meters; optional)
- `LocationName` (string; optional)
- `LocationAddress` (string; optional)
- `LocationSource` (`pin | place | live`)
- `LocationIsLive` (boolean)
## Provider notes
- **Telegram**: venues map to `LocationName/LocationAddress`; live locations use `live_period`.
- **WhatsApp**: `locationMessage.comment` and `liveLocationMessage.caption` are appended as the caption line.
+15 -4
View File
@@ -14,7 +14,7 @@ Clawdbot has two log “surfaces”:
## File-based logger
Clawdbot uses a file logger backed by `tslog` (`src/logging.ts`).
Clawdbot uses a file logger backed by `tslog` ([`src/logging.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/logging.ts)).
- Default rolling log file is under `/tmp/clawdbot/` (one file per day): `clawdbot-YYYY-MM-DD.log`
- The log file path and level can be configured via `~/.clawdbot/clawdbot.json`:
@@ -33,7 +33,7 @@ The file format is one JSON object per line.
## Console capture
The CLI entrypoint enables console capture (`src/index.ts` calls `enableConsoleCapture()`).
The CLI entrypoint enables console capture ([`src/index.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/index.ts) calls `enableConsoleCapture()`).
That means every `console.log/info/warn/error/debug/trace` is also written into the file logs,
while still behaving normally on stdout/stderr.
@@ -42,6 +42,17 @@ You can tune console verbosity independently via:
- `logging.consoleLevel` (default `info`)
- `logging.consoleStyle` (`pretty` | `compact` | `json`)
## Tool summary redaction
Verbose tool summaries (e.g. `🛠️ bash: ...`) can mask sensitive tokens before they hit the
console stream. This is **tools-only** and does not alter file logs.
- `logging.redactSensitive`: `off` | `tools` (default: `tools`)
- `logging.redactPatterns`: array of regex strings (overrides defaults)
- Use raw regex strings (auto `gi`), or `/pattern/flags` if you need custom flags.
- Matches are masked by keeping the first 6 + last 4 chars (length >= 18), otherwise `***`.
- Defaults cover common key assignments, CLI flags, JSON fields, bearer headers, PEM blocks, and popular token prefixes.
## Gateway WebSocket logs
The gateway prints WebSocket protocol logs in two modes:
@@ -78,8 +89,8 @@ clawdbot gateway --verbose --ws-log full
Clawdbot formats console logs via a small wrapper on top of the existing stack:
- **tslog** for structured file logs (`src/logging.ts`)
- **chalk** for colors (`src/globals.ts`)
- **tslog** for structured file logs ([`src/logging.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/logging.ts))
- **chalk** for colors ([`src/globals.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/globals.ts))
The console formatter is **TTY-aware** and prints consistent, prefixed lines.
Subsystem loggers are created via `createSubsystemLogger("gateway")`.
+11 -11
View File
@@ -15,7 +15,7 @@ Goal: ship **Clawdbot.app** with a self-contained relay binary that can run both
App bundle layout:
- `Clawdbot.app/Contents/Resources/Relay/clawdbot`
- bun `--compile` relay executable built from `dist/macos/relay.js`
- bun `--compile` relay executable built from [`dist/macos/relay.js`](https://github.com/clawdbot/clawdbot/blob/main/dist/macos/relay.js)
- Supports:
- `clawdbot …` (CLI)
- `clawdbot gateway-daemon …` (LaunchAgent daemon)
@@ -31,7 +31,7 @@ Why the sidecar files matter:
## Build pipeline
Packaging script:
- `scripts/package-mac-app.sh`
- [`scripts/package-mac-app.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/package-mac-app.sh)
It builds:
- TS: `pnpm exec tsc`
@@ -47,7 +47,7 @@ Important bundler flags:
Version injection:
- `--define "__CLAWDBOT_VERSION__=\"<pkg version>\""`
- `src/version.ts` also supports `__CLAWDBOT_VERSION__` (and `CLAWDBOT_BUNDLED_VERSION`) so `--version` doesn’t depend on reading `package.json` at runtime.
- [`src/version.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/version.ts) also supports `__CLAWDBOT_VERSION__` (and `CLAWDBOT_BUNDLED_VERSION`) so `--version` doesn’t depend on reading `package.json` at runtime.
## Launchd (Gateway as LaunchAgent)
@@ -58,7 +58,7 @@ Plist location (per-user):
- `~/Library/LaunchAgents/com.clawdbot.gateway.plist`
Manager:
- `apps/macos/Sources/Clawdbot/GatewayLaunchAgentManager.swift`
- [`apps/macos/Sources/Clawdbot/GatewayLaunchAgentManager.swift`](https://github.com/clawdbot/clawdbot/blob/main/apps/macos/Sources/Clawdbot/GatewayLaunchAgentManager.swift)
Behavior:
- “Clawdbot Active” enables/disables the LaunchAgent.
@@ -77,7 +77,7 @@ Symptom (when mis-signed):
Fix:
- The bun executable needs JIT-ish permissions under hardened runtime.
- `scripts/codesign-mac-app.sh` signs `Relay/clawdbot` with:
- [`scripts/codesign-mac-app.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/codesign-mac-app.sh) signs `Relay/clawdbot` with:
- `com.apple.security.cs.allow-jit`
- `com.apple.security.cs.allow-unsigned-executable-memory`
@@ -87,17 +87,17 @@ Problem:
- bun can’t load some native Node addons like `sharp` (and we don’t want to ship native addon trees for the gateway).
Solution:
- Central helper `src/media/image-ops.ts`
- Central helper [`src/media/image-ops.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/media/image-ops.ts)
- Prefers `/usr/bin/sips` on macOS (esp. when running under bun)
- Falls back to `sharp` when available (Node/dev)
- Used by:
- `src/web/media.ts` (optimize inbound/outbound images)
- `src/browser/screenshot.ts`
- `src/agents/pi-tools.ts` (image sanitization)
- [`src/web/media.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/web/media.ts) (optimize inbound/outbound images)
- [`src/browser/screenshot.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/browser/screenshot.ts)
- [`src/agents/pi-tools.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/agents/pi-tools.ts) (image sanitization)
## Browser control server
The Gateway starts the browser control server (loopback only) from `src/gateway/server.ts`.
The Gateway starts the browser control server (loopback only) from [`src/gateway/server.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/gateway/server.ts).
It’s started from the relay daemon process, so the relay binary includes Playwright deps.
## Tests / smoke checks
@@ -125,7 +125,7 @@ Bun may leave dotfiles like `*.bun-build` in the repo root or subfolders.
## DMG styling (human installer)
`scripts/create-dmg.sh` styles the DMG via Finder AppleScript.
[`scripts/create-dmg.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/create-dmg.sh) styles the DMG via Finder AppleScript.
Rules of thumb:
- Use a **72dpi** background image that matches the Finder window size in points.
+2 -2
View File
@@ -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`.
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/clawdbot-mac.md`.
- 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)
+17 -2
View File
@@ -57,11 +57,26 @@ The macOS app requires a symlink named `clawdbot` in `/usr/local/bin` or `/opt/h
Alternatively, you can manually link it from your Admin account:
```bash
sudo ln -sf "/Users/$(whoami)/clawdbot/dist/Clawdbot.app/Contents/Resources/Relay/clawdbot" /usr/local/bin/clawdbot
sudo ln -sf "/Users/$(whoami)/Projects/clawdbot/dist/Clawdbot.app/Contents/Resources/Relay/clawdbot" /usr/local/bin/clawdbot
```
## Troubleshooting
### Build Fails: Toolchain or SDK Mismatch
The macOS app build expects the latest macOS SDK and Swift 6.2 toolchain.
**System dependencies (required):**
- **Latest macOS version available in Software Update** (required by Xcode 26.2 SDKs)
- **Xcode 26.2** (Swift 6.2 toolchain)
**Checks:**
```bash
xcodebuild -version
xcrun swift --version
```
If versions don’t match, update macOS/Xcode and re-run the build.
### App Crashes on Permission Grant
If the app crashes when you try to allow **Speech Recognition** or **Microphone** access, it may be due to a corrupted TCC cache or signature mismatch.
@@ -70,7 +85,7 @@ If the app crashes when you try to allow **Speech Recognition** or **Microphone*
```bash
tccutil reset All com.clawdbot.mac.debug
```
2. If that fails, change the `BUNDLE_ID` temporarily in `scripts/package-mac-app.sh` to force a "clean slate" from macOS.
2. If that fails, change the `BUNDLE_ID` temporarily in [`scripts/package-mac-app.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/package-mac-app.sh) to force a "clean slate" from macOS.
### Gateway "Starting..." indefinitely
If the gateway status stays on "Starting...", check if a zombie process is holding the port:
+1 -1
View File
@@ -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` (`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`.
Loaded 100 of 545 files, more files were not shown because too many files have changed in this diff. Show more