Compare commits

..
Author SHA1 Message Date
Peter Steinberger 0b5f672353 docs(changelog): mention Dockerfile Bun install (#284) 2026-01-06 15:02:27 +01:00
Peter Steinberger 9c13819f39 Merge remote-tracking branch 'origin/main' into fix/dockerfile-install-bun 2026-01-06 15:00:54 +01:00
Peter Steinberger dbac51e60f chore(protocol): regenerate GatewayModels.swift 2026-01-06 09:55:58 +01: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
Lukáš LoukotaandClaude Opus 4.5 971153f4de fix: install Bun in Dockerfile for build scripts
The build script was updated to use Bun (`bun scripts/canvas-a2ui-copy.ts`)
but the Dockerfile was not updated to install Bun, causing Docker builds to fail
with "bun: not found".

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 09:40:27 +01: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
362 changed files with 20360 additions and 4423 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/
+10 -2
View File
@@ -7,7 +7,9 @@
## Build, Test, and Development Commands
- Install deps: `pnpm install`
- Run CLI in dev: `pnpm clawdbot ...` (tsx entry) or `pnpm dev` for `src/index.ts`.
- 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 +32,10 @@
- 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.
- When working on a PR: add a changelog entry with the PR ID 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.
- When merging a PR from a new contributor: add their avatar to the README “Thanks to all clawtributors” thumbnail list.
## Security & Configuration Tips
- Web provider stores creds at `~/.clawdbot/credentials/`; rerun `clawdbot login` if logged out.
@@ -45,13 +51,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.
+114 -1
View File
@@ -4,8 +4,114 @@
## Unreleased
### Breaking
- 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
- 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: 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.
- 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`).
- 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: 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: 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).
- 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.
- 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: track compaction count in session status; verbose mode announces auto-compactions.
- 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.
### Maintenance
- 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
@@ -14,6 +120,7 @@
- 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.
@@ -24,6 +131,7 @@
- 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).
@@ -31,11 +139,16 @@
- 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 (`pnpm ui:build`).
- 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).
+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
+285 -161
View File
@@ -16,191 +16,246 @@
</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 surfaces 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) · Nix: [nix-clawdbot](https://github.com/clawdbot/nix-clawdbot) · Docker: [`docs/docker.md`](docs/docker.md) · Discord: https://discord.gg/clawd
[Website](https://clawdbot.com) · [Docs](https://github.com/clawdbot/clawdbot/blob/main/docs/index.md) · Showcase: [https://github.com/clawdbot/clawdbot/blob/main/docs/showcase.md](https://github.com/clawdbot/clawdbot/blob/main/docs/showcase.md) · FAQ: [https://github.com/clawdbot/clawdbot/blob/main/docs/faq.md](https://github.com/clawdbot/clawdbot/blob/main/docs/faq.md) · Wizard: [https://github.com/clawdbot/clawdbot/blob/main/docs/wizard.md](https://github.com/clawdbot/clawdbot/blob/main/docs/wizard.md) · Nix: [https://github.com/clawdbot/nix-clawdbot](https://github.com/clawdbot/nix-clawdbot) · Docker: [https://github.com/clawdbot/clawdbot/blob/main/docs/docker.md](https://github.com/clawdbot/clawdbot/blob/main/docs/docker.md) · 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.
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://github.com/clawdbot/clawdbot/blob/main/docs/onboarding.md).
## Recommended setup (from source)
Do **not** download prebuilt binaries. Build from source.
```bash
# Clone this repo
git clone https://github.com/clawdbot/clawdbot.git
cd clawdbot
bun install
bun run build
bun run ui:build
bun run clawdbot onboard
```
## Quick start (from source)
Runtime: **Node ≥22**.
From source, **pnpm** is the default workflow. Bun is supported as an optional local workflow; see [`docs/bun.md`](docs/bun.md).
```bash
# Install deps (no Bun lockfile)
bun install --no-save
# Build TypeScript
bun run build
# Build Control UI
bun install --cwd ui --no-save
bun run --cwd ui build
# Recommended: run the onboarding wizard
bun run clawdbot onboard
# Link WhatsApp (stores creds in ~/.clawdbot/credentials)
bun run clawdbot login
# Start the gateway
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? `clawdbot doctor`.
If you run from source, prefer `bun run clawdbot …` or `pnpm clawdbot …` (not global `clawdbot`).
## 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://github.com/clawdbot/clawdbot/blob/main/docs/gateway.md)** — single control plane for sessions, providers, tools, and events.
- **[Multi-surface inbox](https://github.com/clawdbot/clawdbot/blob/main/docs/surface.md)** — WhatsApp, Telegram, Slack, Discord, Signal, iMessage, WebChat, macOS, iOS/Android.
- **[Voice Wake](https://github.com/clawdbot/clawdbot/blob/main/docs/voicewake.md) + [Talk Mode](https://github.com/clawdbot/clawdbot/blob/main/docs/talk.md)** — always-on speech for macOS/iOS/Android with ElevenLabs.
- **[Live Canvas](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/canvas.md)** — agent-driven visual workspace with [A2UI](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/canvas.md#canvas-a2ui).
- **[First-class tools](https://github.com/clawdbot/clawdbot/blob/main/docs/tools.md)** — browser, canvas, nodes, cron, sessions, and Discord/Slack actions.
- **[Companion apps](https://github.com/clawdbot/clawdbot/blob/main/docs/macos.md)** — macOS menu bar app + iOS/Android [nodes](https://github.com/clawdbot/clawdbot/blob/main/docs/nodes.md).
- **[Onboarding](https://github.com/clawdbot/clawdbot/blob/main/docs/wizard.md) + [skills](https://github.com/clawdbot/clawdbot/blob/main/docs/skills.md)** — 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://github.com/clawdbot/clawdbot/blob/main/docs/gateway.md) with sessions, presence, config, cron, webhooks, [Control UI](https://github.com/clawdbot/clawdbot/blob/main/docs/web.md), and [Canvas host](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/canvas.md#canvas-a2ui).
- [CLI surface](https://github.com/clawdbot/clawdbot/blob/main/docs/agent-send.md): gateway, agent, send, [wizard](https://github.com/clawdbot/clawdbot/blob/main/docs/wizard.md), and [doctor](https://github.com/clawdbot/clawdbot/blob/main/docs/doctor.md).
- [Pi agent runtime](https://github.com/clawdbot/clawdbot/blob/main/docs/agent.md) in RPC mode with tool streaming and block streaming.
- [Session model](https://github.com/clawdbot/clawdbot/blob/main/docs/session.md): `main` for direct chats, group isolation, activation modes, queue modes, reply-back. Group rules: [Groups](https://github.com/clawdbot/clawdbot/blob/main/docs/groups.md).
- [Media pipeline](https://github.com/clawdbot/clawdbot/blob/main/docs/images.md): images/audio/video, transcription hooks, size caps, temp file lifecycle. Audio details: [Audio](https://github.com/clawdbot/clawdbot/blob/main/docs/audio.md).
### 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](https://github.com/clawdbot/clawdbot/blob/main/docs/surface.md): [WhatsApp](https://github.com/clawdbot/clawdbot/blob/main/docs/whatsapp.md) (Baileys), [Telegram](https://github.com/clawdbot/clawdbot/blob/main/docs/telegram.md) (grammY), [Slack](https://github.com/clawdbot/clawdbot/blob/main/docs/slack.md) (Bolt), [Discord](https://github.com/clawdbot/clawdbot/blob/main/docs/discord.md) (discord.js), [Signal](https://github.com/clawdbot/clawdbot/blob/main/docs/signal.md) (signal-cli), [iMessage](https://github.com/clawdbot/clawdbot/blob/main/docs/imessage.md) (imsg), [WebChat](https://github.com/clawdbot/clawdbot/blob/main/docs/webchat.md).
- [Group routing](https://github.com/clawdbot/clawdbot/blob/main/docs/group-messages.md): mention gating, reply tags, per-surface chunking and routing. Surface rules: [Surface routing](https://github.com/clawdbot/clawdbot/blob/main/docs/surface.md).
### 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://github.com/clawdbot/clawdbot/blob/main/docs/macos.md): menu bar control plane, [Voice Wake](https://github.com/clawdbot/clawdbot/blob/main/docs/voicewake.md)/PTT, [Talk Mode](https://github.com/clawdbot/clawdbot/blob/main/docs/talk.md) overlay, [WebChat](https://github.com/clawdbot/clawdbot/blob/main/docs/webchat.md), debug tools, [remote gateway](https://github.com/clawdbot/clawdbot/blob/main/docs/remote.md) control.
- [iOS node](https://github.com/clawdbot/clawdbot/blob/main/docs/ios.md): [Canvas](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/canvas.md), [Voice Wake](https://github.com/clawdbot/clawdbot/blob/main/docs/voicewake.md), [Talk Mode](https://github.com/clawdbot/clawdbot/blob/main/docs/talk.md), camera, screen recording, Bonjour pairing.
- [Android node](https://github.com/clawdbot/clawdbot/blob/main/docs/android.md): [Canvas](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/canvas.md), [Talk Mode](https://github.com/clawdbot/clawdbot/blob/main/docs/talk.md), camera, screen recording, optional SMS.
- [macOS node mode](https://github.com/clawdbot/clawdbot/blob/main/docs/nodes.md): 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://github.com/clawdbot/clawdbot/blob/main/docs/browser.md): dedicated clawd Chrome/Chromium, snapshots, actions, uploads, profiles.
- [Canvas](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/canvas.md): [A2UI](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/canvas.md#canvas-a2ui) push/reset, eval, snapshot.
- [Nodes](https://github.com/clawdbot/clawdbot/blob/main/docs/nodes.md): camera snap/clip, screen record, [location.get](https://github.com/clawdbot/clawdbot/blob/main/docs/location-command.md), notifications.
- [Cron + wakeups](https://github.com/clawdbot/clawdbot/blob/main/docs/cron.md); [webhooks](https://github.com/clawdbot/clawdbot/blob/main/docs/webhook.md); [Gmail Pub/Sub](https://github.com/clawdbot/clawdbot/blob/main/docs/gmail-pubsub.md).
- [Skills platform](https://github.com/clawdbot/clawdbot/blob/main/docs/skills.md): 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.
## Changes since 2026.1.4 (2026-01-04)
### 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).
### 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.
### 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.
- [Control UI](https://github.com/clawdbot/clawdbot/blob/main/docs/web.md) + [WebChat](https://github.com/clawdbot/clawdbot/blob/main/docs/webchat.md) served directly from the Gateway.
- [Tailscale Serve/Funnel](https://github.com/clawdbot/clawdbot/blob/main/docs/tailscale.md) or [SSH tunnels](https://github.com/clawdbot/clawdbot/blob/main/docs/remote.md) with token/password auth.
- [Nix mode](https://github.com/clawdbot/clawdbot/blob/main/docs/nix.md) for declarative config; [Docker](https://github.com/clawdbot/clawdbot/blob/main/docs/docker.md)-based installs.
- [Doctor](https://github.com/clawdbot/clawdbot/blob/main/docs/doctor.md) migrations, [logging](https://github.com/clawdbot/clawdbot/blob/main/docs/logging.md).
## 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
```
## Key subsystems
- **[Gateway WebSocket network](https://github.com/clawdbot/clawdbot/blob/main/docs/architecture.md)** — single WS control plane for clients, tools, and events (plus ops: [Gateway runbook](https://github.com/clawdbot/clawdbot/blob/main/docs/gateway.md)).
- **[Tailscale exposure](https://github.com/clawdbot/clawdbot/blob/main/docs/tailscale.md)** — Serve/Funnel for the Gateway dashboard + WS (remote access: [Remote](https://github.com/clawdbot/clawdbot/blob/main/docs/remote.md)).
- **[Browser control](https://github.com/clawdbot/clawdbot/blob/main/docs/browser.md)** — clawd‑managed Chrome/Chromium with CDP control.
- **[Canvas + A2UI](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/canvas.md)** — agent‑driven visual workspace (A2UI host: [Canvas/A2UI](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/canvas.md#canvas-a2ui)).
- **[Voice Wake](https://github.com/clawdbot/clawdbot/blob/main/docs/voicewake.md) + [Talk Mode](https://github.com/clawdbot/clawdbot/blob/main/docs/talk.md)** — always‑on speech and continuous conversation.
- **[Nodes](https://github.com/clawdbot/clawdbot/blob/main/docs/nodes.md)** — Canvas, camera snap/clip, screen record, `location.get`, notifications, plus macOS‑only `system.run`/`system.notify`.
## Tailscale access (Gateway dashboard)
Clawdbot can auto-configure Tailscale **Serve** (tailnet-only) or **Funnel** (public) while the Gateway stays bound to loopback. Configure `gateway.tailscale.mode`:
- `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).
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.
Details: [Tailscale guide](https://github.com/clawdbot/clawdbot/blob/main/docs/tailscale.md) · [Web surfaces](https://github.com/clawdbot/clawdbot/blob/main/docs/web.md)
## Remote Gateway (Linux is great)
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.
- **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://github.com/clawdbot/clawdbot/blob/main/docs/remote.md) · [Nodes](https://github.com/clawdbot/clawdbot/blob/main/docs/nodes.md) · [Security](https://github.com/clawdbot/clawdbot/blob/main/docs/security.md)
## 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://github.com/clawdbot/clawdbot/blob/main/docs/nodes.md) · [macOS app](https://github.com/clawdbot/clawdbot/blob/main/docs/macos.md) · [Gateway protocol](https://github.com/clawdbot/clawdbot/blob/main/docs/architecture.md)
## 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://github.com/clawdbot/clawdbot/blob/main/docs/session-tool.md)
## 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
## Quick start (from source)
Runtime: **Node ≥22** + **pnpm**.
```bash
pnpm install
pnpm build
pnpm ui:build
# Recommended: run the onboarding wizard
pnpm clawdbot onboard
# Link WhatsApp (stores creds in ~/.clawdbot/credentials)
pnpm clawdbot login
# Start the gateway
pnpm clawdbot gateway --port 18789 --verbose
# Dev loop (auto-reload on TS changes)
pnpm gateway:watch
# Send a message
pnpm clawdbot send --to +1234567890 --message "Hello from Clawdbot"
# Talk to the assistant (optionally deliver back to WhatsApp/Telegram/Discord)
pnpm clawdbot agent --message "Ship checklist" --thinking high
```
If you run from source, prefer `pnpm clawdbot …` (not global `clawdbot`).
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://github.com/clawdbot/clawdbot/blob/main/docs/ios.md).
### 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://github.com/clawdbot/clawdbot/blob/main/docs/android.md).
## Agent workspace + skills
@@ -210,44 +265,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"
}
}
```
Env vars: loaded from `.env` in the current working directory, plus a global fallback at `~/.clawdbot/.env` (aka `$CLAWDBOT_STATE_DIR/.env`) without overriding existing values.
[Full configuration reference (all keys + examples).](https://github.com/clawdbot/clawdbot/blob/main/docs/configuration.md)
Optional: import missing keys from your login shell env (sources your shell profile) via config or env var:
## Security model (important)
```json5
{
env: {
shellEnv: {
enabled: true,
timeoutMs: 15000
}
}
}
```
- **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`; denylist `browser`, `canvas`, `nodes`, `cron`, `discord`, `gateway`.
- Env var: `CLAWDBOT_LOAD_SHELL_ENV=1`
- Timeout override: `CLAWDBOT_SHELL_ENV_TIMEOUT_MS=15000`
- Behavior: only imports known/expected keys, never overrides existing `process.env`.
Details: [Security guide](https://github.com/clawdbot/clawdbot/blob/main/docs/security.md) · [Docker + sandboxing](https://github.com/clawdbot/clawdbot/blob/main/docs/docker.md) · [Sandbox config](https://github.com/clawdbot/clawdbot/blob/main/docs/configuration.md)
### WhatsApp
### [WhatsApp](https://github.com/clawdbot/clawdbot/blob/main/docs/whatsapp.md)
- 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://github.com/clawdbot/clawdbot/blob/main/docs/telegram.md)
- 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
{
@@ -257,7 +304,11 @@ Optional: import missing keys from your login shell env (sources your shell prof
}
```
### Discord
### [Slack](https://github.com/clawdbot/clawdbot/blob/main/docs/slack.md)
- Set `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` (or `slack.botToken` + `slack.appToken`).
### [Discord](https://github.com/clawdbot/clawdbot/blob/main/docs/discord.md)
- Set `DISCORD_BOT_TOKEN` or `discord.token` (env wins).
- Optional: set `discord.slashCommand`, `discord.dm.allowFrom`, `discord.guilds`, or `discord.mediaMaxMb` as needed.
@@ -270,6 +321,19 @@ Optional: import missing keys from your login shell env (sources your shell prof
}
```
### [Signal](https://github.com/clawdbot/clawdbot/blob/main/docs/signal.md)
- Requires `signal-cli` and a `signal` config section.
### [iMessage](https://github.com/clawdbot/clawdbot/blob/main/docs/imessage.md)
- macOS only; Messages must be signed in.
- If `imessage.groups` is set, it becomes a group allowlist; include `"*"` to allow all.
### [WebChat](https://github.com/clawdbot/clawdbot/blob/main/docs/webchat.md)
- Uses the Gateway WebSocket; no separate WebChat port/config.
Browser control (optional):
```json5
@@ -284,39 +348,99 @@ 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://github.com/clawdbot/clawdbot/blob/main/docs/index.md)
- [Read the architecture overview for the gateway + protocol model.](https://github.com/clawdbot/clawdbot/blob/main/docs/architecture.md)
- [Use the full configuration reference when you need every key and example.](https://github.com/clawdbot/clawdbot/blob/main/docs/configuration.md)
- [Run the Gateway by the book with the operational runbook.](https://github.com/clawdbot/clawdbot/blob/main/docs/gateway.md)
- [Learn how the Control UI/Web surfaces work and how to expose them safely.](https://github.com/clawdbot/clawdbot/blob/main/docs/web.md)
- [Understand remote access over SSH tunnels or tailnets.](https://github.com/clawdbot/clawdbot/blob/main/docs/remote.md)
- [Follow the onboarding wizard flow for a guided setup.](https://github.com/clawdbot/clawdbot/blob/main/docs/wizard.md)
- [Wire external triggers via the webhook surface.](https://github.com/clawdbot/clawdbot/blob/main/docs/webhook.md)
- [Set up Gmail Pub/Sub triggers.](https://github.com/clawdbot/clawdbot/blob/main/docs/gmail-pubsub.md)
- [Learn the macOS menu bar companion details.](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/menu-bar.md)
- [Platform guides: Windows](https://github.com/clawdbot/clawdbot/blob/main/docs/windows.md), [Linux](https://github.com/clawdbot/clawdbot/blob/main/docs/linux.md), [macOS](https://github.com/clawdbot/clawdbot/blob/main/docs/macos.md), [iOS](https://github.com/clawdbot/clawdbot/blob/main/docs/ios.md), [Android](https://github.com/clawdbot/clawdbot/blob/main/docs/android.md)
- [Debug common failures with the troubleshooting guide.](https://github.com/clawdbot/clawdbot/blob/main/docs/troubleshooting.md)
- [Review security guidance before exposing anything.](https://github.com/clawdbot/clawdbot/blob/main/docs/security.md)
## Advanced docs (discovery + control)
- [Discovery + transports](https://github.com/clawdbot/clawdbot/blob/main/docs/discovery.md)
- [Bonjour/mDNS](https://github.com/clawdbot/clawdbot/blob/main/docs/bonjour.md)
- [Gateway pairing](https://github.com/clawdbot/clawdbot/blob/main/docs/gateway/pairing.md)
- [Remote gateway README](https://github.com/clawdbot/clawdbot/blob/main/docs/remote-gateway-readme.md)
- [Control UI](https://github.com/clawdbot/clawdbot/blob/main/docs/control-ui.md)
- [Dashboard](https://github.com/clawdbot/clawdbot/blob/main/docs/dashboard.md)
## Operations & troubleshooting
- [Health checks](https://github.com/clawdbot/clawdbot/blob/main/docs/health.md)
- [Gateway lock](https://github.com/clawdbot/clawdbot/blob/main/docs/gateway-lock.md)
- [Background process](https://github.com/clawdbot/clawdbot/blob/main/docs/background-process.md)
- [Browser troubleshooting (Linux)](https://github.com/clawdbot/clawdbot/blob/main/docs/browser-linux-troubleshooting.md)
- [Logging](https://github.com/clawdbot/clawdbot/blob/main/docs/logging.md)
## Deep dives
- [Agent loop](https://github.com/clawdbot/clawdbot/blob/main/docs/agent-loop.md)
- [Presence](https://github.com/clawdbot/clawdbot/blob/main/docs/presence.md)
- [TypeBox schemas](https://github.com/clawdbot/clawdbot/blob/main/docs/typebox.md)
- [RPC adapters](https://github.com/clawdbot/clawdbot/blob/main/docs/rpc.md)
- [Queue](https://github.com/clawdbot/clawdbot/blob/main/docs/queue.md)
## Workspace & skills
- [Skills config](https://github.com/clawdbot/clawdbot/blob/main/docs/skills-config.md)
- [Default AGENTS](https://github.com/clawdbot/clawdbot/blob/main/docs/AGENTS.default.md)
- [Templates: AGENTS](https://github.com/clawdbot/clawdbot/blob/main/docs/templates/AGENTS.md)
- [Templates: BOOTSTRAP](https://github.com/clawdbot/clawdbot/blob/main/docs/templates/BOOTSTRAP.md)
- [Templates: IDENTITY](https://github.com/clawdbot/clawdbot/blob/main/docs/templates/IDENTITY.md)
- [Templates: SOUL](https://github.com/clawdbot/clawdbot/blob/main/docs/templates/SOUL.md)
- [Templates: TOOLS](https://github.com/clawdbot/clawdbot/blob/main/docs/templates/TOOLS.md)
- [Templates: USER](https://github.com/clawdbot/clawdbot/blob/main/docs/templates/USER.md)
## Platform internals
- [macOS dev setup](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/dev-setup.md)
- [macOS menu bar](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/menu-bar.md)
- [macOS voice wake](https://github.com/clawdbot/clawdbot/blob/main/docs/mac/voicewake.md)
- [iOS node](https://github.com/clawdbot/clawdbot/blob/main/docs/ios.md)
- [Android node](https://github.com/clawdbot/clawdbot/blob/main/docs/android.md)
- [Windows app](https://github.com/clawdbot/clawdbot/blob/main/docs/windows.md)
- [Linux app](https://github.com/clawdbot/clawdbot/blob/main/docs/linux.md)
## Email hooks (Gmail)
[Gmail Pub/Sub wiring (gcloud + gogcli), hook tokens, and auto-watch behavior are documented here.](https://github.com/clawdbot/clawdbot/blob/main/docs/gmail-pubsub.md)
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>
</p>
+34 -50
View File
@@ -3,69 +3,53 @@
<channel>
<title>Clawdis</title>
<item>
<title>2026.1.5</title>
<pubDate>Mon, 05 Jan 2026 03:00:34 +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>3085</sparkle:version>
<sparkle:shortVersionString>2026.1.5</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 2026.1.5</h2>
<h3>Highlights</h3>
<ul>
<li>Models: add image-specific model config (<code>agent.imageModel</code> + fallbacks) and scan support.</li>
<li>Agent tools: new <code>image</code> tool routed to the image model (when configured).</li>
<li>Config: default model shorthands (<code>opus</code>, <code>sonnet</code>, <code>gpt</code>, <code>gpt-mini</code>, <code>gemini</code>, <code>gemini-flash</code>).</li>
<li>Docs: document built-in model shorthands + precedence (user config wins).</li>
</ul>
<description><![CDATA[<h2>Clawdbot 2026.1.5-3</h2>
<h3>Fixes</h3>
<ul>
<li>Control UI: render Markdown in tool result cards.</li>
<li>Control UI: prevent overlapping action buttons in Discord guild rules on narrow layouts.</li>
<li>Android: tapping the foreground service notification brings the app to the front. (#179) — thanks @Syhids</li>
<li>Cron tool uses <code>id</code> for update/remove/run/runs (aligns with gateway params). (#180) — thanks @adamgall</li>
<li>Control UI: chat view uses page scroll with sticky header/sidebar and fixed composer (no inner scroll frame).</li>
<li>macOS: treat location permission as always-only to avoid iOS-only enums. (#165) — thanks @Nachx639</li>
<li>macOS: make generated gateway protocol models <code>Sendable</code> for Swift 6 strict concurrency. (#195) — thanks @andranik-sahakyan</li>
<li>macOS: bundle QR code renderer modules so DMG gateway boot doesn't crash on missing qrcode-terminal vendor files.</li>
<li>WhatsApp: suppress typing indicator during heartbeat background tasks. (#190) — thanks @mcinteerj</li>
<li>WhatsApp: mark offline history sync messages as read without auto-reply. (#193) — thanks @mcinteerj</li>
<li>Discord: avoid duplicate replies when a provider emits late streaming <code>text_end</code> events (OpenAI/GPT).</li>
<li>CLI: use tailnet IP for local gateway calls when bind is tailnet/auto (fixes #176).</li>
<li>Env: load global <code>$CLAWDBOT_STATE_DIR/.env</code> (<code>~/.clawdbot/.env</code>) as a fallback after CWD <code>.env</code>.</li>
<li>Env: optional login-shell env fallback (opt-in; imports expected keys without overriding existing env).</li>
<li>Agent tools: OpenAI-compatible tool JSON Schemas (fix <code>browser</code>, normalize union schemas).</li>
<li>Onboarding: when running from source, auto-build missing Control UI assets (<code>pnpm ui:build</code>).</li>
<li>Discord/Slack: route reaction + system notifications to the correct session (no main-session bleed).</li>
<li>Agent tools: honor <code>agent.tools</code> allow/deny policy even when sandbox is off.</li>
<li>Discord: avoid duplicate replies when OpenAI emits repeated <code>message_end</code> events.</li>
<li>Commands: unify /status (inline) and command auth across providers; group bypass for authorized control commands; remove Discord /clawd slash handler.</li>
<li>Control UI: render Markdown in chat messages (sanitized).</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/v2026.1.5/Clawdbot-2026.1.5.zip" length="150249505" type="application/octet-stream" sparkle:edSignature="1ogyZdbfVm1YSFOpcbSU6G++JnXjqAenXWavRc6szzgdc0hQYyR4/fLRj0DkJPgqQW8imWAPGoJnmhgXGjQcAg=="/>
<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>2.0.0-beta2</title>
<pubDate>Sun, 21 Dec 2025 12:37:47 +0100</pubDate>
<link>https://raw.githubusercontent.com/steipete/clawdis/main/appcast.xml</link>
<sparkle:version>2024</sparkle:version>
<sparkle:shortVersionString>2.0.0-beta2</sparkle:shortVersionString>
<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>
<enclosure url="https://github.com/steipete/clawdis/releases/download/v2.0.0-beta3/Clawdis-2.0.0-beta2.zip" length="72075472" type="application/octet-stream" sparkle:edSignature="EDk0Iwu290psw1q84MpkaAoskq9eOrWyO4rMp7PPQNqW+OYvMCQSv0xgW1HQ5q15zIqT09/9mOve1RgHlEMSAQ=="/>
<sparkle:deltas>
<enclosure url="https://github.com/steipete/clawdis/releases/download/v2.0.0-beta3/Clawdis2024-2.0.0-beta3.delta" sparkle:deltaFrom="2.0.0-beta3" length="9166434" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="860528" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,uk" sparkle:edSignature="2QuKayKEtBAhG+ObjovPjhPvGDfEEDW25ing2OWC8uVMhPhyr2Ndec58xxJqx5+wZzlhyroCP8nEQN6baa7dDA=="/>
<enclosure url="https://github.com/steipete/clawdis/releases/download/v2.0.0-beta3/Clawdis2024-2.0.0-beta2.delta" sparkle:deltaFrom="2.0.0-beta2" length="7291386" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="860528" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,uk" sparkle:edSignature="IBisZ9lU/8vQQ/EDbHk1kzYrYnrigkSVkrQsMBdOnSx+EkBCuTGGvQMf+LbHAbfPNdg674Wbt7oizcvTyVWwDQ=="/>
</sparkle:deltas>
<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>2.0.0-beta2</title>
<pubDate>Sun, 21 Dec 2025 04:04:20 +0100</pubDate>
<link>https://raw.githubusercontent.com/steipete/clawdis/main/appcast.xml</link>
<sparkle:version>2.0.0-beta2</sparkle:version>
<sparkle:shortVersionString>2.0.0-beta2</sparkle:shortVersionString>
<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>
<enclosure url="https://github.com/steipete/clawdis/releases/download/v2.0.0-beta3/Clawdis-2.0.0-beta2.dSYM.zip" length="67404637" type="application/octet-stream" sparkle:edSignature="sVup4e9DryEYUDQ7tTuoy7DR1IyDnbJmIp+P37DQsv6qa7VH1jcjiuz9u51OFGwyfKRJ8lWsxx+SGXMKiPr6AQ=="/>
<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>
@@ -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"] }
}
},
+35 -30
View File
@@ -401,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:
@@ -413,37 +411,44 @@ 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
var root = await ConfigStore.load()
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 }
root["gateway"] = gateway
try? await ConfigStore.save(root)
}
}
func triggerVoiceEars(ttl: TimeInterval? = 5) {
@@ -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 {
@@ -323,6 +323,9 @@ struct CronJobEditor: View {
Text("whatsapp").tag(GatewayAgentChannel.whatsapp)
Text("telegram").tag(GatewayAgentChannel.telegram)
Text("discord").tag(GatewayAgentChannel.discord)
Text("slack").tag(GatewayAgentChannel.slack)
Text("signal").tag(GatewayAgentChannel.signal)
Text("imessage").tag(GatewayAgentChannel.imessage)
}
.labelsHidden()
.pickerStyle(.segmented)
@@ -10,6 +10,9 @@ enum GatewayAgentChannel: String, Codable, CaseIterable, Sendable {
case whatsapp
case telegram
case discord
case slack
case signal
case imessage
case webchat
init(raw: String?) {
@@ -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
@@ -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
@@ -369,6 +369,43 @@ public struct SendParams: Codable, Sendable {
}
}
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 idempotencykey: String
public init(
to: String,
question: String,
options: [String],
maxselections: Int?,
durationhours: Int?,
provider: String?,
idempotencykey: String
) {
self.to = to
self.question = question
self.options = options
self.maxselections = maxselections
self.durationhours = durationhours
self.provider = provider
self.idempotencykey = idempotencykey
}
private enum CodingKeys: String, CodingKey {
case to
case question
case options
case maxselections = "maxSelections"
case durationhours = "durationHours"
case provider
case idempotencykey = "idempotencyKey"
}
}
public struct AgentParams: Codable, Sendable {
public let message: String
public let to: String?
@@ -424,21 +461,17 @@ public struct AgentParams: Codable, Sendable {
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"
}
}
@@ -622,23 +655,27 @@ public struct SessionsListParams: Codable, Sendable {
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"
}
}
@@ -648,6 +685,7 @@ public struct SessionsPatchParams: Codable, Sendable {
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 +695,7 @@ public struct SessionsPatchParams: Codable, Sendable {
verboselevel: AnyCodable?,
elevatedlevel: AnyCodable?,
model: AnyCodable?,
spawnedby: AnyCodable?,
sendpolicy: AnyCodable?,
groupactivation: AnyCodable?
) {
@@ -665,6 +704,7 @@ public struct SessionsPatchParams: Codable, Sendable {
self.verboselevel = verboselevel
self.elevatedlevel = elevatedlevel
self.model = model
self.spawnedby = spawnedby
self.sendpolicy = sendpolicy
self.groupactivation = groupactivation
}
@@ -674,6 +714,7 @@ public struct SessionsPatchParams: Codable, Sendable {
case verboselevel = "verboseLevel"
case elevatedlevel = "elevatedLevel"
case model
case spawnedby = "spawnedBy"
case sendpolicy = "sendPolicy"
case groupactivation = "groupActivation"
}
@@ -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")
}
}
@@ -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"] }
}
},
+7 -7
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"
+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`.
- CLI: `agentCommand` in `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 surface handling
- `createAgentEventHandler` in `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`
- `src/gateway/server-methods/agent-job.ts`
- `src/commands/agent.ts`
- `src/agents/pi-embedded-runner.ts`
- `src/agents/pi-embedded-subscribe.ts`
- `src/gateway/server-chat.ts`
+14 -12
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:
@@ -47,7 +49,7 @@ dns-sd -B _clawdbot-bridge._tcp local.
More debugging notes: `docs/bonjour.md`.
### 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:
@@ -56,7 +58,7 @@ Android NSD/mDNS discovery won’t cross networks. If your Android node and the
Details and example CoreDNS config: `docs/bonjour.md`.
## 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:
@@ -80,7 +82,7 @@ clawdbot nodes approve <requestId>
Pairing details: `docs/gateway/pairing.md`.
## 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.
+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`)
- 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`): 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` (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`).
- **Agent process (Pi)**
- Spawned by the Gateway on demand for `agent` calls; streams events back over the same WS connection.
- **WebChat**
+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 {
+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` |
+4
View File
@@ -287,3 +287,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.md](./browser-linux-troubleshooting.md).
+56
View File
@@ -0,0 +1,56 @@
# Bun (optional)
Goal: allow running this repo with Bun without maintaining a Bun lockfile or losing pnpm patch behavior.
## Status
- pnpm remains the primary package manager/runtime for this repo.
- Bun can be used for local installs/builds/tests, but Bun currently **cannot use** `pnpm-lock.yaml` and will ignore it.
## Install (no Bun lockfile)
Use Bun without writing `bun.lock`/`bun.lockb`:
```sh
bun install --no-save
```
This avoids maintaining two lockfiles. (`bun.lock`/`bun.lockb` are gitignored.)
## 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` 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.
+7 -2
View File
@@ -147,12 +147,13 @@ Example:
- Session files: `~/.clawdbot/sessions/{{SessionId}}.jsonl`
- Session metadata (token usage, last route, etc): `~/.clawdbot/sessions/sessions.json` (legacy: `~/.clawdbot/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`).
- 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
{
@@ -193,5 +194,9 @@ Logs live under `/tmp/clawdbot/` (default: `clawdbot-YYYY-MM-DD.log`).
- 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)
- macOS menu bar companion: [Clawdbot macOS app](./macos.md)
- iOS node app: [iOS app](./ios.md)
- Android node app: [Android app](./android.md)
- Windows status: [Windows app](./windows.md)
- Linux status: [Linux app](./linux.md)
- Security: [Security](./security.md)
+162 -43
View File
@@ -9,7 +9,7 @@ CLAWDBOT reads an optional **JSON5** config from `~/.clawdbot/clawdbot.json` (co
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`)
@@ -89,13 +89,50 @@ Env var equivalent:
- `CLAWDBOT_LOAD_SHELL_ENV=1`
- `CLAWDBOT_SHELL_ENV_TIMEOUT_MS=15000`
### Auth storage (OAuth + API keys)
Clawdbot stores **auth profiles** (OAuth + API keys) in:
- `~/.clawdbot/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:
- `~/.clawdbot/agent/auth.json` (managed automatically; don’t edit manually)
Overrides:
- OAuth dir (legacy import only): `CLAWDBOT_OAUTH_DIR`
- Agent dir: `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
{
@@ -126,6 +163,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
{
@@ -133,15 +173,22 @@ 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.allowFrom`
Allowlist of E.164 phone numbers that may trigger WhatsApp auto-replies (DMs only).
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).
For groups, use `whatsapp.groupPolicy` + `whatsapp.groupAllowFrom`.
```json5
{
@@ -159,6 +206,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
{
@@ -171,7 +219,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
@@ -190,6 +238,51 @@ 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`.
### `routing.queue`
Controls how inbound messages behave when an agent run is already active.
@@ -417,20 +510,45 @@ Default: `~/clawd`.
If `agent.sandbox` is enabled, non-main sessions can override this with their
own per-session workspaces under `agent.sandbox.workspaceRoot`.
### `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.
@@ -456,14 +574,12 @@ 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).
`modelFallbacks` lists ordered fallback models to try when the default fails.
`imageModel` selects an image-capable model for the `image` tool.
`imageModelFallbacks` lists ordered fallback image models for the `image` tool.
`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 `modelAliases` shorthands (when an `agent` section exists):
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`
@@ -477,23 +593,24 @@ If you configure the same alias name (case-insensitive) yourself, your value win
```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"
]
},
modelFallbacks: [
"openrouter/deepseek/deepseek-r1:free",
"openrouter/meta-llama/llama-3.3-70b-instruct:free"
],
imageModel: "openrouter/qwen/qwen-2.5-vl-72b-instruct:free",
imageModelFallbacks: [
"openrouter/google/gemini-2.0-flash-vision:free"
],
thinkingDefault: "low",
verboseDefault: "off",
elevatedDefault: "on",
@@ -528,8 +645,8 @@ 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
@@ -542,6 +659,7 @@ Z.AI models are available as `zai/<model>` (e.g. `zai/glm-4.7`) and require
- `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`).
- `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery (default: 30).
`agent.bash` configures background bash defaults:
- `backgroundMs`: time before auto-background (ms, default 10000)
@@ -690,11 +808,16 @@ When `models.providers` is present, Clawdbot writes/merges a `models.json` into
- 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: {
@@ -727,14 +850,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: {
+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`).
+12 -1
View File
@@ -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] [--channel last|whatsapp|telegram|discord|slack|signal|imessage] [--to <dest>]`
- `clawdbot cron edit <id> ...` (patch-by-flags, non-interactive)
- `clawdbot cron rm <id>`
+6
View File
@@ -123,6 +123,7 @@ Example “single server, only allow me, only allow #help”:
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
@@ -154,6 +155,7 @@ Notes:
discord: {
enabled: true,
token: "abc.123",
groupPolicy: "open",
mediaMaxMb: 8,
actions: {
reactions: true,
@@ -202,10 +204,14 @@ Notes:
}
```
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.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.
+7
View File
@@ -59,6 +59,12 @@ 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.
@@ -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
+5 -2
View File
@@ -26,6 +26,8 @@
"group": "Getting Started",
"pages": [
"index",
"showcase",
"hubs",
"onboarding",
"clawd",
"faq"
@@ -70,8 +72,9 @@
"mac/dev-setup",
"mac/menu-bar",
"mac/voicewake",
"ios/connect",
"android/connect",
"macos",
"ios",
"android",
"webchat",
"web"
]
+6
View File
@@ -15,6 +15,7 @@ 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).
## 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 +27,13 @@ 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`
## Usage
+56 -26
View File
@@ -14,6 +14,9 @@ Everything lives under `~/.clawdbot/`:
| Path | Purpose |
|------|---------|
| `~/.clawdbot/clawdbot.json` | Main config (JSON5) |
| `~/.clawdbot/credentials/oauth.json` | OAuth credentials (Anthropic/OpenAI, etc.) |
| `~/.clawdbot/agent/auth-profiles.json` | Auth profiles (OAuth + API keys) |
| `~/.clawdbot/agent/auth.json` | Runtime API key cache (managed automatically) |
| `~/.clawdbot/credentials/` | WhatsApp/Telegram auth tokens |
| `~/.clawdbot/sessions/` | Conversation history & state |
| `~/.clawdbot/sessions/sessions.json` | Session metadata |
@@ -40,10 +43,10 @@ Some features are platform-specific:
- **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 +79,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,18 +108,18 @@ 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/credentials/oauth.json` to your server. The auth is just a JSON file.
### How are env vars loaded?
@@ -146,7 +149,7 @@ Or set `CLAWDBOT_LOAD_SHELL_ENV=1` (timeout: `CLAWDBOT_SHELL_ENV_TIMEOUT_MS=1500
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/credentials/oauth.json` to the container.
2. **SSH tunnel** — `ssh -L 18789:localhost:18789 user@server`
3. **Tailscale** — Put both machines on your tailnet.
@@ -227,7 +230,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).
@@ -299,7 +302,7 @@ Claude Opus has a 200k token context window, and Clawdbot uses **autocompaction*
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?
@@ -464,6 +467,21 @@ cd ~/path/to/clawdbot
codex --full-auto "debug why clawdbot gateway won't start"
```
### Gateway stops after I log out (Linux)
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
sudo loginctl enable-linger $USER
```
**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.
@@ -474,6 +492,9 @@ The gateway runs under a supervisor that auto-restarts it. You need to stop the
# Check if running
launchctl list | grep clawdbot
# Stop (disable does NOT stop a running job)
clawdbot gateway stop
# Stop and disable
launchctl disable gui/$UID/com.clawdbot.gateway
launchctl bootout gui/$UID/com.clawdbot.gateway
@@ -481,6 +502,9 @@ launchctl bootout gui/$UID/com.clawdbot.gateway
# Re-enable later
launchctl enable gui/$UID/com.clawdbot.gateway
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.clawdbot.gateway.plist
# Or just restart
clawdbot gateway restart
```
**Linux (systemd)**
@@ -490,7 +514,11 @@ launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.clawdbot.gateway.plist
systemctl list-units | grep -i clawdbot
# Stop and disable
sudo systemctl disable --now clawdbot
clawdbot gateway stop
systemctl --user disable --now clawdbot-gateway.service
# Or just restart
clawdbot gateway restart
```
**pm2 (if used)**
@@ -514,10 +542,10 @@ sudo systemctl disable --now clawdbot
pkill -f "clawdbot"
# Remove data
rm -rf ~/.clawdbot
trash ~/.clawdbot
# Remove repo and re-clone
rm -rf ~/clawdbot
trash ~/clawdbot
git clone https://github.com/clawdbot/clawdbot.git
cd clawdbot && pnpm install && pnpm build
pnpm clawdbot onboard
@@ -533,6 +561,9 @@ Quick reference (send these in chat):
|---------|--------|
| `/status` | Health + session info |
| `/new` or `/reset` | Reset the session |
| `/compact` | Compact session context |
Slash commands are owner-only (gated by `whatsapp.allowFrom` and command authorization on other surfaces).
| `/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) |
@@ -554,24 +585,21 @@ Use `/model` to switch without restarting:
/model gemini-flash
```
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 allowed models and aliases in `clawdbot.json`:
**Setup:** Configure models and aliases in `clawdbot.json`:
```json
{
"agent": {
"model": "anthropic/claude-opus-4-5",
"allowedModels": [
"anthropic/claude-opus-4-5",
"anthropic/claude-sonnet-4-5",
"anthropic/claude-haiku-4-5"
],
"modelAliases": {
"opus": "anthropic/claude-opus-4-5",
"sonnet": "anthropic/claude-sonnet-4-5",
"haiku": "anthropic/claude-haiku-4-5"
"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" }
}
}
}
@@ -587,7 +615,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" },
models: { "openrouter/anthropic/claude-sonnet-4": {} },
env: { OPENROUTER_API_KEY: "sk-or-..." }
}
}
@@ -597,7 +626,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: "..." }
}
}
+31 -6
View File
@@ -155,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
@@ -168,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).
@@ -195,6 +219,7 @@ 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 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.
## Migration guidance
+1 -1
View File
@@ -18,7 +18,7 @@ Updated: 2025-12-07
- **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`.
- **Config knobs:** `telegram.botToken`, `telegram.groups` (allowlist + mention defaults), `telegram.allowFrom`, `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
+6 -4
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,9 +7,11 @@ 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.
- 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` 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.
@@ -56,7 +58,7 @@ Only the owner number (from `whatsapp.allowFrom`, defaulting to the bot’s own
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; 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).
+54 -2
View File
@@ -1,11 +1,11 @@
---
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>`).
@@ -16,6 +16,54 @@ Clawdbot treats group chats consistently across surfaces: WhatsApp, Telegram, Di
- UI labels use `displayName` when available, formatted as `surface:<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,8 +99,12 @@ 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`
+1 -1
View File
@@ -22,7 +22,7 @@ Short guide to verify the WhatsApp Web / Baileys stack without guessing.
## 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.
+12 -3
View File
@@ -10,10 +10,10 @@ 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`.
- 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.
@@ -22,6 +22,13 @@ 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
@@ -32,7 +39,8 @@ message. If the reply is only `HEARTBEAT_OK`, it is dropped.
model: "anthropic/claude-opus-4-5",
target: "last", // last | whatsapp | telegram | none
to: "+15551234567", // optional override for whatsapp/telegram
prompt: "HEARTBEAT" // optional override
prompt: "HEARTBEAT", // optional override
ackMaxChars: 30 // max chars allowed after HEARTBEAT_OK
}
}
}
@@ -48,6 +56,7 @@ message. If the reply is only `HEARTBEAT_OK`, it is dropped.
- `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`).
- `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery (default: 30).
## Behavior
- Runs in the main session (`main`, or `global` when scope is global).
+148
View File
@@ -0,0 +1,148 @@
---
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](./index.md)
- [Onboarding](./onboarding.md)
- [Wizard](./wizard.md)
- [Setup](./setup.md)
- [FAQ](./faq.md)
- [Configuration](./configuration.md)
- [Clawd (personal assistant)](./clawd.md)
- [Lore](./lore.md)
## Installation + distribution
- [Docker](./docker.md)
- [Nix](./nix.md)
## Core concepts
- [Architecture](./architecture.md)
- [Agent runtime](./agent.md)
- [Agent loop](./agent-loop.md)
- [Sessions](./session.md)
- [Sessions (alias)](./sessions.md)
- [Session tools](./session-tool.md)
- [Queue](./queue.md)
- [RPC adapters](./rpc.md)
- [TypeBox schemas](./typebox.md)
- [Presence](./presence.md)
- [Discovery + transports](./discovery.md)
- [Bonjour](./bonjour.md)
- [Surface routing](./surface.md)
- [Groups](./groups.md)
- [Group messages](./group-messages.md)
## Providers + ingress
- [WhatsApp](./whatsapp.md)
- [Telegram](./telegram.md)
- [Telegram (grammY notes)](./grammy.md)
- [Slack](./slack.md)
- [Discord](./discord.md)
- [Signal](./signal.md)
- [iMessage](./imessage.md)
- [WebChat](./webchat.md)
- [Webhooks](./webhook.md)
- [Gmail Pub/Sub](./gmail-pubsub.md)
## Gateway + operations
- [Gateway runbook](./gateway.md)
- [Gateway pairing](./gateway/pairing.md)
- [Gateway lock](./gateway-lock.md)
- [Background process](./background-process.md)
- [Health](./health.md)
- [Heartbeat](./heartbeat.md)
- [Doctor](./doctor.md)
- [Logging](./logging.md)
- [Dashboard](./dashboard.md)
- [Control UI](./control-ui.md)
- [Control API (legacy)](./control-api.md)
- [Remote access](./remote.md)
- [Remote gateway README](./remote-gateway-readme.md)
- [Tailscale](./tailscale.md)
- [Security](./security.md)
- [Troubleshooting](./troubleshooting.md)
## Tools + automation
- [Tools surface](./tools.md)
- [Bash tool](./bash.md)
- [Elevated mode](./elevated.md)
- [Cron + wakeups](./cron.md)
- [Thinking + verbose](./thinking.md)
- [Models](./models.md)
- [Agent send CLI](./agent-send.md)
- [Terminal UI](./tui.md)
- [Browser control](./browser.md)
- [Browser (Linux troubleshooting)](./browser-linux-troubleshooting.md)
## Nodes, media, voice
- [Nodes overview](./nodes.md)
- [Camera](./camera.md)
- [Images](./images.md)
- [Audio](./audio.md)
- [Location command](./location-command.md)
- [Voice wake](./voicewake.md)
- [Talk mode](./talk.md)
## Platforms
- [macOS app overview](./macos.md)
- [macOS dev setup](./mac/dev-setup.md)
- [macOS menu bar](./mac/menu-bar.md)
- [macOS voice wake](./mac/voicewake.md)
- [macOS voice overlay](./mac/voice-overlay.md)
- [macOS WebChat](./mac/webchat.md)
- [macOS Canvas](./mac/canvas.md)
- [macOS child process](./mac/child-process.md)
- [macOS health](./mac/health.md)
- [macOS icon](./mac/icon.md)
- [macOS logging](./mac/logging.md)
- [macOS permissions](./mac/permissions.md)
- [macOS remote](./mac/remote.md)
- [macOS signing](./mac/signing.md)
- [macOS release](./mac/release.md)
- [macOS bun gateway](./mac/bun.md)
- [macOS XPC](./mac/xpc.md)
- [macOS skills](./mac/skills.md)
- [macOS Peekaboo plan](./mac/peekaboo.md)
- [iOS node](./ios.md)
- [Android node](./android.md)
- [Windows app](./windows.md)
- [Linux app](./linux.md)
- [Web surfaces](./web.md)
## Workspace + templates
- [Skills](./skills.md)
- [Skills config](./skills-config.md)
- [Default AGENTS](./AGENTS.default.md)
- [Templates: AGENTS](./templates/AGENTS.md)
- [Templates: BOOTSTRAP](./templates/BOOTSTRAP.md)
- [Templates: IDENTITY](./templates/IDENTITY.md)
- [Templates: SOUL](./templates/SOUL.md)
- [Templates: TOOLS](./templates/TOOLS.md)
- [Templates: USER](./templates/USER.md)
## Experiments + proposals
- [Onboarding config protocol](./onboarding-config-protocol.md)
- [Research: memory](./research/memory.md)
- [Proposal: model config](./proposals/model-config.md)
## Testing + release
- [Testing](./test.md)
- [Release checklist](./RELEASING.md)
- [Device models](./device-models.md)
+1
View File
@@ -37,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
+5 -1
View File
@@ -27,6 +27,8 @@ Status: external CLI integration. No daemon.
cliPath: "imsg",
dbPath: "~/Library/Messages/chat.db",
allowFrom: ["+15555550123", "user@example.com", "chat_id:123"],
groupPolicy: "open",
groupAllowFrom: ["chat_id:123"],
includeAttachments: false,
mediaMaxMb: 16,
service: "auto",
@@ -37,6 +39,8 @@ Status: external CLI integration. No daemon.
Notes:
- `allowFrom` accepts handles (phone/email) or `chat_id:<id>` entries.
- `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 +59,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
+14 -2
View File
@@ -12,13 +12,14 @@ read_when:
</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="https://github.com/clawdbot/clawdbot/blob/main/docs/index.md">Docs</a> ·
<a href="./clawd.md">Clawd setup</a>
</p>
@@ -41,7 +42,8 @@ WhatsApp / Telegram / Discord
├─ 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.
@@ -62,12 +64,14 @@ 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
- 🔐 **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.
@@ -124,6 +128,7 @@ Example:
## Docs
- Start here:
- [Docs hubs (all pages linked)](./hubs.md)
- [FAQ](./faq.md) ← *common questions answered*
- [Configuration](./configuration.md)
- [Nix mode](./nix.md)
@@ -147,6 +152,12 @@ Example:
- [WhatsApp group messages](./group-messages.md)
- [Media: images](./images.md)
- [Media: audio](./audio.md)
- Companion apps:
- [macOS app](./macos.md)
- [iOS app](./ios.md)
- [Android app](./android.md)
- [Windows app](./windows.md)
- [Linux app](./linux.md)
- Ops and safety:
- [Sessions](./session.md)
- [Cron + wakeups](./cron.md)
@@ -170,6 +181,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
+186 -50
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`.
#### 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.
## 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)
### 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`).
- 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:
@@ -79,7 +244,7 @@ CLI (headless approvals):
- `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,15 +258,15 @@ 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)
#### Minimal protocol additions (v1)
Add to `src/gateway/protocol/schema.ts` (and regenerate Swift models):
**Identity**
@@ -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` (gateway runbook)
- `docs/gateway/pairing.md` (approval + storage)
- `docs/bonjour.md` (discovery debugging)
- `docs/discovery.md` (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.
+11
View File
@@ -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:
+1 -1
View File
@@ -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`.
## Agent commands (current)
+1 -1
View File
@@ -23,7 +23,7 @@ Peekaboo’s privileged execution moved from “CLI → XPC helper” to “CLI
- It lets us piggyback on **either** Peekaboo.app’s permissions **or** Clawdbot.app’s permissions (whichever is running).
- It avoids “two apps with two TCC bubbles” unless needed.
Reference (Peekaboo submodule): `docs/bridge-host.md`.
Reference (Peekaboo submodule): `Peekaboo/docs/bridge-host.md`.
## Architecture
### Processes
+1 -1
View File
@@ -23,7 +23,7 @@ This app now ships Sparkle auto-updates. Release builds must be Developer ID–s
## Build & package
Notes:
- `APP_BUILD` maps to `CFBundleVersion`/`sparkle:version`; keep it numeric + monotonic (no `-beta`), or Sparkle compares it as equal.
- Universal by default (`arm64 x86_64`). Override with `BUILD_ARCHS="arm64"` if you need arm-only.
- Defaults to the current architecture (`$(uname -m)`). For release/universal builds, set `BUILD_ARCHS="arm64 x86_64"` (or `BUILD_ARCHS=all`).
```bash
# From repo root; set release IDs so Sparkle feed is enabled.
+1 -1
View File
@@ -94,7 +94,7 @@ Notes:
- In remote mode, Clawdbot will use the configured remote tunnel/endpoint.
## Build & dev workflow (native)
- `cd native && swift build` (debug) / `swift build -c release`.
- `cd apps/macos && swift build` (debug) / `swift build -c release`.
- Run app for dev: `swift run Clawdbot` (or Xcode scheme).
- Package app + CLI: `scripts/package-mac-app.sh` (builds bun CLI + gateway).
- Tests: add Swift Testing suites under `apps/macos/Tests`.
+75
View File
@@ -0,0 +1,75 @@
---
summary: "How Clawdbot rotates auth profiles and falls back across models"
read_when:
- Diagnosing auth profile rotation, cooldowns, or model fallback behavior
- Updating failover rules for auth profiles or models
---
# Model failover
Clawdbot handles failures in two stages:
1) **Auth profile rotation** within the current provider.
2) **Model fallback** to the next model in `agent.model.fallbacks`.
This doc explains the runtime rules and the data that backs them.
## Profile IDs
OAuth logins create distinct profiles so multiple accounts can coexist.
- Default: `provider:default` when no email is available.
- OAuth with email: `provider:<email>` (for example `google-antigravity:user@gmail.com`).
Profiles live in `~/.clawdbot/agent/auth-profiles.json` under `profiles`.
## Rotation order
When a provider has multiple profiles, Clawdbot chooses an order like this:
1) **Explicit config**: `auth.order[provider]` (if set).
2) **Configured profiles**: `auth.profiles` filtered by provider.
3) **Stored profiles**: entries in `auth-profiles.json` for the provider.
If no explicit order is configured, Clawdbot uses a round‑robin order:
- **Primary key:** `usageStats.lastUsed` (oldest first).
- **Secondary key:** profile type (OAuth before API keys).
- **Cooldown profiles** are moved to the end, ordered by soonest cooldown expiry.
## Cooldowns
When a profile fails due to auth/rate‑limit errors (or a timeout that looks
like rate limiting), Clawdbot marks it in cooldown and moves to the next profile.
Cooldowns use exponential backoff:
- 1 minute
- 5 minutes
- 25 minutes
- 1 hour (cap)
State is stored in `auth-profiles.json` under `usageStats`:
```json
{
"usageStats": {
"provider:profile": {
"lastUsed": 1736160000000,
"cooldownUntil": 1736160600000,
"errorCount": 2
}
}
}
```
## Model fallback
If all profiles for a provider fail, Clawdbot moves to the next model in
`agent.model.fallbacks`. This applies to auth failures, rate limits, and
timeouts that exhausted profile rotation.
## Related config
See `docs/configuration.md` for:
- `auth.profiles` / `auth.order`
- `agent.model.primary` / `agent.model.fallbacks`
- `agent.imageModel` routing
See `docs/models.md` for the broader model selection and fallback overview.
+24 -23
View File
@@ -16,35 +16,32 @@ that prefers tool-call + image-capable models and maintains ordered fallbacks.
- default: configured models only
- flags: `--all` (full catalog), `--local`, `--provider <name>`, `--json`, `--plain`
- `clawdbot models status`
- show default model + aliases + fallbacks + allowlist
- show default model + aliases + fallbacks + configured models
- `clawdbot models set <modelOrAlias>`
- writes `agent.model` in config
- writes `agent.model.primary` and ensures `agent.models` entry
- `clawdbot models set-image <modelOrAlias>`
- writes `agent.imageModel` in config
- writes `agent.imageModel.primary` and ensures `agent.models` entry
- `clawdbot models aliases list|add|remove`
- writes `agent.modelAliases`
- writes `agent.models.*.alias`
- `clawdbot models fallbacks list|add|remove|clear`
- writes `agent.modelFallbacks`
- writes `agent.model.fallbacks`
- `clawdbot models image-fallbacks list|add|remove|clear`
- writes `agent.imageModelFallbacks`
- writes `agent.imageModel.fallbacks`
- `clawdbot models scan`
- OpenRouter :free scan; probe tool-call + image; interactive selection
## Config changes
- Add `agent.modelFallbacks: string[]` (ordered list of provider/model IDs).
- Add `agent.imageModel?: string` (optional image-capable model for image tool).
- Add `agent.imageModelFallbacks?: string[]` (ordered list for image tool).
- Keep existing:
- `agent.model` (default)
- `agent.allowedModels` (list filter)
- `agent.modelAliases` (shortcut names)
- `agent.models` (configured model catalog + aliases).
- `agent.model.primary` + `agent.model.fallbacks`.
- `agent.imageModel.primary` + `agent.imageModel.fallbacks` (optional).
- `auth.profiles` + `auth.order` for per-provider auth failover.
## Scan behavior (models scan)
Input
- OpenRouter `/models` list (filter `:free`)
- Requires `OPENROUTER_API_KEY` (or stored OpenRouter key in auth storage)
- Requires OpenRouter API key from auth profiles or `OPENROUTER_API_KEY`
- Optional filters: `--max-age-days`, `--min-params`, `--provider`, `--max-candidates`
- Probe controls: `--timeout`, `--concurrency`
@@ -66,17 +63,21 @@ Interactive selection (TTY)
- Non-TTY: auto-select; require `--yes`/`--no-input` to apply.
Output
- Writes `agent.modelFallbacks` ordered.
- Writes `agent.imageModelFallbacks` ordered (image-capable models).
- Optional `--set-default` to set `agent.model`.
- Optional `--set-image` to set `agent.imageModel`.
- Writes `agent.model.fallbacks` ordered.
- Writes `agent.imageModel.fallbacks` ordered (image-capable models).
- Ensures `agent.models` entries exist for selected models.
- Optional `--set-default` to set `agent.model.primary`.
- Optional `--set-image` to set `agent.imageModel.primary`.
## Runtime fallback
- On model failure: try `agent.modelFallbacks` in order.
- Ignore fallback entries not in `agent.allowedModels` (if allowlist set).
- Persist last successful provider/model to session entry.
- `/status` shows last used model (not just default).
- On model failure: try `agent.model.fallbacks` in order.
- Per-provider auth failover uses `auth.order` (or stored profile order) **before**
moving to the next model.
- Image routing uses `agent.imageModel` **only when configured** and the primary
model lacks image input.
- Persist last successful provider/model to session entry; auth profile success is global.
- See `docs/model-failover.md` for auth profile rotation, cooldowns, and timeout handling.
## Tests
@@ -86,5 +87,5 @@ Output
## Docs
- Update `docs/configuration.md` with `agent.modelFallbacks`.
- Update `docs/configuration.md` with `agent.models` + `agent.model` + `agent.imageModel`.
- Keep this doc current when CLI surface or scan logic changes.
+5
View File
@@ -1,3 +1,8 @@
---
summary: "RPC protocol notes for onboarding wizard and config schema"
read_when: "Changing onboarding wizard steps or config schema endpoints"
---
# Onboarding + Config Protocol
Purpose: shared onboarding + config surfaces across CLI, macOS app, and Web UI.
+25 -19
View File
@@ -1,17 +1,17 @@
---
summary: "Planned first-run onboarding flow for Clawdbot (local vs remote, Anthropic OAuth, workspace bootstrap ritual)"
summary: "Planned first-run onboarding flow for Clawdbot (local vs remote, OAuth auth, workspace bootstrap ritual)"
read_when:
- Designing the macOS onboarding assistant
- Implementing Anthropic auth or identity setup
- Implementing Anthropic/OpenAI auth or identity setup
---
# Onboarding (macOS app)
This doc describes the intended **first-run onboarding** for Clawdbot. The goal is a good “day 0” experience: pick where the Gateway runs, bind Claude (Anthropic) auth for the embedded agent runtime, and then let the **agent bootstrap itself** via a first-run ritual in the workspace.
This doc describes the intended **first-run onboarding** for Clawdbot. The goal is a good “day 0” experience: pick where the Gateway runs, bind subscription auth (Anthropic or OpenAI) for the embedded agent runtime, and then let the **agent bootstrap itself** via a first-run ritual in the workspace.
## Page order (high level)
1) **Local vs Remote**
2) **(Local only)** Connect Claude (Anthropic OAuth) — optional, but recommended
2) **(Local only)** Connect subscription auth (Anthropic / OpenAI OAuth) — optional, but recommended
3) **Connect Gmail (optional)** — run `clawdbot hooks gmail setup` to configure Pub/Sub hooks
4) **Onboarding chat** — dedicated session where the agent introduces itself and guides setup
@@ -19,7 +19,7 @@ This doc describes the intended **first-run onboarding** for Clawdbot. The goal
First question: where does the **Gateway** run?
- **Local (this Mac):** onboarding can run the Anthropic OAuth flow and write the Clawdbot token store locally.
- **Local (this Mac):** onboarding can run OAuth flows and write OAuth credentials locally.
- **Remote (over SSH/tailnet):** onboarding must not run OAuth locally, because credentials must exist on the **gateway host**.
Gateway auth tip:
@@ -28,11 +28,11 @@ Gateway auth tip:
Implementation note (2025-12-19): in local mode, the macOS app bundles the Gateway and enables it via a per-user launchd LaunchAgent (no global npm install/Node requirement for the user).
## 2) Local-only: Connect Claude (Anthropic OAuth)
## 2) Local-only: Connect subscription auth (Anthropic / OpenAI OAuth)
This is the “bind Clawdbot to Anthropic” step. It is explicitly the **Anthropic (Claude Pro/Max) OAuth flow**, not a generic “login”.
This is the “bind Clawdbot to subscription auth” step. It is explicitly the **Anthropic (Claude Pro/Max)** or **OpenAI (ChatGPT/Codex)** OAuth flow, not a generic “login”.
### Recommended: OAuth
### Recommended: OAuth (Anthropic)
The macOS app should:
- Start the Anthropic OAuth (PKCE) flow in the user’s browser.
@@ -41,7 +41,16 @@ The macOS app should:
- `~/.clawdbot/credentials/oauth.json` (file mode `0600`, directory mode `0700`)
Why this location matters: it’s the Clawdbot-owned OAuth store.
On first run, Clawdbot can import existing OAuth tokens from legacy p/Claude locations if present.
Clawdbot also imports `oauth.json` into the agent auth profile store (`~/.clawdbot/agent/auth-profiles.json`) on first use.
### Recommended: OAuth (OpenAI Codex)
The macOS app should:
- Start the OpenAI Codex OAuth (PKCE) flow in the user’s browser.
- Auto-capture the callback on `http://127.0.0.1:1455/auth/callback` when possible.
- If the callback fails, prompt the user to paste the redirect URL or code.
- Store credentials in `~/.clawdbot/credentials/oauth.json` (same OAuth store as Anthropic).
- Set `agent.model` to `openai-codex/gpt-5.2` when the model is unset or `openai/*`.
### Alternative: API key (instructions only)
@@ -94,7 +103,7 @@ Once setup is complete, the user can switch to the normal chat (`main`) via the
We no longer collect identity in the onboarding wizard. Instead, the **first agent run** performs a playful bootstrap ritual using files in the workspace:
- Workspace is created implicitly (default `~/.clawdbot/workspace`) when local is selected,
- Workspace is created implicitly (default `~/clawd`, configurable via `agent.workspace`) when local is selected,
but only if the folder is empty or already contains `AGENTS.md`.
- Files are seeded: `AGENTS.md`, `BOOTSTRAP.md`, `IDENTITY.md`, `USER.md`.
- `BOOTSTRAP.md` tells the agent to keep it conversational:
@@ -123,7 +132,7 @@ The workspace is created automatically as part of agent bootstrap (no dedicated
Recommendation: treat the workspace as the agent’s “memory” and make it a git repo (ideally private) so identity + memories are backed up:
```bash
cd ~/.clawdbot/workspace
cd ~/clawd
git init
git add AGENTS.md
git commit -m "Add agent workspace"
@@ -136,11 +145,11 @@ Daily memory lives under `memory/` in the workspace:
## Remote mode note (why OAuth is hidden)
If the Gateway runs on another machine, the Anthropic OAuth credentials must be created/stored on that host (where the agent runtime runs).
If the Gateway runs on another machine, OAuth credentials must be created/stored on that host (where the agent runtime runs).
For now, remote onboarding should:
- explain why OAuth isn't shown
- point the user at the credential location (`~/.clawdbot/credentials/oauth.json`) and the workspace location on the gateway host
- point the user at the credential location (`~/.clawdbot/credentials/oauth.json`) and the auth profile store (`~/.clawdbot/agent/auth-profiles.json`) on the gateway host
- mention that the **bootstrap ritual happens on the gateway host** (same BOOTSTRAP/IDENTITY/USER files)
### Manual credential setup
@@ -149,17 +158,14 @@ On the gateway host, create `~/.clawdbot/credentials/oauth.json` with this exact
```json
{
"anthropic": {
"access": "sk-ant-oat01-...",
"refresh": "sk-ant-ort01-...",
"expires": 1767304352803
}
"anthropic": { "type": "oauth", "access": "sk-ant-oat01-...", "refresh": "sk-ant-ort01-...", "expires": 1767304352803 },
"openai-codex": { "type": "oauth", "access": "eyJhbGciOi...", "refresh": "oai-refresh-...", "expires": 1767304352803, "accountId": "acct_..." }
}
```
Set permissions: `chmod 600 ~/.clawdbot/credentials/oauth.json`
**Note:** Clawdbot can auto-import from legacy pi-coding-agent paths (`~/.pi/agent/oauth.json`, etc.) but this does NOT work with Claude Code credentials — different file and format.
**Note:** Clawdbot auto-imports from legacy pi-coding-agent paths (`~/.pi/agent/oauth.json`, etc.) but this does NOT work with Claude Code credentials — different file and format.
### Using Claude Code credentials
+72
View File
@@ -0,0 +1,72 @@
---
summary: "Harden cron.add input handling, align schemas, and improve cron UI/agent tooling"
owner: "clawdbot"
status: "complete"
last_updated: "2026-01-05"
---
# Cron Add Hardening & Schema Alignment
## Context
Recent gateway logs show repeated `cron.add` failures with invalid parameters (missing `sessionTarget`, `wakeMode`, `payload`, and malformed `schedule`). This indicates that at least one client (likely the agent tool call path) is sending wrapped or partially specified job payloads. Separately, there is drift between cron channel enums in TypeScript, gateway schema, CLI flags, and UI form types, plus a UI mismatch for `cron.status` (expects `jobCount` while gateway returns `jobs`).
## Goals
- Stop `cron.add` INVALID_REQUEST spam by normalizing common wrapper payloads and inferring missing `kind` fields.
- Align cron channel lists across gateway schema, cron types, CLI docs, and UI forms.
- Make agent cron tool schema explicit so the LLM produces correct job payloads.
- Fix the Control UI cron status job count display.
- Add tests to cover normalization and tool behavior.
## Non-goals
- Change cron scheduling semantics or job execution behavior.
- Add new schedule kinds or cron expression parsing.
- Overhaul the UI/UX for cron beyond the necessary field fixes.
## Findings (current gaps)
- `CronPayloadSchema` in gateway excludes `signal` + `imessage`, while TS types include them.
- Control UI CronStatus expects `jobCount`, but gateway returns `jobs`.
- Agent cron tool schema allows arbitrary `job` objects, enabling malformed inputs.
- Gateway strictly validates `cron.add` with no normalization, so wrapped payloads fail.
## Proposed Approach
1. **Normalize** incoming `cron.add` payloads (unwrap `data`/`job`, infer `schedule.kind` and `payload.kind`, default `wakeMode` + `sessionTarget` when safe).
2. **Harden** the agent cron tool schema using the canonical gateway `CronAddParamsSchema` and normalize before sending to the gateway.
3. **Align** channel enums and cron status fields across gateway schema, TS types, CLI descriptions, and UI form controls.
4. **Test** normalization in gateway tests and tool behavior in agent tests.
## Multi-phase Execution Plan
### Phase 1 — Schema + type alignment
- [x] Expand gateway `CronPayloadSchema` channel enum to include `signal` and `imessage`.
- [x] Update CLI `--channel` descriptions to include `slack` (already supported by gateway).
- [x] Update UI Cron payload/channel union types to include all supported channels.
- [x] Fix UI CronStatus type to match gateway (`jobs` instead of `jobCount`).
- [x] Update cron UI channel select to include Discord/Slack/Signal/iMessage.
- [x] Update macOS CronJobEditor channel picker + enum to include Slack/Signal/iMessage.
- [x] Document cron compatibility normalization policy in `docs/cron.md`.
### Phase 2 — Input normalization + tooling hardening
- [x] Add shared cron input normalization helpers (`normalizeCronJobCreate`/`normalizeCronJobPatch`).
- [x] Apply normalization in gateway `cron.add` (and patch normalization in `cron.update`).
- [x] Tighten agent cron tool schema to `CronAddParamsSchema` and normalize job/patch before sending.
### Phase 3 — Tests
- [x] Add gateway test covering wrapped `cron.add` payload normalization.
- [x] Add cron tool test to assert normalization and defaulting for `cron.add`.
- [x] Add gateway test covering `cron.update` normalization.
- [x] Add UI + Swift conformance test for cron channels + status fields.
### Phase 4 — Verification
- [x] Run tests (full suite executed via `pnpm test -- cron-tool`).
## Rollout/Monitoring
- Watch gateway logs for reduced `cron.add` INVALID_REQUEST errors.
- Confirm Control UI cron status shows job count after refresh.
- If errors persist, extend normalization for additional common shapes (e.g., `schedule.at`, `payload.message` without `kind`).
## Optional Follow-ups
- Manual Control UI smoke: add cron job per channel + verify status job count.
## Open Questions
- Should `cron.add` accept explicit `state` from clients (currently disallowed by schema)?
- Should we allow `webchat` as an explicit delivery channel (currently filtered in delivery resolution)?
+121
View File
@@ -0,0 +1,121 @@
# Engineering Execution Spec: groupPolicy Hardening (Telegram Allowlist Parity)
**Date**: 2026-01-05
**Status**: Complete
**PR**: #216 (feat/whatsapp-group-policy)
---
## Executive Summary
Follow-up hardening work ensures Telegram allowlists behave consistently across inbound group/DM filtering and outbound send normalization. The focus is on prefix parity (`telegram:` / `tg:`), case-insensitive matching for prefixes, and resilience to accidental whitespace in config entries. Documentation and tests were updated to reflect and lock in this behavior.
---
## Findings Analysis
### [MED] F1: Telegram Allowlist Prefix Handling Is Case-Sensitive and Excludes `tg:`
**Location**: `src/telegram/bot.ts`
**Problem**: Inbound allowlist normalization only stripped a lowercase `telegram:` prefix. This rejected `TG:123` / `Telegram:123` and did not accept the `tg:` shorthand even though outbound send normalization already accepts `tg:` and case-insensitive prefixes.
**Impact**:
- DMs and group allowlists fail when users copy/paste prefixed IDs from logs or existing send format.
- Behavior is inconsistent between inbound filtering and outbound send normalization.
**Fix**: Normalize allowlist entries by trimming whitespace and stripping `telegram:` / `tg:` prefixes case-insensitively at pre-compute time.
---
### [LOW] F2: Allowlist Entries Are Not Trimmed
**Location**: `src/telegram/bot.ts`
**Problem**: Allowlist entries are not trimmed; accidental whitespace causes mismatches.
**Fix**: Trim and drop empty entries while normalizing allowlist inputs.
---
## Implementation Phases
### Phase 1: Normalize Telegram Allowlist Inputs
**File**: `src/telegram/bot.ts`
**Changes**:
1. Trim allowlist entries and drop empty values.
2. Strip `telegram:` / `tg:` prefixes case-insensitively.
3. Simplify DM allowlist check to rely on normalized values.
---
### Phase 2: Add Coverage for Prefix + Whitespace
**File**: `src/telegram/bot.test.ts`
**Add Tests**:
- DM allowlist accepts `TG:` prefix with surrounding whitespace.
- Group allowlist accepts `TG:` prefix case-insensitively.
---
### Phase 3: Documentation Updates
**Files**:
- `docs/groups.md`
- `docs/telegram.md`
**Changes**:
- Document `tg:` alias and case-insensitive prefixes for Telegram allowlists.
---
### Phase 4: Verification
1. Run targeted Telegram tests (`pnpm test -- src/telegram/bot.test.ts`).
2. If time allows, run full suite (`pnpm test`).
---
## Files Modified
| File | Change Type | Description |
|------|-------------|-------------|
| `src/telegram/bot.ts` | Fix | Trim allowlist values; strip `telegram:` / `tg:` prefixes case-insensitively |
| `src/telegram/bot.test.ts` | Test | Add DM + group allowlist coverage for `TG:` prefix + whitespace |
| `docs/groups.md` | Docs | Mention `tg:` alias + case-insensitive prefixes |
| `docs/telegram.md` | Docs | Mention `tg:` alias + case-insensitive prefixes |
---
## Success Criteria
- [x] Telegram allowlist accepts `telegram:` / `tg:` prefixes case-insensitively.
- [x] Telegram allowlist tolerates whitespace in config entries.
- [x] DM and group allowlist tests cover prefixed cases.
- [x] Docs updated to reflect allowlist formats.
- [x] Targeted tests pass.
- [x] Full test suite passes.
---
## Risk Assessment
| Risk | Severity | Mitigation |
|------|----------|------------|
| Behavior change for malformed entries | Low | Normalization is additive and trims only whitespace |
| Test fragility | Low | Isolated unit tests; no external dependencies |
| Doc drift | Low | Updated docs alongside code |
---
## Estimated Complexity
- **Phase 1**: Low (normalization helpers)
- **Phase 2**: Low (2 new tests)
- **Phase 3**: Low (doc edits)
- **Phase 4**: Low (verification)
**Total**: ~20 minutes
+52
View File
@@ -0,0 +1,52 @@
---
summary: "Poll sending via gateway + CLI"
read_when:
- Adding or modifying poll support
- Debugging poll sends from the CLI or gateway
---
# Polls
Updated: 2026-01-06
## Supported providers
- WhatsApp (web provider)
- Discord
## CLI
```bash
# WhatsApp
clawdbot poll --to +15555550123 -q "Lunch today?" -o "Yes" -o "No" -o "Maybe"
clawdbot poll --to 123456789@g.us -q "Meeting time?" -o "10am" -o "2pm" -o "4pm" -s 2
# Discord
clawdbot poll --to channel:123456789 -q "Snack?" -o "Pizza" -o "Sushi" --provider discord
clawdbot poll --to channel:123456789 -q "Plan?" -o "A" -o "B" --provider discord --duration-hours 48
```
Options:
- `--provider`: `whatsapp` (default) or `discord`
- `--max-selections`: how many choices a voter can select (default: 1)
- `--duration-hours`: Discord-only (defaults to 24 when omitted)
## Gateway RPC
Method: `poll`
Params:
- `to` (string, required)
- `question` (string, required)
- `options` (string[], required)
- `maxSelections` (number, optional)
- `durationHours` (number, optional)
- `provider` (string, optional, default: `whatsapp`)
- `idempotencyKey` (string, required)
## Provider differences
- WhatsApp: 2-12 options, `maxSelections` must be within option count, ignores `durationHours`.
- Discord: 2-10 options, `durationHours` clamped to 1-768 hours (default 24). `maxSelections > 1` enables multi-select; Discord does not support a strict selection count.
## Agent tool (Discord)
The Discord tool action `poll` still uses `question`, `answers`, optional `allowMultiselect`, `durationHours`, and `content`. The gateway/CLI poll model maps `allowMultiselect` to `maxSelections > 1`.
Note: Discord has no “pick exactly N” mode; `maxSelections` is treated as a boolean (`> 1` = multiselect).
+147
View File
@@ -0,0 +1,147 @@
---
summary: "Proposal: model config, auth profiles, and fallback behavior"
read_when:
- Designing model selection, auth profiles, or fallback behavior
- Migrating model config schema
---
# Model config proposal
Goals
- Multi OAuth + multi API key per provider
- Model selection via `/model` with sensible fallback
- Global (not per-session) fallback logic
- Keep last-known-good auth profile when switching models
- Profile override only when explicitly requested
- Image routing override only when explicitly configured
Non-goals (v1)
- Auto-discovery of provider capabilities beyond catalog input tags
- Per-model auth profile order (see open questions)
## Proposed config shape
```json
{
"auth": {
"profiles": {
"anthropic:default": {
"provider": "anthropic",
"mode": "oauth"
},
"anthropic:work": {
"provider": "anthropic",
"mode": "api_key"
},
"openai:default": {
"provider": "openai",
"mode": "oauth"
}
},
"order": {
"anthropic": ["anthropic:default", "anthropic:work"],
"openai": ["openai:default"]
}
},
"agent": {
"models": {
"anthropic/claude-opus-4-5": {
"alias": "Opus"
},
"openai/gpt-5.2": {
"alias": "gpt52"
}
},
"model": {
"primary": "anthropic/claude-opus-4-5",
"fallbacks": ["openai/gpt-5.2"]
},
"imageModel": {
"primary": "openai/gpt-5.2",
"fallbacks": ["anthropic/claude-opus-4-5"]
}
}
}
```
Notes
- Canonical model keys are full `provider/model`.
- `alias` optional; used by `/model` resolution.
- `auth.profiles` is keyed. Default CLI login creates `provider:default`.
- `auth.order[provider]` controls rotation order for that provider.
## CLI / UX
Login
- `clawdbot login anthropic` → create/replace `anthropic:default`.
- `clawdbot login anthropic --profile work` → create/replace `anthropic:work`.
Model selection
- `/model Opus` → resolve alias to full id.
- `/model anthropic/claude-opus-4-5` → explicit.
- Optional: `/model Opus@anthropic:work` (explicit profile override for session only).
Model listing
- `/model` list shows:
- model id
- alias
- provider
- auth order (from `auth.order`)
- auth source for the current provider (auth-profiles.json/env/shell env/models.json)
## Fallback behavior (global)
Fallback list
- Use `agent.model.fallbacks` globally.
- No per-session fallback list; last-known-good is per-session but uses global ordering.
Auth profile rotation
- If provider auth error (401/403/invalid refresh):
- advance to next profile in `auth.order[provider]`.
- if all fail, fall back to next model.
Rate limiting
- If rate limit / quota error:
- rotate auth profile first (same provider)
- if still failing, fall back to next model.
Model not found / capability mismatch
- immediate model fallback.
## Image routing
Rule
- Only use `agent.imageModel` when explicitly configured.
- If `agent.imageModel` is configured and the current text model lacks image input, use it.
Support detection
- From model catalog `input` tags when available (e.g. `image` in models.json).
- If unknown: treat as text-only and use `agent.imageModel`.
## Migration (doctor + gateway auto-run)
Inputs
- Legacy keys (pre-migration):
- `agent.model` (string)
- `agent.modelFallbacks` (string[])
- `agent.imageModel` (string)
- `agent.imageModelFallbacks` (string[])
- `agent.allowedModels` (string[])
- `agent.modelAliases` (record)
Outputs
- `agent.models` map with keys for all referenced models
- `agent.model.primary/fallbacks`
- `agent.imageModel.primary/fallbacks`
- Auth profile store seeded from current auth-profiles.json/auth.json + oauth.json + env (as `provider:default`)
- `auth.order` seeded with `["provider:default"]` when config is updated
Auto-run
- Gateway start detects legacy keys and runs doctor migration.
## Decisions
- Auth order is per-provider (`auth.order`).
- Doctor migration is required; gateway will auto-run on startup when legacy keys detected.
- `/model Opus@profile` is explicit session override only.
- Image routing override only when `agent.imageModel` is explicitly configured.
@@ -1,58 +0,0 @@
---
summary: "Refactor: simplify browser control API + implementation"
read_when:
- Refactoring browser control routes, client, or CLI
- Auditing agent-facing browser tool surface
date: 2025-12-20
---
# Refactor: Browser control simplification
Goal: make the browser-control surface **small, stable, and agent-oriented**, and remove “implementation-shaped” APIs (Playwright/CDP specifics, one-off endpoints, and debugging helpers).
## Why
- The previous API accreted many narrow endpoints (`/click`, `/type`, `/press`, …) plus debug utilities.
- Some actions are inherently racy when modeled as “do X *when* the event is already visible” (file chooser, dialogs).
- We want a single, coherent contract that keeps “how it’s implemented” private.
## Target contract (vNext)
**Basics**
- `GET /` status
- `POST /start`, `POST /stop`
- `GET /tabs`, `POST /tabs/open`, `POST /tabs/focus`, `DELETE /tabs/:targetId`
**Agent actions**
- `POST /navigate` `{ url, targetId? }`
- `POST /act` `{ kind, targetId?, ... }` where `kind` is one of:
- `click`, `type`, `press`, `hover`, `drag`, `select`, `fill`, `wait`, `resize`, `close`, `evaluate`
- `POST /screenshot` `{ targetId?, fullPage?, ref?, element?, type? }`
- `GET /snapshot` `?format=ai|aria&targetId?&limit?`
- `GET /console` `?level?&targetId?`
- `POST /pdf` `{ targetId? }`
**Hooks (pre-setup / arming)**
- `POST /hooks/file-chooser` `{ targetId?, paths, timeoutMs? }`
- `POST /hooks/dialog` `{ targetId?, accept, promptText?, timeoutMs? }`
Semantics:
- Hook endpoints **arm** the next matching event within `timeoutMs` (default 2 minutes, clamped to max 2 minutes).
- Last arm wins per page (new arm replaces previous).
## Work checklist
- [x] Replace action endpoints with `POST /act`
- [x] Remove legacy endpoints (`/click`, `/type`, `/wait`, …) and any CLI wrappers that no longer make sense
- [x] Remove `/back` and any history-specific routes
- [x] Convert `upload` + `dialog` to hook/arming endpoints
- [x] Unify screenshots behind `POST /screenshot` (no GET variant)
- [x] Trim inspect/debug endpoints (`/query`, `/dom`) unless explicitly needed
- [x] Update docs/browser.md to describe contract without implementation details
- [x] Update tests (server + client) to cover vNext contract
## Notes / decisions
- Keep Playwright as an internal implementation detail for now.
- Prefer ref-based interactions (`aria-ref`) over coordinate-based ones.
- Keep the code split “routes vs. engine” small and obvious; avoid scattering logic across too many files.
-93
View File
@@ -1,93 +0,0 @@
---
summary: "Refactor: host A2UI from the Gateway (HTTP), remove app-bundled shells"
read_when:
- Refactoring Canvas/A2UI ownership or assets
- Moving UI rendering from native bundles into the Gateway
- Updating node canvas navigation or A2UI command flows
---
# Canvas / A2UI — HTTP-hosted from Gateway
Status: Implemented · Date: 2025-12-20
## Goal
- Make the **Gateway (TypeScript)** the single owner of A2UI.
- Remove **app-bundled A2UI shells** (macOS, iOS, Android).
- A2UI renders only when the **Gateway is reachable** (acceptable failure mode).
## Decision
All A2UI HTML/JS assets are **served by the Gateway canvas host** on
`canvasHost.port` (default `18793`), bound to the **bridge interface**. Nodes
(mac/iOS/Android) **navigate to the advertised `canvasHostUrl`** before applying
A2UI messages. No local custom-scheme or bundled fallback remains.
## Why
- One source of truth (TS) for A2UI rendering.
- Faster iteration (no app release required for A2UI updates).
- iOS/Android/macOS all behave identically.
## New behavior (summary)
1) `canvas.a2ui.*` on any node:
- Ensure Canvas is visible.
- Navigate the node WebView to the Gateway A2UI URL.
- Apply/reset A2UI messages once the page is ready.
2) If Gateway is unreachable:
- A2UI fails with an explicit error (no fallback).
## Gateway changes
### Serve A2UI assets
Add A2UI HTML/JS to the Gateway Canvas host (standalone HTTP server on
`canvasHost.port`), e.g.:
```
/__clawdbot__/a2ui/ -> index.html
/__clawdbot__/a2ui/a2ui.bundle.js -> bundled A2UI runtime
```
Serve Canvas files at `/__clawdbot__/canvas/` and A2UI at `/__clawdbot__/a2ui/`.
Use the shared Canvas host handler (`src/canvas-host/server.ts`) to serve these
assets and inject the action bridge + live reload if desired.
### Canonical host URL
The Gateway exposes a **canonical** `canvasHostUrl` in hello/bridge payloads
so nodes don’t need to guess.
## Node changes (mac/iOS/Android)
### Navigation path
Before applying A2UI:
- Navigate to `${canvasHostUrl}/__clawdbot__/a2ui/`.
### Remove bundled shells
Remove all fallback logic that serves A2UI from local bundles:
- macOS: remove custom-scheme fallback for `/__clawdbot__/a2ui/`
- iOS/Android: remove packaged A2UI assets and "default scaffold" assumptions
### Error behavior
If `canvasHostUrl` is missing or unreachable:
- `canvas.a2ui.push/reset` returns a clear error:
- `A2UI_HOST_UNAVAILABLE` or `A2UI_HOST_NOT_CONFIGURED`
## Security / transport
- For non-TLS Gateway URLs (http), iOS/Android will need ATS exceptions.
- For TLS (https), prefer WSS + HTTPS with a valid cert.
## Implementation plan
1) Gateway
- Add A2UI assets under `src/canvas-host/`.
- Serve them at `/__clawdbot__/a2ui/` (align with existing naming).
- Serve Canvas files at `/__clawdbot__/canvas/` on `canvasHost.port`.
- Expose `canvasHostUrl` in handshake + bridge hello payloads.
2) Node runtimes
- Update `canvas.a2ui.*` to navigate to `canvasHostUrl`.
- Remove custom-scheme A2UI fallback and bundled assets.
3) Tests
- TS: verify `/__clawdbot__/a2ui/` responds with HTML + JS.
- Node: verify A2UI fails when host is unreachable and succeeds when reachable.
4) Docs
- Update `docs/mac/canvas.md`, `docs/ios/spec.md`, `docs/android/connect.md`
to remove local fallback assumptions and point to gateway-hosted A2UI.
## Notes
- iOS/Android may still require ATS exceptions for `http://` canvas hosts.
-64
View File
@@ -1,64 +0,0 @@
---
summary: "Refactor: unify on the clawdbot CLI + gateway-first control; retire clawdbot-mac"
read_when:
- Removing or replacing the macOS CLI helper
- Adding node capabilities or permissions metadata
- Updating macOS app packaging/install flows
---
# CLI unification (clawdbot-only)
Status: active refactor · Date: 2025-12-20
## Goals
- **Single CLI**: use `clawdbot` for all automation (local + remote). Retire `clawdbot-mac`.
- **Gateway-first**: all agent actions flow through the Gateway WebSocket + node.invoke.
- **Permission awareness**: nodes advertise permission state so the agent can decide what to run.
- **No duplicate paths**: remove macOS control socket + Swift CLI surface.
## Non-goals
- Keep legacy `clawdbot-mac` compatibility.
- Support agent control when no Gateway is running.
## Key decisions
1) **No Gateway → no control**
- If the macOS app is running but the Gateway is not, remote commands (canvas/run/notify) are unavailable.
- This is acceptable to keep one network surface.
2) **Remove ensure-permissions CLI**
- Permissions are **advertised by the node** (e.g., screen recording granted/denied).
- Commands will still fail with explicit errors when permissions are missing.
3) **Mac app installs/symlinks `clawdbot`**
- Bundle a standalone `clawdbot` binary in the app (bun-compiled).
- Install/symlink that binary to `/usr/local/bin/clawdbot` and `/opt/homebrew/bin/clawdbot`.
- No `clawdbot-mac` helper remains.
4) **Canvas parity across node types**
- Use `node.invoke` commands consistently (`canvas.present|navigate|eval|snapshot|a2ui.*`).
- The TS CLI provides convenient wrappers so agents never have to craft raw `node.invoke` calls.
## Command surface (new/normalized)
- `clawdbot nodes invoke --command canvas.*` remains valid.
- New CLI wrappers for convenience:
- `clawdbot canvas present|navigate|eval|snapshot|a2ui push|a2ui reset`
- New node commands (mac-only initially):
- `system.run` (shell execution)
- `system.notify` (local notifications)
## Permission advertising
- Node hello/pairing includes a `permissions` map:
- Example keys: `screenRecording`, `accessibility`, `microphone`, `notifications`, `speechRecognition`.
- Values: boolean (`true` = granted, `false` = not granted).
- Gateway `node.list` / `node.describe` surfaces the map.
## Gateway mode + config
- Gateways should only auto-start when explicitly configured for **local** mode.
- When config is missing or explicitly remote, `clawdbot gateway` should refuse to auto-start unless forced.
## Implementation checklist
- Add bun-compiled `clawdbot` binary to macOS app bundle; update codesign + install flows.
- Remove `ClawdbotCLI` target and control socket server.
- Add node command(s) for `system.run` and `system.notify` on macOS.
- Add permission map to node hello/pairing + gateway responses.
- Update TS CLI + docs to use `clawdbot` only.
-31
View File
@@ -1,31 +0,0 @@
---
summary: "Refactor notes for the macOS gateway client typed API migration (Dec 2025)."
read_when:
- Refactoring macOS gateway client or typed gateway methods
- Auditing agent routing or channel semantics
---
# Gateway Client Refactor (Dec 2025)
Goal: remove stringly-typed gateway calls from the macOS app, centralize routing/channel semantics, and improve error handling.
## Progress
- [x] Fold legacy “AgentRPC” into `GatewayConnection` (single layer; no separate client object).
- [x] Typed gateway API: `GatewayConnection.Method` + `requestDecoded/requestVoid` + typed helpers (status/agent/chat/cron/etc).
- [x] Centralize agent routing/channel semantics via `GatewayAgentChannel` + `GatewayAgentInvocation`.
- [x] Improve gateway error model (structured `GatewayResponseError` + decoding errors include method).
- [x] Migrate mac call sites to typed helpers (leave only intentionally dynamic forwarding paths).
- [x] Convert remaining UI raw channel strings to `GatewayAgentChannel` (Cron editor).
- [x] Cleanup naming: rename remaining tests/docs that still reference “RPC/AgentRPC”.
### Notes
- Intentionally string-based:
- `BridgeServer` dynamic request forwarding (method is data-driven).
- `ControlChannel` request wrapper (generic escape hatch).
## Notes / Non-goals
- No functional behavior changes intended (beyond better errors and removing “magic strings”).
- Keep changes incremental: introduce typed APIs first, then migrate call sites, then remove old helpers.
-99
View File
@@ -1,99 +0,0 @@
---
summary: "Refactor notes for the macOS gateway client: single shared websocket + follow-ups"
read_when:
- Investigating duplicate/stale Gateway WS connections
- Refactoring macOS gateway client architecture
- Debugging noisy reconnect storms on gateway restart
---
# Gateway Refactor Notes (macOS client)
Last updated: 2025-12-12
This document captures the rationale and outcome of the macOS app’s Gateway client refactor: **one shared websocket connection per app process**, with an in-process event bus for server push frames.
Related docs:
- `docs/refactor/new-arch.md` (overall gateway protocol/server plan)
- `docs/gateway.md` (gateway operations/runbook)
- `docs/presence.md` (presence semantics and dedupe)
- `docs/mac/webchat.md` (WebChat surfaces and debugging)
---
## Background: what was wrong
Symptoms:
- Restarting the gateway produced a *storm* of reconnects/log spam (`gateway/ws in connect`, `hello`, `hello-ok`) and elevated `clients=` counts.
- Even with “one panel open”, the mac app could hold tens of websocket connections to `ws://127.0.0.1:18789`.
Root cause (historical bug):
- The mac app was repeatedly “reconfiguring” a gateway client on a timer (via health polling), creating a new websocket owner each time.
- Old websocket owners were not fully torn down and could keep watchdog/tick tasks alive, leading to **connection accumulation** over time.
---
## What changed
- **One socket owner:** `GatewayConnection.shared` is the only supported entry point for gateway RPC.
- **No global notifications:** server push frames are delivered via `GatewayConnection.shared.subscribe(...) -> AsyncStream<GatewayPush>` (no `NotificationCenter` fan-out).
- **No tunnel side effects:** `GatewayConnection` does not create/ensure SSH tunnels in remote mode; it consumes the already-established forwarded port.
---
## Current architecture (as of 2025-12-12)
Goal: enforce the invariant **“one gateway websocket per app process (per effective config)”**.
Key elements:
- `GatewayConnection.shared` owns the one websocket and is the *only* supported entry point for app code that needs gateway RPC.
- Consumers (e.g. Control UI, agent invocations, SwiftUI WebChat) call `GatewayConnection.shared.request(...)` and do not create their own sockets.
- If the effective connection config changes (local ↔ remote tunnel port, token change), `GatewayConnection` replaces the underlying connection.
- The transport (`GatewayChannelActor`) is an internal detail and forwards push frames back into `GatewayConnection`.
- Server-push frames are delivered via `GatewayConnection.shared.subscribe(...) -> AsyncStream<GatewayPush>` (in-process event bus).
Notes:
- Remote mode requires an SSH control tunnel. `GatewayConnection` **does not** start tunnels; it consumes the already-established forwarded port (owned by `ConnectionModeCoordinator` / `RemoteTunnelManager`).
---
## Design constraints / principles
- **Single ownership:** Exactly one component owns the actual socket and reconnect policy.
- **Explicit config changes:** Recreate/reconnect only when config changes, not as a side effect of periodic work.
- **No implicit fan-out sockets:** Adding new UI features must not accidentally add new persistent gateway connections.
- **Testable seams:** Connection config and websocket session creation should be overridable in tests.
---
## Status / remaining work
- ✅ One shared websocket per app process (per config)
- ✅ Event streaming moved into `GatewayConnection` (`AsyncStream<GatewayPush>`) and replays latest snapshot to new subscribers
- ✅ `NotificationCenter` removed for in-process gateway events (ControlChannel / Instances / WebChatSwiftUI)
- ✅ Remote tunnel lifecycle is not started implicitly by random RPC calls
- ✅ Payload decoding helpers extracted so UI adapters stay thin
- ✅ Dedicated resolved-endpoint publisher for remote mode (`GatewayEndpointStore`)
---
## Testing strategy (what we want to cover)
Minimum invariants:
- Repeated requests under the same config do **not** create additional websocket tasks.
- Concurrent requests still create **exactly one** websocket and reuse it.
- Shutdown prevents any reconnect loop after failures.
- Config changes (token / endpoint) cancel the old socket and reconnect once.
Nice-to-have integration coverage:
- Multiple “consumers” (Control UI + agent invocations + SwiftUI WebChat) all call through the shared connection and still produce only one websocket.
Additional coverage added (macOS):
- Subscribing after connect replays the latest snapshot.
- Sequence gaps emit an explicit `GatewayPush.seqGap(...)` before the corresponding event.
---
## Debug notes (operational)
When diagnosing “too many connections”:
- Prefer counting actual TCP connections on port 18789 and grouping by PID to see which process is holding sockets.
- Gateway `--verbose` prints *every* connect/hello and event broadcast; use it only when needed and filter output if you’re just sanity-checking.
-171
View File
@@ -1,171 +0,0 @@
---
summary: "Implementation plan for the new gateway architecture and protocol"
read_when:
- Executing the gateway refactor
---
# New Gateway Architecture – Implementation Plan (detailed)
Last updated: 2025-12-09
Goal: replace legacy gateway/stdin/TCP control with a single WebSocket Gateway, typed protocol, and first-frame snapshot. No backward compatibility.
---
## Phase 0 — Foundations
- **Naming**: CLI subcommand `clawdbot gateway`; internal namespace `Gateway`.
- **Protocol folder**: create `protocol/` for schemas and build artifacts. ✅ `src/gateway/protocol`.
- **Schema tooling**:
- Prefer **TypeBox** (or ArkType) as source-of-truth types. ✅ TypeBox in `schema.ts`.
- `pnpm protocol:gen`: emits JSON Schema (`dist/protocol.schema.json`). ✅
- `pnpm protocol:gen:swift`: generates Swift `Codable` models (`apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift`). ✅
- AJV compile step for server validators. ✅
- **CI**: add a job that fails if schema or generated Swift is stale. ✅ `pnpm protocol:check` (runs gen + git diff).
## Phase 1 — Protocol specification
- Frames (WS text JSON, all with explicit `type`):
- `req {type:"req", id, method:"connect", params:{minProtocol,maxProtocol,client:{name,version,platform,deviceFamily?,modelIdentifier?,mode,instanceId}, caps, auth:{token?}, locale?, userAgent?}}`
- `res {type:"res", id, ok:true, payload: hello-ok }` (or `ok:false` then close)
- `hello-ok {type:"hello-ok", protocol:<chosen>, server:{version,commit,host,connId}, features:{methods,events}, snapshot:{presence[], health, stateVersion:{presence,health}, uptimeMs}, policy:{maxPayload, maxBufferedBytes, tickIntervalMs}}`
- `req {type:"req", id, method, params?}`
- `res {type:"res", id, ok, payload?, error?}` where `error` = `{code,message,details?,retryable?,retryAfterMs?}`
- `event {type:"event", event, payload, seq?, stateVersion?}` (presence/tick/shutdown/agent)
- `close` (standard WS close codes; policy uses 1008 for slow consumer/unauthorized, 1012/1001 for restart)
- Payload types:
- `PresenceEntry {host, ip, version, platform?, deviceFamily?, modelIdentifier?, mode, lastInputSeconds?, ts, reason?, tags?[], instanceId?}`
- `HealthSnapshot` (match existing `clawdbot health --json` fields)
- `AgentEvent` (streamed tool/output; `{runId, seq, stream, data, ts}`)
- `TickEvent {ts}`
- `ShutdownEvent {reason, restartExpectedMs?}`
- Error codes: `NOT_LINKED`, `AGENT_TIMEOUT`, `INVALID_REQUEST`, `UNAVAILABLE`.
- Error shape: `{code, message, details?, retryable?, retryAfterMs?}`
- Rules:
- First frame must be `req` with `method:"connect"`; otherwise close. Add handshake timeout (e.g., 3s) for silent clients.
- Negotiate protocol: server picks within `[minProtocol,maxProtocol]`; if none, reply `res ok:false` and close.
- Protocol version bump on breaking changes; `hello-ok` must include `minClient` when needed.
- `stateVersion` increments for presence/health to drop stale deltas.
- Stable IDs: client sends `instanceId`; server issues per-connection `connId` in `hello-ok`; presence entries may include `instanceId` to dedupe reconnects.
- Token-based auth: bearer token in `auth.token`; required except for loopback development.
- Presence is primarily connection-derived; client may add hints (e.g., lastInputSeconds); entries expire via TTL to keep the map bounded (e.g., 5m TTL, max 200 entries).
- Idempotency keys: required for `send` and `agent` to safely retry after disconnects.
- Size limits: bound first-frame size by `maxPayload`; reject early if exceeded.
- Close on any non-JSON or wrong `type` before connect.
- Per-op idempotency keys: client SHOULD supply an explicit key per `send`/`agent`; if omitted, server may derive a scoped key from `instanceId+connId`, but explicit keys are safer across reconnects.
- Locale/userAgent are informational; server may log them for analytics but must not rely on them for access control.
## Phase 2 — Gateway WebSocket server
- New module `src/gateway/server.ts`:
- Bind 127.0.0.1:18789 (configurable).
- On connect: validate `connect` params, send snapshot payload, start event pump.
- Per-connection queues with backpressure (bounded; drop oldest non-critical).
- WS-level caps: set `maxPayload` to cap frame size before JSON parse.
- Emit `tick` every N seconds when idle (or WS ping/pong if adequate).
- Emit `shutdown` before exit; then close sockets.
- Methods implemented:
- `health`, `status`, `system-presence`, `system-event`, `send`, `agent`.
- Optional: `set-heartbeats` removed/renamed if heartbeat concept is retired.
- Events implemented:
- `agent`, `presence` (deltas, with `stateVersion`), `tick`, `shutdown`.
- All events include `seq` for loss/out-of-order detection.
- Logging: structured logs on connect/close/error; include client fingerprint.
- Slow consumer policy:
- Per-connection outbound queue limit (bytes/messages). If exceeded, drop non-critical events (presence/tick) or close with a policy violation / retryable code; clients reconnect with backoff.
- Handshake edge cases:
- Close on handshake timeout.
- Close on over-limit first frame (maxPayload).
- Close immediately on non-JSON or wrong `type` before connect.
- Default guardrails: `maxPayload` ~512 KB, handshake timeout ~3 s, outbound buffered amount cap ~1.5 MB (tune as you implement).
- Dedupe cache: bound TTL (~5m) and max size (~1000 entries); evict oldest first (LRU) to prevent memory growth.
## Phase 3 — Gateway CLI entrypoint
- Add `clawdbot gateway` command in CLI program:
- Reads config (port, WS options).
- Foreground process; exit non-zero on fatal errors.
- Flags: `--port`, `--no-tick` (optional), `--log-json` (optional).
- System supervision docs for launchd/systemd (see `gateway.md`).
## Phase 4 — Presence/health snapshot & stateVersion
- `hello-ok.snapshot` includes:
- `presence[]` (current list)
- `health` (full snapshot)
- `stateVersion {presence:int, health:int}`
- `uptimeMs`
- `policy {maxPayload, maxBufferedBytes, tickIntervalMs}`
- Emit `presence` deltas with updated `stateVersion.presence`.
- Emit `tick` to indicate liveness when no other events occur.
- Keep `health` method for manual refresh; not required after connect.
- Presence expiry: prune entries older than TTL; enforce a max map size; include `stateVersion` in presence events.
## Phase 5 — Clients migration
- **macOS app**:
- Replace stdio/SSH RPC with WS client (tunneled via SSH/Tailscale for remote). ✅ GatewayConnection/ControlChannel now use Gateway WS.
- Implement handshake, snapshot hydration, subscriptions to `presence`, `tick`, `agent`, `shutdown`. ✅ snapshot + presence events broadcast to InstancesStore; agent events still to wire to UI if desired.
- Remove immediate `health/system-presence` fetch on connect. ✅ presence hydrated from snapshot; periodic refresh kept as fallback.
- Handle connect failures (`res ok:false`) and retry with backoff if version/token mismatched. ✅ macOS GatewayChannel reconnects with exponential backoff.
- **CLI**:
- Add lightweight WS client helper for `status/health/send/agent` when Gateway is up. ✅ `gateway` subcommands use the Gateway over WS.
- Consider a “local only” flag to avoid accidental remote connects. (optional; not needed with tunnel-first model.)
- **WebChat backend**:
- Single WS to Gateway; seed UI from snapshot; forward `presence/tick/agent` to browser. ✅ implemented via the WebChat gateway client in `webchat/server.ts`.
- Fail fast if handshake fails; no fallback transports. ✅ (webchat returns gateway unavailable)
## Phase 6 — Send/agent path hardening
- Ensure only the Gateway can open Baileys; no IPC fallback.
- `send` executes in-process; respond with explicit result/error, not via heartbeat.
- `agent` spawns Pi; respond quickly with `{runId,status:"accepted"}` (ack); stream `event:agent {runId, seq, stream, data, ts}`; final `res:agent {runId, status:"ok"|"error", summary}` completes request (idempotent via key).
- Idempotency: side-effecting methods (`send`, `agent`) accept an idempotency key; keep a short-lived dedupe cache to avoid double-send on client retries. Client retry flow: on timeout/close, retry with same key; Gateway returns cached result when available; cache TTL ~5m and bounded.
- Agent stream ordering: enforce monotonic `seq` per runId; if gap detected by server, terminate stream with error; if detected by client, issue a retry with same idempotency key.
- Send response shape: `{messageId?, toJid?, error?}` and always include `runId` when available for traceability.
## Phase 7 — Keepalive and shutdown semantics
- Keepalive: `tick` events (or WS ping/pong) at fixed interval; clients treat missing ticks as disconnect and reconnect.
- Shutdown: send `event:shutdown {reason, restartExpectedMs?}` then close sockets; clients auto-reconnect.
- Restart semantics: close sockets with a standard retryable close code; on reconnect, `hello-ok` snapshot must be sufficient to rebuild UI without event replay.
- Use a standard close code (e.g., 1012 service restart or 1001 going away) for planned restart; 1008 policy violation for slow consumers.
- Include `policy` in `hello-ok` so clients know the tick interval and buffer limits to tune their expectations.
## Phase 8 — Cleanup and deprecation
- Retire `clawdbot rpc` as default path; keep only if explicitly requested (documented as legacy).
- Remove reliance on `src/infra/control-channel.ts` for new clients; mark as legacy or delete after migration. ✅ file removed; mac app now uses Gateway WS.
- Update README, docs (`architecture.md`, `gateway.md`, `webchat.md`) to final shapes; remove `control-api.md` references if obsolete.
- Presence hygiene:
- Presence derived primarily from connection (server-fills host/ip/version/connId/instanceId); allow client hints (e.g., lastInputSeconds).
- Add TTL/expiry; prune to keep map bounded (e.g., 5m TTL, max 200 entries).
## Edge cases and ordering
- Event ordering: all events carry `seq`; clients detect gaps and should re-fetch snapshot (or targeted refresh) on gap.
- Partial handshakes: if client connects and never sends `req:connect`, server closes after handshake timeout.
- Garbage/oversize first frame: bounded by `maxPayload`; server closes immediately on parse failure.
- Duplicate delivery on reconnect: clients must send idempotency keys; Gateway dedupe cache prevents double-send/agent execution.
- Snapshot sufficiency: `hello-ok.snapshot` must contain enough to render UI after reconnect without event replay.
- Client reconnect guidance: exponential backoff with jitter; reuse same `instanceId` across reconnects to avoid duplicate presence; resend idempotency keys for in-flight sends/agents; on seq gap, issue `health`/`system-presence` refresh.
- Presence TTL/defaults: set a concrete TTL (e.g., 5 minutes) and prune periodically; cap the presence map size with LRU if needed.
- Replay policy: if seq gap detected, server does not replay; clients must pull fresh `health` + `system-presence` and continue.
## Phase 9 — Testing & validation
- Unit: frame validation, handshake failure, auth/token, stateVersion on presence events, agent stream fanout, send dedupe. ✅
- Integration: connect → snapshot → req/res → streaming agent → shutdown. ✅ Covered in gateway WS tests (connect/health/status/presence, agent ack+final, shutdown broadcast).
- Load: multiple concurrent WS clients; backpressure behavior under burst. ✅ Basic fanout test with 3 clients receiving presence broadcast; heavier soak still recommended.
- Mac app smoke: presence/health render from snapshot; reconnect on tick loss. (Manual: open Instances tab, verify snapshot after connect, induce seq gap by toggling wifi, ensure UI refreshes.)
- WebChat smoke: snapshot seed + event updates; tunnel scenario. ✅ Offline snapshot harness in `src/webchat/server.test.ts` (mock gateway) now passes; live tunnel still recommended for manual.
- Idempotency tests: retry send/agent with same key after forced disconnect; expect deduped result. ✅ send + agent dedupe + reconnect retry covered in gateway tests.
- Seq-gap handling: ✅ clients now detect seq gaps (WebChat gateway client + mac `GatewayConnection/GatewayChannel`) and refresh health/presence (webchat) or trigger UI refresh (mac). Load-test still optional.
## Phase 10 — Rollout
- Version bump; release notes: breaking change to control plane (WS only).
- Ship launchd/systemd templates for `clawdbot gateway`.
- Recommend Tailscale/SSH tunnel for remote access; no additional auth layer assumed in this model.
---
- Quick checklist
- [x] Protocol types & schemas (TS + JSON Schema + Swift via quicktype)
- [x] AJV validators wired
- [x] WS server with connect → snapshot → events
- [x] Tick + shutdown events
- [x] stateVersion + presence deltas
- [x] Gateway CLI command
- [x] macOS app WS client (Gateway WS for control; presence events live; agent stream UI pending)
- [x] WebChat WS client
- [x] Remove legacy stdin/TCP paths from default flows (file removed; mac app/CLI on Gateway)
- [x] Tests (unit/integration/load) — unit + integration + basic fanout/reconnect; heavier load/soak optional
- [x] Docs updated and legacy docs flagged
-26
View File
@@ -1,26 +0,0 @@
---
summary: "Refactor plan: Gateway TUI parity with pi-mono interactive UI"
read_when:
- Building or refactoring the Gateway TUI
- Syncing TUI slash commands with Clawdbot behavior
---
# Gateway TUI refactor plan
Updated: 2026-01-03
## Goals
- Match pi-mono interactive TUI feel (editor, streaming, tool cards, selectors).
- Keep Clawdbot semantics: Gateway WS only, session store owns state, no branching/export.
- Work locally or remotely via Gateway URL/token.
## Non-goals
- Branching, export, OAuth flows, or hook UIs.
- File-system operations on the Gateway host from the TUI.
## Checklist
- [x] Protocol + server: sessions.patch supports model overrides; agent events include tool results (text-only payloads).
- [x] Gateway TUI client: add session/model helpers + stricter typing.
- [x] TUI UI kit: theme + components (editor, message feed, tool cards, selectors).
- [x] TUI controller: keybindings + Clawdbot slash commands + history/stream wiring.
- [x] Docs + changelog updated for the new TUI behavior.
- [x] Gate: lint, build, tests, docs list.
@@ -1,37 +0,0 @@
---
summary: "Troubleshooting guide for the web gateway/Baileys stack"
read_when:
- Diagnosing web gateway socket or login issues
---
# Web Gateway Troubleshooting (Nov 26, 2025)
## Symptoms & quick fixes
- **Stream Errored / Conflict / status 409–515:** WhatsApp closed the socket because another session is active or creds went stale. Run `clawdbot logout`, then `clawdbot login`, then restart the Gateway.
- **Logged out:** Console prints “session logged out”; re-link with `clawdbot login`.
- **Repeated retries then exit:** Tune reconnect behavior via config `web.reconnect` and restart the Gateway.
- **No inbound messages:** Ensure the QR-linked account is online in WhatsApp, and check logs for `web-heartbeat` to confirm auth age/connection.
- **Status 515 right after pairing:** The QR login flow now auto-restarts once; you should not need a manual gateway restart after scanning.
- **Fast nuke:** From an allowed WhatsApp sender you can send `/restart` to request a supervised restart (launchd/mac app setups); wait a few seconds for it to come back.
## Helpful commands
- Start the Gateway: `clawdbot gateway --verbose`
- Logout (clear creds): `clawdbot logout`
- Relink (show QR): `clawdbot login --verbose`
- Tail logs (default): `tail -f /tmp/clawdbot/clawdbot-*.log`
## Reading the logs
- `web-reconnect`: close reasons, retry/backoff, max-attempt exit.
- `web-heartbeat`: connectionId, messagesHandled, authAgeMs, uptimeMs (every 60s by default).
- `web-auto-reply`: inbound/outbound message records with correlation IDs.
## When to tweak knobs
- High churn networks: increase `web.reconnect.maxAttempts`.
- Slow links: raise `web.reconnect.maxMs` to give more headroom before bailing.
- Chatty monitors: increase `web.heartbeatSeconds` if log volume is high.
## If it keeps failing
1) `clawdbot logout` → `clawdbot login` (fresh QR link).
2) Ensure no other device/browser is using the same WA Web session.
3) Check WhatsApp mobile app is online and not in low-power mode.
4) If status is 515, let the client restart once after pairing (already handled automatically).
5) Capture the last `web-reconnect` entry and the status code before escalating.
-44
View File
@@ -1,44 +0,0 @@
---
summary: "WebChat session migration notes (Gateway WS-only)"
read_when:
- Changing WebChat Gateway methods/events
---
# WebAgent session migration (WS-only)
Context: web chat currently lives in a WKWebView that loads the pi-web bundle. Sends go over HTTP `/rpc` to the webchat server, and updates come from `/socket` snapshots based on session JSONL file changes. The Gateway itself already speaks WebSocket to the webchat server, and Pi writes the session JSONL files. This doc tracks the plan to move WebChat to a single Gateway WebSocket and drop the HTTP shim/file-watching.
## Target state
- Gateway WS adds methods:
- `chat.history { sessionKey }` → `{ sessionKey, messages[], thinkingLevel }` (reads the existing JSONL + session store).
- `chat.send { sessionKey, message, attachments?, thinking?, deliver?, timeoutMs<=30000, idempotencyKey }` → `res { runId, status:"accepted" }` or `res ok:false` on validation/timeout.
- Gateway WS emits `chat` events `{ runId, sessionKey, seq, state:"delta"|"final"|"error", message?, errorMessage?, usage?, stopReason? }`. Streaming is optional; minimum is a single `state:"final"` per send.
- Client consumes only WS: bootstrap via `chat.history`, send via `chat.send`, live updates via `chat` events. No file watchers.
- Health gate: client subscribes to `health` and blocks send when health is not OK; 30s client-side timeout for sends.
- Tunneling: only the Gateway WS port needs to be forwarded; HTTP server remains for static assets but no RPC endpoints.
## Server work (Node)
- Implement `chat.history` and `chat.send` handlers in `src/gateway/server.ts`; update protocol schemas/tests.
- Emit `chat` events by plumbing `agentCommand`/`emitAgentEvent` outputs; include assistant text/tool results.
- Remove `/rpc` and `/socket` routes + file-watch broadcast from `src/webchat/server.ts`; leave static host only.
## Client work (pi-web bundle)
- Replace `NativeTransport` with a Gateway WS client:
- `connect` → `chat.history` for initial state.
- Listen to `chat/presence/tick/health`; update UI from events only.
- Send via `chat.send`; mark pending until `chat state:final|error`.
- Enforce health gate + 30s timeout.
- Remove reliance on session file snapshots and `/rpc`.
## Persistence
- Keep passing `--session <.../.clawdbot/sessions/{{SessionId}}.jsonl>` to Pi so it continues writing JSONL. The WS history reader uses the same file; no new store introduced.
## Docs to update when shipping
- `docs/webchat.md` (WS-only flow, methods/events, health gate, tunnel WS port).
- `docs/mac/webchat.md` (WKWebView now talks Gateway WS; `/rpc`/file-watch removed).
- `docs/architecture.md` / `typebox.md` if protocol methods are listed.
- Optional: add a concise Gateway chat protocol appendix if needed.
## Open decisions
- Streaming granularity: start with `state:"final"` only, or include token/tool deltas immediately?
- Attachments over WS: text-only initially is OK; confirm before wiring binary/upload path.
- Error shape: use `res ok:false` for validation/timeout, `chat state:"error"` for model/runtime failures.
+5
View File
@@ -1,3 +1,8 @@
---
summary: "SSH tunnel setup for Clawdbot.app connecting to a remote gateway"
read_when: "Connecting the macOS app to a remote gateway over SSH"
---
# Running Clawdbot.app with a Remote Gateway
Clawdbot.app uses SSH tunneling to connect to a remote gateway. This guide shows you how to set it up.
+35
View File
@@ -12,6 +12,7 @@ Goal: small, hard-to-misuse tool surface so agents can list sessions, fetch hist
- `sessions_list`
- `sessions_history`
- `sessions_send`
- `sessions_spawn`
## Key Model
- Main direct chat bucket is always the literal key `"main"`.
@@ -34,6 +35,7 @@ Parameters:
Behavior:
- `messageLimit > 0` fetches `chat.history` per session and includes the last N messages.
- Tool results are filtered out in list output; use `sessions_history` for tool messages.
- When running in a **sandboxed** agent session, session tools default to **spawned-only visibility** (see below).
Row shape (JSON):
- `key`: session key (string)
@@ -117,3 +119,36 @@ Runtime override (per session entry):
Enforcement points:
- `chat.send` / `agent` (gateway)
- auto-reply delivery logic
## sessions_spawn
Spawn a sub-agent run in an isolated session and announce the result back to the requester chat surface.
Parameters:
- `task` (required)
- `label?` (optional; used for logs/UI)
- `timeoutSeconds?` (default 0; 0 = fire-and-forget)
- `cleanup?` (`delete|keep`, default `delete`)
Behavior:
- Starts a new `subagent:<uuid>` session with `deliver: false`.
- Sub-agents default to the full tool surface **minus session tools** (configurable via `agent.subagents.tools`).
- Sub-agents are not allowed to call `sessions_spawn` (no sub-agent → sub-agent spawning).
- After completion (or best-effort wait), Clawdbot runs a sub-agent **announce step** and posts the result to the requester chat surface.
- Reply exactly `ANNOUNCE_SKIP` during the announce step to stay silent.
## Sandbox Session Visibility
Sandboxed sessions can use session tools, but by default they only see sessions they spawned via `sessions_spawn`.
Config:
```json5
{
agent: {
sandbox: {
// default: "spawned"
sessionToolsVisibility: "spawned" // or "all"
}
}
}
```
+1
View File
@@ -77,6 +77,7 @@ Runtime override (owner only):
- `pnpm clawdbot sessions --json` — dumps every entry (filter with `--active <minutes>`).
- `pnpm clawdbot gateway call sessions.list --params '{}'` — fetch sessions from the running gateway (use `--url`/`--token` for remote gateway access).
- Send `/status` in chat to see whether the agent is reachable, how much of the session context is used, current thinking/verbose toggles, and when your WhatsApp web creds were last refreshed (helps spot relink needs).
- Send `/compact` (optional instructions) to summarize older context and free up window space.
- JSONL transcripts can be opened directly to review full turns.
## Tips
+14 -1
View File
@@ -109,10 +109,23 @@ pnpm clawdbot health
- Keep `~/clawd` and `~/.clawdbot/` as “your stuff”; don’t put personal prompts/config into the `clawdbot` repo.
- Updating source: `git pull` + `pnpm install` (when lockfile changed) + keep using `pnpm gateway:watch`.
## Linux (systemd user service)
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 for you (may prompt for sudo). If it’s still off, run:
```bash
sudo loginctl enable-linger $USER
```
For always-on or multi-user servers, consider a **system** service instead of a
user service (no lingering needed). See `docs/gateway.md` for the systemd notes.
## Related docs
- `docs/gateway.md` (Gateway runbook; flags, supervision, ports)
- `docs/configuration.md` (config schema + examples)
- `docs/discord.md` and `docs/telegram.md` (reply tags + replyToMode settings)
- `docs/clawd.md` (personal assistant setup)
- `docs/clawdbot-mac.md` (macOS app behavior; gateway lifecycle + “Attach only”)
- `docs/macos.md` (macOS app behavior; gateway lifecycle + “Attach only”)
+36
View File
@@ -0,0 +1,36 @@
---
summary: "Real-world showcases of what Clawdbot can do"
read_when:
- You want inspiration or proof of capability
---
# Showcase
Real projects from the community. Highlights from #showcase (Jan 2–5, 2026).
## Automation & real-world outcomes
- **Grocery autopilot (Picnic)** — Skill built around an unofficial Picnic API client. Pulls order history, infers preferred brands, maps recipes to cart, completes order in minutes. https://github.com/timkrase/clawdis-picnic-skill
- **Grocery autopilot (Picnic, alt)** — Another Picnic-based skill built via the `picnic-api` package. https://github.com/MRVDH/picnic-api
- **German rail planning** — Go CLI for Deutsche Bahn; skill picks best connections given time windows and preferences. https://github.com/timkrase/dbrest-cli + https://github.com/timkrase/clawdis-skills/tree/main/db-bahn
- **Accounting intake** — Collect PDFs from email, prep for tax consultant (monthly accounting batch). (No link shared.)
## Knowledge & memory systems
- **WhatsApp memory vault** — Ingests full exports, transcribes 1k+ voice notes, cross‑checks with git logs, outputs linked MD reports + ongoing indexing. (No link shared.)
- **Karakeep semantic search** — Sidecar adds vector search to Karakeep bookmarks (Qdrant + OpenAI/Ollama), includes Clawdis skill. https://github.com/jamesbrooksco/karakeep-semantic-search
- **Inside‑Out‑2 style memory** — Separate memory manager app turns session files into memories → beliefs → self model. (No link shared.)
## Voice, docs, and assistants on the phone
- **Clawdia phone bridge** — Vapi voice assistant ↔ Clawdis HTTP bridge; near‑real‑time phone calls. https://github.com/alejandroOPI/clawdia-bridge
- **Google Docs edit skill** — Rich‑text editing skill built fast with Claude Code. (No link shared.)
- **OpenRouter transcription skill** — Multi‑lingual audio transcription via OpenRouter (Gemini etc). ClawdHub: https://clawdhub.com/obviyus/openrouter-transcribe (user/slug link)
## Infrastructure & deployment
- **Home Assistant OS gateway add‑on** — Clawdbot gateway running on HA OS (Raspberry Pi), with SSH tunnel support + persistent state in /config. https://github.com/ngutman/clawdbot-ha-addon
- **Home Assistant skill** — Control/automate HA via ClawdHub. https://clawdhub.com/skills/homeassistant
- **Nix packaging** — Batteries‑included nixified clawdis config. https://github.com/joshp123/nix-clawdis
- **CalDAV skill** — khal/vdirsyncer based calendar skill. ClawdHub: caldav-calendar → https://clawdhub.com/skills/caldav-calendar
## Home + hardware
- **Roborock integration** — Plugin for robot vacuum control. https://github.com/joshp123/gohome/tree/main/plugins/roborock
## Community builds (non‑Clawdis but made with/around it)
- **StarSwap marketplace** — Full astronomy gear marketplace. https://star-swap.com/
+6 -2
View File
@@ -50,8 +50,12 @@ You can still run Clawdbot on your own Signal account if your goal is “respond
httpHost: "127.0.0.1",
httpPort: 8080,
// Who is allowed to talk to the bot
allowFrom: ["+15557654321"] // your personal number (or "*")
// Who is allowed to talk to the bot (DMs)
allowFrom: ["+15557654321"], // your personal number (or "*")
// Group policy + allowlist
groupPolicy: "open",
groupAllowFrom: ["+15557654321"]
}
}
```
+58 -1
View File
@@ -1,3 +1,8 @@
---
summary: "Slack socket mode setup and Clawdbot config"
read_when: "Setting up Slack or debugging Slack socket mode"
---
# Slack (socket mode)
## Setup
@@ -50,8 +55,14 @@ Use this Slack app manifest to create the app quickly (adjust the name/command i
"channels:history",
"channels:read",
"groups:history",
"groups:read",
"groups:write",
"im:history",
"im:read",
"im:write",
"mpim:history",
"mpim:read",
"mpim:write",
"users:read",
"app_mentions:read",
"reactions:read",
@@ -87,6 +98,44 @@ Use this Slack app manifest to create the app quickly (adjust the name/command i
}
```
## Scopes (current vs optional)
Slack's Conversations API is type-scoped: you only need the scopes for the
conversation types you actually touch (channels, groups, im, mpim). See
https://api.slack.com/docs/conversations-api for the overview.
### Required by current code
- `chat:write` (send/update/delete messages via `chat.postMessage`)
https://api.slack.com/methods/chat.postMessage
- `im:write` (open DMs via `conversations.open` for user DMs)
https://api.slack.com/methods/conversations.open
- `channels:history`, `groups:history`, `im:history`, `mpim:history`
(`conversations.history` in `src/slack/actions.ts`)
https://api.slack.com/methods/conversations.history
- `channels:read`, `groups:read`, `im:read`, `mpim:read`
(`conversations.info` in `src/slack/monitor.ts`)
https://api.slack.com/methods/conversations.info
- `users:read` (`users.info` in `src/slack/monitor.ts` + `src/slack/actions.ts`)
https://api.slack.com/methods/users.info
- `reactions:read`, `reactions:write` (`reactions.get` / `reactions.add`)
https://api.slack.com/methods/reactions.get
https://api.slack.com/methods/reactions.add
- `pins:read`, `pins:write` (`pins.list` / `pins.add` / `pins.remove`)
https://api.slack.com/scopes/pins:read
https://api.slack.com/scopes/pins:write
- `emoji:read` (`emoji.list`)
https://api.slack.com/scopes/emoji:read
- `files:write` (uploads via `files.uploadV2`)
https://api.slack.com/messaging/files/uploading
### Not needed today (but likely future)
- `mpim:write` (only if we add group-DM open/DM start via `conversations.open`)
- `groups:write` (only if we add private-channel management: create/rename/invite/archive)
- `chat:write.public` (only if we want to post to channels the bot isn't in)
https://api.slack.com/scopes/chat:write.public
- `users:read.email` (only if we need email fields from `users.info`)
https://api.slack.com/changelog/2017-04-narrowing-email-access
- `files:read` (only if we start listing/reading file metadata)
## Config
Slack uses Socket Mode only (no HTTP webhook server). Provide both tokens:
@@ -96,6 +145,7 @@ Slack uses Socket Mode only (no HTTP webhook server). Provide both tokens:
"enabled": true,
"botToken": "xoxb-...",
"appToken": "xapp-...",
"groupPolicy": "open",
"dm": {
"enabled": true,
"allowFrom": ["U123", "U456", "*"],
@@ -131,11 +181,18 @@ Tokens can also be supplied via env vars:
- `SLACK_BOT_TOKEN`
- `SLACK_APP_TOKEN`
Ack reactions are controlled globally via `messages.ackReaction` +
`messages.ackReactionScope`.
## Sessions + routing
- DMs share the `main` session (like WhatsApp/Telegram).
- Channels map to `slack:channel:<channelId>` sessions.
- Slash commands use `slack:slash:<userId>` sessions.
## Group policy
- `slack.groupPolicy` controls channel handling (`open|disabled|allowlist`).
- `allowlist` requires channels to be listed in `slack.channels`.
## Delivery targets
Use these with cron/CLI sends:
- `user:<id>` for DMs
@@ -153,6 +210,6 @@ Slack tool actions can be gated with `slack.actions.*`:
| emojiList | enabled | Custom emoji list |
## Notes
- Mention gating is controlled via `slack.channels` (set `requireMention` to `true`).
- Mention gating is controlled via `slack.channels` (set `requireMention` to `true`); `routing.groupChat.mentionPatterns` also count as mentions.
- Reaction notifications follow `slack.reactionNotifications` (use `reactionAllowlist` with mode `allowlist`).
- Attachments are downloaded to the media store when permitted and under the size limit.
+72
View File
@@ -0,0 +1,72 @@
---
summary: "Sub-agents: spawning isolated agent runs that announce results back to the requester chat"
read_when:
- You want background/parallel work via the agent
- You are changing sessions_spawn or sub-agent tool policy
---
# Sub-agents
Sub-agents are background agent runs spawned from an existing agent run. They run in their own session (`subagent:<uuid>`) and, when finished, **announce** their result back to the requester chat surface.
Primary goals:
- Parallelize “research / long task / slow tool” work without blocking the main run.
- Keep sub-agents isolated by default (session separation + optional sandboxing).
- Keep the tool surface hard to misuse: sub-agents do **not** get session tools by default.
- Avoid nested fan-out: sub-agents cannot spawn sub-agents.
## Tool
Use `sessions_spawn`:
- Starts a sub-agent run (`deliver: false`, global lane: `subagent`)
- Then runs an announce step and posts the announce reply to the requester chat surface
Tool params:
- `task` (required)
- `label?` (optional)
- `timeoutSeconds?` (default `0`; `0` = fire-and-forget)
- `cleanup?` (`delete|keep`, default `delete`)
## Announce
Sub-agents report back via an announce step:
- The announce step runs inside the sub-agent session (not the requester session).
- If the sub-agent replies exactly `ANNOUNCE_SKIP`, nothing is posted.
- Otherwise the announce reply is posted to the requester chat surface via the gateway `send` method.
## Tool Policy (sub-agent tools)
By default, sub-agents get **all tools except session tools**:
- `sessions_list`
- `sessions_history`
- `sessions_send`
- `sessions_spawn`
Override via config:
```json5
{
agent: {
subagents: {
maxConcurrent: 1,
tools: {
// deny wins
deny: ["gateway", "cron"],
// if allow is set, it becomes allow-only (deny still wins)
// allow: ["read", "bash", "process"]
}
}
}
}
```
## Concurrency
Sub-agents use a dedicated in-process queue lane:
- Lane name: `subagent`
- Concurrency: `agent.subagents.maxConcurrent` (default `1`)
## Limitations
- Sub-agent announce is **best-effort**. If the gateway restarts, pending “announce back” work is lost.
- Sub-agents still share the same gateway process resources; treat `maxConcurrent` as a safety valve.
+11 -6
View File
@@ -24,8 +24,10 @@ Status: ready for bot-mode use with grammY (long-polling by default; webhook sup
- The webhook listener currently binds to `0.0.0.0:8787` and serves `POST /telegram-webhook` by default.
- If you need a different public port/host, set `telegram.webhookUrl` to the externally reachable URL and use a reverse proxy to forward to `:8787`.
4) Direct chats: user sends the first message; all subsequent turns land in the shared `main` session (default, no extra config).
5) Groups: add the bot, disable privacy mode (or make it admin) so it can read messages; group threads stay on `telegram:group:<chatId>` and require mention/command by default (override via `telegram.groups`).
6) Optional allowlist: use `telegram.allowFrom` for direct chats by chat id (`123456789` or `telegram:123456789`).
5) Groups: add the bot, disable privacy mode (or make it admin) so it can read messages; group threads stay on `telegram:group:<chatId>`. When `telegram.groups` is set, it becomes a group allowlist (use `"*"` to allow all). Mention/command gating defaults come from `telegram.groups`.
6) Optional allowlist:
- Direct chats: `telegram.allowFrom` by chat id (`123456789`, `telegram:123456789`, or `tg:123456789`; prefixes are case-insensitive).
- Groups: set `telegram.groupPolicy = "allowlist"` and list senders in `telegram.groupAllowFrom` (fallback: explicit `telegram.allowFrom`).
## Capabilities & limits (Bot API)
- Sees only messages sent after it’s added to a chat; no pre-history access.
@@ -35,9 +37,10 @@ Status: ready for bot-mode use with grammY (long-polling by default; webhook sup
## Planned implementation details
- Library: grammY is the only client for send + gateway (fetch fallback removed); grammY throttler is enabled by default to stay under Bot API limits.
- Inbound normalization: maps Bot API updates to `MsgContext` with `Surface: "telegram"`, `ChatType: direct|group`, `SenderName`, `MediaPath`/`MediaType` when attachments arrive, `Timestamp`, and reply-to metadata (`ReplyToId`, `ReplyToBody`, `ReplyToSender`) when the user replies; reply context is appended to `Body` as a `[Replying to ...]` block (includes `id:` when available); groups require @bot mention by default (override per chat in config).
- Inbound normalization: maps Bot API updates to `MsgContext` with `Surface: "telegram"`, `ChatType: direct|group`, `SenderName`, `MediaPath`/`MediaType` when attachments arrive, `Timestamp`, and reply-to metadata (`ReplyToId`, `ReplyToBody`, `ReplyToSender`) when the user replies; reply context is appended to `Body` as a `[Replying to ...]` block (includes `id:` when available); groups require @bot mention or a `routing.groupChat.mentionPatterns` match by default (override per chat in config).
- Outbound: text and media (photo/video/audio/document) with optional caption; chunked to limits. Typing cue sent best-effort.
- Config: `TELEGRAM_BOT_TOKEN` env or `telegram.botToken` required; `telegram.groups`, `telegram.allowFrom`, `telegram.mediaMaxMb`, `telegram.replyToMode`, `telegram.proxy`, `telegram.webhookSecret`, `telegram.webhookUrl`, `telegram.webhookPath` supported.
- Config: `TELEGRAM_BOT_TOKEN` env or `telegram.botToken` required; `telegram.groups` (group allowlist + mention defaults), `telegram.allowFrom`, `telegram.groupAllowFrom`, `telegram.groupPolicy`, `telegram.mediaMaxMb`, `telegram.replyToMode`, `telegram.proxy`, `telegram.webhookSecret`, `telegram.webhookUrl`, `telegram.webhookPath` supported.
- Ack reactions are controlled globally via `messages.ackReaction` + `messages.ackReactionScope`.
- Mention gating precedence (most specific wins): `telegram.groups.<chatId>.requireMention` → `telegram.groups."*".requireMention` → default `true`.
Example config:
@@ -48,10 +51,12 @@ Example config:
botToken: "123:abc",
replyToMode: "off",
groups: {
"*": { requireMention: true },
"*": { requireMention: true }, // allow all groups
"123456789": { requireMention: false } // group chat id
},
allowFrom: ["123456789"], // direct chat ids allowed (or "*")
groupPolicy: "allowlist",
groupAllowFrom: ["tg:123456789", "@alice"],
mediaMaxMb: 5,
proxy: "socks5://localhost:9050",
webhookSecret: "mysecret",
@@ -65,7 +70,7 @@ Example config:
## Group etiquette
- Keep privacy mode off if you expect the bot to read all messages; with privacy on, it only sees commands/mentions.
- Make the bot an admin if you need it to send in restricted groups or channels.
- Mention the bot (`@yourbot`) or use commands to trigger; per-group overrides live in `telegram.groups` if you want always-on behavior.
- Mention the bot (`@yourbot`) or use a `routing.groupChat.mentionPatterns` trigger; per-group overrides live in `telegram.groups` if you want always-on behavior. If `telegram.groups` is set, add `"*"` to keep existing allow-all behavior.
## Reply tags
To request a threaded reply, the model can include one tag in its output:
+46 -3
View File
@@ -16,18 +16,28 @@ If `BOOTSTRAP.md` exists, that's your birth certificate. Follow it, figure out w
Before doing anything else:
1. Read `SOUL.md` — this is who you are
2. Read `USER.md` — this is who you're helping
3. Read `memory.md` + today's and yesterday's files in `memory/`
3. Read `memory/YYYY-MM-DD.md` (today + yesterday) for recent context
4. **If in MAIN SESSION** (direct chat with your human): Also read `MEMORY.md`
Don't ask permission. Just do it.
## Memory
You wake up fresh each session. These files are your continuity:
- **Daily notes:** `memory/YYYY-MM-DD.md` (create `memory/` if needed)
- **Long-term:** `memory.md` for durable facts, preferences, open loops
- **Daily notes:** `memory/YYYY-MM-DD.md` (create `memory/` if needed) — raw logs of what happened
- **Long-term:** `MEMORY.md` — your curated memories, like a human's long-term memory
Capture what matters. Decisions, context, things to remember. Skip the secrets unless asked to keep them.
### 🧠 MEMORY.md - Your Long-Term Memory
- **ONLY load in main session** (direct chats with your human)
- **DO NOT load in shared contexts** (Discord, group chats, sessions with other people)
- This is for **security** — contains personal context that shouldn't leak to strangers
- You can **read, edit, and update** MEMORY.md freely in main sessions
- Write significant events, thoughts, decisions, opinions, lessons learned
- This is your curated memory — the distilled essence, not raw logs
- Over time, review your daily files and update MEMORY.md with what's worth keeping
### 📝 Write It Down - No "Mental Notes"!
- **Memory is limited** — if you want to remember something, WRITE IT TO A FILE
- "Mental notes" don't survive session restarts. Files do.
@@ -69,6 +79,29 @@ Vectors + BM25 + reranking finds things even with different wording.
You have access to your human's stuff. That doesn't mean you *share* their stuff. In groups, you're a participant — not their voice, not their proxy. Think before you speak.
### 💬 Know When to Speak!
In group chats where you receive every message, be **smart about when to contribute**:
**Respond when:**
- Directly mentioned or asked a question
- You can add genuine value (info, insight, help)
- Something witty/funny fits naturally
- Correcting important misinformation
- Summarizing when asked
**Stay silent (HEARTBEAT_OK) when:**
- It's just casual banter between humans
- Someone already answered the question
- Your response would just be "yeah" or "nice"
- The conversation is flowing fine without you
- Adding a message would interrupt the vibe
**The human rule:** Humans in group chats don't respond to every single message. Neither should you. Quality > quantity. If you wouldn't send it in a real group chat with friends, don't send it.
**Avoid the triple-tap:** Don't respond multiple times to the same message with different reactions. One thoughtful response beats three fragments.
Participate, don't dominate.
## Tools
Skills provide your tools. When you need one, check its `SKILL.md`. Keep local notes (camera names, SSH details, voice preferences) in `TOOLS.md`.
@@ -118,6 +151,16 @@ When you receive a `HEARTBEAT` message, don't just reply `HEARTBEAT_OK` every ti
- Check on projects (git status, etc.)
- Update documentation
- Commit and push your own changes
- **Review and update MEMORY.md** (see below)
### 🔄 Memory Maintenance (During Heartbeats)
Periodically (every few days), use a heartbeat to:
1. Read through recent `memory/YYYY-MM-DD.md` files
2. Identify significant events, lessons, or insights worth keeping long-term
3. Update `MEMORY.md` with distilled learnings
4. Remove outdated info from MEMORY.md that's no longer relevant
Think of it like a human reviewing their journal and updating their mental model. Daily files are raw notes; MEMORY.md is curated wisdom.
The goal: Be helpful without being annoying. Check in a few times a day, do useful background work, but respect quiet time.
+9 -1
View File
@@ -14,7 +14,7 @@ read_when:
Script: `scripts/bench-model.ts`
Usage:
- `source ~/.profile && pnpm tsx scripts/bench-model.ts --runs 10`
- `source ~/.profile && bun scripts/bench-model.ts --runs 10`
- Optional env: `MINIMAX_API_KEY`, `MINIMAX_BASE_URL`, `MINIMAX_MODEL`, `ANTHROPIC_API_KEY`
- Default prompt: “Reply with a single word: ok. No punctuation or extra text.”
@@ -33,3 +33,11 @@ scripts/e2e/onboard-docker.sh
```
This script drives the interactive wizard via a pseudo-tty, verifies config/workspace/session files, then starts the gateway and runs `clawdbot health`.
## QR import smoke (Docker)
Ensures `qrcode-terminal` loads under Node 22+ in Docker:
```bash
pnpm test:docker:qr
```
+40
View File
@@ -0,0 +1,40 @@
---
summary: "Timezone handling for agents, envelopes, and prompts"
read_when:
- You need to understand how timestamps are normalized for the model
- Configuring the user timezone for system prompts
---
# Timezones
Clawdbot standardizes timestamps so the model sees a **single reference time**.
## Message envelopes (UTC)
Inbound messages are wrapped in an envelope like:
```
[Surface ... 2026-01-05T21:26Z] message text
```
The timestamp in the envelope is **always UTC**, with minutes precision.
## Tool payloads (raw provider data)
Tool calls (`discord.readMessages`, `slack.readMessages`, etc.) return **raw provider timestamps**.
These are typically UTC ISO strings (Discord) or UTC epoch strings (Slack). We do not rewrite them.
## User timezone for the system prompt
Set `agent.userTimezone` to tell the model the user's local time zone. If it is
unset, Clawdbot resolves the **host timezone at runtime** (no config write).
```json5
{
agent: { userTimezone: "America/Chicago" }
}
```
The system prompt includes:
- `User timezone: America/Chicago`
- `Current user time: 2026-01-05 15:26`
+3 -3
View File
@@ -73,7 +73,7 @@ Common parameters:
- `controlUrl` (defaults from config)
- `profile` (optional; defaults to `browser.defaultProfile`)
Notes:
- Requires `browser.enabled=true` in `~/.clawdbot/clawdbot.json`.
- Requires `browser.enabled=true` (default is `true`; set `false` to disable).
- Uses `browser.controlUrl` unless `controlUrl` is passed explicitly.
- All actions accept optional `profile` parameter for multi-instance support.
- When `profile` is omitted, uses `browser.defaultProfile` (defaults to "clawd").
@@ -126,7 +126,7 @@ Core parameters:
- `maxBytesMb` (optional size cap)
Notes:
- Only available when `agent.imageModel` or `agent.imageModelFallbacks` is set.
- Only available when `agent.imageModel` is configured (primary or fallbacks).
- Uses the image model directly (independent of the main chat model).
### `cron`
@@ -139,7 +139,7 @@ Core actions:
Notes:
- `add` expects a full cron job object (same schema as `cron.add` RPC).
- `update` uses `{ jobId, patch }`.
- `update` uses `{ id, patch }`.
### `gateway`
Restart the running Gateway process (in-place).
+34 -4
View File
@@ -29,8 +29,8 @@ cat ~/.clawdbot/clawdbot.json | jq '.whatsapp.allowFrom'
**Check 2:** For group chats, is mention required?
```bash
# The message must match mentionPatterns or explicit mentions; defaults live in whatsapp.groups
cat ~/.clawdbot/clawdbot.json | jq '.routing.groupChat, .whatsapp.groups'
# The message must match mentionPatterns or explicit mentions; defaults live in provider groups/guilds.
cat ~/.clawdbot/clawdbot.json | jq '.routing.groupChat, .whatsapp.groups, .telegram.groups, .imessage.groups, .discord.guilds'
```
**Check 3:** Check the logs
@@ -100,7 +100,7 @@ If you’re logged out / unlinked:
```bash
clawdbot logout
rm -rf ~/.clawdbot/credentials # if logout can't cleanly remove everything
trash ~/.clawdbot/credentials # if logout can't cleanly remove everything
clawdbot login --verbose # re-scan QR
```
@@ -160,6 +160,13 @@ lsof -nP -i :18789
kill -9 <PID>
```
If the gateway is supervised by launchd, killing the PID will just respawn it.
Stop the supervisor instead:
```bash
clawdbot gateway stop
# Or: launchctl bootout gui/$UID/com.clawdbot.gateway
```
**Fix 2: Check embedded gateway**
Ensure the gateway relay was properly bundled. Run `./scripts/package-mac-app.sh` and ensure `bun` is installed.
@@ -203,7 +210,7 @@ tail -20 /tmp/clawdbot/clawdbot-*.log
Nuclear option:
```bash
rm -rf ~/.clawdbot
trash ~/.clawdbot
clawdbot login # re-pair WhatsApp
clawdbot gateway # start the Gateway again
```
@@ -225,3 +232,26 @@ clawdbot gateway # start the Gateway again
*"Have you tried turning it off and on again?"* — Every IT person ever
🦞🔧
### Browser Not Starting (Linux)
If you see `"Failed to start Chrome CDP on port 18800"`:
**Most likely cause:** Snap-packaged Chromium on Ubuntu.
**Quick fix:** Install Google Chrome instead:
```bash
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo dpkg -i google-chrome-stable_current_amd64.deb
```
Then set in config:
```json
{
"browser": {
"executablePath": "/usr/bin/google-chrome-stable"
}
}
```
**Full guide:** See [browser-linux-troubleshooting.md](./browser-linux-troubleshooting.md)
+3 -1
View File
@@ -48,13 +48,15 @@ Use SSH tunneling or Tailscale to reach the Gateway WS.
- `/help`
- `/status`
- `/session <key>` (or `/sessions`)
- `/model <provider/model>` (or `/models`)
- `/model <provider/model>` (or `/model list`, `/models`)
- `/think <off|minimal|low|medium|high>`
- `/verbose <on|off>`
- `/elevated <on|off>`
- `/elev <on|off>`
- `/activation <mention|always>`
- `/deliver <on|off>`
- `/new` or `/reset`
- `/compact [instructions]`
- `/abort`
- `/settings`
- `/exit`
+2 -2
View File
@@ -110,6 +110,6 @@ Open:
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
```
+1 -1
View File
@@ -91,7 +91,7 @@ Mapping options (summary):
- `hooks.mappings` lets you define `match`, `action`, and templates in config.
- `hooks.transformsDir` + `transform.module` loads a JS/TS module for custom logic.
- Use `match.source` to keep a generic ingest endpoint (payload-driven routing).
- TS transforms require a TS loader (e.g. `tsx`) or precompiled `.js` at runtime.
- TS transforms require a TS loader (e.g. `bun`) or precompiled `.js` at runtime.
- `clawdbot hooks gmail setup` writes `hooks.gmail` config for `clawdbot hooks gmail run`.
## Responses
+7 -2
View File
@@ -49,6 +49,8 @@ WhatsApp requires a real mobile number for verification. VoIP and virtual number
- Direct chats use E.164; groups use group JID.
- **Allowlist**: `whatsapp.allowFrom` enforced for direct chats only.
- If `whatsapp.allowFrom` is empty, default allowlist = self number (self-chat mode).
- **Group policy**: `whatsapp.groupPolicy` controls group handling (`open|disabled|allowlist`).
- `allowlist` uses `whatsapp.groupAllowFrom` (fallback: explicit `whatsapp.allowFrom`).
- **Self-chat mode**: avoids auto read receipts and ignores mention JIDs.
- Read receipts sent for non-self-chat DMs.
@@ -69,6 +71,7 @@ WhatsApp requires a real mobile number for verification. VoIP and virtual number
## Groups
- Groups map to `whatsapp:group:<jid>` sessions.
- Group policy: `whatsapp.groupPolicy = open|disabled|allowlist` (default `open`).
- Activation modes:
- `mention` (default): requires @mention or regex match.
- `always`: always triggers.
@@ -118,7 +121,9 @@ WhatsApp requires a real mobile number for verification. VoIP and virtual number
## Config quick map
- `whatsapp.allowFrom` (DM allowlist).
- `whatsapp.groups` (group mention gating defaults/overrides)
- `whatsapp.groupAllowFrom` (group sender allowlist).
- `whatsapp.groupPolicy` (group policy).
- `whatsapp.groups` (group allowlist + mention gating defaults; use `"*"` to allow all)
- `routing.groupChat.mentionPatterns`
- `routing.groupChat.historyLimit`
- `messages.messagePrefix` (inbound prefix)
@@ -136,7 +141,7 @@ WhatsApp requires a real mobile number for verification. VoIP and virtual number
## Logs + troubleshooting
- Subsystems: `whatsapp/inbound`, `whatsapp/outbound`, `web-heartbeat`, `web-reconnect`.
- Log file: `/tmp/clawdbot/clawdbot-YYYY-MM-DD.log` (configurable).
- Troubleshooting guide: `docs/refactor/web-gateway-troubleshooting.md`.
- Troubleshooting guide: `docs/troubleshooting.md`.
## Tests
- `src/web/auto-reply.test.ts` (mention gating, history injection, reply flow)
+11
View File
@@ -0,0 +1,11 @@
---
summary: "Windows app status + contribution call"
read_when:
- Looking for Windows companion app status
- Planning platform coverage or contributions
---
# Windows App
Clawdbot core is fully supported on Windows. The core is written in TypeScript, so it runs anywhere Node runs.
We do not have a Windows companion app yet. It is planned, and we would love contributions to make it happen.
+9 -1
View File
@@ -26,7 +26,7 @@ clawdbot configure
## What the wizard does
**Local mode (default)** walks you through:
- Model/auth (Anthropic OAuth recommended, API key optional, Minimax M2.1 via LM Studio)
- Model/auth (Anthropic or OpenAI Codex OAuth recommended, API key optional, Minimax M2.1 via LM Studio)
- Workspace location + bootstrap files
- Gateway settings (port/bind/auth/tailscale)
- Providers (WhatsApp, Telegram, Discord, Signal)
@@ -48,9 +48,13 @@ It does **not** install or change anything on the remote host.
2) **Model/Auth**
- **Anthropic OAuth (recommended)**: browser flow; paste the `code#state`.
- **OpenAI Codex OAuth**: browser flow; paste the `code#state`.
- Sets `agent.model` to `openai-codex/gpt-5.2` when model is unset or `openai/*`.
- **API key**: stores the key for you.
- **Minimax M2.1 (LM Studio)**: config is auto‑written for the LM Studio endpoint.
- **Skip**: no auth configured yet.
- Wizard runs a model check and warns if the configured model is unknown or missing auth.
- OAuth credentials live in `~/.clawdbot/credentials/oauth.json`; auth profiles live in `~/.clawdbot/agent/auth-profiles.json` (API keys + OAuth).
3) **Workspace**
- Default `~/clawd` (configurable).
@@ -70,8 +74,12 @@ It does **not** install or change anything on the remote host.
6) **Daemon install**
- macOS: LaunchAgent
- Requires a logged-in user session; for headless, use a custom LaunchDaemon (not shipped).
- Linux: systemd user unit
- Wizard attempts to enable lingering via `loginctl enable-linger <user>` so the Gateway stays up after logout.
- May prompt for sudo (writes `/var/lib/systemd/linger`); it tries without sudo first.
- Windows: Scheduled Task
- Runs on user logon; headless/system services are not configured by default.
7) **Health check**
- Starts the Gateway (if needed) and runs `clawdbot health`.
+42 -30
View File
@@ -1,6 +1,6 @@
{
"name": "clawdbot",
"version": "2026.1.5",
"version": "2026.1.5-3",
"description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent",
"type": "module",
"main": "dist/index.js",
@@ -15,17 +15,25 @@
"dist/cli/**",
"dist/commands/**",
"dist/config/**",
"dist/control-ui/**",
"dist/cron/**",
"dist/daemon/**",
"dist/discord/**",
"dist/gateway/**",
"dist/hooks/**",
"dist/imessage/**",
"dist/infra/**",
"dist/macos/**",
"dist/media/**",
"dist/process/**",
"dist/sessions/**",
"dist/providers/**",
"dist/signal/**",
"dist/slack/**",
"dist/telegram/**",
"dist/tui/**",
"dist/web/**",
"dist/wizard/**",
"dist/*.js",
"dist/*.json",
"docs/**",
@@ -36,19 +44,20 @@
"LICENSE"
],
"scripts": {
"dev": "tsx src/entry.ts",
"docs:list": "tsx scripts/docs-list.ts",
"dev": "bun src/entry.ts",
"postinstall": "node scripts/postinstall.js",
"docs:list": "bun scripts/docs-list.ts",
"docs:dev": "cd docs && mint dev",
"docs:build": "cd docs && pnpm dlx mint broken-links",
"build": "tsc -p tsconfig.json && tsx scripts/canvas-a2ui-copy.ts",
"release:check": "tsx scripts/release-check.ts",
"ui:install": "pnpm -C ui install",
"ui:dev": "pnpm -C ui dev",
"ui:build": "pnpm -C ui build",
"start": "tsx src/entry.ts",
"clawdbot": "tsx src/entry.ts",
"gateway:watch": "tsx watch --clear-screen=false --include 'src/**/*.ts' src/entry.ts gateway --force",
"clawdbot:rpc": "tsx src/entry.ts agent --mode rpc --json",
"build": "tsc -p tsconfig.json && bun scripts/canvas-a2ui-copy.ts",
"release:check": "bun scripts/release-check.ts",
"ui:install": "node scripts/ui.js install",
"ui:dev": "node scripts/ui.js dev",
"ui:build": "node scripts/ui.js build",
"start": "bun src/entry.ts",
"clawdbot": "bun src/entry.ts",
"gateway:watch": "bun --watch src/entry.ts gateway --force",
"clawdbot:rpc": "bun src/entry.ts agent --mode rpc --json",
"lint": "biome check src test && oxlint --type-aware src test --ignore-pattern src/canvas-host/a2ui/a2ui.bundle.js",
"lint:swift": "swiftlint lint --config .swiftlint.yml && (cd apps/ios && swiftlint lint --config .swiftlint.yml)",
"lint:fix": "biome check --write --unsafe src && biome format --write src",
@@ -56,11 +65,12 @@
"format:swift": "swiftformat --lint --config .swiftformat apps/macos/Sources apps/ios/Sources apps/shared/ClawdbotKit/Sources",
"format:fix": "biome format src --write",
"test": "vitest",
"test:force": "tsx scripts/test-force.ts",
"test:force": "bun scripts/test-force.ts",
"test:coverage": "vitest run --coverage",
"test:e2e": "vitest run --config vitest.e2e.config.ts",
"protocol:gen": "tsx scripts/protocol-gen.ts",
"protocol:gen:swift": "tsx scripts/protocol-gen-swift.ts",
"test:docker:qr": "bash scripts/e2e/qr-import-docker.sh",
"protocol:gen": "bun scripts/protocol-gen.ts",
"protocol:gen:swift": "bun scripts/protocol-gen-swift.ts",
"protocol:check": "pnpm protocol:gen && pnpm protocol:gen:swift && git diff --exit-code -- dist/protocol.schema.json apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift",
"canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh"
},
@@ -75,13 +85,13 @@
"@clack/prompts": "^0.11.0",
"@grammyjs/transformer-throttler": "^1.2.1",
"@homebridge/ciao": "^1.3.4",
"@mariozechner/pi-agent-core": "^0.32.3",
"@mariozechner/pi-ai": "^0.32.3",
"@mariozechner/pi-coding-agent": "^0.32.3",
"@mariozechner/pi-tui": "^0.32.3",
"@slack/bolt": "^4.5.0",
"@slack/web-api": "^7.11.1",
"@mariozechner/pi-agent-core": "^0.37.2",
"@mariozechner/pi-ai": "^0.37.2",
"@mariozechner/pi-coding-agent": "^0.37.2",
"@mariozechner/pi-tui": "^0.37.2",
"@sinclair/typebox": "0.34.46",
"@slack/bolt": "^4.6.0",
"@slack/web-api": "^7.13.0",
"@whiskeysockets/baileys": "7.0.0-rc.9",
"ajv": "^8.17.1",
"body-parser": "^2.2.1",
@@ -91,24 +101,25 @@
"commander": "^14.0.2",
"croner": "^9.1.0",
"detect-libc": "^2.1.2",
"discord-api-types": "^0.38.33",
"discord-api-types": "^0.38.37",
"discord.js": "^14.25.1",
"dotenv": "^17.2.3",
"express": "^5.2.1",
"file-type": "^21.2.0",
"file-type": "^21.3.0",
"grammy": "^1.39.2",
"json5": "^2.2.3",
"long": "5.3.2",
"playwright-core": "1.57.0",
"proper-lockfile": "^4.1.2",
"qrcode-terminal": "^0.12.0",
"sharp": "^0.34.5",
"tslog": "^4.10.2",
"undici": "^7.16.0",
"ws": "^8.18.3",
"zod": "^4.3.4"
"undici": "^7.18.0",
"ws": "^8.19.0",
"zod": "^4.3.5"
},
"devDependencies": {
"@biomejs/biome": "^2.3.10",
"@biomejs/biome": "^2.3.11",
"@lit-labs/signals": "^0.2.0",
"@lit/context": "^1.1.6",
"@mariozechner/mini-lit": "0.2.1",
@@ -116,6 +127,7 @@
"@types/express": "^5.0.6",
"@types/markdown-it": "^14.1.2",
"@types/node": "^25.0.3",
"@types/proper-lockfile": "^4.1.4",
"@types/qrcode-terminal": "^0.12.2",
"@types/ws": "^8.18.1",
"@vitest/coverage-v8": "^4.0.16",
@@ -125,12 +137,11 @@
"lucide": "^0.562.0",
"markdown-it": "^14.1.0",
"ollama": "^0.6.3",
"oxlint": "^1.36.0",
"oxlint": "^1.37.0",
"oxlint-tsgolint": "^0.10.1",
"quicktype-core": "^23.2.6",
"rolldown": "1.0.0-beta.58",
"signal-utils": "^0.21.1",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vitest": "^4.0.16",
"wireit": "^0.14.12"
@@ -140,7 +151,8 @@
"@sinclair/typebox": "0.34.46"
},
"patchedDependencies": {
"@mariozechner/pi-ai": "patches/@mariozechner__pi-ai.patch"
"@mariozechner/pi-ai": "patches/@mariozechner__pi-ai.patch",
"qrcode-terminal": "patches/qrcode-terminal.patch"
}
},
"vitest": {
+86 -5
View File
@@ -1,8 +1,52 @@
diff --git a/dist/providers/google-shared.js b/dist/providers/google-shared.js
index 7bc0a9f5d6241f191cd607ecb37b3acac8d58267..76166a34784cbc0718d4b9bd1fa6336a6dd394ec 100644
index 7bc0a9f5d6241f191cd607ecb37b3acac8d58267..56866774e47444b5d333961c9b20fce582363124 100644
--- a/dist/providers/google-shared.js
+++ b/dist/providers/google-shared.js
@@ -51,9 +51,19 @@ export function convertMessages(model, context) {
@@ -10,13 +10,27 @@ import { transformMessages } from "./transorm-messages.js";
export function convertMessages(model, context) {
const contents = [];
const transformedMessages = transformMessages(context.messages, model);
+
+ /**
+ * Helper to add content while merging consecutive messages of the same role.
+ * Gemini/Cloud Code Assist requires strict role alternation (user/model/user/model).
+ * Consecutive messages of the same role cause "function call turn" errors.
+ */
+ function addContent(role, parts) {
+ if (parts.length === 0) return;
+ const lastContent = contents[contents.length - 1];
+ if (lastContent?.role === role) {
+ // Merge into existing message of same role
+ lastContent.parts.push(...parts);
+ } else {
+ contents.push({ role, parts });
+ }
+ }
+
for (const msg of transformedMessages) {
if (msg.role === "user") {
if (typeof msg.content === "string") {
- contents.push({
- role: "user",
- parts: [{ text: sanitizeSurrogates(msg.content) }],
- });
+ addContent("user", [{ text: sanitizeSurrogates(msg.content) }]);
}
else {
const parts = msg.content.map((item) => {
@@ -35,10 +49,7 @@ export function convertMessages(model, context) {
const filteredParts = !model.input.includes("image") ? parts.filter((p) => p.text !== undefined) : parts;
if (filteredParts.length === 0)
continue;
- contents.push({
- role: "user",
- parts: filteredParts,
- });
+ addContent("user", filteredParts);
}
}
else if (msg.role === "assistant") {
@@ -51,9 +62,19 @@ export function convertMessages(model, context) {
parts.push({ text: sanitizeSurrogates(block.text) });
}
else if (block.type === "thinking") {
@@ -25,7 +69,7 @@ index 7bc0a9f5d6241f191cd607ecb37b3acac8d58267..76166a34784cbc0718d4b9bd1fa6336a
parts.push({
thought: true,
text: sanitizeSurrogates(block.thinking),
@@ -61,6 +71,7 @@ export function convertMessages(model, context) {
@@ -61,6 +82,7 @@ export function convertMessages(model, context) {
});
}
else {
@@ -33,7 +77,44 @@ index 7bc0a9f5d6241f191cd607ecb37b3acac8d58267..76166a34784cbc0718d4b9bd1fa6336a
parts.push({
text: `<thinking>\n${sanitizeSurrogates(block.thinking)}\n</thinking>`,
});
@@ -146,6 +157,77 @@ export function convertMessages(model, context) {
@@ -85,10 +107,7 @@ export function convertMessages(model, context) {
}
if (parts.length === 0)
continue;
- contents.push({
- role: "model",
- parts,
- });
+ addContent("model", parts);
}
else if (msg.role === "toolResult") {
// Extract text and image content
@@ -125,27 +144,94 @@ export function convertMessages(model, context) {
}
// Cloud Code Assist API requires all function responses to be in a single user turn.
// Check if the last content is already a user turn with function responses and merge.
+ // Use addContent for proper role alternation handling.
const lastContent = contents[contents.length - 1];
if (lastContent?.role === "user" && lastContent.parts?.some((p) => p.functionResponse)) {
lastContent.parts.push(functionResponsePart);
}
else {
- contents.push({
- role: "user",
- parts: [functionResponsePart],
- });
+ addContent("user", [functionResponsePart]);
}
// For older models, add images in a separate user message
+ // Note: This may create consecutive user messages, but addContent will merge them
if (hasImages && !supportsMultimodalFunctionResponse) {
- contents.push({
- role: "user",
- parts: [{ text: "Tool result image:" }, ...imageParts],
- });
+ addContent("user", [{ text: "Tool result image:" }, ...imageParts]);
}
}
}
return contents;
}
@@ -111,7 +192,7 @@ index 7bc0a9f5d6241f191cd607ecb37b3acac8d58267..76166a34784cbc0718d4b9bd1fa6336a
/**
* Convert tools to Gemini function declarations format.
*/
@@ -157,7 +239,7 @@ export function convertTools(tools) {
@@ -157,7 +243,7 @@ export function convertTools(tools) {
functionDeclarations: tools.map((tool) => ({
name: tool.name,
description: tool.description,
@@ -1,17 +0,0 @@
diff --git a/dist/config.js b/dist/config.js
index 7caa66d2676933b102431ec8d92c571eb9d6d82c..77103b9d9573e56c26014c8c7c918e1f853afcdc 100644
--- a/dist/config.js
+++ b/dist/config.js
@@ -10,8 +10,11 @@ const __dirname = dirname(__filename);
/**
* Detect if we're running as a Bun compiled binary.
* Bun binaries have import.meta.url containing "$bunfs", "~BUN", or "%7EBUN" (Bun's virtual filesystem path)
+ * Some packaging workflows keep import.meta.url as a file:// path, so fall back to execPath next to package.json.
*/
-export const isBunBinary = import.meta.url.includes("$bunfs") || import.meta.url.includes("~BUN") || import.meta.url.includes("%7EBUN");
+const bunBinaryByUrl = import.meta.url.includes("$bunfs") || import.meta.url.includes("~BUN") || import.meta.url.includes("%7EBUN");
+const bunBinaryByExecPath = existsSync(join(dirname(process.execPath), "package.json"));
+export const isBunBinary = bunBinaryByUrl || bunBinaryByExecPath;
// =============================================================================
// Package Asset Paths (shipped with executable)
// =============================================================================
+12
View File
@@ -0,0 +1,12 @@
diff --git a/lib/main.js b/lib/main.js
index 488cc1aea9802b3d6ae13aee27556403bec55d1c..3de1f934868d81e8204f00e6a4bf2696a05f7340 100644
--- a/lib/main.js
+++ b/lib/main.js
@@ -1,5 +1,5 @@
-var QRCode = require('./../vendor/QRCode'),
- QRErrorCorrectLevel = require('./../vendor/QRCode/QRErrorCorrectLevel'),
+var QRCode = require('./../vendor/QRCode/index.js'),
+ QRErrorCorrectLevel = require('./../vendor/QRCode/QRErrorCorrectLevel.js'),
black = "\033[40m \033[0m",
white = "\033[47m \033[0m",
toCell = function (isBlack) {
Loaded 100 of 362 files, more files were not shown because too many files have changed in this diff. Show more