Compare commits

...

231 Commits

Author SHA1 Message Date
Peter Steinberger
3ad11051c3 docs: expand Slack scope notes 2026-01-06 01:52:08 +01:00
Peter Steinberger
3b6b8e7696 Merge branch 'main' into docs/fix-slack-scopes 2026-01-06 00:39:12 +00: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 Steinberger
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
Manuel Hettich
f1b023dc6c docs(slack): add missing scopes for DM replies
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.
2026-01-05 19:14:31 +00:00
Peter Steinberger
38e63cbe0e docs: refresh README + architecture links 2026-01-05 20:10:56 +01:00
Peter Steinberger
c75b2a7067 refactor: unify reply dispatch across providers 2026-01-05 19:43:54 +01:00
Peter Steinberger
bfe7f5f126 docs: add recommended source setup 2026-01-05 19:40:05 +01:00
Peter Steinberger
cc790f2c84 docs(agent): annotate stream invariants 2026-01-05 18:10:03 +00:00
Peter Steinberger
86ad703f53 refactor(agent): extract block chunker + tool adapter 2026-01-05 18:05:40 +00:00
Peter Steinberger
7c89ce93b5 fix(agent): align tools + preserve indentation 2026-01-05 17:55:20 +00:00
Peter Steinberger
196eb86e38 fix(ui): animate reading indicator dots 2026-01-05 17:40:15 +00:00
Peter Steinberger
ad6bec4612 fix: enable systemd lingering for gateway 2026-01-05 18:38:43 +01:00
Peter Steinberger
0fb30db819 test: expand fenced block chunking coverage 2026-01-05 18:38:43 +01:00
Peter Steinberger
22105c8496 fix(agent): finalize block chunking 2026-01-05 17:22:29 +00:00
Peter Steinberger
b7e708c764 fix(chat): stabilize web UI tool runs 2026-01-05 17:22:29 +00:00
Peter Steinberger
86c404c48b chore: fix reply commands lint 2026-01-05 18:16:39 +01:00
Peter Steinberger
f0abd619be chore: add model-usage skill 2026-01-05 18:16:29 +01:00
Julian Engel
110e2255c4 fix: pass custom tools via customTools parameter to pi-coding-agent SDK
The SDK's tools parameter only accepts built-in tools (read, bash, edit, write).
Custom clawdbot tools (browser, canvas, nodes, cron, etc.) were being filtered
out, causing 'Tool not found' errors at runtime.

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

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

why: isolate multi-instance gateways

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

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

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

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

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

* fix(mac): make generated protocol types Sendable

* chore(mac): drop redundant Sendable extensions

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

* chore(swiftformat): exclude generated protocol models

---------

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

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

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

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

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

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

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

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

Fixes the parameter name while keeping the external tool API unchanged (still
accepts 'jobId' from callers).
2026-01-04 17:18:29 +01:00
Peter Steinberger
4e072d59c1 chore(protocol): regenerate GatewayModels 2026-01-04 16:05:47 +00:00
Peter Steinberger
94da41dc52 docs: document sandbox image recovery 2026-01-04 16:02:28 +00:00
Peter Steinberger
718299b25a feat(doctor): repair sandbox images 2026-01-04 16:02:24 +00:00
Peter Steinberger
e80bd1882f chore: bump Peekaboo submodule 2026-01-04 16:02:16 +00:00
Peter Steinberger
ca09078934 docs: add Discord writing style guide to skill 2026-01-04 15:42:32 +00:00
Peter Steinberger
c54fcd1e74 docs: document legacy doctor migrations 2026-01-04 15:41:25 +00:00
Peter Steinberger
5f09d801d0 feat(doctor): migrate legacy Clawdis config 2026-01-04 15:40:06 +00:00
Peter Steinberger
65ad956ab4 feat(daemon): add legacy Clawdis service cleanup 2026-01-04 15:40:06 +00:00
Peter Steinberger
20e41c5a10 docs: update changelog and README 2026-01-04 16:36:40 +01:00
Peter Steinberger
5d29985c4f fix: avoid sendable issue in mac location timeout 2026-01-04 16:27:17 +01:00
Peter Steinberger
026a25d164 chore: lint and format cleanup 2026-01-04 16:24:17 +01:00
Peter Steinberger
fd95ededaa refactor: streamline node invoke handling 2026-01-04 16:24:17 +01:00
Peter Steinberger
c0b248f291 refactor: split connections settings/store 2026-01-04 16:24:17 +01:00
Peter Steinberger
e8de7d083d feat: update onboard ASCII art to seafood shack lobster theme 2026-01-04 16:24:17 +01:00
Peter Steinberger
21826cdfb9 chore: update Peekaboo submodule 2026-01-04 16:24:17 +01:00
Peter Steinberger
8f53e9093d test: align google-shared expectations 2026-01-04 15:02:42 +00:00
Peter Steinberger
30d5511058 test: add config for gateway sigterm 2026-01-04 14:59:49 +00:00
Peter Steinberger
c6b8235862 style: format tests and helpers 2026-01-04 14:57:57 +00:00
Peter Steinberger
557aa74ee8 test: update google-shared expectations 2026-01-04 14:57:57 +00:00
Peter Steinberger
7ff318d3f2 docs: note canvasHost reload requires restart 2026-01-04 15:45:42 +01:00
Peter Steinberger
8ff802a072 chore: bump Peekaboo submodule 2026-01-04 14:42:12 +00:00
Peter Steinberger
b79fdd2be8 chore: ignore module cache 2026-01-04 14:41:25 +00:00
Peter Steinberger
246adaa119 chore: rename project to clawdbot 2026-01-04 14:38:51 +00:00
Peter Steinberger
d48dc71fa4 feat: add canvasHost liveReload option 2026-01-04 15:22:47 +01:00
Peter Steinberger
1e555e693a fix: dedupe canvas host watcher 2026-01-04 15:15:46 +01:00
Peter Steinberger
ec09b06636 fix: wire slack deps and stabilize sigterm test 2026-01-04 15:13:23 +01:00
992 changed files with 23682 additions and 12055 deletions

7
.gitignore vendored
View File

@@ -15,13 +15,14 @@ ui/test-results/
# Bun build artifacts
*.bun-build
apps/macos/.build/
apps/shared/ClawdisKit/.build/
apps/shared/ClawdbotKit/.build/
**/ModuleCache/
bin/
bin/clawdis-mac
bin/clawdbot-mac
bin/docs-list
apps/macos/.build-local/
apps/macos/.swiftpm/
apps/shared/ClawdisKit/.swiftpm/
apps/shared/ClawdbotKit/.swiftpm/
Core/
apps/ios/*.xcodeproj/
apps/ios/*.xcworkspace/

View File

@@ -48,4 +48,4 @@
--allman false
# Exclusions
--exclude .build,.swiftpm,DerivedData,node_modules,dist,coverage,xcuserdata,apps/macos/Sources/ClawdisProtocol
--exclude .build,.swiftpm,DerivedData,node_modules,dist,coverage,xcuserdata,apps/macos/Sources/ClawdisProtocol,apps/macos/Sources/ClawdbotProtocol

View File

@@ -108,6 +108,10 @@ function_body_length:
warning: 150
error: 300
function_parameter_count:
warning: 7
error: 10
file_length:
warning: 1500
error: 2500

View File

@@ -7,7 +7,7 @@
## Build, Test, and Development Commands
- Install deps: `pnpm install`
- Run CLI in dev: `pnpm clawdis ...` (tsx entry) or `pnpm dev` for `src/index.ts`.
- Run CLI in dev: `pnpm clawdbot ...` (tsx entry) or `pnpm dev` for `src/index.ts`.
- Type-check/build: `pnpm build` (tsc)
- Lint/format: `pnpm lint` (biome check), `pnpm format` (biome format)
- Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage`
@@ -32,47 +32,49 @@
- PRs should summarize scope, note testing performed, and mention any user-facing changes or new flags.
## Security & Configuration Tips
- Web provider stores creds at `~/.clawdis/credentials/`; rerun `clawdis login` if logged out.
- Pi sessions live under `~/.clawdis/sessions/` by default; the base directory is not configurable.
- Web provider stores creds at `~/.clawdbot/credentials/`; rerun `clawdbot login` if logged out.
- Pi sessions live under `~/.clawdbot/sessions/` by default; the base directory is not configurable.
- Never commit or publish real phone numbers, videos, or live configuration values. Use obviously fake placeholders in docs, tests, and examples.
## Agent-Specific Notes
- Gateway currently runs only as the menubar app (launchctl shows `application.com.steipete.clawdis.debug.*`), there is no separate LaunchAgent/helper label installed. Restart via the Clawdis Mac app or `scripts/restart-mac.sh`; to verify/kill use `launchctl print gui/$UID | grep clawdis` rather than expecting `com.steipete.clawdis`. **When debugging on macOS, start/stop the gateway via the app, not ad-hoc tmux sessions; kill any temporary tunnels before handoff.**
- macOS logs: use `./scripts/clawlog.sh` (aka `vtlog`) to query unified logs for subsystem `com.steipete.clawdis`; it supports follow/tail/category filters and expects passwordless sudo for `/usr/bin/log`.
- Gateway currently runs only as the menubar app (launchctl shows `application.com.steipete.clawdbot.debug.*`), there is no separate LaunchAgent/helper label installed. Restart via the Clawdbot Mac app or `scripts/restart-mac.sh`; to verify/kill use `launchctl print gui/$UID | grep clawdbot` rather than expecting `com.steipete.clawdbot`. **When debugging on macOS, start/stop the gateway via the app, not ad-hoc tmux sessions; kill any temporary tunnels before handoff.**
- macOS logs: use `./scripts/clawlog.sh` (aka `vtlog`) to query unified logs for subsystem `com.steipete.clawdbot`; it supports follow/tail/category filters and expects passwordless sudo for `/usr/bin/log`.
- Also read the shared guardrails at `~/Projects/oracle/AGENTS.md` and `~/Projects/agent-scripts/AGENTS.MD` before making changes; align with any cross-repo rules noted there.
- SwiftUI state management (iOS/macOS): prefer the `Observation` framework (`@Observable`, `@Bindable`) over `ObservableObject`/`@StateObject`; don’t introduce new `ObservableObject` unless required for compatibility, and migrate existing usages when touching related code.
- Connection providers: when adding a new connection, update every UI surface and docs (macOS app, web UI, mobile if applicable, onboarding/overview docs) and add matching status + configuration forms so provider lists and settings stay in sync.
- **Restart apps:** “restart iOS/Android apps” means rebuild (recompile/install) and relaunch, not just kill/launch.
- **Device checks:** before testing, verify connected real devices (iOS/Android) before reaching for simulators/emulators.
- iOS Team ID lookup: `security find-identity -p codesigning -v` → use Apple Development (…) TEAMID. Fallback: `defaults read com.apple.dt.Xcode IDEProvisioningTeamIdentifiers`.
- A2UI bundle hash: `src/canvas-host/a2ui/.bundle.hash` is auto-generated; 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.
- When asked to open a “session” file, open the Pi session logs under `~/.clawdis/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 Clawdis variants, runs `swift build`, packages, relaunches). Icon dimming depends on MenuBarExtraAccess wiring in AppMain; keep `appearsDisabled` updates intact when touching the status item.
- **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.
- Never send streaming/partial replies to external messaging surfaces (WhatsApp, Telegram); only final replies should be delivered there. Streaming/tool events may still go to internal UIs/control channel.
- Voice wake forwarding tips:
- Command template should stay `clawdis-mac agent --message "${text}" --thinking low`; `VoiceWakeForwarder` already shell-escapes `${text}`. Don’t add extra quotes.
- launchd PATH is minimal; ensure the app’s launch agent sets PATH to include `/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/steipete/Library/pnpm` so `pnpm`/`clawdis` binaries resolve when invoked via `clawdis-mac`.
- For manual `clawdis send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tool’s escaping.
- Command template should stay `clawdbot-mac agent --message "${text}" --thinking low`; `VoiceWakeForwarder` already shell-escapes `${text}`. Don’t add extra quotes.
- launchd PATH is minimal; ensure the app’s launch agent sets PATH to include `/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/steipete/Library/pnpm` so `pnpm`/`clawdbot` binaries resolve when invoked via `clawdbot-mac`.
- For manual `clawdbot send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tool’s escaping.
## Exclamation Mark Escaping Workaround
The Claude Code Bash tool escapes `!` to `\\!` in command arguments. When using `clawdis send` with messages containing exclamation marks, use heredoc syntax:
The Claude Code Bash tool escapes `!` to `\\!` in command arguments. When using `clawdbot send` with messages containing exclamation marks, use heredoc syntax:
```bash
# WRONG - will send "Hello\\!" with backslash
clawdis send --to "+1234" --message 'Hello!'
clawdbot send --to "+1234" --message 'Hello!'
# CORRECT - use heredoc to avoid escaping
clawdis send --to "+1234" --message "$(cat <<'EOF'
clawdbot send --to "+1234" --message "$(cat <<'EOF'
Hello!
EOF
)"
```
This is a Claude Code quirk, not a clawdis bug.
This is a Claude Code quirk, not a clawdbot bug.

View File

@@ -1,610 +1,196 @@
# Changelog
**Why this looks different:** the project was renamed from **Clawdis → Clawdbot**. To make the transition clear, releases now use **date-based versions** (`YYYY.M.D`) and the changelog is **compressed** into milestone summaries. Full detail still lives in git history and the docs.
## Unreleased
### Breaking
- Identifiers: rename bundle IDs and internal domains to `com.clawdis.*` (macOS: `com.clawdis.mac`, iOS: `com.clawdis.ios`, Android: `com.clawdis.android`) and update the gateway LaunchAgent label to `com.clawdis.gateway`.
- Agent tools: drop the `clawdis_` prefix (`browser`, `canvas`, `nodes`, `cron`, `gateway`).
- Bash tool: remove `stdinMode: "pty"`/node-pty support; use the tmux skill for real TTYs.
- Sessions: primary session key is fixed to `main` (or `global` for global scope); `session.mainKey` is ignored.
### Features
- Gateway: support `gateway.port` + `CLAWDIS_GATEWAY_PORT` across CLI, TUI, and macOS app.
- Gateway: add config hot reload with hybrid restart strategy (`gateway.reload`) and per-section reload handling.
- UI: centralize tool display metadata and show action/detail summaries across Web Chat, SwiftUI, Android, and the TUI.
- Control UI: support configurable base paths (`gateway.controlUi.basePath`, default unchanged) for hosting under URL prefixes.
- Onboarding: shared wizard engine powering CLI + macOS via gateway wizard RPC.
- Config: expose schema + UI hints for generic config forms (Web UI + future clients).
- Skills: add blogwatcher skill for RSS/Atom monitoring — thanks @Hyaxia.
- Discord: emit system events for reaction add/remove with per-guild reaction notifications (off|own|all|allowlist) (#140) — thanks @thewilloftheshadow.
- Agent: add optional per-session Docker sandbox for tool execution (`agent.sandbox`) with allow/deny policy and auto-pruning.
- Agent: add sandboxed Chromium browser (CDP + optional noVNC observer) for sandboxed sessions.
- Nodes: add `location.get` with Always/Precise settings on macOS/iOS/Android plus CLI/tool support.
- Sessions: add agent‑to‑agent post step with `ANNOUNCE_SKIP` to suppress channel announcements.
- 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).
### Fixes
- Gateway/macOS: keep node presence fresh with periodic beacons + show presence status in Instances (#168) — thanks @mbelinky.
- CI: fix lint ordering after merge cleanup (#156) — thanks @steipete.
- CI: consolidate checks to avoid redundant installs (#144) — thanks @thewilloftheshadow.
- WhatsApp: support `gifPlayback` for MP4 GIF sends via CLI/gateway.
- Sessions: prevent `sessions_send` timeouts by running nested agent turns on a separate lane.
- Sessions: use per-send run IDs for gateway agent calls to avoid wait collisions.
- Auto-reply: drop final payloads when block streaming to avoid duplicate Discord sends.
- Auto-reply: fix typing TTL to 2 minutes and log TTL with s/m units.
- Bash tool: default auto-background delay to 10s.
- Telegram: chunk block-stream replies to avoid “message is too long” errors (#124) — thanks @mukhtharcm.
- Block streaming: default to text_end and suppress duplicate block sends while in-flight.
- Block streaming: avoid duplicate block chunks when providers repeat full content on text_end.
- Block streaming: drop final payloads after soft chunking to keep Discord order intact.
- Gmail hooks: resolve gcloud Python to a real executable when PATH uses mise shims — thanks @joargp.
- Control UI: generate UUIDs when `crypto.randomUUID()` is unavailable over HTTP — thanks @ratulsarna.
- Control UI: stream live tool output cards in Chat (agent events include sessionKey).
- Chat UI: render assistant `<think>`/`<final>` markup as italic thinking text in history + streaming instead of showing raw tags.
- Agent: add soft block-stream chunking (800–1200 chars default) with paragraph/newline preference.
- Agent: route embedded run lifecycle logs through subsystem console formatting and reduce log noise.
- Agent tools: scope the Discord tool to Discord surface runs.
- Agent tools: format verbose tool summaries without brackets, with unique emojis and `tool: detail` style.
- Agent tools: emit verbose tool summaries at tool start (no debounce).
- Gateway: split server helpers/tests into hooks/session-utils/ws-log/net modules for better isolation; add unit coverage for hooks/session utils/ws log.
- Gateway: extract WS method handling + HTTP/provider/constant helpers to shrink server wiring and improve testability.
- Gateway: prevent deleting the main session and abort active runs before deleting other sessions.
- Onboarding: fix Control UI basePath usage when showing/opening gateway URLs.
- Onboarding: clarify provider requirements (WhatsApp/Signal phone numbers, iMessage Apple ID guidance) in the provider picker.
- macOS Connections: move to sidebar + detail layout with structured sections and header actions.
- macOS onboarding: increase window height so the permissions page fits without scrolling.
- Thinking: default to low for reasoning-capable models when no /think or config default is set.
- Logging: decouple file log levels from console verbosity; verbose-only details are captured when `logging.level` is debug/trace.
- Build: fix regex literal in tool-meta path detection (watch build error).
- Build: require AVX2 Bun for x86_64 relay packaging (reject baseline builds).
- Build: drop stale ClawdisCLI product from macOS build-and-run script.
- Auto-reply: add run-level telemetry + typing TTL guardrails to diagnose stuck replies.
- WhatsApp: honor per-group mention gating overrides when group ids are stored as session keys.
- Dependencies: bump pi-mono packages to 0.32.3.
- Onboarding: resolve CLI entrypoint when running via `npx` so gateway daemon install works without a build step.
- 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.
- 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.
- Docs: clarify auth storage, migration, and OpenAI Codex OAuth onboarding.
- Sandbox: copy inbound media into sandbox workspaces so agent tools can read attachments.
- 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).
- 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: unify tool/block/final delivery across providers and apply consistent heartbeat/prefix handling. Thanks @MSch for PR #225 (superseded commit 92c953d0749143eb2a3f31f3cd6ad0e8eabf48c3).
- Heartbeat: make HEARTBEAT_OK ack padding configurable across heartbeat and cron delivery. (#238) — thanks @jalehman
- 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.
- 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.
### Maintenance
- 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.
- Deps: update to latest across the repo.
## 2026.1.5-3
### Fixes
- NPM package: include missing runtime dist folders (slack/signal/imessage/tui/wizard/control-ui/daemon) to avoid `ERR_MODULE_NOT_FOUND` in Node 25 npx installs.
## 2026.1.5-2
### Fixes
- NPM package: include `dist/sessions` so `clawdbot agent` resolves session helpers in npx installs.
- Node 25: avoid unsupported directory import by targeting `qrcode-terminal/vendor/QRCode/*.js` modules.
## 2026.1.5-1
### Fixes
- NPM package: include `dist/sessions` so `clawdbot agent` resolves session helpers in npx installs.
- Node 25: avoid unsupported directory import by targeting `qrcode-terminal/vendor/QRCode/index.js`.
## 2026.1.5
### Highlights
- Models: add image-specific model config (`agent.imageModel` + fallbacks) and scan support.
- Agent tools: new `image` tool routed to the image model (when configured).
- Config: default model shorthands (`opus`, `sonnet`, `gpt`, `gpt-mini`, `gemini`, `gemini-flash`).
- Docs: document built-in model shorthands + precedence (user config wins).
### Fixes
- Control UI: render Markdown in tool result cards.
- Control UI: prevent overlapping action buttons in Discord guild rules on narrow layouts.
- Android: tapping the foreground service notification brings the app to the front. (#179) — thanks @Syhids
- Cron tool uses `id` for update/remove/run/runs (aligns with gateway params). (#180) — thanks @adamgall
- Control UI: chat view uses page scroll with sticky header/sidebar and fixed composer (no inner scroll frame).
- macOS: treat location permission as always-only to avoid iOS-only enums. (#165) — thanks @Nachx639
- macOS: make generated gateway protocol models `Sendable` for Swift 6 strict concurrency. (#195) — thanks @andranik-sahakyan
- macOS: bundle QR code renderer modules so DMG gateway boot doesn't crash on missing qrcode-terminal vendor files.
- macOS: parse JSON5 config safely to avoid wiping user settings when comments are present.
- WhatsApp: suppress typing indicator during heartbeat background tasks. (#190) — thanks @mcinteerj
- WhatsApp: mark offline history sync messages as read without auto-reply. (#193) — thanks @mcinteerj
- Discord: avoid duplicate replies when a provider emits late streaming `text_end` events (OpenAI/GPT).
- CLI: use tailnet IP for local gateway calls when bind is tailnet/auto (fixes #176).
- Env: load global `$CLAWDBOT_STATE_DIR/.env` (`~/.clawdbot/.env`) as a fallback after CWD `.env`.
- Env: optional login-shell env fallback (opt-in; imports expected keys without overriding existing env).
- Agent tools: OpenAI-compatible tool JSON Schemas (fix `browser`, normalize union schemas).
- Onboarding: when running from source, auto-build missing Control UI assets (`pnpm 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.
- Control UI: render Markdown in chat messages (sanitized).
## 2026.1.4
### Highlights
- Rename completion: all CLIs, paths, bundle IDs, env vars, and docs standardized on **Clawdbot**.
- Agent-to-agent relay: `sessions_send` ping‑pong with `REPLY_SKIP` plus announce step with `ANNOUNCE_SKIP`.
- Gateway quality-of-life: config hot reload, port config support, and Control UI base paths.
- Sandbox additions: per-session Docker sandbox with hardened limits + optional sandboxed Chromium.
- New node capability: `location.get` across macOS/iOS/Android (CLI + tools).
- Models CLI: scan OpenRouter free models (tools/images), manage aliases/fallbacks, and show last-used model in status.
### Breaking
- Tool names drop the `clawdbot_` prefix (`browser`, `canvas`, `nodes`, `cron`, `gateway`).
- Bash tool removes node-pty `stdinMode: "pty"` support (use tmux for real TTYs).
- Primary session key is fixed to `main` (or `global` for global scope).
### Fixes
- Doctor migrates legacy Clawdis config/service installs and normalizes sandbox Docker names.
- Doctor checks sandbox image availability and offers to build or fall back to legacy images.
- 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 and UI polish.
- Clearer tool summaries, reduced log noise, and safer watchdog/queue behavior.
- Canvas host watcher resilience; build and packaging edge cases cleaned up.
### Docs
- Skills: add Sheets/Docs examples to gog skill (#128) — thanks @mbelinky.
- Skills: clarify bear-notes token + callback usage (#120) — thanks @tylerwince.
- Skills: document Discord `sendMessage` media attachments and `to` format clarification.
- Skills: expand peekaboo skill examples + common parameters.
- Skills: add tmux skill + interactive coding guidance in coding-agent.
- Gateway: document port configuration + multi-instance isolation.
- Gateway: document config hot reload + reload matrix.
- Onboarding/Config: add protocol notes for wizard + schema RPC.
- Queue: clarify steer-backlog behavior with inline commands and update examples for streaming surfaces.
- Sandbox: document per-session agent sandbox setup, browser image, and Docker build.
- macOS: clarify menu bar uses sessionKey from agent events.
- Sessions: document agent-to-agent post step and `ANNOUNCE_SKIP`.
- Sandbox setup, hot reload, port config, and session announce step coverage.
- Skills and onboarding clarifications + additional examples.
## 2.0.0-beta5 — 2026-01-03
### Fixed
- Media: preserve GIF animation when uploading to Discord/other providers (skip JPEG optimization for image/gif).
- Agent runtime: update pi-mono dependencies to 0.31.1 (agent-core split).
- Dependencies: bump to latest compatible versions (TypeBox, grammY, Zod, Rolldown, oxlint-tsgolint).
- Tests: cover read tool image metadata + text output.
- Tests: add queue mode coverage (collect/followup + directive parsing).
## 2026.1.3 (beta 5)
### Breaking
- Skills config schema moved under `skills.*`:
- `skillsLoad.extraDirs` → `skills.load.extraDirs`
- `skillsInstall.*` → `skills.install.*`
- per-skill config map moved to `skills.entries` (e.g. `skills.peekaboo.enabled` → `skills.entries.peekaboo.enabled`)
- new optional bundled allowlist: `skills.allowBundled` (only affects bundled skills)
- Sessions: group keys now use `surface:group:<id>` / `surface:channel:<id>`; legacy `group:*` keys migrate on next message; `groupdm` keys are no longer recognized.
- Discord: remove legacy `discord.allowFrom`, `discord.guildAllowFrom`, and `discord.requireMention`; use `discord.dm` + `discord.guilds`.
- Providers: Discord/Telegram no longer auto-start from env tokens alone; add `discord: { enabled: true }` / `telegram: { enabled: true }` to your config when using `DISCORD_BOT_TOKEN` / `TELEGRAM_BOT_TOKEN`.
- Config: remove `routing.allowFrom`; use `whatsapp.allowFrom` instead (run `clawdis doctor` to migrate).
- Config: remove `routing.groupChat.requireMention` + `telegram.requireMention`; use `whatsapp.groups`, `imessage.groups`, and `telegram.groups` defaults instead (run `clawdis doctor` to migrate).
- Skills config moved under `skills.*` (new `skills.entries`, `skills.allowBundled`).
- Group session keys now `surface:group:<id>` / `surface:channel:<id>`; legacy `group:*` removed.
- Discord config refactor; `discord.allowFrom` + `discord.requireMention` removed.
- Discord/Telegram require `enabled: true` in config when using env tokens.
- Routing `allowFrom`/mention settings moved to per-surface group settings.
### Features
- Discord: expand `discord` tool actions (reactions, stickers, polls, threads, search, moderation gates) (#115) — thanks @thewilloftheshadow.
- Discord/Telegram: add reply tags (`[[reply_to_current]]`, `[[reply_to:<id>]]`) with per-provider `replyToMode` (off|first|all) for native threaded replies.
- Talk mode: continuous speech conversations (macOS/iOS/Android) with ElevenLabs TTS, reply directives, and optional interrupt-on-speech.
- Auto-reply: expand queue modes (steer/followup/collect/steer-backlog) with debounce/cap/drop options and followup backlog handling.
- UI: add optional `ui.seamColor` accent to tint the Talk Mode side bubble (macOS/iOS/Android).
- Nix mode: opt-in declarative config + read-only settings UI when `CLAWDIS_NIX_MODE=1` (thanks @joshp123 for the persistence — earned my trust; I'll merge these going forward).
- CLI: add Google Antigravity OAuth auth option for Claude Opus 4.5/Gemini 3 (#88) — thanks @mukhtharcm.
- Agent runtime: accept legacy `Z_AI_API_KEY` for Z.AI provider auth (maps to `ZAI_API_KEY`).
- Groups: add per-group mention gating defaults/overrides for Telegram/WhatsApp/iMessage via `*.groups` with `"*"` defaults; Discord now supports `discord.guilds."*"` as a default.
- Discord: add user-installed slash command handling with per-user sessions and auto-registration (#94) — thanks @thewilloftheshadow.
- Discord: add DM enable/allowlist plus guild channel/user/guild allowlists with id/name matching.
- Signal: add `signal-cli` JSON-RPC support for send/receive via the Signal provider.
- iMessage: add imsg JSON-RPC integration (stdio), chat_id routing, and group chat support.
- Chat UI: add recent-session dropdown switcher (main first) in macOS/iOS/Android + Control UI.
- UI: add Discord/Signal/iMessage connection panels in macOS + Control UI (thanks @thewilloftheshadow).
- Discord: allow agent-triggered reactions via `clawdis_discord` when enabled, and surface message ids in context.
- Discord: revamp guild routing config with per-guild/channel rules and slugged display names; add optional group DM support (default off).
- Discord: remove legacy guild/channel ignore lists in favor of per-guild allowlists (and proposed per-guild ignore lists).
- Skills: add Trello skill for board/list/card management (thanks @clawd).
- Docker: add containerized gateway/CLI setup via Dockerfile, compose, and setup script (thanks @dan-dr).
- Tests: add a Z.AI live test gate for smoke validation when keys are present.
- macOS Debug: add app log verbosity and rolling file log toggle for swift-log-backed app logs.
- CLI: add onboarding wizard (gateway + workspace + skills) with daemon installers and Anthropic/Minimax setup paths.
- CLI: add ASCII banner header to wizard entry points.
- CLI: add `configure`, `doctor`, and `update` wizards for ongoing setup, health checks, and modernization.
- CLI: add Signal CLI auto-install from GitHub releases in the wizard and persist wizard run metadata in config.
- CLI: add remote gateway client config (gateway.remote.*) with Bonjour-assisted discovery.
- CLI: enhance `clawdis tui` with model/session pickers, tool cards, and slash commands (local or remote).
- Gateway: allow `sessions.patch` to set per-session model overrides (used by the TUI `/model` flow).
- Skills: allow `bun` as a node manager for skill installs.
- Skills: add `things-mac` (Things 3 CLI) for read/search plus add/update via URL scheme.
- Skills: add Apple Notes + Reminders skills via memo CLI (thanks @tylerwince).
- Tests: add a Docker-based onboarding E2E harness.
- Tests: harden wizard E2E flows for reset, providers, skills, and remote non-interactive runs.
- Browser tools: add remote CDP URL support, Linux launcher options (`executablePath`, `noSandbox`), and surface `cdpUrl` in status.
- Skills: add tmux-first coding-agent skill + `requires.anyBins` gate for multi-CLI setup (thanks @sreekaransrinath).
### Highlights
- Talk Mode (continuous voice) with ElevenLabs TTS on macOS/iOS/Android.
- Discord: expanded tool actions, richer routing, and threaded reply tags.
- Auto-reply queue modes + session model overrides; TUI upgrades.
- Nix mode (declarative config) and Docker setup flow.
- Onboarding wizard + configure/doctor/update flows.
- Signal + iMessage providers; new skills (Trello, Things, Notes/Reminders, tmux coding).
- Browser tooling upgrades (remote CDP, no-sandbox, profiles).
### Fixes
- macOS codesign: make ad-hoc signing opt-in with loud warnings and document TCC permission fragility — thanks @mcinteerj.
- Gog calendar: format date ranges as RFC 3339 with timezone to satisfy Google Calendar API (thanks @jayhickey).
- macOS onboarding: add scrollable page gutter for overflowing content (#105) — thanks @thewilloftheshadow.
- Chat UI: keep the chat scrolled to the latest message after switching sessions.
- Chat UI: show rich session display names in Web Chat + SwiftUI + Android.
- Auto-reply: stream completed reply blocks as soon as they finish (configurable default + break); skip empty tool-only blocks unless verbose.
- Discord: avoid duplicate sends when block streaming is enabled (race with typing hook).
- Providers: make outbound text chunk limits configurable via `*.textChunkLimit` (defaults remain 4000/Discord 2000).
- CLI onboarding: persist gateway token in config so local CLI auth works; recommend auth Off unless you need multi-machine access.
- Control UI: accept a `?token=` URL param to auto-fill Gateway auth; onboarding now opens the dashboard with token auth when configured.
- Agent prompt: remove hardcoded user name in system prompt example.
- Chat UI: add extra top padding before the first message bubble in Web Chat (macOS/iOS/Android).
- Control UI: refine Web Chat session selector styling (chevron spacing + background).
- WebChat: stream live updates for sessions even when runs start outside the chat UI.
- Gateway CLI: read `CLAWDIS_GATEWAY_PASSWORD` from environment in `callGateway()` — allows `doctor`/`health` commands to auth without explicit `--password` flag.
- Gateway: add password auth support for remote gateway connections (thanks @jeffersonwarrior).
- Auto-reply: strip stray leading/trailing `HEARTBEAT_OK` from normal replies; drop short (≤ 30 chars) heartbeat acks.
- WhatsApp auto-reply: default to self-only when no config is present.
- Logging: trim provider prefix duplication in Discord/Signal/Telegram runtime log lines.
- Logging/Signal: treat signal-cli "Failed …" lines as errors in gateway logs.
- Discord: include recent guild context when replying to mentions and add `discord.historyLimit` to tune how many messages are captured.
- Discord: include author tag + id in group context `[from:]` lines for ping-ready replies (thanks @thewilloftheshadow).
- Discord: include replied-to message context when a Discord message references another message (thanks @thewilloftheshadow).
- Discord: preserve newlines when stripping reply tags from agent output.
- Gateway: fix TypeScript build by aligning hook mapping `channel` types and removing a dead Group DM branch in Discord monitor.
- Skills: switch imsg installer to brew tap formula.
- Skills: gate macOS-only skills by OS and surface block reasons in the Skills UI.
- Onboarding: show skill descriptions in the macOS setup flow and surface clearer Gateway/skills error messages.
- Onboarding: auto-verify Claude OAuth tokens, show “verified” when detected working, and avoid re-auth prompts unless verification fails.
- CLI onboarding: include exit code + a useful one-line summary when skill dependency installs fail.
- CLI onboarding: explain Tailscale exposure options (Off/Serve/Funnel) and colorize provider status (linked/configured/needs setup).
- CLI onboarding: add provider primers (WhatsApp/Telegram/Discord/Signal) incl. Discord bot token setup steps.
- CLI onboarding: allow skipping the “install missing skill dependencies” selection without canceling the wizard.
- CLI onboarding: always prompt for WhatsApp `whatsapp.allowFrom` and print (optionally open) the Control UI URL when done.
- CLI onboarding: detect gateway reachability and annotate Local/Remote choices (helps pick the right mode).
- macOS settings: colorize provider status subtitles to distinguish healthy vs degraded states.
- macOS: keep config writes on the main actor to satisfy Swift concurrency rules.
- macOS menu: show multi-line gateway error details, add an always-visible gateway row, avoid duplicate gateway status rows, suppress transient `cancelled` device refresh errors, and auto-recover the control channel on disconnect.
- macOS menu: show session last-used timestamps in the list and add recent-message previews in session submenus.
- macOS menu: tighten session row padding and time out session preview loading with cached fallback.
- macOS: log health refresh failures and recovery to make gateway issues easier to diagnose.
- macOS codesign: skip hardened runtime for ad-hoc signing and avoid empty options args (#70) — thanks @petter-b
- macOS codesign: include camera entitlement so permission prompts work in the menu bar app.
- Agent tools: bash tool supports real TTY via `stdinMode: "pty"` with node-pty, warning + fallback on load/start failure.
- Agent tools: map `camera.snap` JPEG payloads to `image/jpeg` to avoid MIME mismatch errors.
- Tests: cover `camera.snap` MIME mapping to prevent image/png vs image/jpeg mismatches.
- macOS camera: wait for exposure/white balance to settle before capturing a snap to avoid dark images.
- Camera snap: add `delayMs` parameter (default 2000ms on macOS) to improve exposure reliability.
- Camera: add `camera.list` and optional `deviceId` selection for snaps/clips.
- Tests: cover camera device selection params in CLI + agent tools.
- macOS packaging: move rpath config into swift build for reliability (#69) — thanks @petter-b
- macOS: prioritize main bundle for device resources to prevent crash (#73) — thanks @petter-b
- macOS remote: route settings through gateway config and avoid local config reads in remote mode.
- Telegram: align token resolution for cron/agent/CLI sends (env/config/tokenFile) to prevent isolated delivery failures (#76).
- Telegram: honor per-group mention gating defaults/overrides via `telegram.groups` and `"*"` defaults (thanks @joshp123).
- Chat UI: clear composer input immediately and allow clear while editing to prevent duplicate sends (#72) — thanks @hrdwdmrbl
- Restart: use systemd on Linux (and report actual restart method) instead of always launchctl.
- Gateway relay: detect Bun binaries via execPath to resolve packaged assets on macOS.
- Cron: prevent `every` schedules without an anchor from firing in a tight loop (thanks @jamesgroat).
- Docs: add manual OAuth setup for remote/headless deployments (#67) — thanks @wstock
- Docs/agent tools: clarify that browser `wait` should be avoided by default and used only in exceptional cases.
- Docs: clarify self-chat mode and group mention gating config (#111) — thanks @rafaelreis-r.
- Browser tools: `upload` supports auto-click refs, direct `inputRef`/`element` file inputs, and emits input/change after `setFiles` so JS-heavy sites pick up attachments.
- Browser tools: harden CDP readiness (HTTP + WS), retry CDP connects, and auto-restart the clawd browser when the socket handshake stalls.
- Browser CLI: add `clawdis browser reset-profile` to move the clawd profile to Trash when it gets wedged.
- Signal: fix daemon startup race (wait for `/api/v1/check`) and normalize JSON-RPC `version` probe parsing.
- Docs/Signal: clarify bot-number vs personal-account setup (self-chat loop protection) and add a quickstart config snippet.
- Docs: refresh the CLI wizard guide and highlight onboarding in the README.
- CLI: tighten onboarding prompt typing to keep bun builds green.
- macOS: Voice Wake now fully tears down the Speech pipeline when disabled (cancel pending restarts, drop stale callbacks) to avoid high CPU in the background.
- macOS menu: add a Talk Mode action alongside the Open Dashboard/Chat/Canvas entries.
- macOS Debug: hide “Restart Gateway” when the app won’t start a local gateway (remote mode / attach-only).
- macOS Debug: add an icon for the App Logging submenu.
- macOS Talk Mode: orb overlay refresh, ElevenLabs request logging, API key status in settings, and auto-select first voice when none is configured.
- macOS Talk Mode: add hard timeout around ElevenLabs TTS synthesis to avoid getting stuck “speaking” forever on hung requests.
- macOS Talk Mode: avoid stuck playback when the audio player never starts (fail-fast + watchdog).
- macOS Talk Mode: fix audio stop ordering so disabling Talk Mode always stops in-flight playback.
- macOS Talk Mode: throttle audio-level updates (avoid per-buffer task creation) to reduce CPU/task churn.
- macOS Talk Mode: increase overlay window size so wave rings don’t clip; close button is hover-only and closer to the orb.
- WebChat: preserve chat run ordering per session so concurrent runs don’t strand the typing indicator.
- Talk Mode: fall back to system TTS when ElevenLabs is unavailable, returns non-audio, or playback fails (macOS/iOS/Android).
- Talk Mode: stream PCM on macOS/iOS for lower latency (incremental playback); Android continues MP3 streaming.
- Talk Mode: validate ElevenLabs v3 stability and latency tier directives before sending requests.
- iOS/Android Talk Mode: auto-select the first ElevenLabs voice when none is configured.
- ElevenLabs: add retry/backoff for 429/5xx and include content-type in errors for debugging.
- Talk Mode: align to the gateway’s main session key and fall back to history polling when chat events drop (prevents stuck “thinking” / missing messages).
- Talk Mode: treat history timestamps as seconds or milliseconds to avoid stale assistant picks (macOS/iOS/Android).
- Chat UI: clear streaming/tool bubbles when external runs finish, preventing duplicate assistant bubbles.
- Chat UI: user bubbles use `ui.seamColor` (fallback to a calmer default blue).
- Android Chat UI: use `onPrimary` for user bubble text to preserve contrast (thanks @Syhids).
- Control UI: sync sidebar navigation with the URL for deep-linking, and auto-scroll chat to the latest message.
- Control UI: disable Web Chat + Talk when no iOS/Android node is connected; refreshed Web Chat styling and keyboard send.
- Control UI: keep chat pinned to the latest message while typing/sending and restore drafts on send failures.
- Control UI: soften chat bubble text opacity for calmer readability.
- macOS Web Chat: improve empty/error states, focus message field on open, keep pill/send inside the input field, and make the composer pill edge-to-edge with square top corners.
- macOS: bundle Control UI assets into the app relay so the packaged app can serve them (thanks @mbelinky).
- Talk Mode: wait for chat history to surface the assistant reply before starting TTS (macOS/iOS/Android).
- iOS Talk Mode: fix chat completion wait to time out even if no events arrive (prevents “Thinking…” hangs).
- iOS Talk Mode: keep recognition running during playback to support interrupt-on-speech.
- iOS Talk Mode: preserve directive voice/model overrides across config reloads and add ElevenLabs request timeouts.
- iOS/Android Talk Mode: explicitly `chat.subscribe` when Talk Mode is active, so completion events arrive even if the Chat UI isn’t open.
- Chat UI: refresh history when another client finishes a run in the same session, so Talk Mode + Voice Wake transcripts appear consistently.
- Gateway: `voice.transcript` now also maps agent bus output to `chat` events, ensuring chat UIs refresh for voice-triggered runs.
- Gateway: auto-migrate legacy config on startup (non-Nix); Nix mode hard-fails with a clear error when legacy keys are present.
- iOS/Android: show a centered Talk Mode orb overlay while Talk Mode is enabled.
- Gateway config: inject `talk.apiKey` from `ELEVENLABS_API_KEY`/shell profile so nodes can fetch it on demand.
- Canvas A2UI: tag requests with `platform=android|ios|macos` and boost Android canvas background contrast.
- iOS/Android nodes: enable scrolling for loaded web pages in the Canvas WebView (default scaffold stays touch-first).
- macOS menu: device list now uses `node.list` (devices only; no agent/tool presence entries).
- macOS menu: device list now shows connected nodes only.
- macOS menu: device rows now pack platform/version on the first line, and command lists wrap in submenus.
- macOS menu: split device platform/version across first and second rows for better fit.
- macOS Canvas: show remote control status in the debug overlay and log A2UI auto-nav decisions.
- Canvas A2UI: polish the debug status HUD styling.
- iOS node: fix ReplayKit screen recording crash caused by queue isolation assertions during capture.
- iOS Talk Mode: avoid audio tap queue assertions when starting recognition.
- macOS: use $HOME/Library/pnpm for SSH PATH exports (thanks @mbelinky).
- macOS remote: harden SSH tunnel recovery/logging, honor `gateway.remote.url` port when forwarding, clarify gateway disconnect status, and add Debug menu tunnel reset.
- iOS/Android nodes: bridge auto-connect refreshes stale tokens and settings now show richer bridge/device details.
- macOS: bundle device model resources to prevent Instances crashes (thanks @mbelinky).
- iOS/Android nodes: status pill now surfaces camera activity instead of overlay toasts.
- iOS/Android/macOS nodes: camera snaps recompress to keep base64 payloads under 5 MB.
- iOS/Android nodes: status pill now surfaces pairing, screen recording, voice wake, and foreground-required states.
- iOS/Android nodes: avoid duplicating “Gateway reconnecting…” when the bridge is already connecting.
- iOS/Android nodes: Talk Mode now lives on a side bubble (with an iOS toggle to hide it), and Android settings no longer show the Talk Mode switch.
- macOS menu: top status line now shows pending node pairing approvals (incl. repairs).
- CLI: avoid spurious gateway close errors after successful request/response cycles.
- Agent runtime: clamp tool-result images to the 5MB Anthropic limit to avoid hard request rejections.
- Agent runtime: write v2 session headers so Pi session branching stays in the Clawdis sessions dir.
- Tests: add Swift Testing coverage for camera errors and Kotest coverage for Android bridge endpoints.
- macOS codesign/TCC hardening and menu/UI stability improvements.
- Streaming/typing fixes; per-provider chunk limit tuning.
- Remote gateway auth + token handling tightened.
- Camera capture reliability and media sizing fixes.
## 2.0.0-beta4 — 2025-12-27
## 2025.12.27 (betas 3–4)
### Highlights
- First-class tools replace `clawdbot-*` skills (browser, canvas, nodes, cron).
- Per-session model selection and custom model providers.
- Group activation commands; Discord provider for DMs/guilds.
- Gateway webhooks + Gmail Pub/Sub hooks.
- Command queue modes + `agent.maxConcurrent` cap.
- Background bash tasks with `process` tool; gateway in-process restart.
### Fixes
- Package contents: include Discord/hooks build outputs in the npm tarball to avoid missing module errors.
- Heartbeat replies now drop any output containing `HEARTBEAT_OK`, preventing stray emoji/text from being delivered.
- macOS menu now refreshes the control channel after the gateway starts and shows “Connecting to gateway…” while the gateway is coming up.
- macOS local mode now waits for the gateway to be ready before configuring the control channel, avoiding false “no connection” flashes.
- WhatsApp watchdog now forces a reconnect even if the socket close event stalls (force-close to unblock reconnect loop).
- Gateway presence now reports macOS product version (via `sw_vers`) instead of Darwin kernel version.
- Packaging fixes, heartbeat cleanup, WhatsApp reconnect reliability.
- macOS menu/Chat UI polish and presence reporting fixes.
## 2.0.0-beta3 — 2025-12-27
## 2025.12.21 (beta 2)
### Highlights
- First-class Clawdis tools (browser, canvas, nodes, cron) replace the old `clawdis-*` skills; tool schemas are now injected directly into the agent runtime.
- Per-session model selection + custom model providers: `models.providers` merges into `~/.clawdis/agent/models.json` (merge/replace modes) for LiteLLM, local OpenAI-compatible servers, Anthropic proxies, etc.
- Group chat activation modes: per-group `/activation mention|always` command with status visibility.
- Discord bot transport for DMs and guild text channels, with allowlists + mention gating.
- Gateway webhooks: external `wake` and isolated `agent` hooks with dedicated token auth.
- Hook mappings + Gmail Pub/Sub helper (`clawdis hooks gmail setup/run`) with auto-renew + Tailscale Funnel support.
- Command queue modes + per-session overrides (`/queue ...`) and new `agent.maxConcurrent` cap for safe parallelism across sessions.
- Background bash tasks: `bash` auto-yields after 20s (or on demand) with a `process` tool to list/poll/log/write/kill sessions.
- Gateway in-process restart: `gateway` tool action triggers a SIGUSR1 restart without needing a supervisor.
- Bundled gateway packaging + DMG distribution pipeline.
- Skills platform (bundled/managed/workspace) with install gating + UI.
- Onboarding polish and agent UX improvements.
- Canvas host served from Gateway; browser control simplification.
## 2025.12.19 (beta 1)
### Highlights
- First Clawdbot release: Gateway WS control plane + optional Bridge.
- macOS menu bar companion app with Voice Wake + WebChat.
- iOS node pairing with Canvas surface.
- WhatsApp groups, thinking/verbose directives, health/status tooling.
### Breaking
- Config refactor: `inbound.*` removed; use top-level `routing` (allowlists + group rules + transcription), `messages` (prefixes/timestamps), and `session` (scoping/store/mainKey). No legacy keys read.
- Heartbeat config moved to `agent.heartbeat`: set `every: "30m"` (duration string) and optional `model`. `agent.heartbeatMinutes` is removed, and heartbeats are disabled unless `agent.heartbeat.every` is set.
- Heartbeats now run via the gateway runner (main session) and deliver to the last used channel by default. WhatsApp reply-heartbeat behavior is removed; use `agent.heartbeat.target`/`to` (or `target: "none"`) to control delivery.
- Browser `act` no longer accepts CSS `selector`; use `snapshot` refs (default `ai`) or `evaluate` as an escape hatch.
- Switched to Pi-only agent runtime; legacy providers removed.
- Gateway became the single source of truth (no ad-hoc direct sends).
### Fixes
- Heartbeat replies now strip repeated `HEARTBEAT_OK` tails to avoid accidental “OK OK” spam.
- Heartbeat delivery now uses the last non-empty payload, preventing tool preambles from swallowing the final reply.
- Heartbeats now skip WhatsApp delivery when the web provider is inactive or unlinked (instead of logging “no active gateway listener”).
- Heartbeat failure logs now include the error reason instead of `[object Object]`.
- Duration strings now accept `h` (hours) where durations are parsed (e.g., heartbeat intervals).
- WhatsApp inbound now normalizes more wrapper types so quoted reply bodies are extracted reliably.
- WhatsApp send now preserves existing JIDs (including group `@g.us`) instead of coercing to `@s.whatsapp.net`. (Thanks @arun-8687.)
- Telegram/WhatsApp: reply context stays in `Body`/`ReplyTo*`, but outbound replies no longer thread to the original message. (Thanks @joshp123 for the PR and follow-up question.)
- Suppressed libsignal session cleanup spam from console logs unless verbose mode is enabled.
- WhatsApp web creds persistence hardened; credentials are restored before auth checks and QR login auto-restarts if it stalls.
- Group chats now honor `routing.groupChat.requireMention=false` as the default activation when no per-group override exists.
- Gateway auth no longer supports PAM/system mode; use token or shared password.
- Tailscale Funnel now requires password auth (no token-only public exposure).
- Group `/new` resets now work with @mentions so activation guidance appears on fresh sessions.
- Group chat activation context is now injected into the system prompt at session start (and after activation changes), including /new greetings.
- Typing indicators now start only once a reply payload is produced (no "thinking" typing for silent runs).
- WhatsApp group typing now starts immediately only when the bot is mentioned; otherwise it waits until real output exists.
- Streamed `<think>` segments are stripped before partial replies are emitted.
- System prompt now tags allowlisted owner numbers as the user identity to avoid mistaken “friend” assumptions.
- LM Studio/Ollama replies now require <final> tags; streaming ignores content until <final> begins.
- LM Studio responses API: tools payloads no longer include `strict: null`, and LM Studio no longer gets forced `<think>/<final>` tags.
- Identity emoji no longer auto-prefixes replies (set `messages.responsePrefix` explicitly if desired).
- Model switches now enqueue a system event so the next run knows the active model.
- `/model status` now lists available models (same as `/model`).
- `process log` pagination is now line-based (omit `offset` to grab the last N lines).
- macOS WebChat: assistant bubbles now update correctly when toggling light/dark mode.
- macOS: avoid spawning a duplicate gateway process when an external listener already exists.
- Node bridge: when binding to a non-loopback host (e.g. Tailnet IP), also listens on `127.0.0.1` for local connections (without creating duplicate loopback listeners for `0.0.0.0`/`127.0.0.1` binds).
- UI perf: pause repeat animations when scenes are inactive (typing dots, onboarding glow, iOS status pulse), throttle voice overlay level updates, and reduce overlay focus churn.
- Canvas defaults/A2UI auto-nav aligned; debug status overlay centered; redundant await removed in `CanvasManager`.
- Gateway launchd loop fixed by removing redundant `kickstart -k`.
- CLI now hints when Peekaboo is unauthorized.
- WhatsApp web inbox listeners now clean up on close to avoid duplicate handlers.
- Gateway startup now brings up browser control before external providers; WhatsApp/Telegram/Discord auto-start can be disabled with `web.enabled`, `telegram.enabled`, or `discord.enabled`.
### Providers & Routing
- New Discord provider for DMs + guild text channels with allowlists and mention-gated replies by default.
- `routing.queue` now controls queue vs interrupt behavior globally + per surface (defaults: WhatsApp/Telegram interrupt, Discord/WebChat queue).
- `/queue <mode>` supports one-shot or per-session overrides; `/queue reset|default` clears overrides.
- `agent.maxConcurrent` caps global parallel runs while keeping per-session serialization.
### macOS app
- Update-ready state surfaced in the menu; menu sections regrouped with session submenus.
- Menu bar now shows a dedicated Nodes section under Context with inline rows, overflow submenu, and iconized actions.
- Nodes now expose consistent inline details with per-node submenus for quick copy of key fields.
- Node rows now show compact app versions (build numbers moved to submenus) and offer SSH launch from Bonjour when available.
- Menu actions are grouped below toggles; Open Canvas hides when disabled and Voice Wake now anchors the mic picker.
- Connections now include Discord provider status + configuration UI.
- Menu bar gains an Allow Camera toggle alongside Canvas.
- Session list polish: sleeping/disconnected/error states, usage bar restored, padding + bar sizing tuned, syncing menu removed, header hidden when disconnected.
- Chat UI polish: tool call cards + merged tool results, glass background, tighter composer spacing, visual effect host tweaks.
- OAuth storage moved; legacy session syncing metadata removed.
- Remote SSH tunnels now get health checks; Debug → Ports highlights unhealthy tunnels and offers Reset SSH tunnel.
- Menu bar session/node sections no longer reflow while open, keeping hover highlights aligned.
- Menu hover highlights now span the full width (including submenu arrows).
- Menu session rows now refresh while open without width changes (no more stuck “Loading sessions…”).
- Menu width no longer grows on hover when moving the mouse across rows.
- Context usage bars now have higher contrast in light mode.
- macOS node timeouts now share a single async timeout helper for consistent behavior.
- WebChat window defaults tightened (narrower width, edge-to-edge layout) and the SwiftUI tag removed from the title.
### Nodes & Canvas
- Debug status overlay gated and toggleable on macOS/iOS/Android nodes.
- Gateway now derives the canvas host URL via a shared helper for bridge + WS handshakes (avoids loopback pitfalls).
- `canvas a2ui push` validates JSONL with line errors, rejects v0.9 payloads, and supports `--text` quick renders.
- `nodes rename` lets you override paired node display names without editing JSON.
- Android scaffold asset cleanup; iOS canvas/voice wake adjustments.
### Logging & Observability
- New subsystem console formatter with color modes, shortened prefixes, and TTY detection; browser/gateway logs route through the subsystem logger.
- WhatsApp console output streamlined; chalk/tslog typing fixes.
### Web UI
- Chat is now the dashboard landing view; health status simplified; initial scroll animation removed.
### Build, Dev, Docs
- Notarization flow added for macOS release artifacts; packaging scripts updated.
- macOS signing auto-selects Developer ID → Apple Distribution → Apple Development; no ad-hoc fallback.
- Added type-aware oxlint; docs list resolves from cwd; formatting/lint cleanup and dependency bumps (Peekaboo).
- Docs refreshed for tools, custom model providers, Discord, queue/routing, group activation commands, logging, restart semantics, release notes, GitHub pages CTAs, and npm pitfalls.
- `pnpm build` now skips A2UI bundling for faster builds (run `pnpm canvas:a2ui:bundle` when needed).
### Tests
- Coverage added for models config merging, WhatsApp reply context, QR login flows, auto-reply behavior, and gateway SIGTERM timeouts.
- Added gateway webhook coverage (auth, validation, and summary posting).
- Vitest now isolates HOME/XDG config roots so tests never touch a real `~/.clawdis` install.
## 2.0.0-beta2 — 2025-12-21
Second beta focused on bundled gateway packaging, skills management, onboarding polish, and provider reliability.
## 2025.12.05–2025.12.03 (pre-Clawdbot)
### Highlights
- Bundled gateway packaging: bun-compiled embedded gateway, new `gateway-daemon` command, launchd support, DMG packaging (zip+DMG).
- Skills platform: managed/bundled skills, install metadata + installers (uv), skill search + website, media/transcription helpers.
- macOS app: new Connections settings w/ provider status + QR login, skills settings redesign w/ install targets, models list loaded from the Gateway, clearer local/remote gateway choices.
- Web/agent UX: tool summary streaming + runtime toggle, WhatsApp QR login tool, agent steering queue, voice wake routes to main session, workspace bootstrap ritual.
- Pi-only agent path and web-only gateway workflow.
- Thinking/verbose directives, group chat support, and heartbeat controls.
- `clawdbot agent` CLI added; session tables and health reporting.
### Gateway & providers
- Gateway: `models.list`, provider status events + RPC coverage, tailscale auth + PAM, bind-mode config, enriched agent WS logs, safer upgrade socket handling, fixed handshake auth crash.
- WhatsApp Web: QR login flow improvements (logged-out clearing, wait flow), self-chat mode handling, removed batching delay, web inbox made non-blocking.
- Telegram: normalized chat IDs with clearer error reporting.
## 2025.11.28–2025.11.25 (early web-only)
### Canvas & browser control
- Canvas host served on Gateway port; removed standalone canvasHost port config; restored action bridge; refreshed A2UI bundle + message context; bridge canvas host for nodes.
- A2UI full-screen gutters + status clearance after successful load to avoid overlay collisions.
- Browser control API simplified; added MCP tool dispatch + native actions; control server can start without Playwright; hook timeouts extended.
### macOS UI polish
- Onboarding chat UI: kickoff flow, bubble tails, spacing + bottom bar refinements, window sizing tweaks, show Dock icon during onboarding.
- Skills UI: stabilized action column, fixed install target access, refined list layout and sizing, always show CLI installer.
- Remote/local gateway: auto-enable local gateway, clearer labels, re-ensure remote tunnel, hide local bridge discovery in remote mode.
### Build, CI, deps
- Bundled playwright-core + chromium-bidi/long; bun gateway bytecode builds; swiftformat/biome CI fixes; iOS lint script updates; Android icon/compiler updates; ignored new ClawdisKit `.swiftpm` path.
### Docs
- README architecture refresh + npm header image fix; onboarding/bootstrap steps; skills install guidance + new skills; browser/canvas control docs; bundled gateway + DMG packaging notes.
## 2.0.0-beta1 — 2025-12-19
First Clawdis release post rebrand. This is a semver-major because we dropped legacy providers/agents and moved defaults to new paths while adding a full macOS companion app, a WebSocket Gateway, and an iOS node.
### Bug Fixes
- macOS: Voice Wake / push-to-talk no longer initialize `AVAudioEngine` at app launch, preventing Bluetooth headphones from switching into headset profile when voice features are unused. (Thanks @Nachx639)
### Breaking
- Renamed to **Clawdis**: defaults now live under `~/.clawdis` (sessions in `~/.clawdis/sessions/`, IPC at `~/.clawdis/clawdis.sock`, logs in `/tmp/clawdis`). Launchd labels and config filenames follow the new name; legacy stores are copied forward on first run.
- Pi only: only the embedded Pi runtime remains, and the agent CLI/CLI flags for Claude/Codex/Gemini were removed. The Pi CLI runs in RPC mode with a persistent worker.
- WhatsApp Web is the only transport; Twilio support and related CLI flags/tests were removed.
- Direct chats now collapse into a single `main` session by default (no config needed); groups stay isolated as `group:<jid>`.
- Gateway is now a loopback-only WebSocket daemon (`ws://127.0.0.1:18789`) that owns all providers/state; clients (CLI, WebChat, macOS app, nodes) connect to it. Start it explicitly (`clawdis gateway …`) or via Clawdis.app; helper subcommands no longer auto-spawn a gateway.
### Gateway, nodes, and automation
- New typed Gateway WS protocol (JSON schema validated) with `clawdis gateway {health,status,send,agent,call}` helpers and structured presence/instance updates for all clients.
- Optional LAN-facing bridge (`tcp://0.0.0.0:18790`) keeps the Gateway loopback-only while enabling direct Bonjour-discovered connections for paired nodes.
- Node pairing + management via `clawdis nodes {pending,approve,reject,invoke}` (used by the iOS node and future remote nodes).
- Cron jobs are Gateway-owned (`clawdis cron …`) with run history stored as JSONL and support for “isolated summary” posting into the main session.
### macOS companion app
- **Clawdis.app menu bar companion**: packaged, signed bundle with gateway start/stop, launchd toggle, project-root and pnpm/node auto-resolution, live log shortcut, restart button, and status/recipient table plus badges/dimming for attention and paused states.
- **On-device Voice Wake**: Apple speech recognizer with wake-word table, language picker, live mic meter, “hold until silence,” animated ears/legs, and main-session routing that replies on the **last used surface** (WhatsApp/Telegram/WebChat). Delivery failures are logged, and the run remains visible via WebChat/session logs.
- **WebChat & Debugging**: bundled WebChat UI, Debug tab with heartbeat sliders, session-store picker, log opener (`clawlog`), gateway restart, health probes, and scrollable settings panes.
- **Browser control**: manage clawd’s dedicated Chrome/Chromium with tab listing/open/focus/close, screenshots, DOM query/dump, and “AI snapshots” (aria/domSnapshot/ai) via `clawdis browser …` and UI controls.
- **Remote gateway control**: Bonjour discovery for local masters plus SSH-tunnel fallback for remote control when multicast is unavailable.
### iOS node
- New iOS companion app that pairs to the Gateway bridge, reports presence as a node, and exposes a WKWebView “Canvas” for agent-driven UI.
- `clawdis nodes invoke` supports `canvas.eval` and `canvas.snapshot` to drive and verify the iOS Canvas (fails fast when the iOS node is backgrounded).
- Voice wake words are configurable in-app; the iOS node reconnects to the last bridge when credentials are still present in Keychain.
### WhatsApp & agent experience
- Group chats fully supported: mention-gated triggers (including media-only captions), sender attribution, session primer with subject/member roster, allowlist bypass when you’re @‑mentioned, and safer handling of view-once/ephemeral media.
- Thinking/verbosity directives: `/think` and `/verbose` acknowledge and persist per session while allowing inline overrides; verbose mode streams tool metadata with emoji/args/previews and coalesces bursts to reduce WhatsApp noise.
- Heartbeats: configurable cadence with CLI/GUI toggles; directive acks suppressed during heartbeats; array/multi-payload replies normalized for Baileys.
- Reply quality: smarter chunking on words/newlines, fallback warnings when media fails to send, self-number mention detection, and primed group sessions send the roster on first turn.
- In-chat `/status`: prints agent readiness, session context usage %, current thinking/verbose options, and when the WhatsApp web creds were refreshed (helps decide when to re-scan QR); still available via `clawdis status` CLI for web session health.
### CLI, RPC, and health
- New `clawdis agent` command plus a persistent Pi RPC worker (auto-started) enables direct agent chats; `clawdis status` renders a colored session/recipient table.
- `clawdis health` probes WhatsApp link status, connect latency, heartbeat interval, session-store recency, and IPC socket presence (JSON mode for monitors).
- Added `--help`/`--version` flags; login/logout accept `--provider` (WhatsApp default). Console output is mirrored into pino logs under `/tmp/clawdis`.
- RPC stability: stdin/stdout loop for Pi, auto-restart worker, raw error surfacing, and deliver-via-RPC when JSON agent output is returned.
### Security & hardening
- Media server blocks symlink/path traversal, clears temporary downloads, and rotates logs daily (24h retention).
- Session store purged on logout; IPC socket directory permissions tightened (0700/0600).
- Launchd PATH and helper lookup hardened for packaged macOS builds; health probes surface missing binaries quickly.
### Docs
- Added `docs/telegram.md` outlining the Telegram Bot API provider (grammY) and how it shares the `main` session. Default grammY throttler keeps Bot API calls under rate limits.
- Gateway can run WhatsApp + Telegram together when configured; `clawdis send --provider telegram …` sends via the Telegram bot (webhook/proxy options documented).
## 1.5.0 — 2025-12-05
### Breaking
- Dropped all non-Pi agents (Claude, Codex, Gemini, Opencode); only the embedded Pi runtime remains and related CLI helpers have been removed.
- Removed Twilio support and all related commands/options (webhook/up/provider flags/wait-poll); CLAWDIS is Baileys Web-only.
### Changes
- Default agent handling now favors Pi RPC while falling back to plain command execution for non-Pi invocations, keeping heartbeat/session plumbing intact.
- Documentation updated to reflect Pi-only support and to mark legacy Claude paths as historical.
- Status command reports web session health + session recipients; config paths are locked to `~/.clawdis` with session metadata stored under `~/.clawdis/sessions/`.
- Simplified send/agent/gateway/heartbeat to web-only delivery; removed Twilio mocks/tests and dead code.
- Pi RPC timeout is now inactivity-based (5m without events) and error messages show seconds only.
- Pi sessions now write to `~/.clawdis/sessions/` by default (legacy session logs from older installs are copied over when present).
- Directive triggers (`/think`, `/verbose`, `/stop` et al.) now reply immediately using normalized bodies (timestamps/group prefixes stripped) without waiting for the agent.
- Directive/system acks carry a `⚙️` prefix and verbose parsing rejects typoed `/ver*` strings so unrelated text doesn’t flip verbosity.
- Batched history blocks no longer trip directive parsing; `/think` in prior messages won't emit stray acknowledgements.
- RPC fallbacks no longer echo the user's prompt (e.g., pasting a link) when the agent returns no assistant text.
- Heartbeat prompts with `/think` no longer send directive acks; heartbeat replies stay silent on settings.
- `clawdis sessions` now renders a colored table (a la oracle) with context usage shown in k tokens and percent of the context window.
## 1.4.1 — 2025-12-04
### Changes
- Added `clawdis agent` CLI command to talk directly to the configured agent using existing session handling (no WhatsApp send), with JSON output and delivery option.
- `/new` reset trigger now works even when inbound messages have timestamp prefixes (e.g., `[Dec 4 17:35]`).
- WhatsApp mention parsing accepts nullable arrays and flattens safely to avoid missed mentions.
## 1.4.0 — 2025-12-03
### Highlights
- **Thinking directives & state:** `/t|/think|/thinking <level>` (aliases off|minimal|low|medium|high|max/highest). Inline applies to that message; directive-only message pins the level for the session; `/think:off` clears. Resolution: inline > session override > `agent.thinkingDefault` > off. Pi gets `--thinking <level>` (except off); other agents append cue words (`think` → `think hard` → `think harder` → `ultrathink`). Heartbeat probe uses `HEARTBEAT /think:high`.
- **Group chats (web provider):** Clawdis now fully supports WhatsApp groups: mention-gated triggers (including image-only @ mentions), recent group history injection, per-group sessions, sender attribution, and a first-turn primer with group subject/member roster; heartbeats are skipped for groups.
- **Group session primer:** The first turn of a group session now tells the agent it is in a WhatsApp group and lists known members/subject so it can address the right speaker.
- **Media failures are surfaced:** When a web auto-reply media fetch/send fails (e.g., HTTP 404), we now append a warning to the fallback text so you know the attachment was skipped.
- **Verbose directives + session hints:** `/v|/verbose on|full|off` mirrors thinking: inline > session > config default. Directive-only replies with an acknowledgement; invalid levels return a hint. When enabled, tool results from JSON-emitting agents (Pi, etc.) are forwarded as metadata-only `[🛠️ <tool-name> <arg>]` messages (now streamed as they happen), and new sessions surface a `🧭 New session: <id>` hint.
- **Verbose tool coalescing:** successive tool results of the same tool within ~1s are batched into one `[🛠️ tool] arg1, arg2` message to reduce WhatsApp noise.
- **Directive confirmations:** Directive-only messages now reply with an acknowledgement (`Thinking level set to high.` / `Thinking disabled.`) and reject unknown levels with a helpful hint (state is unchanged).
- **Pi stability:** RPC replies buffered until the assistant turn finishes; parsers return consistent `texts[]`; web auto-replies keep a warm Pi RPC process to avoid cold starts.
- **Claude prompt flow:** One-time `sessionIntro` with per-message `/think:high` bodyPrefix; system prompt always sent on first turn even with `sendSystemOnce`.
- **Heartbeat UX:** Backpressure skips reply heartbeats while other commands run; skips don’t refresh session `updatedAt`; web heartbeats normalize array payloads and optional `heartbeatCommand`.
- **Control via WhatsApp:** Send `/restart` to restart the launchd service (`com.steipete.clawdis`) from your allowed numbers.
- **Pi completion signal:** RPC now resolves on Pi’s `agent_end` (or process exit) so late assistant messages aren’t truncated; 5-minute hard cap only as a failsafe.
### Reliability & UX
- Outbound chunking prefers newlines/word boundaries and enforces caps (~4000 chars for web/WhatsApp).
- Web auto-replies fall back to caption-only if media send fails; hosted media MIME-sniffed and cleaned up immediately.
- IPC gateway send shows typing indicator; batched inbound messages keep timestamps; watchdog restarts WhatsApp after long inactivity.
- Early `allowFrom` filtering prevents decryption errors; same-phone mode supported with echo suppression.
- All console output is now mirrored into pino logs (still printed to stdout/stderr), so verbose runs keep full traces.
- `--verbose` now forces log level `trace` (was `debug`) to capture every event.
- Verbose tool messages now include emoji + args + a short result preview for bash/read/edit/write/attach (derived from RPC tool start/end events).
### Security / Hardening
- IPC socket hardened (0700 dir / 0600 socket, no symlinks/foreign owners); `clawdis logout` also prunes session store.
- Media server blocks symlinks and enforces path containment; logging rotates daily and prunes >24h.
### Bug Fixes
- Web group chats now bypass the second `allowFrom` check (we still enforce it on the group participant at inbox ingest), so mentioned group messages reply even when the group JID isn’t in your allowlist.
- `logVerbose` also writes to the configured Pino logger at debug level (without breaking stdout).
- Group auto-replies now append the triggering sender (`[from: Name (+E164)]`) to the batch body so agents can address the right person in group chats.
- Media-only pings now pick up mentions inside captions (image/video/etc.), so @-mentions on media-only messages trigger replies.
- MIME sniffing and redirect handling for downloads/hosted media.
- Response prefix applied to heartbeat alerts; heartbeat array payloads handled for both providers.
- Pi RPC typing exposes `signal`/`killed`; NDJSON parsers normalized across agents.
- Pi session resumes now append `--continue`, so existing history/think level are reloaded instead of starting empty.
### Testing
- Fixtures isolate session stores; added coverage for thinking directives, stateful levels, heartbeat backpressure, and agent parsing.
## 1.3.0 — 2025-12-02
### Highlights
- **Pluggable agents (Claude, Pi, Codex, Opencode):** agent selection via config/CLI plus per-agent argv builders and NDJSON parsers enable swapping without template changes.
- **Safety stop words:** `stop|esc|abort|wait|exit` immediately reply “Agent was aborted.” and mark the session so the next prompt is prefixed with an abort reminder.
- **Agent session reliability:** Only Claude returns a stable `session_id`; others may reset between runs.
### Bug Fixes
- Empty `result` fields no longer leak raw JSON to users.
- Heartbeat alerts now honor `responsePrefix`.
- Command failures return user-friendly messages.
- Test session isolation to avoid touching real `sessions.json`.
- (Removed in 2.0.0) IPC reuse for `clawdis send/heartbeat` prevents Signal/WhatsApp session corruption.
- Web send respects media kind (image/audio/video/document) with correct limits.
### Changes
- (Removed in 2.0.0) IPC gateway socket at `~/.clawdis/ipc/gateway.sock` with automatic CLI fallback.
- Batched inbound messages with timestamps; typing indicator after sends.
- Watchdog restarts WhatsApp after long inactivity; heartbeat logging includes minutes since last message.
- Early `allowFrom` filtering before decryption.
- Same-phone mode with echo detection and optional message prefix marker.
## 1.2.2 — 2025-11-28
### Changes
- Manual heartbeat sends: `clawdis heartbeat --message/--body` (web provider only); `--dry-run` previews payloads.
## 1.2.1 — 2025-11-28
### Changes
- Media MIME-first handling; hosted media extensions derived from detected MIME with tests.
### Planned / in progress (from prior notes)
- Heartbeat targeting quality: clearer recipient resolution and verbose logs.
- Heartbeat delivery preview (Claude path) dry-run.
- Simulated inbound hook for local testing.
## 1.2.0 — 2025-11-27
### Changes
- Heartbeat interval default 10m for command mode; prompt `HEARTBEAT /think:high`; skips don’t refresh session; session `heartbeatIdleMinutes` support.
- Heartbeat tooling: `--session-id`, `--heartbeat-now` (inline flag on `gateway`) for immediate startup probes.
- Prompt structure: `sessionIntro` plus per-message `/think:high`; session idle up to 7 days.
- Thinking directives: `/think:<level>`; Pi uses `--thinking`; others append cue; `/think:off` no-op.
- Robustness: Baileys/WebSocket guards; global unhandled error handlers; WhatsApp LID mapping; hosted media MIME-sniffing and cleanup.
- Docs: README Clawd setup; `docs/claude-config.md` for live config.
## 1.1.0 — 2025-11-26
### Changes
- Web auto-replies resize/recompress media and honor `agent.mediaMaxMb`.
- Detect media kind, enforce provider caps (images ≤6MB, audio/video ≤16MB, docs ≤100MB).
- `session.sendSystemOnce` and optional `sessionIntro`.
- Typing indicator refresh during commands; configurable via `agent.typingIntervalSeconds`.
- Optional audio transcription via external CLI.
- Command replies return structured payload/meta; respect `mediaMaxMb`; log Claude metadata; include `cwd` in timeout messages.
- Web provider refactor; logout command; web-only gateway start helper.
- Structured reconnect/heartbeat logging; bounded backoff with CLI/config knobs; troubleshooting guide.
- Relay help prints effective heartbeat/backoff when in web mode.
## 1.0.4 — 2025-11-25
### Changes
- Timeout fallbacks send partial stdout (≤800 chars) to the user instead of silence; tests added.
- Web gateway auto-reconnects after Baileys/WebSocket drops; close propagation tests.
## 0.1.3 — 2025-11-25
### Changes
- Auto-replies send a WhatsApp fallback message on command/Claude timeout with truncated stdout.
- Added tests for timeout fallback and partial-output truncation.
- Heartbeat CLI + interval handling.
- Media MIME sniffing, size caps, and timeout fallbacks.
- Web provider reconnects and early stability fixes.

View File

@@ -1,9 +1,9 @@
# Contributing to Clawdis
# Contributing to Clawdbot
Welcome to the lobster tank! 🦞
## Quick Links
- **GitHub:** https://github.com/steipete/clawdis
- **GitHub:** https://github.com/clawdbot/clawdbot
- **Discord:** https://discord.gg/qkhbAGHRBT
- **X/Twitter:** [@steipete](https://x.com/steipete) / [@clawdbot](https://x.com/clawdbot)
@@ -20,11 +20,11 @@ Welcome to the lobster tank! 🦞
## How to Contribute
1. **Bugs & small fixes** → Open a PR!
2. **New features / architecture** → Start a [GitHub Discussion](https://github.com/steipete/clawdis/discussions) or ask in Discord first
2. **New features / architecture** → Start a [GitHub Discussion](https://github.com/clawdbot/clawdbot/discussions) or ask in Discord first
3. **Questions** → Discord #setup-help
## Before You PR
- Test locally with your Clawdis instance
- Test locally with your Clawdbot instance
- Run linter: `npm run lint`
- Keep PRs focused (one thing per PR)
- Describe what & why

View File

@@ -19,9 +19,9 @@ RUN apt-get update \
xvfb \
&& rm -rf /var/lib/apt/lists/*
COPY scripts/sandbox-browser-entrypoint.sh /usr/local/bin/clawdis-sandbox-browser
RUN chmod +x /usr/local/bin/clawdis-sandbox-browser
COPY scripts/sandbox-browser-entrypoint.sh /usr/local/bin/clawdbot-sandbox-browser
RUN chmod +x /usr/local/bin/clawdbot-sandbox-browser
EXPOSE 9222 5900 6080
CMD ["clawdis-sandbox-browser"]
CMD ["clawdbot-sandbox-browser"]

397
README.md
View File

@@ -1,7 +1,7 @@
# 🦞 CLAWDIS — Personal AI Assistant
# 🦞 CLAWDBOT — Personal AI Assistant
<p align="center">
<img src="https://raw.githubusercontent.com/steipete/clawdis/main/docs/whatsapp-clawd.jpg" alt="CLAWDIS" width="400">
<img src="https://raw.githubusercontent.com/clawdbot/clawdbot/main/docs/whatsapp-clawd.jpg" alt="CLAWDBOT" width="400">
</p>
<p align="center">
@@ -9,56 +9,42 @@
</p>
<p align="center">
<a href="https://github.com/steipete/clawdis/actions/workflows/ci.yml?branch=main"><img src="https://img.shields.io/github/actions/workflow/status/steipete/clawdis/ci.yml?branch=main&style=for-the-badge" alt="CI status"></a>
<a href="https://github.com/steipete/clawdis/releases"><img src="https://img.shields.io/github/v/release/steipete/clawdis?include_prereleases&style=for-the-badge" alt="GitHub release"></a>
<a href="https://github.com/clawdbot/clawdbot/actions/workflows/ci.yml?branch=main"><img src="https://img.shields.io/github/actions/workflow/status/clawdbot/clawdbot/ci.yml?branch=main&style=for-the-badge" alt="CI status"></a>
<a href="https://github.com/clawdbot/clawdbot/releases"><img src="https://img.shields.io/github/v/release/clawdbot/clawdbot?include_prereleases&style=for-the-badge" alt="GitHub release"></a>
<a href="https://discord.gg/clawd"><img src="https://img.shields.io/discord/1456350064065904867?label=Discord&logo=discord&logoColor=white&color=5865F2&style=for-the-badge" alt="Discord"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge" alt="MIT License"></a>
</p>
**Clawdis** 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.
**Clawdbot** is a *personal AI assistant* you run on your own devices.
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) · Docker (optional): [`docs/docker.md`](docs/docker.md) · Discord: https://discord.gg/clawd
Website: [https://clawdbot.com](https://clawdbot.com) · Docs: [https://docs.clawdbot.com](https://docs.clawdbot.com/) · Showcase: [https://docs.clawdbot.com/showcase](https://docs.clawdbot.com/showcase) · FAQ: [https://docs.clawdbot.com/faq](https://docs.clawdbot.com/faq) · Wizard: [https://docs.clawdbot.com/wizard](https://docs.clawdbot.com/wizard) · Nix: [https://github.com/clawdbot/nix-clawdbot](https://github.com/clawdbot/nix-clawdbot) · Docker: [https://docs.clawdbot.com/docker](https://docs.clawdbot.com/docker) · Discord: [https://discord.gg/clawd](https://discord.gg/clawd)
Preferred setup: run the onboarding wizard (`clawdis onboard`). It walks through gateway, workspace, providers, and skills. The CLI wizard is the recommended path and works on **macOS, Windows, and Linux**.
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://docs.clawdbot.com/onboarding).
## 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
pnpm install
pnpm build
pnpm ui:build
pnpm clawdbot onboard
```
Your surfaces
│
▼
┌───────────────────────────────┐
│ Gateway │ ws://127.0.0.1:18789
│ (control plane) │ tcp://0.0.0.0:18790 (optional Bridge)
└──────────────┬────────────────┘
│
├─ Pi agent (RPC)
├─ CLI (clawdis …)
├─ WebChat (browser)
├─ macOS app (Clawdis.app)
└─ iOS node (Canvas + voice)
```
## What Clawdis does
- **Personal assistant** — one user, one identity, one memory surface.
- **Multi-surface inbox** — WhatsApp, Telegram, Discord, iMessage, WebChat, macOS, iOS. Signal support via `signal-cli` (see `docs/signal.md`). iMessage uses `imsg` (see `docs/imessage.md`).
- **Voice wake + push-to-talk** — local speech recognition on macOS/iOS.
- **Canvas** — a live visual workspace you can drive from the agent.
- **Automation-ready** — browser control, media handling, and tool streaming.
- **Local-first control plane** — the Gateway owns state, everything else connects.
- **Group chats** — mention-based by default, `/activation always|mention` per group (owner-only).
- **Nix mode** — opt-in declarative config + read-only UI when `CLAWDIS_NIX_MODE=1`.
## How it works (short)
- **Gateway** is the single source of truth for sessions/providers.
- **Loopback-first**: `ws://127.0.0.1:18789` by default.
- **Bridge** (optional) exposes a paired-node port for iOS/Android.
- **Agent runtime** is **Pi** in RPC mode.
## Quick start (from source)
@@ -70,29 +56,156 @@ pnpm build
pnpm ui:build
# Recommended: run the onboarding wizard
pnpm clawdis onboard
pnpm clawdbot onboard
# Link WhatsApp (stores creds in ~/.clawdis/credentials)
pnpm clawdis login
# Link WhatsApp (stores creds in ~/.clawdbot/credentials)
pnpm clawdbot login
# Start the gateway
pnpm clawdis gateway --port 18789 --verbose
pnpm clawdbot gateway --port 18789 --verbose
# Dev loop (auto-reload on TS changes)
pnpm gateway:watch
# Send a message
pnpm clawdis send --to +1234567890 --message "Hello from Clawdis"
pnpm clawdbot send --to +1234567890 --message "Hello from Clawdbot"
# Talk to the assistant (optionally deliver back to WhatsApp/Telegram/Discord)
pnpm clawdis agent --message "Ship checklist" --thinking high
# Talk to the assistant (optionally deliver back to WhatsApp/Telegram/Slack/Discord)
pnpm clawdbot agent --message "Ship checklist" --thinking high
```
If you run from source, prefer `pnpm clawdis …` (not global `clawdis`).
Upgrading? `clawdbot doctor`.
If you run from source, prefer `pnpm clawdbot …` (not global `clawdbot`).
## Highlights
- **[Local-first Gateway](https://docs.clawdbot.com/gateway)** — single control plane for sessions, providers, tools, and events.
- **[Multi-surface inbox](https://docs.clawdbot.com/surface)** — WhatsApp, Telegram, Slack, Discord, Signal, iMessage, WebChat, macOS, iOS/Android.
- **[Voice Wake](https://docs.clawdbot.com/voicewake) + [Talk Mode](https://docs.clawdbot.com/talk)** — always-on speech for macOS/iOS/Android with ElevenLabs.
- **[Live Canvas](https://docs.clawdbot.com/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.clawdbot.com/refactor/canvas-a2ui).
- **[First-class tools](https://docs.clawdbot.com/tools)** — browser, canvas, nodes, cron, sessions, and Discord/Slack actions.
- **[Companion apps](https://docs.clawdbot.com/macos)** — macOS menu bar app + iOS/Android [nodes](https://docs.clawdbot.com/nodes).
- **[Onboarding](https://docs.clawdbot.com/wizard) + [skills](https://docs.clawdbot.com/skills)** — wizard-driven setup with bundled/managed/workspace skills.
## Everything we built so far
### Core platform
- [Gateway WS control plane](https://docs.clawdbot.com/gateway) with sessions, presence, config, cron, webhooks, [Control UI](https://docs.clawdbot.com/web), and [Canvas host](https://docs.clawdbot.com/refactor/canvas-a2ui).
- [CLI surface](https://docs.clawdbot.com/agent-send): gateway, agent, send, [wizard](https://docs.clawdbot.com/wizard), and [doctor](https://docs.clawdbot.com/doctor).
- [Pi agent runtime](https://docs.clawdbot.com/agent) in RPC mode with tool streaming and block streaming.
- [Session model](https://docs.clawdbot.com/session): `main` for direct chats, group isolation, activation modes, queue modes, reply-back. Group rules: [Groups](https://docs.clawdbot.com/groups).
- [Media pipeline](https://docs.clawdbot.com/images): images/audio/video, transcription hooks, size caps, temp file lifecycle. Audio details: [Audio](https://docs.clawdbot.com/audio).
### Surfaces + providers
- [Providers](https://docs.clawdbot.com/surface): [WhatsApp](https://docs.clawdbot.com/whatsapp) (Baileys), [Telegram](https://docs.clawdbot.com/telegram) (grammY), [Slack](https://docs.clawdbot.com/slack) (Bolt), [Discord](https://docs.clawdbot.com/discord) (discord.js), [Signal](https://docs.clawdbot.com/signal) (signal-cli), [iMessage](https://docs.clawdbot.com/imessage) (imsg), [WebChat](https://docs.clawdbot.com/webchat).
- [Group routing](https://docs.clawdbot.com/group-messages): mention gating, reply tags, per-surface chunking and routing. Surface rules: [Surface routing](https://docs.clawdbot.com/surface).
### Apps + nodes
- [macOS app](https://docs.clawdbot.com/macos): menu bar control plane, [Voice Wake](https://docs.clawdbot.com/voicewake)/PTT, [Talk Mode](https://docs.clawdbot.com/talk) overlay, [WebChat](https://docs.clawdbot.com/webchat), debug tools, [remote gateway](https://docs.clawdbot.com/remote) control.
- [iOS node](https://docs.clawdbot.com/ios): [Canvas](https://docs.clawdbot.com/mac/canvas), [Voice Wake](https://docs.clawdbot.com/voicewake), [Talk Mode](https://docs.clawdbot.com/talk), camera, screen recording, Bonjour pairing.
- [Android node](https://docs.clawdbot.com/android): [Canvas](https://docs.clawdbot.com/mac/canvas), [Talk Mode](https://docs.clawdbot.com/talk), camera, screen recording, optional SMS.
- [macOS node mode](https://docs.clawdbot.com/nodes): system.run/notify + canvas/camera exposure.
### Tools + automation
- [Browser control](https://docs.clawdbot.com/browser): dedicated clawd Chrome/Chromium, snapshots, actions, uploads, profiles.
- [Canvas](https://docs.clawdbot.com/mac/canvas): [A2UI](https://docs.clawdbot.com/refactor/canvas-a2ui) push/reset, eval, snapshot.
- [Nodes](https://docs.clawdbot.com/nodes): camera snap/clip, screen record, [location.get](https://docs.clawdbot.com/location-command), notifications.
- [Cron + wakeups](https://docs.clawdbot.com/cron); [webhooks](https://docs.clawdbot.com/webhook); [Gmail Pub/Sub](https://docs.clawdbot.com/gmail-pubsub).
- [Skills platform](https://docs.clawdbot.com/skills): bundled, managed, and workspace skills with install gating + UI.
### Ops + packaging
- [Control UI](https://docs.clawdbot.com/web) + [WebChat](https://docs.clawdbot.com/webchat) served directly from the Gateway.
- [Tailscale Serve/Funnel](https://docs.clawdbot.com/tailscale) or [SSH tunnels](https://docs.clawdbot.com/remote) with token/password auth.
- [Nix mode](https://docs.clawdbot.com/nix) for declarative config; [Docker](https://docs.clawdbot.com/docker)-based installs.
- [Doctor](https://docs.clawdbot.com/doctor) migrations, [logging](https://docs.clawdbot.com/logging).
## How it works (short)
```
WhatsApp / Telegram / Slack / Discord / Signal / iMessage / WebChat
│
▼
┌───────────────────────────────┐
│ Gateway │ ws://127.0.0.1:18789
│ (control plane) │ bridge: tcp://0.0.0.0:18790
└──────────────┬────────────────┘
│
├─ Pi agent (RPC)
├─ CLI (clawdbot …)
├─ WebChat UI
├─ macOS app
└─ iOS/Android nodes
```
## Key subsystems
- **[Gateway WebSocket network](https://docs.clawdbot.com/architecture)** — single WS control plane for clients, tools, and events (plus ops: [Gateway runbook](https://docs.clawdbot.com/gateway)).
- **[Tailscale exposure](https://docs.clawdbot.com/tailscale)** — Serve/Funnel for the Gateway dashboard + WS (remote access: [Remote](https://docs.clawdbot.com/remote)).
- **[Browser control](https://docs.clawdbot.com/browser)** — clawd‑managed Chrome/Chromium with CDP control.
- **[Canvas + A2UI](https://docs.clawdbot.com/mac/canvas)** — agent‑driven visual workspace (A2UI host: [Canvas/A2UI](https://docs.clawdbot.com/refactor/canvas-a2ui)).
- **[Voice Wake](https://docs.clawdbot.com/voicewake) + [Talk Mode](https://docs.clawdbot.com/talk)** — always‑on speech and continuous conversation.
- **[Nodes](https://docs.clawdbot.com/nodes)** — 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://docs.clawdbot.com/tailscale) · [Web surfaces](https://docs.clawdbot.com/web)
## 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://docs.clawdbot.com/remote) · [Nodes](https://docs.clawdbot.com/nodes) · [Security](https://docs.clawdbot.com/security)
## macOS permissions via the Gateway protocol
The macOS app can run in **node mode** and advertises its capabilities + permission map over the Gateway WebSocket (`node.list` / `node.describe`). Clients can then execute local actions via `node.invoke`:
- `system.run` runs a local command and returns stdout/stderr/exit code; set `needsScreenRecording: true` to require screen-recording permission (otherwise you’ll get `PERMISSION_MISSING`).
- `system.notify` posts a user notification and fails if notifications are denied.
- `canvas.*`, `camera.*`, `screen.record`, and `location.get` are also routed via `node.invoke` and follow TCC permission status.
Elevated bash (host permissions) is separate from macOS TCC:
- Use `/elevated on|off` to toggle per‑session elevated access when enabled + allowlisted.
- Gateway persists the per‑session toggle via `sessions.patch` (WS method) alongside `thinkingLevel`, `verboseLevel`, `model`, `sendPolicy`, and `groupActivation`.
Details: [Nodes](https://docs.clawdbot.com/nodes) · [macOS app](https://docs.clawdbot.com/macos) · [Gateway protocol](https://docs.clawdbot.com/architecture)
## Agent to Agent (sessions_* tools)
- Use these to coordinate work across sessions without jumping between chat surfaces.
- `sessions_list` — discover active sessions (agents) and their metadata.
- `sessions_history` — fetch transcript logs for a session.
- `sessions_send` — message another session; optional reply‑back ping‑pong + announce step (`REPLY_SKIP`, `ANNOUNCE_SKIP`).
Details: [Session tools](https://docs.clawdbot.com/session-tool)
## Skills registry (ClawdHub)
ClawdHub is a minimal skill registry. With ClawdHub enabled, the agent can search for skills automatically and pull in new ones as needed.
https://ClawdHub.com
## Chat commands
Send these in WhatsApp/Telegram/WebChat (group commands are owner-only):
Send these in WhatsApp/Telegram/Slack/WebChat (group commands are owner-only):
- `/status` — health + session info (group shows activation mode)
- `/new` or `/reset` — reset the session
@@ -101,49 +214,39 @@ Send these in WhatsApp/Telegram/WebChat (group commands are owner-only):
- `/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 `clawdis://` deep‑link interception (`ScreenController`).
- **Voice + deep links**: voice wake sends `voice.transcript` events; `clawdis://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 (Clawdis.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 `clawdis nodes …`.
- Controlled via `clawdbot nodes …`.
Runbook: `docs/ios/connect.md`.
Runbook: [iOS connect](https://docs.clawdbot.com/ios).
### Android node (internal)
### Android node (optional)
- Pairs via the same Bridge + pairing flow as iOS.
- Exposes Canvas, Camera, and Screen capture commands.
- Runbook: `docs/android/connect.md`.
- Runbook: [Android connect](https://docs.clawdbot.com/android).
## Agent workspace + skills
@@ -153,22 +256,32 @@ Runbook: `docs/ios/connect.md`.
## Configuration
Minimal `~/.clawdis/clawdis.json`:
Minimal `~/.clawdbot/clawdbot.json` (model + defaults):
```json5
{
whatsapp: {
allowFrom: ["+1234567890"]
agent: {
model: "anthropic/claude-opus-4-5"
}
}
```
### WhatsApp
[Full configuration reference (all keys + examples).](https://docs.clawdbot.com/configuration)
- Link the device: `pnpm clawdis login` (stores creds in `~/.clawdis/credentials`).
## Security model (important)
- **Default:** tools run on the host for the **main** session, so the agent has full access when it’s just you.
- **Group/channel safety:** set `agent.sandbox.mode: "non-main"` to run **non‑main sessions** (groups/channels) inside per‑session Docker sandboxes; bash then runs in Docker for those sessions.
- **Sandbox defaults:** allowlist `bash`, `process`, `read`, `write`, `edit`; denylist `browser`, `canvas`, `nodes`, `cron`, `discord`, `gateway`.
Details: [Security guide](https://docs.clawdbot.com/security) · [Docker + sandboxing](https://docs.clawdbot.com/docker) · [Sandbox config](https://docs.clawdbot.com/configuration)
### [WhatsApp](https://docs.clawdbot.com/whatsapp)
- Link the device: `pnpm clawdbot login` (stores creds in `~/.clawdbot/credentials`).
- Allowlist who can talk to the assistant via `whatsapp.allowFrom`.
### Telegram
### [Telegram](https://docs.clawdbot.com/telegram)
- Set `TELEGRAM_BOT_TOKEN` or `telegram.botToken` (env wins).
- Optional: set `telegram.groups` (with `telegram.groups."*".requireMention`), `telegram.allowFrom`, or `telegram.webhookUrl` as needed.
@@ -181,7 +294,11 @@ Minimal `~/.clawdis/clawdis.json`:
}
```
### Discord
### [Slack](https://docs.clawdbot.com/slack)
- Set `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` (or `slack.botToken` + `slack.appToken`).
### [Discord](https://docs.clawdbot.com/discord)
- Set `DISCORD_BOT_TOKEN` or `discord.token` (env wins).
- Optional: set `discord.slashCommand`, `discord.dm.allowFrom`, `discord.guilds`, or `discord.mediaMaxMb` as needed.
@@ -194,6 +311,18 @@ Minimal `~/.clawdis/clawdis.json`:
}
```
### [Signal](https://docs.clawdbot.com/signal)
- Requires `signal-cli` and a `signal` config section.
### [iMessage](https://docs.clawdbot.com/imessage)
- macOS only; Messages must be signed in.
### [WebChat](https://docs.clawdbot.com/webchat)
- Uses the Gateway WebSocket; no separate WebChat port/config.
Browser control (optional):
```json5
@@ -208,39 +337,97 @@ Browser control (optional):
## Docs
- [`docs/index.md`](docs/index.md) (overview)
- [`docs/configuration.md`](docs/configuration.md)
- [`docs/group-messages.md`](docs/group-messages.md)
- [`docs/gateway.md`](docs/gateway.md)
- [`docs/web.md`](docs/web.md)
- [`docs/discovery.md`](docs/discovery.md)
- [`docs/agent.md`](docs/agent.md)
- [`docs/discord.md`](docs/discord.md)
- [`docs/wizard.md`](docs/wizard.md)
- Webhooks + external triggers: [`docs/webhook.md`](docs/webhook.md)
- Gmail hooks (email → wake): [`docs/gmail-pubsub.md`](docs/gmail-pubsub.md)
Use these when you’re past the onboarding flow and want the deeper reference.
- [Start with the docs index for navigation and “what’s where.”](https://docs.clawdbot.com/)
- [Read the architecture overview for the gateway + protocol model.](https://docs.clawdbot.com/architecture)
- [Use the full configuration reference when you need every key and example.](https://docs.clawdbot.com/configuration)
- [Run the Gateway by the book with the operational runbook.](https://docs.clawdbot.com/gateway)
- [Learn how the Control UI/Web surfaces work and how to expose them safely.](https://docs.clawdbot.com/web)
- [Understand remote access over SSH tunnels or tailnets.](https://docs.clawdbot.com/remote)
- [Follow the onboarding wizard flow for a guided setup.](https://docs.clawdbot.com/wizard)
- [Wire external triggers via the webhook surface.](https://docs.clawdbot.com/webhook)
- [Set up Gmail Pub/Sub triggers.](https://docs.clawdbot.com/gmail-pubsub)
- [Learn the macOS menu bar companion details.](https://docs.clawdbot.com/mac/menu-bar)
- [Platform guides: Windows](https://docs.clawdbot.com/windows), [Linux](https://docs.clawdbot.com/linux), [macOS](https://docs.clawdbot.com/macos), [iOS](https://docs.clawdbot.com/ios), [Android](https://docs.clawdbot.com/android)
- [Debug common failures with the troubleshooting guide.](https://docs.clawdbot.com/troubleshooting)
- [Review security guidance before exposing anything.](https://docs.clawdbot.com/security)
## Advanced docs (discovery + control)
- [Discovery + transports](https://docs.clawdbot.com/discovery)
- [Bonjour/mDNS](https://docs.clawdbot.com/bonjour)
- [Gateway pairing](https://docs.clawdbot.com/gateway/pairing)
- [Remote gateway README](https://docs.clawdbot.com/remote-gateway-readme)
- [Control UI](https://docs.clawdbot.com/control-ui)
- [Dashboard](https://docs.clawdbot.com/dashboard)
## Operations & troubleshooting
- [Health checks](https://docs.clawdbot.com/health)
- [Gateway lock](https://docs.clawdbot.com/gateway-lock)
- [Background process](https://docs.clawdbot.com/background-process)
- [Browser troubleshooting (Linux)](https://docs.clawdbot.com/browser-linux-troubleshooting)
- [Logging](https://docs.clawdbot.com/logging)
## Deep dives
- [Agent loop](https://docs.clawdbot.com/agent-loop)
- [Presence](https://docs.clawdbot.com/presence)
- [TypeBox schemas](https://docs.clawdbot.com/typebox)
- [RPC adapters](https://docs.clawdbot.com/rpc)
- [Queue](https://docs.clawdbot.com/queue)
## Workspace & skills
- [Skills config](https://docs.clawdbot.com/skills-config)
- [Default AGENTS](https://docs.clawdbot.com/AGENTS.default)
- [Templates: AGENTS](https://docs.clawdbot.com/templates/AGENTS)
- [Templates: BOOTSTRAP](https://docs.clawdbot.com/templates/BOOTSTRAP)
- [Templates: IDENTITY](https://docs.clawdbot.com/templates/IDENTITY)
- [Templates: SOUL](https://docs.clawdbot.com/templates/SOUL)
- [Templates: TOOLS](https://docs.clawdbot.com/templates/TOOLS)
- [Templates: USER](https://docs.clawdbot.com/templates/USER)
## Platform internals
- [macOS dev setup](https://docs.clawdbot.com/mac/dev-setup)
- [macOS menu bar](https://docs.clawdbot.com/mac/menu-bar)
- [macOS voice wake](https://docs.clawdbot.com/mac/voicewake)
- [iOS node](https://docs.clawdbot.com/ios)
- [Android node](https://docs.clawdbot.com/android)
- [Windows app](https://docs.clawdbot.com/windows)
- [Linux app](https://docs.clawdbot.com/linux)
## Email hooks (Gmail)
[Gmail Pub/Sub wiring (gcloud + gogcli), hook tokens, and auto-watch behavior are documented here.](https://docs.clawdbot.com/gmail-pubsub)
Gateway auto-starts the watcher when `hooks.enabled=true` and `hooks.gmail.account` is set; `clawdbot hooks gmail run` is the manual daemon wrapper if you don’t want auto-start.
```bash
clawdis hooks gmail setup --account you@gmail.com
clawdis hooks gmail run
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/clawdis-mac.md`](docs/clawdis-mac.md)
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines, maintainers, and how to submit PRs.
AI/vibe-coded PRs welcome! 🤖
## Clawd
Clawdis 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>
</p>

View File

@@ -3,210 +3,53 @@
<channel>
<title>Clawdis</title>
<item>
<title>2.0.0-beta5</title>
<pubDate>Sat, 03 Jan 2026 07:15:16 +0100</pubDate>
<link>https://raw.githubusercontent.com/steipete/clawdis/main/appcast.xml</link>
<sparkle:version>2765</sparkle:version>
<sparkle:shortVersionString>2.0.0-beta5</sparkle:shortVersionString>
<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>3095</sparkle:version>
<sparkle:shortVersionString>2026.1.5-3</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>Clawdis 2.0.0-beta5</h2>
<h3>Fixed</h3>
<ul>
<li>Media: preserve GIF animation when uploading to Discord/other providers (skip JPEG optimization for image/gif).</li>
<li>Agent runtime: update pi-mono dependencies to 0.31.1 (agent-core split).</li>
<li>Dependencies: bump to latest compatible versions (TypeBox, grammY, Zod, Rolldown, oxlint-tsgolint).</li>
<li>Tests: cover read tool image metadata + text output.</li>
<li>Tests: add queue mode coverage (collect/followup + directive parsing).</li>
</ul>
<h3>Breaking</h3>
<ul>
<li>Skills config schema moved under <code>skills.*</code>:</li>
</ul>
- <code>skillsLoad.extraDirs</code> → <code>skills.load.extraDirs</code>
- <code>skillsInstall.*</code> → <code>skills.install.*</code>
- per-skill config map moved to <code>skills.entries</code> (e.g. <code>skills.peekaboo.enabled</code> → <code>skills.entries.peekaboo.enabled</code>)
- new optional bundled allowlist: <code>skills.allowBundled</code> (only affects bundled skills)
<ul>
<li>Sessions: group keys now use <code>surface:group:<id></code> / <code>surface:channel:<id></code>; legacy <code>group:*</code> keys migrate on next message; <code>groupdm</code> keys are no longer recognized.</li>
<li>Discord: remove legacy <code>discord.allowFrom</code>, <code>discord.guildAllowFrom</code>, and <code>discord.requireMention</code>; use <code>discord.dm</code> + <code>discord.guilds</code>.</li>
<li>Providers: Discord/Telegram no longer auto-start from env tokens alone; add <code>discord: { enabled: true }</code> / <code>telegram: { enabled: true }</code> to your config when using <code>DISCORD_BOT_TOKEN</code> / <code>TELEGRAM_BOT_TOKEN</code>.</li>
<li>Config: remove <code>routing.allowFrom</code>; use <code>whatsapp.allowFrom</code> instead (run <code>clawdis doctor</code> to migrate).</li>
<li>Config: remove <code>routing.groupChat.requireMention</code> + <code>telegram.requireMention</code>; use <code>whatsapp.groups</code>, <code>imessage.groups</code>, and <code>telegram.groups</code> defaults instead (run <code>clawdis doctor</code> to migrate).</li>
</ul>
<h3>Features</h3>
<ul>
<li>Discord: expand <code>discord</code> tool actions (reactions, stickers, polls, threads, search, moderation gates) (#115) — thanks @thewilloftheshadow.</li>
<li>Discord/Telegram: add reply tags (<code>[[reply_to_current]]</code>, <code>[[reply_to:<id>]]</code>) with per-provider <code>replyToMode</code> (off|first|all) for native threaded replies.</li>
<li>Talk mode: continuous speech conversations (macOS/iOS/Android) with ElevenLabs TTS, reply directives, and optional interrupt-on-speech.</li>
<li>Auto-reply: expand queue modes (steer/followup/collect/steer-backlog) with debounce/cap/drop options and followup backlog handling.</li>
<li>UI: add optional <code>ui.seamColor</code> accent to tint the Talk Mode side bubble (macOS/iOS/Android).</li>
<li>Nix mode: opt-in declarative config + read-only settings UI when <code>CLAWDIS_NIX_MODE=1</code> (thanks @joshp123 for the persistence — earned my trust; I'll merge these going forward).</li>
<li>CLI: add Google Antigravity OAuth auth option for Claude Opus 4.5/Gemini 3 (#88) — thanks @mukhtharcm.</li>
<li>Agent runtime: accept legacy <code>Z_AI_API_KEY</code> for Z.AI provider auth (maps to <code>ZAI_API_KEY</code>).</li>
<li>Groups: add per-group mention gating defaults/overrides for Telegram/WhatsApp/iMessage via <code>*.groups</code> with <code>"*"</code> defaults; Discord now supports <code>discord.guilds."*"</code> as a default.</li>
<li>Discord: add user-installed slash command handling with per-user sessions and auto-registration (#94) — thanks @thewilloftheshadow.</li>
<li>Discord: add DM enable/allowlist plus guild channel/user/guild allowlists with id/name matching.</li>
<li>Signal: add <code>signal-cli</code> JSON-RPC support for send/receive via the Signal provider.</li>
<li>iMessage: add imsg JSON-RPC integration (stdio), chat_id routing, and group chat support.</li>
<li>Chat UI: add recent-session dropdown switcher (main first) in macOS/iOS/Android + Control UI.</li>
<li>UI: add Discord/Signal/iMessage connection panels in macOS + Control UI (thanks @thewilloftheshadow).</li>
<li>Discord: allow agent-triggered reactions via <code>clawdis_discord</code> when enabled, and surface message ids in context.</li>
<li>Discord: revamp guild routing config with per-guild/channel rules and slugged display names; add optional group DM support (default off).</li>
<li>Discord: remove legacy guild/channel ignore lists in favor of per-guild allowlists (and proposed per-guild ignore lists).</li>
<li>Skills: add Trello skill for board/list/card management (thanks @clawd).</li>
<li>Docker: add containerized gateway/CLI setup via Dockerfile, compose, and setup script (thanks @dan-dr).</li>
<li>Tests: add a Z.AI live test gate for smoke validation when keys are present.</li>
<li>macOS Debug: add app log verbosity and rolling file log toggle for swift-log-backed app logs.</li>
<li>CLI: add onboarding wizard (gateway + workspace + skills) with daemon installers and Anthropic/Minimax setup paths.</li>
<li>CLI: add ASCII banner header to wizard entry points.</li>
<li>CLI: add <code>configure</code>, <code>doctor</code>, and <code>update</code> wizards for ongoing setup, health checks, and modernization.</li>
<li>CLI: add Signal CLI auto-install from GitHub releases in the wizard and persist wizard run metadata in config.</li>
<li>CLI: add remote gateway client config (gateway.remote.*) with Bonjour-assisted discovery.</li>
<li>CLI: enhance <code>clawdis tui</code> with model/session pickers, tool cards, and slash commands (local or remote).</li>
<li>Gateway: allow <code>sessions.patch</code> to set per-session model overrides (used by the TUI <code>/model</code> flow).</li>
<li>Skills: allow <code>bun</code> as a node manager for skill installs.</li>
<li>Skills: add <code>things-mac</code> (Things 3 CLI) for read/search plus add/update via URL scheme.</li>
<li>Skills: add Apple Notes + Reminders skills via memo CLI (thanks @tylerwince).</li>
<li>Tests: add a Docker-based onboarding E2E harness.</li>
<li>Tests: harden wizard E2E flows for reset, providers, skills, and remote non-interactive runs.</li>
<li>Browser tools: add remote CDP URL support, Linux launcher options (<code>executablePath</code>, <code>noSandbox</code>), and surface <code>cdpUrl</code> in status.</li>
<li>Skills: add tmux-first coding-agent skill + <code>requires.anyBins</code> gate for multi-CLI setup (thanks @sreekaransrinath).</li>
</ul>
<description><![CDATA[<h2>Clawdbot 2026.1.5-3</h2>
<h3>Fixes</h3>
<ul>
<li>Gog calendar: format date ranges as RFC 3339 with timezone to satisfy Google Calendar API (thanks @jayhickey).</li>
<li>macOS onboarding: add scrollable page gutter for overflowing content (#105) — thanks @thewilloftheshadow.</li>
<li>Chat UI: keep the chat scrolled to the latest message after switching sessions.</li>
<li>Chat UI: show rich session display names in Web Chat + SwiftUI + Android.</li>
<li>Auto-reply: stream completed reply blocks as soon as they finish (configurable default + break); skip empty tool-only blocks unless verbose.</li>
<li>Discord: avoid duplicate sends when block streaming is enabled (race with typing hook).</li>
<li>Providers: make outbound text chunk limits configurable via <code>*.textChunkLimit</code> (defaults remain 4000/Discord 2000).</li>
<li>CLI onboarding: persist gateway token in config so local CLI auth works; recommend auth Off unless you need multi-machine access.</li>
<li>Control UI: accept a <code>?token=</code> URL param to auto-fill Gateway auth; onboarding now opens the dashboard with token auth when configured.</li>
<li>Agent prompt: remove hardcoded user name in system prompt example.</li>
<li>Chat UI: add extra top padding before the first message bubble in Web Chat (macOS/iOS/Android).</li>
<li>Control UI: refine Web Chat session selector styling (chevron spacing + background).</li>
<li>WebChat: stream live updates for sessions even when runs start outside the chat UI.</li>
<li>Gateway CLI: read <code>CLAWDIS_GATEWAY_PASSWORD</code> from environment in <code>callGateway()</code> — allows <code>doctor</code>/<code>health</code> commands to auth without explicit <code>--password</code> flag.</li>
<li>Gateway: add password auth support for remote gateway connections (thanks @jeffersonwarrior).</li>
<li>Auto-reply: strip stray leading/trailing <code>HEARTBEAT_OK</code> from normal replies; drop short (≤ 30 chars) heartbeat acks.</li>
<li>WhatsApp auto-reply: default to self-only when no config is present.</li>
<li>Logging: trim provider prefix duplication in Discord/Signal/Telegram runtime log lines.</li>
<li>Logging/Signal: treat signal-cli "Failed …" lines as errors in gateway logs.</li>
<li>Discord: include recent guild context when replying to mentions and add <code>discord.historyLimit</code> to tune how many messages are captured.</li>
<li>Discord: include author tag + id in group context <code>[from:]</code> lines for ping-ready replies (thanks @thewilloftheshadow).</li>
<li>Discord: include replied-to message context when a Discord message references another message (thanks @thewilloftheshadow).</li>
<li>Discord: preserve newlines when stripping reply tags from agent output.</li>
<li>Gateway: fix TypeScript build by aligning hook mapping <code>channel</code> types and removing a dead Group DM branch in Discord monitor.</li>
<li>Skills: switch imsg installer to brew tap formula.</li>
<li>Skills: gate macOS-only skills by OS and surface block reasons in the Skills UI.</li>
<li>Onboarding: show skill descriptions in the macOS setup flow and surface clearer Gateway/skills error messages.</li>
<li>Onboarding: auto-verify Claude OAuth tokens, show “verified” when detected working, and avoid re-auth prompts unless verification fails.</li>
<li>CLI onboarding: include exit code + a useful one-line summary when skill dependency installs fail.</li>
<li>CLI onboarding: explain Tailscale exposure options (Off/Serve/Funnel) and colorize provider status (linked/configured/needs setup).</li>
<li>CLI onboarding: add provider primers (WhatsApp/Telegram/Discord/Signal) incl. Discord bot token setup steps.</li>
<li>CLI onboarding: allow skipping the “install missing skill dependencies” selection without canceling the wizard.</li>
<li>CLI onboarding: always prompt for WhatsApp <code>whatsapp.allowFrom</code> and print (optionally open) the Control UI URL when done.</li>
<li>CLI onboarding: detect gateway reachability and annotate Local/Remote choices (helps pick the right mode).</li>
<li>macOS settings: colorize provider status subtitles to distinguish healthy vs degraded states.</li>
<li>macOS: keep config writes on the main actor to satisfy Swift concurrency rules.</li>
<li>macOS menu: show multi-line gateway error details, add an always-visible gateway row, avoid duplicate gateway status rows, suppress transient <code>cancelled</code> device refresh errors, and auto-recover the control channel on disconnect.</li>
<li>macOS menu: show session last-used timestamps in the list and add recent-message previews in session submenus.</li>
<li>macOS menu: tighten session row padding and time out session preview loading with cached fallback.</li>
<li>macOS: log health refresh failures and recovery to make gateway issues easier to diagnose.</li>
<li>macOS codesign: skip hardened runtime for ad-hoc signing and avoid empty options args (#70) — thanks @petter-b</li>
<li>macOS codesign: include camera entitlement so permission prompts work in the menu bar app.</li>
<li>Agent tools: bash tool supports real TTY via <code>stdinMode: "pty"</code> with node-pty, warning + fallback on load/start failure.</li>
<li>Agent tools: map <code>camera.snap</code> JPEG payloads to <code>image/jpeg</code> to avoid MIME mismatch errors.</li>
<li>Tests: cover <code>camera.snap</code> MIME mapping to prevent image/png vs image/jpeg mismatches.</li>
<li>macOS camera: wait for exposure/white balance to settle before capturing a snap to avoid dark images.</li>
<li>Camera snap: add <code>delayMs</code> parameter (default 2000ms on macOS) to improve exposure reliability.</li>
<li>Camera: add <code>camera.list</code> and optional <code>deviceId</code> selection for snaps/clips.</li>
<li>Tests: cover camera device selection params in CLI + agent tools.</li>
<li>macOS packaging: move rpath config into swift build for reliability (#69) — thanks @petter-b</li>
<li>macOS: prioritize main bundle for device resources to prevent crash (#73) — thanks @petter-b</li>
<li>macOS remote: route settings through gateway config and avoid local config reads in remote mode.</li>
<li>Telegram: align token resolution for cron/agent/CLI sends (env/config/tokenFile) to prevent isolated delivery failures (#76).</li>
<li>Telegram: honor per-group mention gating defaults/overrides via <code>telegram.groups</code> and <code>"*"</code> defaults (thanks @joshp123).</li>
<li>Chat UI: clear composer input immediately and allow clear while editing to prevent duplicate sends (#72) — thanks @hrdwdmrbl</li>
<li>Restart: use systemd on Linux (and report actual restart method) instead of always launchctl.</li>
<li>Gateway relay: detect Bun binaries via execPath to resolve packaged assets on macOS.</li>
<li>Cron: prevent <code>every</code> schedules without an anchor from firing in a tight loop (thanks @jamesgroat).</li>
<li>Docs: add manual OAuth setup for remote/headless deployments (#67) — thanks @wstock</li>
<li>Docs/agent tools: clarify that browser <code>wait</code> should be avoided by default and used only in exceptional cases.</li>
<li>Docs: clarify self-chat mode and group mention gating config (#111) — thanks @rafaelreis-r.</li>
<li>Browser tools: <code>upload</code> supports auto-click refs, direct <code>inputRef</code>/<code>element</code> file inputs, and emits input/change after <code>setFiles</code> so JS-heavy sites pick up attachments.</li>
<li>Browser tools: harden CDP readiness (HTTP + WS), retry CDP connects, and auto-restart the clawd browser when the socket handshake stalls.</li>
<li>Browser CLI: add <code>clawdis browser reset-profile</code> to move the clawd profile to Trash when it gets wedged.</li>
<li>Signal: fix daemon startup race (wait for <code>/api/v1/check</code>) and normalize JSON-RPC <code>version</code> probe parsing.</li>
<li>Docs/Signal: clarify bot-number vs personal-account setup (self-chat loop protection) and add a quickstart config snippet.</li>
<li>Docs: refresh the CLI wizard guide and highlight onboarding in the README.</li>
<li>CLI: tighten onboarding prompt typing to keep bun builds green.</li>
<li>macOS: Voice Wake now fully tears down the Speech pipeline when disabled (cancel pending restarts, drop stale callbacks) to avoid high CPU in the background.</li>
<li>macOS menu: add a Talk Mode action alongside the Open Dashboard/Chat/Canvas entries.</li>
<li>macOS Debug: hide “Restart Gateway” when the app won’t start a local gateway (remote mode / attach-only).</li>
<li>macOS Debug: add an icon for the App Logging submenu.</li>
<li>macOS Talk Mode: orb overlay refresh, ElevenLabs request logging, API key status in settings, and auto-select first voice when none is configured.</li>
<li>macOS Talk Mode: add hard timeout around ElevenLabs TTS synthesis to avoid getting stuck “speaking” forever on hung requests.</li>
<li>macOS Talk Mode: avoid stuck playback when the audio player never starts (fail-fast + watchdog).</li>
<li>macOS Talk Mode: fix audio stop ordering so disabling Talk Mode always stops in-flight playback.</li>
<li>macOS Talk Mode: throttle audio-level updates (avoid per-buffer task creation) to reduce CPU/task churn.</li>
<li>macOS Talk Mode: increase overlay window size so wave rings don’t clip; close button is hover-only and closer to the orb.</li>
<li>WebChat: preserve chat run ordering per session so concurrent runs don’t strand the typing indicator.</li>
<li>Talk Mode: fall back to system TTS when ElevenLabs is unavailable, returns non-audio, or playback fails (macOS/iOS/Android).</li>
<li>Talk Mode: stream PCM on macOS/iOS for lower latency (incremental playback); Android continues MP3 streaming.</li>
<li>Talk Mode: validate ElevenLabs v3 stability and latency tier directives before sending requests.</li>
<li>iOS/Android Talk Mode: auto-select the first ElevenLabs voice when none is configured.</li>
<li>ElevenLabs: add retry/backoff for 429/5xx and include content-type in errors for debugging.</li>
<li>Talk Mode: align to the gateway’s main session key and fall back to history polling when chat events drop (prevents stuck “thinking” / missing messages).</li>
<li>Talk Mode: treat history timestamps as seconds or milliseconds to avoid stale assistant picks (macOS/iOS/Android).</li>
<li>Chat UI: clear streaming/tool bubbles when external runs finish, preventing duplicate assistant bubbles.</li>
<li>Chat UI: user bubbles use <code>ui.seamColor</code> (fallback to a calmer default blue).</li>
<li>Android Chat UI: use <code>onPrimary</code> for user bubble text to preserve contrast (thanks @Syhids).</li>
<li>Control UI: sync sidebar navigation with the URL for deep-linking, and auto-scroll chat to the latest message.</li>
<li>Control UI: disable Web Chat + Talk when no iOS/Android node is connected; refreshed Web Chat styling and keyboard send.</li>
<li>Control UI: keep chat pinned to the latest message while typing/sending and restore drafts on send failures.</li>
<li>Control UI: soften chat bubble text opacity for calmer readability.</li>
<li>macOS Web Chat: improve empty/error states, focus message field on open, keep pill/send inside the input field, and make the composer pill edge-to-edge with square top corners.</li>
<li>macOS: bundle Control UI assets into the app relay so the packaged app can serve them (thanks @mbelinky).</li>
<li>Talk Mode: wait for chat history to surface the assistant reply before starting TTS (macOS/iOS/Android).</li>
<li>iOS Talk Mode: fix chat completion wait to time out even if no events arrive (prevents “Thinking…” hangs).</li>
<li>iOS Talk Mode: keep recognition running during playback to support interrupt-on-speech.</li>
<li>iOS Talk Mode: preserve directive voice/model overrides across config reloads and add ElevenLabs request timeouts.</li>
<li>iOS/Android Talk Mode: explicitly <code>chat.subscribe</code> when Talk Mode is active, so completion events arrive even if the Chat UI isn’t open.</li>
<li>Chat UI: refresh history when another client finishes a run in the same session, so Talk Mode + Voice Wake transcripts appear consistently.</li>
<li>Gateway: <code>voice.transcript</code> now also maps agent bus output to <code>chat</code> events, ensuring chat UIs refresh for voice-triggered runs.</li>
<li>Gateway: auto-migrate legacy config on startup (non-Nix); Nix mode hard-fails with a clear error when legacy keys are present.</li>
<li>iOS/Android: show a centered Talk Mode orb overlay while Talk Mode is enabled.</li>
<li>Gateway config: inject <code>talk.apiKey</code> from <code>ELEVENLABS_API_KEY</code>/shell profile so nodes can fetch it on demand.</li>
<li>Canvas A2UI: tag requests with <code>platform=android|ios|macos</code> and boost Android canvas background contrast.</li>
<li>iOS/Android nodes: enable scrolling for loaded web pages in the Canvas WebView (default scaffold stays touch-first).</li>
<li>macOS menu: device list now uses <code>node.list</code> (devices only; no agent/tool presence entries).</li>
<li>macOS menu: device list now shows connected nodes only.</li>
<li>macOS menu: device rows now pack platform/version on the first line, and command lists wrap in submenus.</li>
<li>macOS menu: split device platform/version across first and second rows for better fit.</li>
<li>macOS Canvas: show remote control status in the debug overlay and log A2UI auto-nav decisions.</li>
<li>Canvas A2UI: polish the debug status HUD styling.</li>
<li>iOS node: fix ReplayKit screen recording crash caused by queue isolation assertions during capture.</li>
<li>iOS Talk Mode: avoid audio tap queue assertions when starting recognition.</li>
<li>macOS: use $HOME/Library/pnpm for SSH PATH exports (thanks @mbelinky).</li>
<li>macOS remote: harden SSH tunnel recovery/logging, honor <code>gateway.remote.url</code> port when forwarding, clarify gateway disconnect status, and add Debug menu tunnel reset.</li>
<li>iOS/Android nodes: bridge auto-connect refreshes stale tokens and settings now show richer bridge/device details.</li>
<li>macOS: bundle device model resources to prevent Instances crashes (thanks @mbelinky).</li>
<li>iOS/Android nodes: status pill now surfaces camera activity instead of overlay toasts.</li>
<li>iOS/Android/macOS nodes: camera snaps recompress to keep base64 payloads under 5 MB.</li>
<li>iOS/Android nodes: status pill now surfaces pairing, screen recording, voice wake, and foreground-required states.</li>
<li>iOS/Android nodes: avoid duplicating “Gateway reconnecting…” when the bridge is already connecting.</li>
<li>iOS/Android nodes: Talk Mode now lives on a side bubble (with an iOS toggle to hide it), and Android settings no longer show the Talk Mode switch.</li>
<li>macOS menu: top status line now shows pending node pairing approvals (incl. repairs).</li>
<li>CLI: avoid spurious gateway close errors after successful request/response cycles.</li>
<li>Agent runtime: clamp tool-result images to the 5MB Anthropic limit to avoid hard request rejections.</li>
<li>Agent runtime: write v2 session headers so Pi session branching stays in the Clawdis sessions dir.</li>
<li>Tests: add Swift Testing coverage for camera errors and Kotest coverage for Android bridge endpoints.</li>
<li>NPM package: include missing runtime dist folders (slack/signal/imessage/tui/wizard/control-ui/daemon) to avoid <code>ERR_MODULE_NOT_FOUND</code> in Node 25 npx installs.</li>
</ul>
<p><a href="https://github.com/steipete/clawdis/blob/main/CHANGELOG.md">View full changelog</a></p>
<p><a href="https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/steipete/clawdis/releases/download/v2.0.0-beta5/Clawdis-2.0.0-beta5.zip" length="145432870" type="application/octet-stream" sparkle:edSignature="qKPcmSx2pAaIYz9NqFp0TY63KrcDlpctUHnNpRs6Q60qQqBWtQycLIhhvhxmGnHupaiEXJfspb/Ad9RgODIzAw=="/>
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.5-3/Clawdbot-2026.1.5-3.zip" length="160800596" type="application/octet-stream" sparkle:edSignature="P8U3nvIFpbGmRItT/NGPmJ/i370OMVvDHYQL/znYsLI0MrbGfXgMGEvR5A0uwW+cJevlX/hrJLiY51zo4rAMBg=="/>
</item>
<item>
<title>2026.1.5-3</title>
<pubDate>Mon, 05 Jan 2026 03:57:59 +0100</pubDate>
<link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
<sparkle:version>3091</sparkle:version>
<sparkle:shortVersionString>2026.1.5-3</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>Clawdbot 2026.1.5-3</h2>
<h3>Fixes</h3>
<ul>
<li>NPM package: include missing runtime dist folders (slack/signal/imessage/tui/wizard/control-ui/daemon) to avoid <code>ERR_MODULE_NOT_FOUND</code> in Node 25 npx installs.</li>
</ul>
<p><a href="https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.5-3/Clawdbot-2026.1.5-3.zip" length="160797048" type="application/octet-stream" sparkle:edSignature="5KYFg0SW7liwLxLJbfzd2KsAxbX06gMH0rH/W3a4V0p4N48hjz4AsSrfFLdGZSnW+6XaJjC3MN6Ynh+l7kffDQ=="/>
</item>
<item>
<title>2026.1.5-2</title>
<pubDate>Mon, 05 Jan 2026 03:51:30 +0100</pubDate>
<link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
<sparkle:version>3089</sparkle:version>
<sparkle:shortVersionString>2026.1.5-2</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>Clawdbot 2026.1.5-2</h2>
<h3>Fixes</h3>
<ul>
<li>NPM package: include <code>dist/sessions</code> so <code>clawdbot agent</code> resolves session helpers in npx installs.</li>
<li>Node 25: avoid unsupported directory import by targeting <code>qrcode-terminal/vendor/QRCode/*.js</code> modules.</li>
</ul>
<p><a href="https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.5-2/Clawdbot-2026.1.5-2.zip" length="150250417" type="application/octet-stream" sparkle:edSignature="ntHNmwyHrv6cPk6NAKOT3AUkwdt5ZadrGU6mJK4GmVxi44uIMT3ZXluvnqK9SxXQwA0H0dXjiGMS/cg8NbgqDA=="/>
</item>
</channel>
</rss>

View File

@@ -1,6 +1,6 @@
## Clawdis Node (Android) (internal)
## Clawdbot Node (Android) (internal)
Modern Android node app: connects to the **Gateway-owned bridge** (`_clawdis-bridge._tcp`) over TCP and exposes **Canvas + Chat + Camera**.
Modern Android node app: connects to the **Gateway-owned bridge** (`_clawdbot-bridge._tcp`) over TCP and exposes **Canvas + Chat + Camera**.
Notes:
- The node keeps the connection alive via a **foreground service** (persistent notification with a Disconnect action).
@@ -25,7 +25,7 @@ cd apps/android
1) Start the gateway (on your “master” machine):
```bash
pnpm clawdis gateway --port 18789 --verbose
pnpm clawdbot gateway --port 18789 --verbose
```
2) In the Android app:
@@ -34,8 +34,8 @@ pnpm clawdis gateway --port 18789 --verbose
3) Approve pairing (on the gateway machine):
```bash
clawdis nodes pending
clawdis nodes approve <requestId>
clawdbot nodes pending
clawdbot nodes approve <requestId>
```
More details: `docs/android/connect.md`.

View File

@@ -6,17 +6,17 @@ plugins {
}
android {
namespace = "com.clawdis.android"
namespace = "com.clawdbot.android"
compileSdk = 36
sourceSets {
getByName("main") {
assets.srcDir(file("../../shared/ClawdisKit/Sources/ClawdisKit/Resources"))
assets.srcDir(file("../../shared/ClawdbotKit/Sources/ClawdbotKit/Resources"))
}
}
defaultConfig {
applicationId = "com.clawdis.android"
applicationId = "com.clawdbot.android"
minSdk = 31
targetSdk = 36
versionCode = 1
@@ -48,6 +48,10 @@ android {
lint {
disable += setOf("IconLauncherShape")
}
testOptions {
unitTests.isIncludeAndroidResources = true
}
}
kotlin {
@@ -64,7 +68,7 @@ dependencies {
implementation("androidx.core:core-ktx:1.17.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
implementation("androidx.activity:activity-compose:1.12.2")
implementation("androidx.webkit:webkit:1.14.0")
implementation("androidx.webkit:webkit:1.15.0")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview")
@@ -96,7 +100,8 @@ dependencies {
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
testImplementation("io.kotest:kotest-runner-junit5-jvm:6.0.7")
testImplementation("io.kotest:kotest-assertions-core-jvm:6.0.7")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.13.3")
testImplementation("org.robolectric:robolectric:4.16")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:6.0.1")
}
tasks.withType<Test>().configureEach {

View File

@@ -32,7 +32,7 @@
android:label="@string/app_name"
android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/Theme.ClawdisNode">
android:theme="@style/Theme.ClawdbotNode">
<service
android:name=".NodeForegroundService"
android:exported="false"

View File

@@ -1,4 +1,4 @@
package com.clawdis.android
package com.clawdbot.android
enum class CameraHudKind {
Photo,

View File

@@ -1,4 +1,4 @@
package com.clawdis.android
package com.clawdbot.android
import android.content.Context
import android.os.Build

View File

@@ -1,4 +1,4 @@
package com.clawdis.android
package com.clawdbot.android
enum class LocationMode(val rawValue: String) {
Off("off"),

View File

@@ -1,4 +1,4 @@
package com.clawdis.android
package com.clawdbot.android
import android.Manifest
import android.content.pm.ApplicationInfo
@@ -18,8 +18,8 @@ import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.clawdis.android.ui.RootScreen
import com.clawdis.android.ui.ClawdisTheme
import com.clawdbot.android.ui.RootScreen
import com.clawdbot.android.ui.ClawdbotTheme
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
@@ -56,7 +56,7 @@ class MainActivity : ComponentActivity() {
}
setContent {
ClawdisTheme {
ClawdbotTheme {
Surface(modifier = Modifier) {
RootScreen(viewModel = viewModel)
}

View File

@@ -1,13 +1,13 @@
package com.clawdis.android
package com.clawdbot.android
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import com.clawdis.android.bridge.BridgeEndpoint
import com.clawdis.android.chat.OutgoingAttachment
import com.clawdis.android.node.CameraCaptureManager
import com.clawdis.android.node.CanvasController
import com.clawdis.android.node.ScreenRecordManager
import com.clawdis.android.node.SmsManager
import com.clawdbot.android.bridge.BridgeEndpoint
import com.clawdbot.android.chat.OutgoingAttachment
import com.clawdbot.android.node.CameraCaptureManager
import com.clawdbot.android.node.CanvasController
import com.clawdbot.android.node.ScreenRecordManager
import com.clawdbot.android.node.SmsManager
import kotlinx.coroutines.flow.StateFlow
class MainViewModel(app: Application) : AndroidViewModel(app) {

View File

@@ -1,4 +1,4 @@
package com.clawdis.android
package com.clawdbot.android
import android.app.Application

View File

@@ -1,4 +1,4 @@
package com.clawdis.android
package com.clawdbot.android
import android.app.Notification
import android.app.NotificationChannel
@@ -29,7 +29,7 @@ class NodeForegroundService : Service() {
override fun onCreate() {
super.onCreate()
ensureChannel()
val initial = buildNotification(title = "Clawdis Node", text = "Starting…")
val initial = buildNotification(title = "Clawdbot Node", text = "Starting…")
startForegroundWithTypes(notification = initial, requiresMic = false)
val runtime = (application as NodeApp).runtime
@@ -44,7 +44,7 @@ class NodeForegroundService : Service() {
) { status, server, connected, voiceMode, voiceListening ->
Quint(status, server, connected, voiceMode, voiceListening)
}.collect { (status, server, connected, voiceMode, voiceListening) ->
val title = if (connected) "Clawdis Node · Connected" else "Clawdis Node"
val title = if (connected) "Clawdbot Node · Connected" else "Clawdbot Node"
val voiceSuffix =
if (voiceMode == VoiceWakeMode.Always) {
if (voiceListening) " · Voice Wake: Listening" else " · Voice Wake: Paused"
@@ -91,21 +91,38 @@ class NodeForegroundService : Service() {
"Connection",
NotificationManager.IMPORTANCE_LOW,
).apply {
description = "Clawdis node connection status"
description = "Clawdbot node connection status"
setShowBadge(false)
}
mgr.createNotificationChannel(channel)
}
private fun buildNotification(title: String, text: String): Notification {
val launchIntent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val launchPending =
PendingIntent.getActivity(
this,
1,
launchIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val stopIntent = Intent(this, NodeForegroundService::class.java).setAction(ACTION_STOP)
val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
val stopPending = PendingIntent.getService(this, 2, stopIntent, flags)
val stopPending =
PendingIntent.getService(
this,
2,
stopIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(title)
.setContentText(text)
.setContentIntent(launchPending)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
@@ -146,7 +163,7 @@ class NodeForegroundService : Service() {
private const val CHANNEL_ID = "connection"
private const val NOTIFICATION_ID = 1
private const val ACTION_STOP = "com.clawdis.android.action.STOP"
private const val ACTION_STOP = "com.clawdbot.android.action.STOP"
fun start(context: Context) {
val intent = Intent(context, NodeForegroundService::class.java)

View File

@@ -1,4 +1,4 @@
package com.clawdis.android
package com.clawdbot.android
import android.Manifest
import android.content.Context
@@ -7,31 +7,31 @@ import android.location.LocationManager
import android.os.Build
import android.os.SystemClock
import androidx.core.content.ContextCompat
import com.clawdis.android.chat.ChatController
import com.clawdis.android.chat.ChatMessage
import com.clawdis.android.chat.ChatPendingToolCall
import com.clawdis.android.chat.ChatSessionEntry
import com.clawdis.android.chat.OutgoingAttachment
import com.clawdis.android.bridge.BridgeDiscovery
import com.clawdis.android.bridge.BridgeEndpoint
import com.clawdis.android.bridge.BridgePairingClient
import com.clawdis.android.bridge.BridgeSession
import com.clawdis.android.node.CameraCaptureManager
import com.clawdis.android.node.LocationCaptureManager
import com.clawdis.android.BuildConfig
import com.clawdis.android.node.CanvasController
import com.clawdis.android.node.ScreenRecordManager
import com.clawdis.android.node.SmsManager
import com.clawdis.android.protocol.ClawdisCapability
import com.clawdis.android.protocol.ClawdisCameraCommand
import com.clawdis.android.protocol.ClawdisCanvasA2UIAction
import com.clawdis.android.protocol.ClawdisCanvasA2UICommand
import com.clawdis.android.protocol.ClawdisCanvasCommand
import com.clawdis.android.protocol.ClawdisScreenCommand
import com.clawdis.android.protocol.ClawdisLocationCommand
import com.clawdis.android.protocol.ClawdisSmsCommand
import com.clawdis.android.voice.TalkModeManager
import com.clawdis.android.voice.VoiceWakeManager
import com.clawdbot.android.chat.ChatController
import com.clawdbot.android.chat.ChatMessage
import com.clawdbot.android.chat.ChatPendingToolCall
import com.clawdbot.android.chat.ChatSessionEntry
import com.clawdbot.android.chat.OutgoingAttachment
import com.clawdbot.android.bridge.BridgeDiscovery
import com.clawdbot.android.bridge.BridgeEndpoint
import com.clawdbot.android.bridge.BridgePairingClient
import com.clawdbot.android.bridge.BridgeSession
import com.clawdbot.android.node.CameraCaptureManager
import com.clawdbot.android.node.LocationCaptureManager
import com.clawdbot.android.BuildConfig
import com.clawdbot.android.node.CanvasController
import com.clawdbot.android.node.ScreenRecordManager
import com.clawdbot.android.node.SmsManager
import com.clawdbot.android.protocol.ClawdbotCapability
import com.clawdbot.android.protocol.ClawdbotCameraCommand
import com.clawdbot.android.protocol.ClawdbotCanvasA2UIAction
import com.clawdbot.android.protocol.ClawdbotCanvasA2UICommand
import com.clawdbot.android.protocol.ClawdbotCanvasCommand
import com.clawdbot.android.protocol.ClawdbotScreenCommand
import com.clawdbot.android.protocol.ClawdbotLocationCommand
import com.clawdbot.android.protocol.ClawdbotSmsCommand
import com.clawdbot.android.voice.TalkModeManager
import com.clawdbot.android.voice.VoiceWakeManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -369,38 +369,38 @@ class NodeRuntime(context: Context) {
private fun buildInvokeCommands(): List<String> =
buildList {
add(ClawdisCanvasCommand.Present.rawValue)
add(ClawdisCanvasCommand.Hide.rawValue)
add(ClawdisCanvasCommand.Navigate.rawValue)
add(ClawdisCanvasCommand.Eval.rawValue)
add(ClawdisCanvasCommand.Snapshot.rawValue)
add(ClawdisCanvasA2UICommand.Push.rawValue)
add(ClawdisCanvasA2UICommand.PushJSONL.rawValue)
add(ClawdisCanvasA2UICommand.Reset.rawValue)
add(ClawdisScreenCommand.Record.rawValue)
add(ClawdbotCanvasCommand.Present.rawValue)
add(ClawdbotCanvasCommand.Hide.rawValue)
add(ClawdbotCanvasCommand.Navigate.rawValue)
add(ClawdbotCanvasCommand.Eval.rawValue)
add(ClawdbotCanvasCommand.Snapshot.rawValue)
add(ClawdbotCanvasA2UICommand.Push.rawValue)
add(ClawdbotCanvasA2UICommand.PushJSONL.rawValue)
add(ClawdbotCanvasA2UICommand.Reset.rawValue)
add(ClawdbotScreenCommand.Record.rawValue)
if (cameraEnabled.value) {
add(ClawdisCameraCommand.Snap.rawValue)
add(ClawdisCameraCommand.Clip.rawValue)
add(ClawdbotCameraCommand.Snap.rawValue)
add(ClawdbotCameraCommand.Clip.rawValue)
}
if (locationMode.value != LocationMode.Off) {
add(ClawdisLocationCommand.Get.rawValue)
add(ClawdbotLocationCommand.Get.rawValue)
}
if (sms.canSendSms()) {
add(ClawdisSmsCommand.Send.rawValue)
add(ClawdbotSmsCommand.Send.rawValue)
}
}
private fun buildCapabilities(): List<String> =
buildList {
add(ClawdisCapability.Canvas.rawValue)
add(ClawdisCapability.Screen.rawValue)
if (cameraEnabled.value) add(ClawdisCapability.Camera.rawValue)
if (sms.canSendSms()) add(ClawdisCapability.Sms.rawValue)
add(ClawdbotCapability.Canvas.rawValue)
add(ClawdbotCapability.Screen.rawValue)
if (cameraEnabled.value) add(ClawdbotCapability.Camera.rawValue)
if (sms.canSendSms()) add(ClawdbotCapability.Sms.rawValue)
if (voiceWakeMode.value != VoiceWakeMode.Off && hasRecordAudioPermission()) {
add(ClawdisCapability.VoiceWake.rawValue)
add(ClawdbotCapability.VoiceWake.rawValue)
}
if (locationMode.value != LocationMode.Off) {
add(ClawdisCapability.Location.rawValue)
add(ClawdbotCapability.Location.rawValue)
}
}
@@ -515,7 +515,6 @@ class NodeRuntime(context: Context) {
}
private fun hasBackgroundLocationPermission(): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return true
return (
ContextCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_BACKGROUND_LOCATION) ==
PackageManager.PERMISSION_GRANTED
@@ -552,7 +551,7 @@ class NodeRuntime(context: Context) {
val actionId = (userActionObj["id"] as? JsonPrimitive)?.content?.trim().orEmpty().ifEmpty {
java.util.UUID.randomUUID().toString()
}
val name = ClawdisCanvasA2UIAction.extractActionName(userActionObj) ?: return@launch
val name = ClawdbotCanvasA2UIAction.extractActionName(userActionObj) ?: return@launch
val surfaceId =
(userActionObj["surfaceId"] as? JsonPrimitive)?.content?.trim().orEmpty().ifEmpty { "main" }
@@ -562,7 +561,7 @@ class NodeRuntime(context: Context) {
val sessionKey = "main"
val message =
ClawdisCanvasA2UIAction.formatAgentMessage(
ClawdbotCanvasA2UIAction.formatAgentMessage(
actionName = name,
sessionKey = sessionKey,
surfaceId = surfaceId,
@@ -596,7 +595,7 @@ class NodeRuntime(context: Context) {
try {
canvas.eval(
ClawdisCanvasA2UIAction.jsDispatchA2UIActionStatus(
ClawdbotCanvasA2UIAction.jsDispatchA2UIActionStatus(
actionId = actionId,
ok = connected && error == null,
error = error,
@@ -713,10 +712,10 @@ class NodeRuntime(context: Context) {
private suspend fun handleInvoke(command: String, paramsJson: String?): BridgeSession.InvokeResult {
if (
command.startsWith(ClawdisCanvasCommand.NamespacePrefix) ||
command.startsWith(ClawdisCanvasA2UICommand.NamespacePrefix) ||
command.startsWith(ClawdisCameraCommand.NamespacePrefix) ||
command.startsWith(ClawdisScreenCommand.NamespacePrefix)
command.startsWith(ClawdbotCanvasCommand.NamespacePrefix) ||
command.startsWith(ClawdbotCanvasA2UICommand.NamespacePrefix) ||
command.startsWith(ClawdbotCameraCommand.NamespacePrefix) ||
command.startsWith(ClawdbotScreenCommand.NamespacePrefix)
) {
if (!isForeground.value) {
return BridgeSession.InvokeResult.error(
@@ -725,13 +724,13 @@ class NodeRuntime(context: Context) {
)
}
}
if (command.startsWith(ClawdisCameraCommand.NamespacePrefix) && !cameraEnabled.value) {
if (command.startsWith(ClawdbotCameraCommand.NamespacePrefix) && !cameraEnabled.value) {
return BridgeSession.InvokeResult.error(
code = "CAMERA_DISABLED",
message = "CAMERA_DISABLED: enable Camera in Settings",
)
}
if (command.startsWith(ClawdisLocationCommand.NamespacePrefix) &&
if (command.startsWith(ClawdbotLocationCommand.NamespacePrefix) &&
locationMode.value == LocationMode.Off
) {
return BridgeSession.InvokeResult.error(
@@ -741,18 +740,18 @@ class NodeRuntime(context: Context) {
}
return when (command) {
ClawdisCanvasCommand.Present.rawValue -> {
ClawdbotCanvasCommand.Present.rawValue -> {
val url = CanvasController.parseNavigateUrl(paramsJson)
canvas.navigate(url)
BridgeSession.InvokeResult.ok(null)
}
ClawdisCanvasCommand.Hide.rawValue -> BridgeSession.InvokeResult.ok(null)
ClawdisCanvasCommand.Navigate.rawValue -> {
ClawdbotCanvasCommand.Hide.rawValue -> BridgeSession.InvokeResult.ok(null)
ClawdbotCanvasCommand.Navigate.rawValue -> {
val url = CanvasController.parseNavigateUrl(paramsJson)
canvas.navigate(url)
BridgeSession.InvokeResult.ok(null)
}
ClawdisCanvasCommand.Eval.rawValue -> {
ClawdbotCanvasCommand.Eval.rawValue -> {
val js =
CanvasController.parseEvalJs(paramsJson)
?: return BridgeSession.InvokeResult.error(
@@ -770,7 +769,7 @@ class NodeRuntime(context: Context) {
}
BridgeSession.InvokeResult.ok("""{"result":${result.toJsonString()}}""")
}
ClawdisCanvasCommand.Snapshot.rawValue -> {
ClawdbotCanvasCommand.Snapshot.rawValue -> {
val snapshotParams = CanvasController.parseSnapshotParams(paramsJson)
val base64 =
try {
@@ -787,7 +786,7 @@ class NodeRuntime(context: Context) {
}
BridgeSession.InvokeResult.ok("""{"format":"${snapshotParams.format.rawValue}","base64":"$base64"}""")
}
ClawdisCanvasA2UICommand.Reset.rawValue -> {
ClawdbotCanvasA2UICommand.Reset.rawValue -> {
val a2uiUrl = resolveA2uiHostUrl()
?: return BridgeSession.InvokeResult.error(
code = "A2UI_HOST_NOT_CONFIGURED",
@@ -803,7 +802,7 @@ class NodeRuntime(context: Context) {
val res = canvas.eval(a2uiResetJS)
BridgeSession.InvokeResult.ok(res)
}
ClawdisCanvasA2UICommand.Push.rawValue, ClawdisCanvasA2UICommand.PushJSONL.rawValue -> {
ClawdbotCanvasA2UICommand.Push.rawValue, ClawdbotCanvasA2UICommand.PushJSONL.rawValue -> {
val messages =
try {
decodeA2uiMessages(command, paramsJson)
@@ -826,7 +825,7 @@ class NodeRuntime(context: Context) {
val res = canvas.eval(js)
BridgeSession.InvokeResult.ok(res)
}
ClawdisCameraCommand.Snap.rawValue -> {
ClawdbotCameraCommand.Snap.rawValue -> {
showCameraHud(message = "Taking photo…", kind = CameraHudKind.Photo)
triggerCameraFlash()
val res =
@@ -840,7 +839,7 @@ class NodeRuntime(context: Context) {
showCameraHud(message = "Photo captured", kind = CameraHudKind.Success, autoHideMs = 1600)
BridgeSession.InvokeResult.ok(res.payloadJson)
}
ClawdisCameraCommand.Clip.rawValue -> {
ClawdbotCameraCommand.Clip.rawValue -> {
val includeAudio = paramsJson?.contains("\"includeAudio\":true") != false
if (includeAudio) externalAudioCaptureActive.value = true
try {
@@ -859,7 +858,7 @@ class NodeRuntime(context: Context) {
if (includeAudio) externalAudioCaptureActive.value = false
}
}
ClawdisLocationCommand.Get.rawValue -> {
ClawdbotLocationCommand.Get.rawValue -> {
val mode = locationMode.value
if (!isForeground.value && mode != LocationMode.Always) {
return BridgeSession.InvokeResult.error(
@@ -912,7 +911,7 @@ class NodeRuntime(context: Context) {
BridgeSession.InvokeResult.error(code = "LOCATION_UNAVAILABLE", message = message)
}
}
ClawdisScreenCommand.Record.rawValue -> {
ClawdbotScreenCommand.Record.rawValue -> {
// Status pill mirrors screen recording state so it stays visible without overlay stacking.
_screenRecordActive.value = true
try {
@@ -928,7 +927,7 @@ class NodeRuntime(context: Context) {
_screenRecordActive.value = false
}
}
ClawdisSmsCommand.Send.rawValue -> {
ClawdbotSmsCommand.Send.rawValue -> {
val res = sms.send(paramsJson)
if (res.ok) {
BridgeSession.InvokeResult.ok(res.payloadJson)
@@ -999,7 +998,7 @@ class NodeRuntime(context: Context) {
val raw = session.currentCanvasHostUrl()?.trim().orEmpty()
if (raw.isBlank()) return null
val base = raw.trimEnd('/')
return "${base}/__clawdis__/a2ui/?platform=android"
return "${base}/__clawdbot__/a2ui/?platform=android"
}
private suspend fun ensureA2uiReady(a2uiUrl: String): Boolean {
@@ -1034,7 +1033,7 @@ class NodeRuntime(context: Context) {
val jsonlField = (obj["jsonl"] as? JsonPrimitive)?.content?.trim().orEmpty()
val hasMessagesArray = obj["messages"] is JsonArray
if (command == ClawdisCanvasA2UICommand.PushJSONL.rawValue || (!hasMessagesArray && jsonlField.isNotBlank())) {
if (command == ClawdbotCanvasA2UICommand.PushJSONL.rawValue || (!hasMessagesArray && jsonlField.isNotBlank())) {
val jsonl = jsonlField
if (jsonl.isBlank()) throw IllegalArgumentException("INVALID_REQUEST: jsonl required")
val messages =
@@ -1091,7 +1090,7 @@ private const val a2uiReadyCheckJS: String =
"""
(() => {
try {
return !!globalThis.clawdisA2UI && typeof globalThis.clawdisA2UI.applyMessages === 'function';
return !!globalThis.clawdbotA2UI && typeof globalThis.clawdbotA2UI.applyMessages === 'function';
} catch (_) {
return false;
}
@@ -1102,8 +1101,8 @@ private const val a2uiResetJS: String =
"""
(() => {
try {
if (!globalThis.clawdisA2UI) return { ok: false, error: "missing clawdisA2UI" };
return globalThis.clawdisA2UI.reset();
if (!globalThis.clawdbotA2UI) return { ok: false, error: "missing clawdbotA2UI" };
return globalThis.clawdbotA2UI.reset();
} catch (e) {
return { ok: false, error: String(e?.message ?? e) };
}
@@ -1114,9 +1113,9 @@ private fun a2uiApplyMessagesJS(messagesJson: String): String {
return """
(() => {
try {
if (!globalThis.clawdisA2UI) return { ok: false, error: "missing clawdisA2UI" };
if (!globalThis.clawdbotA2UI) return { ok: false, error: "missing clawdbotA2UI" };
const messages = $messagesJson;
return globalThis.clawdisA2UI.applyMessages(messages);
return globalThis.clawdbotA2UI.applyMessages(messages);
} catch (e) {
return { ok: false, error: String(e?.message ?? e) };
}

View File

@@ -1,4 +1,4 @@
package com.clawdis.android
package com.clawdbot.android
import android.content.pm.PackageManager
import android.content.Intent
@@ -115,7 +115,7 @@ class PermissionRequester(private val activity: ComponentActivity) {
private fun buildRationaleMessage(permissions: List<String>): String {
val labels = permissions.map { permissionLabel(it) }
return "Clawdis needs ${labels.joinToString(", ")} permissions to continue."
return "Clawdbot needs ${labels.joinToString(", ")} permissions to continue."
}
private fun buildSettingsMessage(permissions: List<String>): String {

View File

@@ -1,4 +1,4 @@
package com.clawdis.android
package com.clawdbot.android
import android.app.Activity
import android.content.Context
@@ -55,7 +55,7 @@ class ScreenCaptureRequester(private val activity: ComponentActivity) {
suspendCancellableCoroutine { cont ->
AlertDialog.Builder(activity)
.setTitle("Screen recording required")
.setMessage("Clawdis needs to record the screen for this command.")
.setMessage("Clawdbot needs to record the screen for this command.")
.setPositiveButton("Continue") { _, _ -> cont.resume(true) }
.setNegativeButton("Not now") { _, _ -> cont.resume(false) }
.setOnCancelListener { cont.resume(false) }

View File

@@ -1,6 +1,6 @@
@file:Suppress("DEPRECATION")
package com.clawdis.android
package com.clawdbot.android
import android.content.Context
import androidx.core.content.edit
@@ -31,7 +31,7 @@ class SecurePrefs(context: Context) {
private val prefs =
EncryptedSharedPreferences.create(
context,
"clawdis.node.secure",
"clawdbot.node.secure",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,

View File

@@ -1,4 +1,4 @@
package com.clawdis.android
package com.clawdbot.android
enum class VoiceWakeMode(val rawValue: String) {
Off("off"),

View File

@@ -1,4 +1,4 @@
package com.clawdis.android
package com.clawdbot.android
object WakeWords {
const val maxWords: Int = 32

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.bridge
package com.clawdbot.android.bridge
object BonjourEscapes {
fun decode(input: String): String {

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.bridge
package com.clawdbot.android.bridge
import android.content.Context
import android.net.ConnectivityManager
@@ -51,9 +51,9 @@ class BridgeDiscovery(
private val nsd = context.getSystemService(NsdManager::class.java)
private val connectivity = context.getSystemService(ConnectivityManager::class.java)
private val dns = DnsResolver.getInstance()
private val serviceType = "_clawdis-bridge._tcp."
private val wideAreaDomain = "clawdis.internal."
private val logTag = "Clawdis/BridgeDiscovery"
private val serviceType = "_clawdbot-bridge._tcp."
private val wideAreaDomain = "clawdbot.internal."
private val logTag = "Clawdbot/BridgeDiscovery"
private val localById = ConcurrentHashMap<String, BridgeEndpoint>()
private val unicastById = ConcurrentHashMap<String, BridgeEndpoint>()

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.bridge
package com.clawdbot.android.bridge
data class BridgeEndpoint(
val stableId: String,

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.bridge
package com.clawdbot.android.bridge
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.bridge
package com.clawdbot.android.bridge
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
@@ -11,7 +11,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import com.clawdis.android.BuildConfig
import com.clawdbot.android.BuildConfig
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
@@ -217,7 +217,7 @@ class BridgeSession(
// Local JVM unit tests use android.jar stubs; Log.d can throw "not mocked".
runCatching {
android.util.Log.d(
"ClawdisBridge",
"ClawdbotBridge",
"canvasHostUrl resolved=${canvasHostUrl ?: "none"} (raw=${rawCanvasUrl ?: "none"})",
)
}

View File

@@ -1,6 +1,6 @@
package com.clawdis.android.chat
package com.clawdbot.android.chat
import com.clawdis.android.bridge.BridgeSession
import com.clawdbot.android.bridge.BridgeSession
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CoroutineScope

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.chat
package com.clawdbot.android.chat
data class ChatMessage(
val id: String,

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.node
package com.clawdbot.android.node
import android.Manifest
import android.content.Context
@@ -20,7 +20,7 @@ import androidx.camera.video.VideoRecordEvent
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.checkSelfPermission
import androidx.core.graphics.scale
import com.clawdis.android.PermissionRequester
import com.clawdbot.android.PermissionRequester
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeout
@@ -152,7 +152,7 @@ class CameraCaptureManager(private val context: Context) {
provider.unbindAll()
provider.bindToLifecycle(owner, selector, videoCapture)
val file = File.createTempFile("clawdis-clip-", ".mp4")
val file = File.createTempFile("clawdbot-clip-", ".mp4")
val outputOptions = FileOutputOptions.Builder(file).build()
val finalized = kotlinx.coroutines.CompletableDeferred<VideoRecordEvent.Finalize>()
@@ -256,7 +256,7 @@ private suspend fun Context.cameraProvider(): ProcessCameraProvider =
private suspend fun ImageCapture.takeJpegBytes(executor: Executor): ByteArray =
suspendCancellableCoroutine { cont ->
val file = File.createTempFile("clawdis-snap-", ".jpg")
val file = File.createTempFile("clawdbot-snap-", ".jpg")
val options = ImageCapture.OutputFileOptions.Builder(file).build()
takePicture(
options,

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.node
package com.clawdbot.android.node
import android.graphics.Bitmap
import android.graphics.Canvas
@@ -17,7 +17,7 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import com.clawdis.android.BuildConfig
import com.clawdbot.android.BuildConfig
import kotlin.coroutines.resume
class CanvasController {
@@ -84,12 +84,12 @@ class CanvasController {
withWebViewOnMain { wv ->
if (currentUrl == null) {
if (BuildConfig.DEBUG) {
Log.d("ClawdisCanvas", "load scaffold: $scaffoldAssetUrl")
Log.d("ClawdbotCanvas", "load scaffold: $scaffoldAssetUrl")
}
wv.loadUrl(scaffoldAssetUrl)
} else {
if (BuildConfig.DEBUG) {
Log.d("ClawdisCanvas", "load url: $currentUrl")
Log.d("ClawdbotCanvas", "load url: $currentUrl")
}
wv.loadUrl(currentUrl)
}
@@ -106,7 +106,7 @@ class CanvasController {
val js = """
(() => {
try {
const api = globalThis.__clawdis;
const api = globalThis.__clawdbot;
if (!api) return;
if (typeof api.setDebugStatusEnabled === 'function') {
api.setDebugStatusEnabled(${if (enabled) "true" else "false"});

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.node
package com.clawdbot.android.node
import kotlin.math.max
import kotlin.math.min

View File

@@ -1,10 +1,12 @@
package com.clawdis.android.node
package com.clawdbot.android.node
import android.annotation.SuppressLint
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationManager
import android.os.CancellationSignal
import androidx.core.content.ContextCompat
import java.time.Instant
import java.time.format.DateTimeFormatter
import kotlinx.coroutines.Dispatchers
@@ -64,6 +66,15 @@ class LocationCaptureManager(private val context: Context) {
providers: List<String>,
maxAgeMs: Long?,
): Location? {
val fineOk =
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
val coarseOk =
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
if (!fineOk && !coarseOk) {
throw IllegalStateException("LOCATION_PERMISSION_REQUIRED: grant Location permission")
}
val now = System.currentTimeMillis()
val candidates =
providers.mapNotNull { provider -> manager.getLastKnownLocation(provider) }
@@ -72,12 +83,20 @@ class LocationCaptureManager(private val context: Context) {
return freshest
}
@SuppressLint("MissingPermission")
private suspend fun requestCurrent(
manager: LocationManager,
providers: List<String>,
timeoutMs: Long,
): Location {
val fineOk =
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
val coarseOk =
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
if (!fineOk && !coarseOk) {
throw IllegalStateException("LOCATION_PERMISSION_REQUIRED: grant Location permission")
}
val resolved =
providers.firstOrNull { manager.isProviderEnabled(it) }
?: throw IllegalStateException("LOCATION_UNAVAILABLE: no providers available")

View File

@@ -1,11 +1,11 @@
package com.clawdis.android.node
package com.clawdbot.android.node
import android.content.Context
import android.hardware.display.DisplayManager
import android.media.MediaRecorder
import android.media.projection.MediaProjectionManager
import android.util.Base64
import com.clawdis.android.ScreenCaptureRequester
import com.clawdbot.android.ScreenCaptureRequester
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
@@ -16,13 +16,13 @@ class ScreenRecordManager(private val context: Context) {
data class Payload(val payloadJson: String)
@Volatile private var screenCaptureRequester: ScreenCaptureRequester? = null
@Volatile private var permissionRequester: com.clawdis.android.PermissionRequester? = null
@Volatile private var permissionRequester: com.clawdbot.android.PermissionRequester? = null
fun attachScreenCaptureRequester(requester: ScreenCaptureRequester) {
screenCaptureRequester = requester
}
fun attachPermissionRequester(requester: com.clawdis.android.PermissionRequester) {
fun attachPermissionRequester(requester: com.clawdbot.android.PermissionRequester) {
permissionRequester = requester
}
@@ -62,7 +62,7 @@ class ScreenRecordManager(private val context: Context) {
val height = metrics.heightPixels
val densityDpi = metrics.densityDpi
val file = File.createTempFile("clawdis-screen-", ".mp4")
val file = File.createTempFile("clawdbot-screen-", ".mp4")
if (includeAudio) ensureMicPermission()
val recorder = MediaRecorder()
@@ -89,7 +89,7 @@ class ScreenRecordManager(private val context: Context) {
val surface = recorder.surface
virtualDisplay =
projection.createVirtualDisplay(
"clawdis-screen",
"clawdbot-screen",
width,
height,
densityDpi,

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.node
package com.clawdbot.android.node
import android.Manifest
import android.content.Context
@@ -11,7 +11,7 @@ import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.encodeToString
import com.clawdis.android.PermissionRequester
import com.clawdbot.android.PermissionRequester
/**
* Sends SMS messages via the Android SMS API.

View File

@@ -1,9 +1,9 @@
package com.clawdis.android.protocol
package com.clawdbot.android.protocol
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
object ClawdisCanvasA2UIAction {
object ClawdbotCanvasA2UIAction {
fun extractActionName(userAction: JsonObject): String? {
val name =
(userAction["name"] as? JsonPrimitive)
@@ -61,6 +61,6 @@ object ClawdisCanvasA2UIAction {
val err = (error ?: "").replace("\\", "\\\\").replace("\"", "\\\"")
val okLiteral = if (ok) "true" else "false"
val idEscaped = actionId.replace("\\", "\\\\").replace("\"", "\\\"")
return "window.dispatchEvent(new CustomEvent('clawdis:a2ui-action-status', { detail: { id: \"${idEscaped}\", ok: ${okLiteral}, error: \"${err}\" } }));"
return "window.dispatchEvent(new CustomEvent('clawdbot:a2ui-action-status', { detail: { id: \"${idEscaped}\", ok: ${okLiteral}, error: \"${err}\" } }));"
}
}

View File

@@ -1,6 +1,6 @@
package com.clawdis.android.protocol
package com.clawdbot.android.protocol
enum class ClawdisCapability(val rawValue: String) {
enum class ClawdbotCapability(val rawValue: String) {
Canvas("canvas"),
Camera("camera"),
Screen("screen"),
@@ -9,7 +9,7 @@ enum class ClawdisCapability(val rawValue: String) {
Location("location"),
}
enum class ClawdisCanvasCommand(val rawValue: String) {
enum class ClawdbotCanvasCommand(val rawValue: String) {
Present("canvas.present"),
Hide("canvas.hide"),
Navigate("canvas.navigate"),
@@ -22,7 +22,7 @@ enum class ClawdisCanvasCommand(val rawValue: String) {
}
}
enum class ClawdisCanvasA2UICommand(val rawValue: String) {
enum class ClawdbotCanvasA2UICommand(val rawValue: String) {
Push("canvas.a2ui.push"),
PushJSONL("canvas.a2ui.pushJSONL"),
Reset("canvas.a2ui.reset"),
@@ -33,7 +33,7 @@ enum class ClawdisCanvasA2UICommand(val rawValue: String) {
}
}
enum class ClawdisCameraCommand(val rawValue: String) {
enum class ClawdbotCameraCommand(val rawValue: String) {
Snap("camera.snap"),
Clip("camera.clip"),
;
@@ -43,7 +43,7 @@ enum class ClawdisCameraCommand(val rawValue: String) {
}
}
enum class ClawdisScreenCommand(val rawValue: String) {
enum class ClawdbotScreenCommand(val rawValue: String) {
Record("screen.record"),
;
@@ -52,7 +52,7 @@ enum class ClawdisScreenCommand(val rawValue: String) {
}
}
enum class ClawdisSmsCommand(val rawValue: String) {
enum class ClawdbotSmsCommand(val rawValue: String) {
Send("sms.send"),
;
@@ -61,7 +61,7 @@ enum class ClawdisSmsCommand(val rawValue: String) {
}
}
enum class ClawdisLocationCommand(val rawValue: String) {
enum class ClawdbotLocationCommand(val rawValue: String) {
Get("location.get"),
;

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.tools
package com.clawdbot.android.tools
import android.content.Context
import kotlinx.serialization.Serializable

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui
package com.clawdbot.android.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box

View File

@@ -1,8 +1,8 @@
package com.clawdis.android.ui
package com.clawdbot.android.ui
import androidx.compose.runtime.Composable
import com.clawdis.android.MainViewModel
import com.clawdis.android.ui.chat.ChatSheetContent
import com.clawdbot.android.MainViewModel
import com.clawdbot.android.ui.chat.ChatSheetContent
@Composable
fun ChatSheet(viewModel: MainViewModel) {

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui
package com.clawdbot.android.ui
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
@@ -9,7 +9,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
@Composable
fun ClawdisTheme(content: @Composable () -> Unit) {
fun ClawdbotTheme(content: @Composable () -> Unit) {
val context = LocalContext.current
val isDark = isSystemInDarkTheme()
val colorScheme = if (isDark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui
package com.clawdbot.android.ui
import android.annotation.SuppressLint
import android.Manifest
@@ -65,8 +65,8 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import androidx.core.content.ContextCompat
import com.clawdis.android.CameraHudKind
import com.clawdis.android.MainViewModel
import com.clawdbot.android.CameraHudKind
import com.clawdbot.android.MainViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -334,7 +334,7 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier)
WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, false)
}
if (isDebuggable) {
Log.d("ClawdisWebView", "userAgent: ${settings.userAgentString}")
Log.d("ClawdbotWebView", "userAgent: ${settings.userAgentString}")
}
isScrollContainer = true
overScrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS
@@ -349,7 +349,7 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier)
) {
if (!isDebuggable) return
if (!request.isForMainFrame) return
Log.e("ClawdisWebView", "onReceivedError: ${error.errorCode} ${error.description} ${request.url}")
Log.e("ClawdbotWebView", "onReceivedError: ${error.errorCode} ${error.description} ${request.url}")
}
override fun onReceivedHttpError(
@@ -360,14 +360,14 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier)
if (!isDebuggable) return
if (!request.isForMainFrame) return
Log.e(
"ClawdisWebView",
"ClawdbotWebView",
"onReceivedHttpError: ${errorResponse.statusCode} ${errorResponse.reasonPhrase} ${request.url}",
)
}
override fun onPageFinished(view: WebView, url: String?) {
if (isDebuggable) {
Log.d("ClawdisWebView", "onPageFinished: $url")
Log.d("ClawdbotWebView", "onPageFinished: $url")
}
viewModel.canvas.onPageFinished()
}
@@ -378,7 +378,7 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier)
): Boolean {
if (isDebuggable) {
Log.e(
"ClawdisWebView",
"ClawdbotWebView",
"onRenderProcessGone didCrash=${detail.didCrash()} priorityAtExit=${detail.rendererPriorityAtExit()}",
)
}
@@ -391,7 +391,7 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier)
if (!isDebuggable) return false
val msg = consoleMessage ?: return false
Log.d(
"ClawdisWebView",
"ClawdbotWebView",
"console ${msg.messageLevel()} @ ${msg.sourceId()}:${msg.lineNumber()} ${msg.message()}",
)
return false
@@ -423,7 +423,7 @@ private class CanvasA2UIActionBridge(private val onMessage: (String) -> Unit) {
}
companion object {
const val interfaceName: String = "clawdisCanvasA2UIAction"
const val interfaceName: String = "clawdbotCanvasA2UIAction"
}
}

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui
package com.clawdbot.android.ui
import android.Manifest
import android.content.Context
@@ -52,11 +52,11 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.clawdis.android.BuildConfig
import com.clawdis.android.LocationMode
import com.clawdis.android.MainViewModel
import com.clawdis.android.NodeForegroundService
import com.clawdis.android.VoiceWakeMode
import com.clawdbot.android.BuildConfig
import com.clawdbot.android.LocationMode
import com.clawdbot.android.MainViewModel
import com.clawdbot.android.NodeForegroundService
import com.clawdbot.android.VoiceWakeMode
@Composable
fun SettingsSheet(viewModel: MainViewModel) {
@@ -133,7 +133,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
if (requestedMode != null) {
viewModel.setLocationMode(requestedMode)
if (requestedMode == LocationMode.Always && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (requestedMode == LocationMode.Always) {
val backgroundOk =
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_BACKGROUND_LOCATION) ==
PackageManager.PERMISSION_GRANTED
@@ -191,7 +191,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
PackageManager.PERMISSION_GRANTED
if (fineOk || coarseOk) {
viewModel.setLocationMode(targetMode)
if (targetMode == LocationMode.Always && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (targetMode == LocationMode.Always) {
val backgroundOk =
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_BACKGROUND_LOCATION) ==
PackageManager.PERMISSION_GRANTED
@@ -436,7 +436,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
Column(verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.fillMaxWidth()) {
ListItem(
headlineContent = { Text("Foreground Only") },
supportingContent = { Text("Listens only while Clawdis is open.") },
supportingContent = { Text("Listens only while Clawdbot is open.") },
trailingContent = {
RadioButton(
selected = voiceWakeMode == VoiceWakeMode.Foreground,
@@ -482,7 +482,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(
onClick = {
val parsed = com.clawdis.android.WakeWords.parseCommaSeparated(wakeWordsText)
val parsed = com.clawdbot.android.WakeWords.parseCommaSeparated(wakeWordsText)
viewModel.setWakeWords(parsed)
},
enabled = isConnected,
@@ -580,7 +580,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
)
ListItem(
headlineContent = { Text("While Using") },
supportingContent = { Text("Only while Clawdis is open.") },
supportingContent = { Text("Only while Clawdbot is open.") },
trailingContent = {
RadioButton(
selected = locationMode == LocationMode.WhileUsing,
@@ -627,7 +627,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
item {
ListItem(
headlineContent = { Text("Prevent Sleep") },
supportingContent = { Text("Keeps the screen awake while Clawdis is open.") },
supportingContent = { Text("Keeps the screen awake while Clawdbot is open.") },
trailingContent = { Switch(checked = preventSleep, onCheckedChange = viewModel::setPreventSleep) },
)
}

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui
package com.clawdbot.android.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
@@ -28,9 +28,9 @@ import androidx.compose.ui.unit.dp
fun StatusPill(
bridge: BridgeState,
voiceEnabled: Boolean,
activity: StatusActivity? = null,
onClick: () -> Unit,
modifier: Modifier = Modifier,
activity: StatusActivity? = null,
) {
Surface(
onClick = onClick,

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui
package com.clawdbot.android.ui
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui.chat
package com.clawdbot.android.ui.chat
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -38,7 +38,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.clawdis.android.chat.ChatSessionEntry
import com.clawdbot.android.chat.ChatSessionEntry
@Composable
fun ChatComposer(

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui.chat
package com.clawdbot.android.ui.chat
import android.graphics.BitmapFactory
import android.util.Base64

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui.chat
package com.clawdbot.android.ui.chat
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -20,8 +20,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.dp
import com.clawdis.android.chat.ChatMessage
import com.clawdis.android.chat.ChatPendingToolCall
import com.clawdbot.android.chat.ChatMessage
import com.clawdbot.android.chat.ChatPendingToolCall
@Composable
fun ChatMessageListCard(

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui.chat
package com.clawdbot.android.ui.chat
import android.graphics.BitmapFactory
import android.util.Base64
@@ -31,10 +31,10 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.Image
import com.clawdis.android.chat.ChatMessage
import com.clawdis.android.chat.ChatMessageContent
import com.clawdis.android.chat.ChatPendingToolCall
import com.clawdis.android.tools.ToolDisplayRegistry
import com.clawdbot.android.chat.ChatMessage
import com.clawdbot.android.chat.ChatMessageContent
import com.clawdbot.android.chat.ChatPendingToolCall
import com.clawdbot.android.tools.ToolDisplayRegistry
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import androidx.compose.ui.platform.LocalContext

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui.chat
package com.clawdbot.android.ui.chat
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -20,7 +20,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.clawdis.android.chat.ChatSessionEntry
import com.clawdbot.android.chat.ChatSessionEntry
@Composable
fun ChatSessionsDialog(

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui.chat
package com.clawdbot.android.ui.chat
import android.content.ContentResolver
import android.net.Uri
@@ -19,8 +19,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.clawdis.android.MainViewModel
import com.clawdis.android.chat.OutgoingAttachment
import com.clawdbot.android.MainViewModel
import com.clawdbot.android.chat.OutgoingAttachment
import java.io.ByteArrayOutputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

View File

@@ -1,6 +1,6 @@
package com.clawdis.android.ui.chat
package com.clawdbot.android.ui.chat
import com.clawdis.android.chat.ChatSessionEntry
import com.clawdbot.android.chat.ChatSessionEntry
private const val MAIN_SESSION_KEY = "main"
private const val RECENT_WINDOW_MS = 24 * 60 * 60 * 1000L

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.voice
package com.clawdbot.android.voice
import android.media.MediaDataSource
import kotlin.math.min

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.voice
package com.clawdbot.android.voice
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.voice
package com.clawdbot.android.voice
import android.Manifest
import android.content.Context
@@ -20,7 +20,7 @@ import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import android.util.Log
import androidx.core.content.ContextCompat
import com.clawdis.android.bridge.BridgeSession
import com.clawdbot.android.bridge.BridgeSession
import java.net.HttpURLConnection
import java.net.URL
import java.util.UUID

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.voice
package com.clawdbot.android.voice
object VoiceWakeCommandExtractor {
fun extractCommand(text: String, triggerWords: List<String>): String? {

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.voice
package com.clawdbot.android.voice
import android.content.Context
import android.content.Intent

View File

@@ -1,4 +1,4 @@
<resources>
<string name="app_name">Clawdis Node</string>
<string name="app_name">Clawdbot Node</string>
</resources>

View File

@@ -1,5 +1,5 @@
<resources>
<style name="Theme.ClawdisNode" parent="Theme.Material3.DayNight.NoActionBar">
<style name="Theme.ClawdbotNode" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">false</item>

View File

@@ -4,7 +4,7 @@
<base-config cleartextTrafficPermitted="true" tools:ignore="InsecureBaseConfiguration" />
<!-- Allow HTTP for tailnet/local dev endpoints (e.g. canvas/background web). -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">clawdis.internal</domain>
<domain includeSubdomains="true">clawdbot.internal</domain>
</domain-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">ts.net</domain>

View File

@@ -0,0 +1,43 @@
package com.clawdbot.android
import android.app.Notification
import android.content.Intent
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class NodeForegroundServiceTest {
@Test
fun buildNotificationSetsLaunchIntent() {
val service = Robolectric.buildService(NodeForegroundService::class.java).get()
val notification = buildNotification(service)
val pendingIntent = notification.contentIntent
assertNotNull(pendingIntent)
val savedIntent = Shadows.shadowOf(pendingIntent).savedIntent
assertNotNull(savedIntent)
assertEquals(MainActivity::class.java.name, savedIntent.component?.className)
val expectedFlags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
assertEquals(expectedFlags, savedIntent.flags and expectedFlags)
}
private fun buildNotification(service: NodeForegroundService): Notification {
val method =
NodeForegroundService::class.java.getDeclaredMethod(
"buildNotification",
String::class.java,
String::class.java,
)
method.isAccessible = true
return method.invoke(service, "Title", "Text") as Notification
}
}

View File

@@ -1,4 +1,4 @@
package com.clawdis.android
package com.clawdbot.android
import org.junit.Assert.assertEquals
import org.junit.Test

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.bridge
package com.clawdbot.android.bridge
import org.junit.Assert.assertEquals
import org.junit.Test
@@ -12,7 +12,7 @@ class BonjourEscapesTest {
@Test
fun decodeDecodesDecimalEscapes() {
assertEquals("Clawdis Gateway", BonjourEscapes.decode("Clawdis\\032Gateway"))
assertEquals("Clawdbot Gateway", BonjourEscapes.decode("Clawdbot\\032Gateway"))
assertEquals("A B", BonjourEscapes.decode("A\\032B"))
assertEquals("Peter\u2019s Mac", BonjourEscapes.decode("Peter\\226\\128\\153s Mac"))
}

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.bridge
package com.clawdbot.android.bridge
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.bridge
package com.clawdbot.android.bridge
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.bridge
package com.clawdbot.android.bridge
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.node
package com.clawdbot.android.node
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.node
package com.clawdbot.android.node
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.node
package com.clawdbot.android.node
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive

View File

@@ -1,28 +1,28 @@
package com.clawdis.android.protocol
package com.clawdbot.android.protocol
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import org.junit.Assert.assertEquals
import org.junit.Test
class ClawdisCanvasA2UIActionTest {
class ClawdbotCanvasA2UIActionTest {
@Test
fun extractActionNameAcceptsNameOrAction() {
val nameObj = Json.parseToJsonElement("{\"name\":\"Hello\"}").jsonObject
assertEquals("Hello", ClawdisCanvasA2UIAction.extractActionName(nameObj))
assertEquals("Hello", ClawdbotCanvasA2UIAction.extractActionName(nameObj))
val actionObj = Json.parseToJsonElement("{\"action\":\"Wave\"}").jsonObject
assertEquals("Wave", ClawdisCanvasA2UIAction.extractActionName(actionObj))
assertEquals("Wave", ClawdbotCanvasA2UIAction.extractActionName(actionObj))
val fallbackObj =
Json.parseToJsonElement("{\"name\":\" \",\"action\":\"Fallback\"}").jsonObject
assertEquals("Fallback", ClawdisCanvasA2UIAction.extractActionName(fallbackObj))
assertEquals("Fallback", ClawdbotCanvasA2UIAction.extractActionName(fallbackObj))
}
@Test
fun formatAgentMessageMatchesSharedSpec() {
val msg =
ClawdisCanvasA2UIAction.formatAgentMessage(
ClawdbotCanvasA2UIAction.formatAgentMessage(
actionName = "Get Weather",
sessionKey = "main",
surfaceId = "main",
@@ -40,9 +40,9 @@ class ClawdisCanvasA2UIActionTest {
@Test
fun jsDispatchA2uiStatusIsStable() {
val js = ClawdisCanvasA2UIAction.jsDispatchA2UIActionStatus(actionId = "a1", ok = true, error = null)
val js = ClawdbotCanvasA2UIAction.jsDispatchA2UIActionStatus(actionId = "a1", ok = true, error = null)
assertEquals(
"window.dispatchEvent(new CustomEvent('clawdis:a2ui-action-status', { detail: { id: \"a1\", ok: true, error: \"\" } }));",
"window.dispatchEvent(new CustomEvent('clawdbot:a2ui-action-status', { detail: { id: \"a1\", ok: true, error: \"\" } }));",
js,
)
}

View File

@@ -0,0 +1,35 @@
package com.clawdbot.android.protocol
import org.junit.Assert.assertEquals
import org.junit.Test
class ClawdbotProtocolConstantsTest {
@Test
fun canvasCommandsUseStableStrings() {
assertEquals("canvas.present", ClawdbotCanvasCommand.Present.rawValue)
assertEquals("canvas.hide", ClawdbotCanvasCommand.Hide.rawValue)
assertEquals("canvas.navigate", ClawdbotCanvasCommand.Navigate.rawValue)
assertEquals("canvas.eval", ClawdbotCanvasCommand.Eval.rawValue)
assertEquals("canvas.snapshot", ClawdbotCanvasCommand.Snapshot.rawValue)
}
@Test
fun a2uiCommandsUseStableStrings() {
assertEquals("canvas.a2ui.push", ClawdbotCanvasA2UICommand.Push.rawValue)
assertEquals("canvas.a2ui.pushJSONL", ClawdbotCanvasA2UICommand.PushJSONL.rawValue)
assertEquals("canvas.a2ui.reset", ClawdbotCanvasA2UICommand.Reset.rawValue)
}
@Test
fun capabilitiesUseStableStrings() {
assertEquals("canvas", ClawdbotCapability.Canvas.rawValue)
assertEquals("camera", ClawdbotCapability.Camera.rawValue)
assertEquals("screen", ClawdbotCapability.Screen.rawValue)
assertEquals("voiceWake", ClawdbotCapability.VoiceWake.rawValue)
}
@Test
fun screenCommandsUseStableStrings() {
assertEquals("screen.record", ClawdbotScreenCommand.Record.rawValue)
}
}

View File

@@ -1,6 +1,6 @@
package com.clawdis.android.ui.chat
package com.clawdbot.android.ui.chat
import com.clawdis.android.chat.ChatSessionEntry
import com.clawdbot.android.chat.ChatSessionEntry
import org.junit.Assert.assertEquals
import org.junit.Test

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.voice
package com.clawdbot.android.voice
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.voice
package com.clawdbot.android.voice
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull

View File

@@ -1,35 +0,0 @@
package com.clawdis.android.protocol
import org.junit.Assert.assertEquals
import org.junit.Test
class ClawdisProtocolConstantsTest {
@Test
fun canvasCommandsUseStableStrings() {
assertEquals("canvas.present", ClawdisCanvasCommand.Present.rawValue)
assertEquals("canvas.hide", ClawdisCanvasCommand.Hide.rawValue)
assertEquals("canvas.navigate", ClawdisCanvasCommand.Navigate.rawValue)
assertEquals("canvas.eval", ClawdisCanvasCommand.Eval.rawValue)
assertEquals("canvas.snapshot", ClawdisCanvasCommand.Snapshot.rawValue)
}
@Test
fun a2uiCommandsUseStableStrings() {
assertEquals("canvas.a2ui.push", ClawdisCanvasA2UICommand.Push.rawValue)
assertEquals("canvas.a2ui.pushJSONL", ClawdisCanvasA2UICommand.PushJSONL.rawValue)
assertEquals("canvas.a2ui.reset", ClawdisCanvasA2UICommand.Reset.rawValue)
}
@Test
fun capabilitiesUseStableStrings() {
assertEquals("canvas", ClawdisCapability.Canvas.rawValue)
assertEquals("camera", ClawdisCapability.Camera.rawValue)
assertEquals("screen", ClawdisCapability.Screen.rawValue)
assertEquals("voiceWake", ClawdisCapability.VoiceWake.rawValue)
}
@Test
fun screenCommandsUseStableStrings() {
assertEquals("screen.record", ClawdisScreenCommand.Record.rawValue)
}
}

View File

@@ -14,6 +14,6 @@ dependencyResolutionManagement {
}
}
rootProject.name = "ClawdisNodeAndroid"
rootProject.name = "ClawdbotNodeAndroid"
include(":app")

View File

@@ -1,4 +1,4 @@
# Clawdis (iOS)
# Clawdbot (iOS)
Internal-only SwiftUI app scaffold.
@@ -11,11 +11,11 @@ brew install swiftformat swiftlint
```bash
cd apps/ios
xcodegen generate
open Clawdis.xcodeproj
open Clawdbot.xcodeproj
```
## Shared packages
- `../shared/ClawdisKit` — shared types/constants used by iOS (and later macOS bridge + gateway routing).
- `../shared/ClawdbotKit` — shared types/constants used by iOS (and later macOS bridge + gateway routing).
## fastlane
```bash

View File

@@ -1,4 +1,4 @@
import ClawdisKit
import ClawdbotKit
import Foundation
import Network
@@ -14,7 +14,7 @@ actor BridgeClient {
{
self.lineBuffer = Data()
let connection = NWConnection(to: endpoint, using: .tcp)
let queue = DispatchQueue(label: "com.clawdis.ios.bridge-client")
let queue = DispatchQueue(label: "com.clawdbot.ios.bridge-client")
defer { connection.cancel() }
try await self.withTimeout(seconds: 8, purpose: "connect") {
try await self.startAndWaitForReady(connection, queue: queue)

View File

@@ -1,4 +1,4 @@
import ClawdisKit
import ClawdbotKit
import Darwin
import Foundation
import Network
@@ -99,7 +99,7 @@ final class BridgeConnectionController {
guard !instanceId.isEmpty else { return }
let token = KeychainStore.loadString(
service: "com.clawdis.bridge",
service: "com.clawdbot.bridge",
account: self.keychainAccount(instanceId: instanceId))?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !token.isEmpty else { return }
@@ -189,7 +189,7 @@ final class BridgeConnectionController {
if !refreshed.isEmpty, refreshed != token {
_ = KeychainStore.saveString(
refreshed,
service: "com.clawdis.bridge",
service: "com.clawdbot.bridge",
account: self.keychainAccount(instanceId: instanceId))
}
appModel.connectToBridge(endpoint: endpoint, hello: self.makeHello(token: resolvedToken))
@@ -217,46 +217,46 @@ final class BridgeConnectionController {
}
private func currentCaps() -> [String] {
var caps = [ClawdisCapability.canvas.rawValue, ClawdisCapability.screen.rawValue]
var caps = [ClawdbotCapability.canvas.rawValue, ClawdbotCapability.screen.rawValue]
// Default-on: if the key doesn't exist yet, treat it as enabled.
let cameraEnabled =
UserDefaults.standard.object(forKey: "camera.enabled") == nil
? true
: UserDefaults.standard.bool(forKey: "camera.enabled")
if cameraEnabled { caps.append(ClawdisCapability.camera.rawValue) }
if cameraEnabled { caps.append(ClawdbotCapability.camera.rawValue) }
let voiceWakeEnabled = UserDefaults.standard.bool(forKey: VoiceWakePreferences.enabledKey)
if voiceWakeEnabled { caps.append(ClawdisCapability.voiceWake.rawValue) }
if voiceWakeEnabled { caps.append(ClawdbotCapability.voiceWake.rawValue) }
let locationModeRaw = UserDefaults.standard.string(forKey: "location.enabledMode") ?? "off"
let locationMode = ClawdisLocationMode(rawValue: locationModeRaw) ?? .off
if locationMode != .off { caps.append(ClawdisCapability.location.rawValue) }
let locationMode = ClawdbotLocationMode(rawValue: locationModeRaw) ?? .off
if locationMode != .off { caps.append(ClawdbotCapability.location.rawValue) }
return caps
}
private func currentCommands() -> [String] {
var commands: [String] = [
ClawdisCanvasCommand.present.rawValue,
ClawdisCanvasCommand.hide.rawValue,
ClawdisCanvasCommand.navigate.rawValue,
ClawdisCanvasCommand.evalJS.rawValue,
ClawdisCanvasCommand.snapshot.rawValue,
ClawdisCanvasA2UICommand.push.rawValue,
ClawdisCanvasA2UICommand.pushJSONL.rawValue,
ClawdisCanvasA2UICommand.reset.rawValue,
ClawdisScreenCommand.record.rawValue,
ClawdbotCanvasCommand.present.rawValue,
ClawdbotCanvasCommand.hide.rawValue,
ClawdbotCanvasCommand.navigate.rawValue,
ClawdbotCanvasCommand.evalJS.rawValue,
ClawdbotCanvasCommand.snapshot.rawValue,
ClawdbotCanvasA2UICommand.push.rawValue,
ClawdbotCanvasA2UICommand.pushJSONL.rawValue,
ClawdbotCanvasA2UICommand.reset.rawValue,
ClawdbotScreenCommand.record.rawValue,
]
let caps = Set(self.currentCaps())
if caps.contains(ClawdisCapability.camera.rawValue) {
commands.append(ClawdisCameraCommand.list.rawValue)
commands.append(ClawdisCameraCommand.snap.rawValue)
commands.append(ClawdisCameraCommand.clip.rawValue)
if caps.contains(ClawdbotCapability.camera.rawValue) {
commands.append(ClawdbotCameraCommand.list.rawValue)
commands.append(ClawdbotCameraCommand.snap.rawValue)
commands.append(ClawdbotCameraCommand.clip.rawValue)
}
if caps.contains(ClawdisCapability.location.rawValue) {
commands.append(ClawdisLocationCommand.get.rawValue)
if caps.contains(ClawdbotCapability.location.rawValue) {
commands.append(ClawdbotLocationCommand.get.rawValue)
}
return commands

View File

@@ -1,4 +1,4 @@
import ClawdisKit
import ClawdbotKit
import Foundation
import Network
import Observation
@@ -51,11 +51,11 @@ final class BridgeDiscoveryModel {
if !self.browsers.isEmpty { return }
self.appendDebugLog("start()")
for domain in ClawdisBonjour.bridgeServiceDomains {
for domain in ClawdbotBonjour.bridgeServiceDomains {
let params = NWParameters.tcp
params.includePeerToPeer = true
let browser = NWBrowser(
for: .bonjour(type: ClawdisBonjour.bridgeServiceType, domain: domain),
for: .bonjour(type: ClawdbotBonjour.bridgeServiceType, domain: domain),
using: params)
browser.stateUpdateHandler = { [weak self] state in
@@ -102,7 +102,7 @@ final class BridgeDiscoveryModel {
}
self.browsers[domain] = browser
browser.start(queue: DispatchQueue(label: "com.clawdis.ios.bridge-discovery.\(domain)"))
browser.start(queue: DispatchQueue(label: "com.clawdbot.ios.bridge-discovery.\(domain)"))
}
}
@@ -200,7 +200,7 @@ final class BridgeDiscoveryModel {
private static func prettifyInstanceName(_ decodedName: String) -> String {
let normalized = decodedName.split(whereSeparator: \.isWhitespace).joined(separator: " ")
let stripped = normalized.replacingOccurrences(of: " (Clawdis)", with: "")
let stripped = normalized.replacingOccurrences(of: " (Clawdbot)", with: "")
.replacingOccurrences(of: #"\s+\(\d+\)$"#, with: "", options: .regularExpression)
return stripped.trimmingCharacters(in: .whitespacesAndNewlines)
}

View File

@@ -1,4 +1,4 @@
import ClawdisKit
import ClawdbotKit
import Foundation
import Network

View File

@@ -1,4 +1,4 @@
import ClawdisKit
import ClawdbotKit
import Foundation
import Network
@@ -78,7 +78,7 @@ actor BridgeSession {
let params = NWParameters.tcp
params.includePeerToPeer = true
let connection = NWConnection(to: endpoint, using: params)
let queue = DispatchQueue(label: "com.clawdis.ios.bridge-session")
let queue = DispatchQueue(label: "com.clawdbot.ios.bridge-session")
self.connection = connection
self.queue = queue

View File

@@ -1,8 +1,8 @@
import Foundation
enum BridgeSettingsStore {
private static let bridgeService = "com.clawdis.bridge"
private static let nodeService = "com.clawdis.node"
private static let bridgeService = "com.clawdbot.bridge"
private static let nodeService = "com.clawdbot.node"
private static let instanceIdDefaultsKey = "node.instanceId"
private static let preferredBridgeStableIDDefaultsKey = "bridge.preferredStableID"

View File

@@ -1,5 +1,5 @@
import AVFoundation
import ClawdisKit
import ClawdbotKit
import Foundation
actor CameraController {
@@ -36,7 +36,7 @@ actor CameraController {
}
}
func snap(params: ClawdisCameraSnapParams) async throws -> (
func snap(params: ClawdbotCameraSnapParams) async throws -> (
format: String,
base64: String,
width: Int,
@@ -109,7 +109,7 @@ actor CameraController {
height: res.heightPx)
}
func clip(params: ClawdisCameraClipParams) async throws -> (
func clip(params: ClawdbotCameraClipParams) async throws -> (
format: String,
base64: String,
durationMs: Int,
@@ -161,9 +161,9 @@ actor CameraController {
await Self.warmUpCaptureSession()
let movURL = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdis-camera-\(UUID().uuidString).mov")
.appendingPathComponent("clawdbot-camera-\(UUID().uuidString).mov")
let mp4URL = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdis-camera-\(UUID().uuidString).mp4")
.appendingPathComponent("clawdbot-camera-\(UUID().uuidString).mp4")
defer {
try? FileManager.default.removeItem(at: movURL)
@@ -228,7 +228,7 @@ actor CameraController {
}
private nonisolated static func pickCamera(
facing: ClawdisCameraFacing,
facing: ClawdbotCameraFacing,
deviceId: String?) -> AVCaptureDevice?
{
if let deviceId, !deviceId.isEmpty {

View File

@@ -1,15 +1,15 @@
import ClawdisChatUI
import ClawdbotChatUI
import SwiftUI
struct ChatSheet: View {
@Environment(\.dismiss) private var dismiss
@State private var viewModel: ClawdisChatViewModel
@State private var viewModel: ClawdbotChatViewModel
private let userAccent: Color?
init(bridge: BridgeSession, sessionKey: String = "main", userAccent: Color? = nil) {
let transport = IOSBridgeChatTransport(bridge: bridge)
self._viewModel = State(
initialValue: ClawdisChatViewModel(
initialValue: ClawdbotChatViewModel(
sessionKey: sessionKey,
transport: transport))
self.userAccent = userAccent
@@ -17,7 +17,7 @@ struct ChatSheet: View {
var body: some View {
NavigationStack {
ClawdisChatView(
ClawdbotChatView(
viewModel: self.viewModel,
showsSessionSwitcher: true,
userAccent: self.userAccent)

View File

@@ -1,8 +1,8 @@
import ClawdisChatUI
import ClawdisKit
import ClawdbotChatUI
import ClawdbotKit
import Foundation
struct IOSBridgeChatTransport: ClawdisChatTransport, Sendable {
struct IOSBridgeChatTransport: ClawdbotChatTransport, Sendable {
private let bridge: BridgeSession
init(bridge: BridgeSession) {
@@ -19,7 +19,7 @@ struct IOSBridgeChatTransport: ClawdisChatTransport, Sendable {
_ = try await self.bridge.request(method: "chat.abort", paramsJSON: json, timeoutSeconds: 10)
}
func listSessions(limit: Int?) async throws -> ClawdisChatSessionsListResponse {
func listSessions(limit: Int?) async throws -> ClawdbotChatSessionsListResponse {
struct Params: Codable {
var includeGlobal: Bool
var includeUnknown: Bool
@@ -28,7 +28,7 @@ struct IOSBridgeChatTransport: ClawdisChatTransport, Sendable {
let data = try JSONEncoder().encode(Params(includeGlobal: true, includeUnknown: false, limit: limit))
let json = String(data: data, encoding: .utf8)
let res = try await self.bridge.request(method: "sessions.list", paramsJSON: json, timeoutSeconds: 15)
return try JSONDecoder().decode(ClawdisChatSessionsListResponse.self, from: res)
return try JSONDecoder().decode(ClawdbotChatSessionsListResponse.self, from: res)
}
func setActiveSessionKey(_ sessionKey: String) async throws {
@@ -38,12 +38,12 @@ struct IOSBridgeChatTransport: ClawdisChatTransport, Sendable {
try await self.bridge.sendEvent(event: "chat.subscribe", payloadJSON: json)
}
func requestHistory(sessionKey: String) async throws -> ClawdisChatHistoryPayload {
func requestHistory(sessionKey: String) async throws -> ClawdbotChatHistoryPayload {
struct Params: Codable { var sessionKey: String }
let data = try JSONEncoder().encode(Params(sessionKey: sessionKey))
let json = String(data: data, encoding: .utf8)
let res = try await self.bridge.request(method: "chat.history", paramsJSON: json, timeoutSeconds: 15)
return try JSONDecoder().decode(ClawdisChatHistoryPayload.self, from: res)
return try JSONDecoder().decode(ClawdbotChatHistoryPayload.self, from: res)
}
func sendMessage(
@@ -51,13 +51,13 @@ struct IOSBridgeChatTransport: ClawdisChatTransport, Sendable {
message: String,
thinking: String,
idempotencyKey: String,
attachments: [ClawdisChatAttachmentPayload]) async throws -> ClawdisChatSendResponse
attachments: [ClawdbotChatAttachmentPayload]) async throws -> ClawdbotChatSendResponse
{
struct Params: Codable {
var sessionKey: String
var message: String
var thinking: String
var attachments: [ClawdisChatAttachmentPayload]?
var attachments: [ClawdbotChatAttachmentPayload]?
var timeoutMs: Int
var idempotencyKey: String
}
@@ -72,16 +72,16 @@ struct IOSBridgeChatTransport: ClawdisChatTransport, Sendable {
let data = try JSONEncoder().encode(params)
let json = String(data: data, encoding: .utf8)
let res = try await self.bridge.request(method: "chat.send", paramsJSON: json, timeoutSeconds: 35)
return try JSONDecoder().decode(ClawdisChatSendResponse.self, from: res)
return try JSONDecoder().decode(ClawdbotChatSendResponse.self, from: res)
}
func requestHealth(timeoutMs: Int) async throws -> Bool {
let seconds = max(1, Int(ceil(Double(timeoutMs) / 1000.0)))
let res = try await self.bridge.request(method: "health", paramsJSON: nil, timeoutSeconds: seconds)
return (try? JSONDecoder().decode(ClawdisGatewayHealthOK.self, from: res))?.ok ?? true
return (try? JSONDecoder().decode(ClawdbotGatewayHealthOK.self, from: res))?.ok ?? true
}
func events() -> AsyncStream<ClawdisChatTransportEvent> {
func events() -> AsyncStream<ClawdbotChatTransportEvent> {
AsyncStream { continuation in
let task = Task {
let stream = await self.bridge.subscribeServerEvents()
@@ -94,16 +94,16 @@ struct IOSBridgeChatTransport: ClawdisChatTransport, Sendable {
continuation.yield(.seqGap)
case "health":
guard let json = evt.payloadJSON, let data = json.data(using: .utf8) else { break }
let ok = (try? JSONDecoder().decode(ClawdisGatewayHealthOK.self, from: data))?.ok ?? true
let ok = (try? JSONDecoder().decode(ClawdbotGatewayHealthOK.self, from: data))?.ok ?? true
continuation.yield(.health(ok: ok))
case "chat":
guard let json = evt.payloadJSON, let data = json.data(using: .utf8) else { break }
if let payload = try? JSONDecoder().decode(ClawdisChatEventPayload.self, from: data) {
if let payload = try? JSONDecoder().decode(ClawdbotChatEventPayload.self, from: data) {
continuation.yield(.chat(payload))
}
case "agent":
guard let json = evt.payloadJSON, let data = json.data(using: .utf8) else { break }
if let payload = try? JSONDecoder().decode(ClawdisAgentEventPayload.self, from: data) {
if let payload = try? JSONDecoder().decode(ClawdbotAgentEventPayload.self, from: data) {
continuation.yield(.agent(payload))
}
default:

View File

@@ -1,7 +1,7 @@
import SwiftUI
@main
struct ClawdisApp: App {
struct ClawdbotApp: App {
@State private var appModel: NodeAppModel
@State private var bridgeController: BridgeConnectionController
@Environment(\.scenePhase) private var scenePhase

View File

@@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Clawdis</string>
<string>Clawdbot</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconName</key>
@@ -29,20 +29,20 @@
</dict>
<key>NSBonjourServices</key>
<array>
<string>_clawdis-bridge._tcp</string>
<string>_clawdbot-bridge._tcp</string>
</array>
<key>NSCameraUsageDescription</key>
<string>Clawdis can capture photos or short video clips when requested via the bridge.</string>
<string>Clawdbot can capture photos or short video clips when requested via the bridge.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Clawdis discovers and connects to your Clawdis bridge on the local network.</string>
<string>Clawdbot discovers and connects to your Clawdbot bridge on the local network.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Clawdis uses your location when you allow location sharing.</string>
<string>Clawdbot uses your location when you allow location sharing.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Clawdis can share your location in the background when you enable Always.</string>
<string>Clawdbot can share your location in the background when you enable Always.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Clawdis needs microphone access for voice wake.</string>
<string>Clawdbot needs microphone access for voice wake.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>Clawdis uses on-device speech recognition for voice wake.</string>
<string>Clawdbot uses on-device speech recognition for voice wake.</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>

View File

@@ -1,4 +1,4 @@
import ClawdisKit
import ClawdbotKit
import CoreLocation
import Foundation
@@ -30,7 +30,7 @@ final class LocationService: NSObject, CLLocationManagerDelegate {
return .fullAccuracy
}
func ensureAuthorization(mode: ClawdisLocationMode) async -> CLAuthorizationStatus {
func ensureAuthorization(mode: ClawdbotLocationMode) async -> CLAuthorizationStatus {
guard CLLocationManager.locationServicesEnabled() else { return .denied }
let status = self.manager.authorizationStatus
@@ -53,8 +53,8 @@ final class LocationService: NSObject, CLLocationManagerDelegate {
}
func currentLocation(
params: ClawdisLocationGetParams,
desiredAccuracy: ClawdisLocationAccuracy,
params: ClawdbotLocationGetParams,
desiredAccuracy: ClawdbotLocationAccuracy,
maxAgeMs: Int?,
timeoutMs: Int?) async throws -> CLLocation
{
@@ -67,7 +67,7 @@ final class LocationService: NSObject, CLLocationManagerDelegate {
}
self.manager.desiredAccuracy = Self.accuracyValue(desiredAccuracy)
let timeout = max(0, timeoutMs ?? 10_000)
let timeout = max(0, timeoutMs ?? 10000)
return try await self.withTimeout(timeoutMs: timeout) {
try await self.requestLocation()
}
@@ -106,14 +106,14 @@ final class LocationService: NSObject, CLLocationManagerDelegate {
}
}
private static func accuracyValue(_ accuracy: ClawdisLocationAccuracy) -> CLLocationAccuracy {
private static func accuracyValue(_ accuracy: ClawdbotLocationAccuracy) -> CLLocationAccuracy {
switch accuracy {
case .coarse:
return kCLLocationAccuracyKilometer
kCLLocationAccuracyKilometer
case .balanced:
return kCLLocationAccuracyHundredMeters
kCLLocationAccuracyHundredMeters
case .precise:
return kCLLocationAccuracyBest
kCLLocationAccuracyBest
}
}

View File

@@ -1,4 +1,4 @@
import ClawdisKit
import ClawdbotKit
import Network
import Observation
import SwiftUI
@@ -89,7 +89,7 @@ final class NodeAppModel {
}()
guard !userAction.isEmpty else { return }
guard let name = ClawdisCanvasA2UIAction.extractActionName(userAction) else { return }
guard let name = ClawdbotCanvasA2UIAction.extractActionName(userAction) else { return }
let actionId: String = {
let id = (userAction["id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return id.isEmpty ? UUID().uuidString : id
@@ -108,15 +108,15 @@ final class NodeAppModel {
let host = UserDefaults.standard.string(forKey: "node.displayName") ?? UIDevice.current.name
let instanceId = (UserDefaults.standard.string(forKey: "node.instanceId") ?? "ios-node").lowercased()
let contextJSON = ClawdisCanvasA2UIAction.compactJSON(userAction["context"])
let contextJSON = ClawdbotCanvasA2UIAction.compactJSON(userAction["context"])
let sessionKey = "main"
let messageContext = ClawdisCanvasA2UIAction.AgentMessageContext(
let messageContext = ClawdbotCanvasA2UIAction.AgentMessageContext(
actionName: name,
session: .init(key: sessionKey, surfaceId: surfaceId),
component: .init(id: sourceComponentId, host: host, instanceId: instanceId),
contextJSON: contextJSON)
let message = ClawdisCanvasA2UIAction.formatAgentMessage(messageContext)
let message = ClawdbotCanvasA2UIAction.formatAgentMessage(messageContext)
let ok: Bool
var errorText: String?
@@ -141,7 +141,7 @@ final class NodeAppModel {
}
}
let js = ClawdisCanvasA2UIAction.jsDispatchA2UIActionStatus(actionId: actionId, ok: ok, error: errorText)
let js = ClawdbotCanvasA2UIAction.jsDispatchA2UIActionStatus(actionId: actionId, ok: ok, error: errorText)
do {
_ = try await self.screen.eval(javaScript: js)
} catch {
@@ -153,7 +153,7 @@ final class NodeAppModel {
guard let raw = await self.bridge.currentCanvasHostUrl() else { return nil }
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, let base = URL(string: trimmed) else { return nil }
return base.appendingPathComponent("__clawdis__/a2ui/").absoluteString + "?platform=ios"
return base.appendingPathComponent("__clawdbot__/a2ui/").absoluteString + "?platform=ios"
}
private func showA2UIOnConnectIfNeeded() async {
@@ -189,7 +189,7 @@ final class NodeAppModel {
self.talkMode.setEnabled(enabled)
}
func requestLocationPermissions(mode: ClawdisLocationMode) async -> Bool {
func requestLocationPermissions(mode: ClawdbotLocationMode) async -> Bool {
guard mode != .off else { return true }
let status = await self.locationService.ensureAuthorization(mode: mode)
switch status {
@@ -250,7 +250,9 @@ final class NodeAppModel {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdisNodeError(code: .unavailable, message: "UNAVAILABLE: node not ready"))
error: ClawdbotNodeError(
code: .unavailable,
message: "UNAVAILABLE: node not ready"))
}
return await self.handleInvoke(req)
})
@@ -439,7 +441,7 @@ final class NodeAppModel {
}
// iOS bridge forwards to the gateway; no local auth prompts here.
// (Key-based unattended auth is handled on macOS for clawdis:// links.)
// (Key-based unattended auth is handled on macOS for clawdbot:// links.)
let data = try JSONEncoder().encode(link)
guard let json = String(bytes: data, encoding: .utf8) else {
throw NSError(domain: "NodeAppModel", code: 2, userInfo: [
@@ -454,17 +456,14 @@ final class NodeAppModel {
return false
}
// swiftlint:disable:next function_body_length cyclomatic_complexity
private func handleInvoke(_ req: BridgeInvokeRequest) async -> BridgeInvokeResponse {
let command = req.command
if command.hasPrefix("canvas.") || command.hasPrefix("camera.") || command.hasPrefix("screen."),
self.isBackgrounded
{
if self.isBackgrounded, self.isBackgroundRestricted(command) {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdisNodeError(
error: ClawdbotNodeError(
code: .backgroundUnavailable,
message: "NODE_BACKGROUND_UNAVAILABLE: canvas/camera/screen commands require foreground"))
}
@@ -473,288 +472,36 @@ final class NodeAppModel {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdisNodeError(
error: ClawdbotNodeError(
code: .unavailable,
message: "CAMERA_DISABLED: enable Camera in iOS Settings → Camera → Allow Camera"))
}
do {
switch command {
case ClawdisLocationCommand.get.rawValue:
let mode = self.locationMode()
guard mode != .off else {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdisNodeError(
code: .unavailable,
message: "LOCATION_DISABLED: enable Location in Settings"))
}
if self.isBackgrounded, mode != .always {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdisNodeError(
code: .backgroundUnavailable,
message: "LOCATION_BACKGROUND_UNAVAILABLE: background location requires Always"))
}
let params = (try? Self.decodeParams(ClawdisLocationGetParams.self, from: req.paramsJSON)) ??
ClawdisLocationGetParams()
let desired = params.desiredAccuracy ??
(self.isLocationPreciseEnabled() ? .precise : .balanced)
let status = self.locationService.authorizationStatus()
if status != .authorizedAlways && status != .authorizedWhenInUse {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdisNodeError(
code: .unavailable,
message: "LOCATION_PERMISSION_REQUIRED: grant Location permission"))
}
if self.isBackgrounded && status != .authorizedAlways {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdisNodeError(
code: .unavailable,
message: "LOCATION_PERMISSION_REQUIRED: enable Always for background access"))
}
let location = try await self.locationService.currentLocation(
params: params,
desiredAccuracy: desired,
maxAgeMs: params.maxAgeMs,
timeoutMs: params.timeoutMs)
let isPrecise = self.locationService.accuracyAuthorization() == .fullAccuracy
let payload = ClawdisLocationPayload(
lat: location.coordinate.latitude,
lon: location.coordinate.longitude,
accuracyMeters: location.horizontalAccuracy,
altitudeMeters: location.verticalAccuracy >= 0 ? location.altitude : nil,
speedMps: location.speed >= 0 ? location.speed : nil,
headingDeg: location.course >= 0 ? location.course : nil,
timestamp: ISO8601DateFormatter().string(from: location.timestamp),
isPrecise: isPrecise,
source: nil)
let json = try Self.encodePayload(payload)
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
case ClawdisCanvasCommand.present.rawValue:
let params = (try? Self.decodeParams(ClawdisCanvasPresentParams.self, from: req.paramsJSON)) ??
ClawdisCanvasPresentParams()
let url = params.url?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if url.isEmpty {
self.screen.showDefaultCanvas()
} else {
self.screen.navigate(to: url)
}
return BridgeInvokeResponse(id: req.id, ok: true)
case ClawdisCanvasCommand.hide.rawValue:
return BridgeInvokeResponse(id: req.id, ok: true)
case ClawdisCanvasCommand.navigate.rawValue:
let params = try Self.decodeParams(ClawdisCanvasNavigateParams.self, from: req.paramsJSON)
self.screen.navigate(to: params.url)
return BridgeInvokeResponse(id: req.id, ok: true)
case ClawdisCanvasCommand.evalJS.rawValue:
let params = try Self.decodeParams(ClawdisCanvasEvalParams.self, from: req.paramsJSON)
let result = try await self.screen.eval(javaScript: params.javaScript)
let payload = try Self.encodePayload(["result": result])
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
case ClawdisCanvasCommand.snapshot.rawValue:
let params = try? Self.decodeParams(ClawdisCanvasSnapshotParams.self, from: req.paramsJSON)
let format = params?.format ?? .jpeg
let maxWidth: CGFloat? = {
if let raw = params?.maxWidth, raw > 0 { return CGFloat(raw) }
// Keep default snapshots comfortably below the gateway client's maxPayload.
// For full-res, clients should explicitly request a larger maxWidth.
return switch format {
case .png: 900
case .jpeg: 1600
}
}()
let base64 = try await self.screen.snapshotBase64(
maxWidth: maxWidth,
format: format,
quality: params?.quality)
let payload = try Self.encodePayload([
"format": format == .jpeg ? "jpeg" : "png",
"base64": base64,
])
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
case ClawdisCanvasA2UICommand.reset.rawValue:
guard let a2uiUrl = await self.resolveA2UIHostURL() else {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdisNodeError(
code: .unavailable,
message: "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host"))
}
self.screen.navigate(to: a2uiUrl)
if await !self.screen.waitForA2UIReady(timeoutMs: 5000) {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdisNodeError(
code: .unavailable,
message: "A2UI_HOST_UNAVAILABLE: A2UI host not reachable"))
}
let json = try await self.screen.eval(javaScript: """
(() => {
if (!globalThis.clawdisA2UI) return JSON.stringify({ ok: false, error: "missing clawdisA2UI" });
return JSON.stringify(globalThis.clawdisA2UI.reset());
})()
""")
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
case ClawdisCanvasA2UICommand.push.rawValue, ClawdisCanvasA2UICommand.pushJSONL.rawValue:
let messages: [AnyCodable]
if command == ClawdisCanvasA2UICommand.pushJSONL.rawValue {
let params = try Self.decodeParams(ClawdisCanvasA2UIPushJSONLParams.self, from: req.paramsJSON)
messages = try ClawdisCanvasA2UIJSONL.decodeMessagesFromJSONL(params.jsonl)
} else {
do {
let params = try Self.decodeParams(ClawdisCanvasA2UIPushParams.self, from: req.paramsJSON)
messages = params.messages
} catch {
// Be forgiving: some clients still send JSONL payloads to `canvas.a2ui.push`.
let params = try Self.decodeParams(ClawdisCanvasA2UIPushJSONLParams.self, from: req.paramsJSON)
messages = try ClawdisCanvasA2UIJSONL.decodeMessagesFromJSONL(params.jsonl)
}
}
guard let a2uiUrl = await self.resolveA2UIHostURL() else {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdisNodeError(
code: .unavailable,
message: "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host"))
}
self.screen.navigate(to: a2uiUrl)
if await !self.screen.waitForA2UIReady(timeoutMs: 5000) {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdisNodeError(
code: .unavailable,
message: "A2UI_HOST_UNAVAILABLE: A2UI host not reachable"))
}
let messagesJSON = try ClawdisCanvasA2UIJSONL.encodeMessagesJSONArray(messages)
let js = """
(() => {
try {
if (!globalThis.clawdisA2UI) return JSON.stringify({ ok: false, error: "missing clawdisA2UI" });
const messages = \(messagesJSON);
return JSON.stringify(globalThis.clawdisA2UI.applyMessages(messages));
} catch (e) {
return JSON.stringify({ ok: false, error: String(e?.message ?? e) });
}
})()
"""
let resultJSON = try await self.screen.eval(javaScript: js)
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: resultJSON)
case ClawdisCameraCommand.list.rawValue:
let devices = await self.camera.listDevices()
struct Payload: Codable {
var devices: [CameraController.CameraDeviceInfo]
}
let payload = try Self.encodePayload(Payload(devices: devices))
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
case ClawdisCameraCommand.snap.rawValue:
self.showCameraHUD(text: "Taking photo…", kind: .photo)
self.triggerCameraFlash()
let params = (try? Self.decodeParams(ClawdisCameraSnapParams.self, from: req.paramsJSON)) ??
ClawdisCameraSnapParams()
let res = try await self.camera.snap(params: params)
struct Payload: Codable {
var format: String
var base64: String
var width: Int
var height: Int
}
let payload = try Self.encodePayload(Payload(
format: res.format,
base64: res.base64,
width: res.width,
height: res.height))
self.showCameraHUD(text: "Photo captured", kind: .success, autoHideSeconds: 1.6)
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
case ClawdisCameraCommand.clip.rawValue:
let params = (try? Self.decodeParams(ClawdisCameraClipParams.self, from: req.paramsJSON)) ??
ClawdisCameraClipParams()
let suspended = (params.includeAudio ?? true) ? self.voiceWake.suspendForExternalAudioCapture() : false
defer { self.voiceWake.resumeAfterExternalAudioCapture(wasSuspended: suspended) }
self.showCameraHUD(text: "Recording…", kind: .recording)
let res = try await self.camera.clip(params: params)
struct Payload: Codable {
var format: String
var base64: String
var durationMs: Int
var hasAudio: Bool
}
let payload = try Self.encodePayload(Payload(
format: res.format,
base64: res.base64,
durationMs: res.durationMs,
hasAudio: res.hasAudio))
self.showCameraHUD(text: "Clip captured", kind: .success, autoHideSeconds: 1.8)
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
case ClawdisScreenCommand.record.rawValue:
let params = (try? Self.decodeParams(ClawdisScreenRecordParams.self, from: req.paramsJSON)) ??
ClawdisScreenRecordParams()
if let format = params.format, format.lowercased() != "mp4" {
throw NSError(domain: "Screen", code: 30, userInfo: [
NSLocalizedDescriptionKey: "INVALID_REQUEST: screen format must be mp4",
])
}
// Status pill mirrors screen recording state so it stays visible without overlay stacking.
self.screenRecordActive = true
defer { self.screenRecordActive = false }
let path = try await self.screenRecorder.record(
screenIndex: params.screenIndex,
durationMs: params.durationMs,
fps: params.fps,
includeAudio: params.includeAudio,
outPath: nil)
defer { try? FileManager.default.removeItem(atPath: path) }
let data = try Data(contentsOf: URL(fileURLWithPath: path))
struct Payload: Codable {
var format: String
var base64: String
var durationMs: Int?
var fps: Double?
var screenIndex: Int?
var hasAudio: Bool
}
let payload = try Self.encodePayload(Payload(
format: "mp4",
base64: data.base64EncodedString(),
durationMs: params.durationMs,
fps: params.fps,
screenIndex: params.screenIndex,
hasAudio: params.includeAudio ?? true))
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
case ClawdbotLocationCommand.get.rawValue:
return try await self.handleLocationInvoke(req)
case ClawdbotCanvasCommand.present.rawValue,
ClawdbotCanvasCommand.hide.rawValue,
ClawdbotCanvasCommand.navigate.rawValue,
ClawdbotCanvasCommand.evalJS.rawValue,
ClawdbotCanvasCommand.snapshot.rawValue:
return try await self.handleCanvasInvoke(req)
case ClawdbotCanvasA2UICommand.reset.rawValue,
ClawdbotCanvasA2UICommand.push.rawValue,
ClawdbotCanvasA2UICommand.pushJSONL.rawValue:
return try await self.handleCanvasA2UIInvoke(req)
case ClawdbotCameraCommand.list.rawValue,
ClawdbotCameraCommand.snap.rawValue,
ClawdbotCameraCommand.clip.rawValue:
return try await self.handleCameraInvoke(req)
case ClawdbotScreenCommand.record.rawValue:
return try await self.handleScreenRecordInvoke(req)
default:
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdisNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command"))
error: ClawdbotNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command"))
}
} catch {
if command.hasPrefix("camera.") {
@@ -764,13 +511,310 @@ final class NodeAppModel {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdisNodeError(code: .unavailable, message: error.localizedDescription))
error: ClawdbotNodeError(code: .unavailable, message: error.localizedDescription))
}
}
private func locationMode() -> ClawdisLocationMode {
private func isBackgroundRestricted(_ command: String) -> Bool {
command.hasPrefix("canvas.") || command.hasPrefix("camera.") || command.hasPrefix("screen.")
}
private func handleLocationInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
let mode = self.locationMode()
guard mode != .off else {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdbotNodeError(
code: .unavailable,
message: "LOCATION_DISABLED: enable Location in Settings"))
}
if self.isBackgrounded, mode != .always {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdbotNodeError(
code: .backgroundUnavailable,
message: "LOCATION_BACKGROUND_UNAVAILABLE: background location requires Always"))
}
let params = (try? Self.decodeParams(ClawdbotLocationGetParams.self, from: req.paramsJSON)) ??
ClawdbotLocationGetParams()
let desired = params.desiredAccuracy ??
(self.isLocationPreciseEnabled() ? .precise : .balanced)
let status = self.locationService.authorizationStatus()
if status != .authorizedAlways, status != .authorizedWhenInUse {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdbotNodeError(
code: .unavailable,
message: "LOCATION_PERMISSION_REQUIRED: grant Location permission"))
}
if self.isBackgrounded, status != .authorizedAlways {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdbotNodeError(
code: .unavailable,
message: "LOCATION_PERMISSION_REQUIRED: enable Always for background access"))
}
let location = try await self.locationService.currentLocation(
params: params,
desiredAccuracy: desired,
maxAgeMs: params.maxAgeMs,
timeoutMs: params.timeoutMs)
let isPrecise = self.locationService.accuracyAuthorization() == .fullAccuracy
let payload = ClawdbotLocationPayload(
lat: location.coordinate.latitude,
lon: location.coordinate.longitude,
accuracyMeters: location.horizontalAccuracy,
altitudeMeters: location.verticalAccuracy >= 0 ? location.altitude : nil,
speedMps: location.speed >= 0 ? location.speed : nil,
headingDeg: location.course >= 0 ? location.course : nil,
timestamp: ISO8601DateFormatter().string(from: location.timestamp),
isPrecise: isPrecise,
source: nil)
let json = try Self.encodePayload(payload)
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
}
private func handleCanvasInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
switch req.command {
case ClawdbotCanvasCommand.present.rawValue:
let params = (try? Self.decodeParams(ClawdbotCanvasPresentParams.self, from: req.paramsJSON)) ??
ClawdbotCanvasPresentParams()
let url = params.url?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if url.isEmpty {
self.screen.showDefaultCanvas()
} else {
self.screen.navigate(to: url)
}
return BridgeInvokeResponse(id: req.id, ok: true)
case ClawdbotCanvasCommand.hide.rawValue:
return BridgeInvokeResponse(id: req.id, ok: true)
case ClawdbotCanvasCommand.navigate.rawValue:
let params = try Self.decodeParams(ClawdbotCanvasNavigateParams.self, from: req.paramsJSON)
self.screen.navigate(to: params.url)
return BridgeInvokeResponse(id: req.id, ok: true)
case ClawdbotCanvasCommand.evalJS.rawValue:
let params = try Self.decodeParams(ClawdbotCanvasEvalParams.self, from: req.paramsJSON)
let result = try await self.screen.eval(javaScript: params.javaScript)
let payload = try Self.encodePayload(["result": result])
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
case ClawdbotCanvasCommand.snapshot.rawValue:
let params = try? Self.decodeParams(ClawdbotCanvasSnapshotParams.self, from: req.paramsJSON)
let format = params?.format ?? .jpeg
let maxWidth: CGFloat? = {
if let raw = params?.maxWidth, raw > 0 { return CGFloat(raw) }
// Keep default snapshots comfortably below the gateway client's maxPayload.
// For full-res, clients should explicitly request a larger maxWidth.
return switch format {
case .png: 900
case .jpeg: 1600
}
}()
let base64 = try await self.screen.snapshotBase64(
maxWidth: maxWidth,
format: format,
quality: params?.quality)
let payload = try Self.encodePayload([
"format": format == .jpeg ? "jpeg" : "png",
"base64": base64,
])
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
default:
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdbotNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command"))
}
}
private func handleCanvasA2UIInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
let command = req.command
switch command {
case ClawdbotCanvasA2UICommand.reset.rawValue:
guard let a2uiUrl = await self.resolveA2UIHostURL() else {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdbotNodeError(
code: .unavailable,
message: "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host"))
}
self.screen.navigate(to: a2uiUrl)
if await !self.screen.waitForA2UIReady(timeoutMs: 5000) {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdbotNodeError(
code: .unavailable,
message: "A2UI_HOST_UNAVAILABLE: A2UI host not reachable"))
}
let json = try await self.screen.eval(javaScript: """
(() => {
if (!globalThis.clawdbotA2UI) return JSON.stringify({ ok: false, error: "missing clawdbotA2UI" });
return JSON.stringify(globalThis.clawdbotA2UI.reset());
})()
""")
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
case ClawdbotCanvasA2UICommand.push.rawValue, ClawdbotCanvasA2UICommand.pushJSONL.rawValue:
let messages: [AnyCodable]
if command == ClawdbotCanvasA2UICommand.pushJSONL.rawValue {
let params = try Self.decodeParams(ClawdbotCanvasA2UIPushJSONLParams.self, from: req.paramsJSON)
messages = try ClawdbotCanvasA2UIJSONL.decodeMessagesFromJSONL(params.jsonl)
} else {
do {
let params = try Self.decodeParams(ClawdbotCanvasA2UIPushParams.self, from: req.paramsJSON)
messages = params.messages
} catch {
// Be forgiving: some clients still send JSONL payloads to `canvas.a2ui.push`.
let params = try Self.decodeParams(ClawdbotCanvasA2UIPushJSONLParams.self, from: req.paramsJSON)
messages = try ClawdbotCanvasA2UIJSONL.decodeMessagesFromJSONL(params.jsonl)
}
}
guard let a2uiUrl = await self.resolveA2UIHostURL() else {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdbotNodeError(
code: .unavailable,
message: "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host"))
}
self.screen.navigate(to: a2uiUrl)
if await !self.screen.waitForA2UIReady(timeoutMs: 5000) {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdbotNodeError(
code: .unavailable,
message: "A2UI_HOST_UNAVAILABLE: A2UI host not reachable"))
}
let messagesJSON = try ClawdbotCanvasA2UIJSONL.encodeMessagesJSONArray(messages)
let js = """
(() => {
try {
if (!globalThis.clawdbotA2UI) return JSON.stringify({ ok: false, error: "missing clawdbotA2UI" });
const messages = \(messagesJSON);
return JSON.stringify(globalThis.clawdbotA2UI.applyMessages(messages));
} catch (e) {
return JSON.stringify({ ok: false, error: String(e?.message ?? e) });
}
})()
"""
let resultJSON = try await self.screen.eval(javaScript: js)
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: resultJSON)
default:
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdbotNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command"))
}
}
private func handleCameraInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
switch req.command {
case ClawdbotCameraCommand.list.rawValue:
let devices = await self.camera.listDevices()
struct Payload: Codable {
var devices: [CameraController.CameraDeviceInfo]
}
let payload = try Self.encodePayload(Payload(devices: devices))
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
case ClawdbotCameraCommand.snap.rawValue:
self.showCameraHUD(text: "Taking photo…", kind: .photo)
self.triggerCameraFlash()
let params = (try? Self.decodeParams(ClawdbotCameraSnapParams.self, from: req.paramsJSON)) ??
ClawdbotCameraSnapParams()
let res = try await self.camera.snap(params: params)
struct Payload: Codable {
var format: String
var base64: String
var width: Int
var height: Int
}
let payload = try Self.encodePayload(Payload(
format: res.format,
base64: res.base64,
width: res.width,
height: res.height))
self.showCameraHUD(text: "Photo captured", kind: .success, autoHideSeconds: 1.6)
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
case ClawdbotCameraCommand.clip.rawValue:
let params = (try? Self.decodeParams(ClawdbotCameraClipParams.self, from: req.paramsJSON)) ??
ClawdbotCameraClipParams()
let suspended = (params.includeAudio ?? true) ? self.voiceWake.suspendForExternalAudioCapture() : false
defer { self.voiceWake.resumeAfterExternalAudioCapture(wasSuspended: suspended) }
self.showCameraHUD(text: "Recording…", kind: .recording)
let res = try await self.camera.clip(params: params)
struct Payload: Codable {
var format: String
var base64: String
var durationMs: Int
var hasAudio: Bool
}
let payload = try Self.encodePayload(Payload(
format: res.format,
base64: res.base64,
durationMs: res.durationMs,
hasAudio: res.hasAudio))
self.showCameraHUD(text: "Clip captured", kind: .success, autoHideSeconds: 1.8)
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
default:
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: ClawdbotNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command"))
}
}
private func handleScreenRecordInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
let params = (try? Self.decodeParams(ClawdbotScreenRecordParams.self, from: req.paramsJSON)) ??
ClawdbotScreenRecordParams()
if let format = params.format, format.lowercased() != "mp4" {
throw NSError(domain: "Screen", code: 30, userInfo: [
NSLocalizedDescriptionKey: "INVALID_REQUEST: screen format must be mp4",
])
}
// Status pill mirrors screen recording state so it stays visible without overlay stacking.
self.screenRecordActive = true
defer { self.screenRecordActive = false }
let path = try await self.screenRecorder.record(
screenIndex: params.screenIndex,
durationMs: params.durationMs,
fps: params.fps,
includeAudio: params.includeAudio,
outPath: nil)
defer { try? FileManager.default.removeItem(atPath: path) }
let data = try Data(contentsOf: URL(fileURLWithPath: path))
struct Payload: Codable {
var format: String
var base64: String
var durationMs: Int?
var fps: Double?
var screenIndex: Int?
var hasAudio: Bool
}
let payload = try Self.encodePayload(Payload(
format: "mp4",
base64: data.base64EncodedString(),
durationMs: params.durationMs,
fps: params.fps,
screenIndex: params.screenIndex,
hasAudio: params.includeAudio ?? true))
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
}
private func locationMode() -> ClawdbotLocationMode {
let raw = UserDefaults.standard.string(forKey: "location.enabledMode") ?? "off"
return ClawdisLocationMode(rawValue: raw) ?? .off
return ClawdbotLocationMode(rawValue: raw) ?? .off
}
private func isLocationPreciseEnabled() -> Bool {

View File

@@ -1,4 +1,4 @@
import ClawdisKit
import ClawdbotKit
import Observation
import SwiftUI
import WebKit
@@ -13,7 +13,7 @@ final class ScreenController {
var urlString: String = ""
var errorText: String?
/// Callback invoked when a clawdis:// deep link is tapped in the canvas
/// Callback invoked when a clawdbot:// deep link is tapped in the canvas
var onDeepLink: ((URL) -> Void)?
/// Callback invoked when the user clicks an A2UI action (e.g. button) inside the canvas web UI.
@@ -101,7 +101,7 @@ final class ScreenController {
let js = """
(() => {
try {
const api = globalThis.__clawdis;
const api = globalThis.__clawdbot;
if (!api) return;
if (typeof api.setDebugStatusEnabled === 'function') {
api.setDebugStatusEnabled(\(enabled ? "true" : "false"));
@@ -124,7 +124,7 @@ final class ScreenController {
let res = try await self.eval(javaScript: """
(() => {
try {
return !!globalThis.clawdisA2UI && typeof globalThis.clawdisA2UI.applyMessages === 'function';
return !!globalThis.clawdbotA2UI && typeof globalThis.clawdbotA2UI.applyMessages === 'function';
} catch (_) { return false; }
})()
""")
@@ -184,7 +184,7 @@ final class ScreenController {
func snapshotBase64(
maxWidth: CGFloat? = nil,
format: ClawdisCanvasSnapshotFormat,
format: ClawdbotCanvasSnapshotFormat,
quality: Double? = nil) async throws -> String
{
let config = WKSnapshotConfiguration()
@@ -229,7 +229,7 @@ final class ScreenController {
subdirectory: String)
-> URL?
{
let bundle = ClawdisKitResources.bundle
let bundle = ClawdbotKitResources.bundle
return bundle.url(forResource: name, withExtension: ext, subdirectory: subdirectory)
?? bundle.url(forResource: name, withExtension: ext)
}
@@ -342,7 +342,7 @@ extension Double {
// MARK: - Navigation Delegate
/// Handles navigation policy to intercept clawdis:// deep links from canvas
/// Handles navigation policy to intercept clawdbot:// deep links from canvas
@MainActor
private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate {
weak var controller: ScreenController?
@@ -357,8 +357,8 @@ private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate {
return
}
// Intercept clawdis:// deep links
if url.scheme == "clawdis" {
// Intercept clawdbot:// deep links
if url.scheme == "clawdbot" {
decisionHandler(.cancel)
self.controller?.onDeepLink?(url)
return
@@ -386,7 +386,7 @@ private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate {
}
private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
static let messageName = "clawdisCanvasA2UIAction"
static let messageName = "clawdbotCanvasA2UIAction"
static let legacyMessageNames = ["canvas", "a2ui", "userAction", "action"]
static let handlerNames = [messageName] + legacyMessageNames

View File

@@ -40,7 +40,6 @@ final class ScreenRecordService: @unchecked Sendable {
}
}
// swiftlint:disable:next cyclomatic_complexity
func record(
screenIndex: Int?,
durationMs: Int?,
@@ -48,165 +47,244 @@ final class ScreenRecordService: @unchecked Sendable {
includeAudio: Bool?,
outPath: String?) async throws -> String
{
let config = try self.makeRecordConfig(
screenIndex: screenIndex,
durationMs: durationMs,
fps: fps,
includeAudio: includeAudio,
outPath: outPath)
let state = CaptureState()
let recordQueue = DispatchQueue(label: "com.clawdbot.screenrecord")
try await self.startCapture(state: state, config: config, recordQueue: recordQueue)
try await Task.sleep(nanoseconds: UInt64(config.durationMs) * 1_000_000)
try await self.stopCapture()
try self.finalizeCapture(state: state)
try await self.finishWriting(state: state)
return config.outURL.path
}
private struct RecordConfig {
let durationMs: Int
let fpsValue: Double
let includeAudio: Bool
let outURL: URL
}
private func makeRecordConfig(
screenIndex: Int?,
durationMs: Int?,
fps: Double?,
includeAudio: Bool?,
outPath: String?) throws -> RecordConfig
{
if let idx = screenIndex, idx != 0 {
throw ScreenRecordError.invalidScreenIndex(idx)
}
let durationMs = Self.clampDurationMs(durationMs)
let fps = Self.clampFps(fps)
let fpsInt = Int32(fps.rounded())
let fpsValue = Double(fpsInt)
let includeAudio = includeAudio ?? true
if let idx = screenIndex, idx != 0 {
throw ScreenRecordError.invalidScreenIndex(idx)
}
let outURL: URL = {
if let outPath, !outPath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return URL(fileURLWithPath: outPath)
}
return FileManager.default.temporaryDirectory
.appendingPathComponent("clawdis-screen-record-\(UUID().uuidString).mp4")
}()
let outURL = self.makeOutputURL(outPath: outPath)
try? FileManager.default.removeItem(at: outURL)
let state = CaptureState()
let recordQueue = DispatchQueue(label: "com.clawdis.screenrecord")
return RecordConfig(
durationMs: durationMs,
fpsValue: fpsValue,
includeAudio: includeAudio,
outURL: outURL)
}
private func makeOutputURL(outPath: String?) -> URL {
if let outPath, !outPath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return URL(fileURLWithPath: outPath)
}
return FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-screen-record-\(UUID().uuidString).mp4")
}
private func startCapture(
state: CaptureState,
config: RecordConfig,
recordQueue: DispatchQueue) async throws
{
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
let handler: @Sendable (CMSampleBuffer, RPSampleBufferType, Error?) -> Void = { sample, type, error in
// ReplayKit can call the capture handler on a background queue.
// Serialize writes to avoid queue asserts.
recordQueue.async {
if let error {
state.withLock { state in
if state.handlerError == nil { state.handlerError = error }
}
return
}
guard CMSampleBufferDataIsReady(sample) else { return }
switch type {
case .video:
let pts = CMSampleBufferGetPresentationTimeStamp(sample)
let shouldSkip = state.withLock { state in
if let lastVideoTime = state.lastVideoTime {
let delta = CMTimeSubtract(pts, lastVideoTime)
return delta.seconds < (1.0 / fpsValue)
}
return false
}
if shouldSkip { return }
if state.withLock({ $0.writer == nil }) {
guard let imageBuffer = CMSampleBufferGetImageBuffer(sample) else {
state.withLock { state in
if state.handlerError == nil {
state.handlerError = ScreenRecordError.captureFailed("Missing image buffer")
}
}
return
}
let width = CVPixelBufferGetWidth(imageBuffer)
let height = CVPixelBufferGetHeight(imageBuffer)
do {
let w = try AVAssetWriter(outputURL: outURL, fileType: .mp4)
let settings: [String: Any] = [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoWidthKey: width,
AVVideoHeightKey: height,
]
let vInput = AVAssetWriterInput(mediaType: .video, outputSettings: settings)
vInput.expectsMediaDataInRealTime = true
guard w.canAdd(vInput) else {
throw ScreenRecordError.writeFailed("Cannot add video input")
}
w.add(vInput)
if includeAudio {
let aInput = AVAssetWriterInput(mediaType: .audio, outputSettings: nil)
aInput.expectsMediaDataInRealTime = true
if w.canAdd(aInput) {
w.add(aInput)
state.withLock { state in
state.audioInput = aInput
}
}
}
guard w.startWriting() else {
throw ScreenRecordError
.writeFailed(w.error?.localizedDescription ?? "Failed to start writer")
}
w.startSession(atSourceTime: pts)
state.withLock { state in
state.writer = w
state.videoInput = vInput
state.started = true
}
} catch {
state.withLock { state in
if state.handlerError == nil { state.handlerError = error }
}
return
}
}
let vInput = state.withLock { $0.videoInput }
let isStarted = state.withLock { $0.started }
guard let vInput, isStarted else { return }
if vInput.isReadyForMoreMediaData {
if vInput.append(sample) {
state.withLock { state in
state.sawVideo = true
state.lastVideoTime = pts
}
} else {
let err = state.withLock { $0.writer?.error }
if let err {
state.withLock { state in
if state.handlerError == nil {
state.handlerError = ScreenRecordError.writeFailed(err.localizedDescription)
}
}
}
}
}
case .audioApp, .audioMic:
let aInput = state.withLock { $0.audioInput }
let isStarted = state.withLock { $0.started }
guard includeAudio, let aInput, isStarted else { return }
if aInput.isReadyForMoreMediaData {
_ = aInput.append(sample)
}
@unknown default:
break
}
}
}
let handler = self.makeCaptureHandler(
state: state,
config: config,
recordQueue: recordQueue)
let completion: @Sendable (Error?) -> Void = { error in
if let error { cont.resume(throwing: error) } else { cont.resume() }
}
Task { @MainActor in
startReplayKitCapture(
includeAudio: includeAudio,
includeAudio: config.includeAudio,
handler: handler,
completion: completion)
}
}
}
try await Task.sleep(nanoseconds: UInt64(durationMs) * 1_000_000)
private func makeCaptureHandler(
state: CaptureState,
config: RecordConfig,
recordQueue: DispatchQueue) -> @Sendable (CMSampleBuffer, RPSampleBufferType, Error?) -> Void
{
{ sample, type, error in
// ReplayKit can call the capture handler on a background queue.
// Serialize writes to avoid queue asserts.
recordQueue.async {
if let error {
state.withLock { state in
if state.handlerError == nil { state.handlerError = error }
}
return
}
guard CMSampleBufferDataIsReady(sample) else { return }
switch type {
case .video:
self.handleVideoSample(sample, state: state, config: config)
case .audioApp, .audioMic:
self.handleAudioSample(sample, state: state, includeAudio: config.includeAudio)
@unknown default:
break
}
}
}
}
private func handleVideoSample(
_ sample: CMSampleBuffer,
state: CaptureState,
config: RecordConfig)
{
let pts = CMSampleBufferGetPresentationTimeStamp(sample)
let shouldSkip = state.withLock { state in
if let lastVideoTime = state.lastVideoTime {
let delta = CMTimeSubtract(pts, lastVideoTime)
return delta.seconds < (1.0 / config.fpsValue)
}
return false
}
if shouldSkip { return }
if state.withLock({ $0.writer == nil }) {
self.prepareWriter(sample: sample, state: state, config: config, pts: pts)
}
let vInput = state.withLock { $0.videoInput }
let isStarted = state.withLock { $0.started }
guard let vInput, isStarted else { return }
if vInput.isReadyForMoreMediaData {
if vInput.append(sample) {
state.withLock { state in
state.sawVideo = true
state.lastVideoTime = pts
}
} else {
let err = state.withLock { $0.writer?.error }
if let err {
state.withLock { state in
if state.handlerError == nil {
state.handlerError = ScreenRecordError.writeFailed(err.localizedDescription)
}
}
}
}
}
}
private func prepareWriter(
sample: CMSampleBuffer,
state: CaptureState,
config: RecordConfig,
pts: CMTime)
{
guard let imageBuffer = CMSampleBufferGetImageBuffer(sample) else {
state.withLock { state in
if state.handlerError == nil {
state.handlerError = ScreenRecordError.captureFailed("Missing image buffer")
}
}
return
}
let width = CVPixelBufferGetWidth(imageBuffer)
let height = CVPixelBufferGetHeight(imageBuffer)
do {
let writer = try AVAssetWriter(outputURL: config.outURL, fileType: .mp4)
let settings: [String: Any] = [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoWidthKey: width,
AVVideoHeightKey: height,
]
let vInput = AVAssetWriterInput(mediaType: .video, outputSettings: settings)
vInput.expectsMediaDataInRealTime = true
guard writer.canAdd(vInput) else {
throw ScreenRecordError.writeFailed("Cannot add video input")
}
writer.add(vInput)
if config.includeAudio {
let aInput = AVAssetWriterInput(mediaType: .audio, outputSettings: nil)
aInput.expectsMediaDataInRealTime = true
if writer.canAdd(aInput) {
writer.add(aInput)
state.withLock { state in
state.audioInput = aInput
}
}
}
guard writer.startWriting() else {
throw ScreenRecordError.writeFailed(
writer.error?.localizedDescription ?? "Failed to start writer")
}
writer.startSession(atSourceTime: pts)
state.withLock { state in
state.writer = writer
state.videoInput = vInput
state.started = true
}
} catch {
state.withLock { state in
if state.handlerError == nil { state.handlerError = error }
}
}
}
private func handleAudioSample(
_ sample: CMSampleBuffer,
state: CaptureState,
includeAudio: Bool)
{
let aInput = state.withLock { $0.audioInput }
let isStarted = state.withLock { $0.started }
guard includeAudio, let aInput, isStarted else { return }
if aInput.isReadyForMoreMediaData {
_ = aInput.append(sample)
}
}
private func stopCapture() async throws {
let stopError = await withCheckedContinuation { cont in
Task { @MainActor in
stopReplayKitCapture { error in cont.resume(returning: error) }
}
}
if let stopError { throw stopError }
}
let handlerErrorSnapshot = state.withLock { $0.handlerError }
if let handlerErrorSnapshot { throw handlerErrorSnapshot }
private func finalizeCapture(state: CaptureState) throws {
if let handlerErrorSnapshot = state.withLock({ $0.handlerError }) {
throw handlerErrorSnapshot
}
let writerSnapshot = state.withLock { $0.writer }
let videoInputSnapshot = state.withLock { $0.videoInput }
let audioInputSnapshot = state.withLock { $0.audioInput }
@@ -217,7 +295,13 @@ final class ScreenRecordService: @unchecked Sendable {
videoInputSnapshot.markAsFinished()
audioInputSnapshot?.markAsFinished()
_ = writerSnapshot
}
private func finishWriting(state: CaptureState) async throws {
guard let writerSnapshot = state.withLock({ $0.writer }) else {
throw ScreenRecordError.captureFailed("Missing writer")
}
let writerBox = UncheckedSendableBox(value: writerSnapshot)
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
writerBox.value.finishWriting {
@@ -231,8 +315,6 @@ final class ScreenRecordService: @unchecked Sendable {
}
}
}
return outURL.path
}
private nonisolated static func clampDurationMs(_ ms: Int?) -> Int {

View File

@@ -1,4 +1,4 @@
import ClawdisKit
import ClawdbotKit
import SwiftUI
struct ScreenTab: View {

View File

@@ -1,4 +1,4 @@
import ClawdisKit
import ClawdbotKit
import SwiftUI
import WebKit

View File

@@ -1,4 +1,4 @@
import ClawdisKit
import ClawdbotKit
import Network
import Observation
import SwiftUI
@@ -23,7 +23,7 @@ struct SettingsTab: View {
@AppStorage("talk.enabled") private var talkEnabled: Bool = false
@AppStorage("talk.button.enabled") private var talkButtonEnabled: Bool = true
@AppStorage("camera.enabled") private var cameraEnabled: Bool = true
@AppStorage("location.enabledMode") private var locationEnabledModeRaw: String = ClawdisLocationMode.off.rawValue
@AppStorage("location.enabledMode") private var locationEnabledModeRaw: String = ClawdbotLocationMode.off.rawValue
@AppStorage("location.preciseEnabled") private var locationPreciseEnabled: Bool = true
@AppStorage("screen.preventSleep") private var preventSleep: Bool = true
@AppStorage("bridge.preferredStableID") private var preferredBridgeStableID: String = ""
@@ -36,7 +36,7 @@ struct SettingsTab: View {
@State private var connectStatus = ConnectStatusStore()
@State private var connectingBridgeID: String?
@State private var localIPAddress: String?
@State private var lastLocationModeRaw: String = ClawdisLocationMode.off.rawValue
@State private var lastLocationModeRaw: String = ClawdbotLocationMode.off.rawValue
var body: some View {
NavigationStack {
@@ -186,9 +186,9 @@ struct SettingsTab: View {
Section("Location") {
Picker("Location Access", selection: self.$locationEnabledModeRaw) {
Text("Off").tag(ClawdisLocationMode.off.rawValue)
Text("While Using").tag(ClawdisLocationMode.whileUsing.rawValue)
Text("Always").tag(ClawdisLocationMode.always.rawValue)
Text("Off").tag(ClawdbotLocationMode.off.rawValue)
Text("While Using").tag(ClawdbotLocationMode.whileUsing.rawValue)
Text("Always").tag(ClawdbotLocationMode.always.rawValue)
}
.pickerStyle(.segmented)
@@ -202,7 +202,7 @@ struct SettingsTab: View {
Section("Screen") {
Toggle("Prevent Sleep", isOn: self.$preventSleep)
Text("Keeps the screen awake while Clawdis is open.")
Text("Keeps the screen awake while Clawdbot is open.")
.font(.footnote)
.foregroundStyle(.secondary)
}
@@ -233,7 +233,7 @@ struct SettingsTab: View {
.onChange(of: self.locationEnabledModeRaw) { _, newValue in
let previous = self.lastLocationModeRaw
self.lastLocationModeRaw = newValue
guard let mode = ClawdisLocationMode(rawValue: newValue) else { return }
guard let mode = ClawdbotLocationMode(rawValue: newValue) else { return }
Task {
let granted = await self.appModel.requestLocationPermissions(mode: mode)
if !granted {
@@ -312,8 +312,8 @@ struct SettingsTab: View {
return "iOS \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)"
}
private var locationMode: ClawdisLocationMode {
ClawdisLocationMode(rawValue: self.locationEnabledModeRaw) ?? .off
private var locationMode: ClawdbotLocationMode {
ClawdbotLocationMode(rawValue: self.locationEnabledModeRaw) ?? .off
}
private func appVersion() -> String {
@@ -342,38 +342,38 @@ struct SettingsTab: View {
}
private func currentCaps() -> [String] {
var caps = [ClawdisCapability.canvas.rawValue, ClawdisCapability.screen.rawValue]
var caps = [ClawdbotCapability.canvas.rawValue, ClawdbotCapability.screen.rawValue]
let cameraEnabled =
UserDefaults.standard.object(forKey: "camera.enabled") == nil
? true
: UserDefaults.standard.bool(forKey: "camera.enabled")
if cameraEnabled { caps.append(ClawdisCapability.camera.rawValue) }
if cameraEnabled { caps.append(ClawdbotCapability.camera.rawValue) }
let voiceWakeEnabled = UserDefaults.standard.bool(forKey: VoiceWakePreferences.enabledKey)
if voiceWakeEnabled { caps.append(ClawdisCapability.voiceWake.rawValue) }
if voiceWakeEnabled { caps.append(ClawdbotCapability.voiceWake.rawValue) }
return caps
}
private func currentCommands() -> [String] {
var commands: [String] = [
ClawdisCanvasCommand.present.rawValue,
ClawdisCanvasCommand.hide.rawValue,
ClawdisCanvasCommand.navigate.rawValue,
ClawdisCanvasCommand.evalJS.rawValue,
ClawdisCanvasCommand.snapshot.rawValue,
ClawdisCanvasA2UICommand.push.rawValue,
ClawdisCanvasA2UICommand.pushJSONL.rawValue,
ClawdisCanvasA2UICommand.reset.rawValue,
ClawdisScreenCommand.record.rawValue,
ClawdbotCanvasCommand.present.rawValue,
ClawdbotCanvasCommand.hide.rawValue,
ClawdbotCanvasCommand.navigate.rawValue,
ClawdbotCanvasCommand.evalJS.rawValue,
ClawdbotCanvasCommand.snapshot.rawValue,
ClawdbotCanvasA2UICommand.push.rawValue,
ClawdbotCanvasA2UICommand.pushJSONL.rawValue,
ClawdbotCanvasA2UICommand.reset.rawValue,
ClawdbotScreenCommand.record.rawValue,
]
let caps = Set(self.currentCaps())
if caps.contains(ClawdisCapability.camera.rawValue) {
commands.append(ClawdisCameraCommand.list.rawValue)
commands.append(ClawdisCameraCommand.snap.rawValue)
commands.append(ClawdisCameraCommand.clip.rawValue)
if caps.contains(ClawdbotCapability.camera.rawValue) {
commands.append(ClawdbotCameraCommand.list.rawValue)
commands.append(ClawdbotCameraCommand.snap.rawValue)
commands.append(ClawdbotCameraCommand.clip.rawValue)
}
return commands
@@ -391,7 +391,7 @@ struct SettingsTab: View {
do {
let statusStore = self.connectStatus
let existing = KeychainStore.loadString(
service: "com.clawdis.bridge",
service: "com.clawdbot.bridge",
account: self.keychainAccount())
let existingToken = (existing?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) ?
existing :
@@ -419,7 +419,7 @@ struct SettingsTab: View {
if !token.isEmpty, token != existingToken {
_ = KeychainStore.saveString(
token,
service: "com.clawdis.bridge",
service: "com.clawdbot.bridge",
account: self.keychainAccount())
}
@@ -465,7 +465,7 @@ struct SettingsTab: View {
do {
let statusStore = self.connectStatus
let existing = KeychainStore.loadString(
service: "com.clawdis.bridge",
service: "com.clawdbot.bridge",
account: self.keychainAccount())
let existingToken = (existing?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) ?
existing :
@@ -493,7 +493,7 @@ struct SettingsTab: View {
if !token.isEmpty, token != existingToken {
_ = KeychainStore.saveString(
token,
service: "com.clawdis.bridge",
service: "com.clawdbot.bridge",
account: self.keychainAccount())
}

View File

@@ -30,7 +30,7 @@ struct VoiceWakeWordsSettingsView: View {
Text("Wake Words")
} footer: {
Text(
"Clawdis reacts when any trigger appears in a transcription. "
"Clawdbot reacts when any trigger appears in a transcription. "
+ "Keep them short to avoid false positives.")
}
}

Some files were not shown because too many files have changed in this diff Show More