shadow-claw 1.23.4 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/README.md +42 -5
  2. package/bin/cli.mjs +166 -0
  3. package/bin/cli.test.mjs +60 -0
  4. package/bin/commands/backup.mjs +70 -0
  5. package/bin/commands/clients.mjs +41 -0
  6. package/bin/commands/peer-id.mjs +76 -0
  7. package/bin/commands/peer-id.test.mjs +104 -0
  8. package/bin/commands/send.mjs +51 -0
  9. package/bin/commands/tasks.mjs +59 -0
  10. package/bin/commands/webrtc-listen.mjs +254 -0
  11. package/bin/prerender-dsd-shell/prerender-dsd-shell.test.mjs +46 -0
  12. package/bin/rolldown.config.test.mjs +401 -8
  13. package/bin/utils/control-client.mjs +243 -0
  14. package/bin/utils/control-client.test.mjs +83 -0
  15. package/bin/utils/ulid.mjs +45 -0
  16. package/bin/utils/webrtc-control-client.mjs +699 -0
  17. package/bin/utils/webrtc-control-client.test.mjs +318 -0
  18. package/dist/electron/main.cjs +1 -1
  19. package/dist/public/AGENTS.md +5 -3
  20. package/dist/public/README.md +42 -5
  21. package/dist/public/agent.worker.js +1 -1
  22. package/dist/public/backup-controller-C_s9in2t.js +1 -0
  23. package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +1 -0
  24. package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +1 -0
  25. package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +1 -1
  26. package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
  27. package/dist/public/components/shadow-claw-settings/shadow-claw-settings.html +1 -1
  28. package/dist/public/config-CrHchneq.js +1 -0
  29. package/dist/public/{configurePeerJs-B4Ym9VMj.js → configurePeerJs-CxpZeDJJ.js} +1 -1
  30. package/dist/public/{connections-BEn6JYdu.js → connections-DvbgdkaB.js} +1 -1
  31. package/dist/public/{custom-element-security-DdYlUozq.js → custom-element-security-NCqQ5lmC.js} +1 -1
  32. package/dist/public/docs/README.md +3 -1
  33. package/dist/public/docs/architecture/overview.md +12 -12
  34. package/dist/public/docs/decisions/bundled-typescript-architecture.md +1 -1
  35. package/dist/public/docs/decisions/indexeddb-and-opfs-storage.md +2 -2
  36. package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
  37. package/dist/public/docs/example/article/index.html +1 -1
  38. package/dist/public/docs/guides/configuring-messaging-channels.md +41 -6
  39. package/dist/public/docs/guides/server-development-configuration.md +24 -0
  40. package/dist/public/docs/publishing/index.html +1 -1
  41. package/dist/public/docs/skill-creator/index.html +1 -1
  42. package/dist/public/docs/subsystems/backup.md +56 -0
  43. package/dist/public/docs/subsystems/cli.md +124 -0
  44. package/dist/public/docs/subsystems/control-plane.md +158 -0
  45. package/dist/public/docs/subsystems/notifications.md +54 -2
  46. package/dist/public/docs/subsystems/reactive-ui.md +1 -0
  47. package/dist/public/downloadGroupFile-By0GR6eV.js +1 -0
  48. package/dist/public/e2e/README.md +5 -1
  49. package/dist/public/{e2e-bridge-BQ4KzUt5.js → e2e-bridge-IGWsRl4i.js} +1 -1
  50. package/dist/public/file-viewer-D0eflgt6.js +1 -0
  51. package/dist/public/getAllTasks-vpBlgGsc.js +1 -0
  52. package/dist/public/getGroupDir-P1h9wl6S.js +1 -0
  53. package/dist/public/{git-DEUvQTUA.js → git-7Vq_SJSp.js} +2 -2
  54. package/dist/public/{iframe-sanitizer-0NS4XLOd.js → iframe-sanitizer-y2Z1pPwY.js} +1 -1
  55. package/dist/public/index.html +1 -1
  56. package/dist/public/index.js +1 -1
  57. package/dist/public/{initChatSplitResize-llGkSJXg.js → initChatSplitResize-CzV2E7Ul.js} +1 -1
  58. package/dist/public/initControlPlane-C9jpnWev.js +1 -0
  59. package/dist/public/llms.txt +1 -1
  60. package/dist/public/main/index.html +1 -1
  61. package/dist/public/main/memory/index.html +1 -1
  62. package/dist/public/{markdown-CyYR4App.js → markdown-BnpKxSxl.js} +1 -1
  63. package/dist/public/{mcp-reconnect-CSx4aMwy.js → mcp-reconnect-9emsqGRe.js} +1 -1
  64. package/dist/public/{memoryStorage-Dwg8NhAZ.js → memoryStorage-C0KvLNUp.js} +1 -1
  65. package/dist/public/{orchestrator-DRfEudXk.js → orchestrator-3h9xxHUm.js} +676 -676
  66. package/dist/public/orchestrator-nJj59RqD.js +15 -0
  67. package/dist/public/{peerjs-CurBuKvk.js → peerjs-BP4laMXj.js} +1 -1
  68. package/dist/public/{push-client-Dii-vPEh.js → push-client-CrjEXK3U.js} +1 -1
  69. package/dist/public/service-worker/push-handler.js +1 -1
  70. package/dist/public/service-worker.js +1 -1
  71. package/dist/public/{shadow-claw-5IHaDDMJ.js → shadow-claw-CbwETBWf.js} +2 -2
  72. package/dist/public/{shadow-claw-accounts-CZvz4iQz.js → shadow-claw-accounts-B4sBTQEj.js} +1 -1
  73. package/dist/public/{shadow-claw-channels-BKXFyRS3.js → shadow-claw-channels-BDWG5Qa3.js} +1 -1
  74. package/dist/public/{shadow-claw-chat-CCLuhWdI.js → shadow-claw-chat-Crv0DTCY.js} +6 -6
  75. package/dist/public/shadow-claw-control-plane-CX1IGbh9.js +199 -0
  76. package/dist/public/{shadow-claw-conversations-DV-87UUh.js → shadow-claw-conversations-_9qAaRXk.js} +1 -1
  77. package/dist/public/shadow-claw-dialog-BrOwAOdk.js +1 -0
  78. package/dist/public/{shadow-claw-file-viewer-Dk2dbAUd.js → shadow-claw-file-viewer-Blf6yGVD.js} +1 -1
  79. package/dist/public/shadow-claw-files-CYySBDCl.js +840 -0
  80. package/dist/public/{shadow-claw-git-DRA7_qck.js → shadow-claw-git-v8S0JjuR.js} +2 -2
  81. package/dist/public/{shadow-claw-integrations-1Xu5snuV.js → shadow-claw-integrations-D7ufpwhX.js} +1 -1
  82. package/dist/public/{shadow-claw-llm-6Aci1BDP.js → shadow-claw-llm-GWgoVfhI.js} +2 -2
  83. package/dist/public/{shadow-claw-mcp-remote-BUjAeVXC.js → shadow-claw-mcp-remote-kY4LMFNN.js} +1 -1
  84. package/dist/public/{shadow-claw-networking-FQe4nCp3.js → shadow-claw-networking-qHbP95z9.js} +1 -1
  85. package/dist/public/{shadow-claw-notifications-DV2wibL3.js → shadow-claw-notifications-YyHOK2Fr.js} +1 -1
  86. package/dist/public/{shadow-claw-page-header-B63S4HTU.js → shadow-claw-page-header-DMKbkv3s.js} +1 -1
  87. package/dist/public/{shadow-claw-pages-DE3jWj5K.js → shadow-claw-pages-DFZHhzFZ.js} +1 -1
  88. package/dist/public/{shadow-claw-settings-S6DAKfYx.js → shadow-claw-settings-DhxrKIfA.js} +10 -3
  89. package/dist/public/{shadow-claw-storage-DaacA3_K.js → shadow-claw-storage-BFynbP0j.js} +2 -2
  90. package/dist/public/{shadow-claw-task-server-IjKX042p.js → shadow-claw-task-server-3BIeb5oZ.js} +25 -4
  91. package/dist/public/{shadow-claw-tasks-Cy0JYmGU.js → shadow-claw-tasks-Caxc9atL.js} +1 -1
  92. package/dist/public/{shadow-claw-terminal-znAt7HyP.js → shadow-claw-terminal-Beb5qF-U.js} +1 -1
  93. package/dist/public/{shadow-claw-tools-Cutv9Si5.js → shadow-claw-tools-BfJx3A9W.js} +1 -1
  94. package/dist/public/{shadow-claw-webvm-BH8mPDEu.js → shadow-claw-webvm-BCS8ykeP.js} +1 -1
  95. package/dist/public/{syncWebMcpRegistration-BqJxaMGR.js → syncWebMcpRegistration-B_M2fCjH.js} +1 -1
  96. package/dist/server.js +286 -189
  97. package/package.json +2 -1
  98. package/src/__mocks__/jszip.cjs +15 -5
  99. package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +35 -0
  100. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +117 -0
  101. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +81 -0
  102. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.test.ts +127 -0
  103. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.ts +292 -0
  104. package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +72 -0
  105. package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +20 -30
  106. package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +116 -0
  107. package/src/components/settings/shadow-claw-llm/model-ranking.test.ts +143 -51
  108. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +307 -4
  109. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.ts +8 -8
  110. package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +31 -0
  111. package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +186 -0
  112. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +76 -0
  113. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +23 -2
  114. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
  115. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +19 -0
  116. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.ts +2 -1
  117. package/src/components/shadow-claw/utils/initChatSplitResize.test.ts +180 -13
  118. package/src/components/shadow-claw/utils/setupEffects.test.ts +39 -0
  119. package/src/components/shadow-claw-a2ui/catalog/basic/shared.test.ts +117 -0
  120. package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +1159 -1
  121. package/src/components/shadow-claw-chat/shadow-claw-chat.test.ts +1523 -19
  122. package/src/components/shadow-claw-chat/utils/escapeHtml.test.ts +13 -0
  123. package/src/components/shadow-claw-chat/utils/getPeerChatDisplayStatus.test.ts +20 -0
  124. package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +494 -0
  125. package/src/components/shadow-claw-dialog/shadow-claw-dialog.ts +13 -2
  126. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +273 -0
  127. package/src/components/shadow-claw-files/shadow-claw-files.test.ts +408 -1
  128. package/src/components/shadow-claw-pdf-viewer/utils/installGetOrInsertComputedPolyfill.test.ts +65 -0
  129. package/src/components/shadow-claw-settings/shadow-claw-settings.html +7 -0
  130. package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +221 -0
  131. package/src/components/shadow-claw-settings/shadow-claw-settings.ts +2 -0
  132. package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +407 -0
  133. package/src/components/shadow-claw-terminal/utils/consumeEscapeSequence.test.ts +58 -0
  134. package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +80 -0
  135. package/src/config/config.ts +10 -0
  136. package/src/core/backup-controller.test.ts +101 -0
  137. package/src/core/backup-controller.ts +137 -0
  138. package/src/core/control-plane-client.test.ts +408 -0
  139. package/src/core/control-plane-client.ts +425 -0
  140. package/src/core/orchestrator/orchestrator.ts +4 -0
  141. package/src/core/orchestrator/utils/compactContext.test.ts +78 -0
  142. package/src/core/orchestrator/utils/operations/channel.test.ts +254 -9
  143. package/src/core/orchestrator/utils/operations/provider.test.ts +326 -34
  144. package/src/core/orchestrator/utils/operations/vm.test.ts +114 -2
  145. package/src/core/orchestrator/utils/syncProxyConfigToServiceWorker.test.ts +60 -0
  146. package/src/core/utils/initControlPlane.test.ts +276 -0
  147. package/src/core/utils/initControlPlane.ts +437 -0
  148. package/src/core/utils/initializeApp.ts +18 -0
  149. package/src/db/getSession.test.ts +13 -1
  150. package/src/server/app.test.ts +12 -0
  151. package/src/server/app.ts +18 -8
  152. package/src/server/client-registry.test.ts +250 -0
  153. package/src/server/client-registry.ts +477 -0
  154. package/src/server/config.ts +15 -0
  155. package/src/server/control-plane-types.ts +120 -0
  156. package/src/server/control-plane.test.ts +472 -0
  157. package/src/server/control-plane.ts +581 -0
  158. package/src/server/routes/backup.test.ts +263 -0
  159. package/src/server/routes/backup.ts +262 -0
  160. package/src/server/routes/integrations-email.test.ts +532 -0
  161. package/src/server/routes/llamafile.test.ts +172 -48
  162. package/src/server/routes/mesh-llm.test.ts +192 -0
  163. package/src/server/routes/ollama.test.ts +49 -0
  164. package/src/server/server-peer.test.ts +86 -0
  165. package/src/server/server-peer.ts +330 -0
  166. package/src/server/server.ts +35 -0
  167. package/src/server/services/llamafile-manager.test.ts +230 -6
  168. package/src/server/services/transformers-runtime.test.ts +186 -18
  169. package/src/server/webrtc.test.ts +21 -0
  170. package/src/server/webrtc.ts +72 -0
  171. package/src/service-worker/fetch-proxy.test.ts +64 -1
  172. package/src/service-worker/push-handler.ts +39 -0
  173. package/src/shell/vm.test.ts +744 -12
  174. package/src/storage/ensureMainGroupIndex.test.ts +153 -0
  175. package/src/storage/getGroupFile.test.ts +66 -0
  176. package/src/storage/memoryStorage.test.ts +82 -2
  177. package/src/storage/writeGroupFileBytes.test.ts +143 -0
  178. package/src/stores/chat-ui.test.ts +20 -0
  179. package/src/stores/orchestrator.test.ts +134 -0
  180. package/src/stores/toast.test.ts +50 -0
  181. package/src/stores/tools.test.ts +147 -10
  182. package/src/subsystems/channels/imessage.test.ts +59 -0
  183. package/src/subsystems/channels/peerjs.test.ts +500 -62
  184. package/src/subsystems/channels/peerjs.ts +71 -4
  185. package/src/subsystems/channels/telegram.test.ts +177 -0
  186. package/src/subsystems/email/catalog.test.ts +1 -0
  187. package/src/subsystems/email/connections.test.ts +39 -1
  188. package/src/subsystems/git/credentials.test.ts +160 -825
  189. package/src/subsystems/git/git.test.ts +463 -0
  190. package/src/subsystems/notifications/__mocks__/node-sqlite.cjs +12 -1
  191. package/src/subsystems/notifications/push-routes.test.ts +33 -0
  192. package/src/subsystems/notifications/push-routes.ts +23 -0
  193. package/src/subsystems/notifications/push-store.ts +1 -1
  194. package/src/subsystems/providers/builtin-ai-tasks.test.ts +74 -0
  195. package/src/subsystems/providers/litert-lm-provider.test.ts +212 -51
  196. package/src/subsystems/providers/model-registry.test.ts +115 -0
  197. package/src/subsystems/providers/providers.test.ts +94 -1
  198. package/src/subsystems/providers/transformers-js-provider.test.ts +279 -0
  199. package/src/subsystems/providers/utils/createModelCacheFetch.test.ts +2 -2
  200. package/src/subsystems/providers/utils/downloadModelToCache.test.ts +2 -2
  201. package/src/subsystems/providers/utils/loadModelStream.test.ts +2 -2
  202. package/src/subsystems/skills/activateSkill.test.ts +101 -0
  203. package/src/subsystems/skills/discoverSkills.test.ts +251 -0
  204. package/src/subsystems/tools/cron.test.ts +64 -0
  205. package/src/subsystems/tools/declarative.test.ts +255 -89
  206. package/src/testing/assets/v86.9pfs/libv86.mjs +63 -0
  207. package/src/testing/assets/v86.ext2/libv86.mjs +3 -0
  208. package/src/testing/assets_missing_v86/assets/v86.9pfs/libv86.mjs +1 -0
  209. package/src/testing/jest-setup.ts +328 -40
  210. package/src/testing/mock-v86.mjs +49 -0
  211. package/src/ui/a2ui/types.ts +4 -3
  212. package/src/ui/a2ui/utils/normaliseComponentsToMap.test.ts +102 -0
  213. package/src/ui/a2ui/utils/resolveChildIds.test.ts +63 -0
  214. package/src/ui/a2ui/utils/resolveDynamicNumber.test.ts +13 -1
  215. package/src/worker/tools/bash/utils/waitForVMReady.test.ts +40 -0
  216. package/src/worker/tools/email/email.test.ts +193 -8
  217. package/src/worker/tools/email/utils/asEmailAttachmentInputs.test.ts +47 -0
  218. package/src/worker/tools/git/git.test.ts +214 -3
  219. package/src/worker/tools/rooms/invite-to-room.test.ts +54 -0
  220. package/src/worker/tools/rooms/leave-room.test.ts +40 -0
  221. package/src/worker/tools/rooms/list-room-members.test.ts +75 -0
  222. package/src/worker/tools/tasks/tasks-utils.test.ts +22 -0
  223. package/src/worker/tools/ui/ask-user.test.ts +50 -0
  224. package/src/worker/tools/ui/clear-chat.test.ts +19 -0
  225. package/src/worker/tools/ui/get-current-time.test.ts +19 -0
  226. package/src/worker/tools/ui/javascript.test.ts +116 -0
  227. package/src/worker/tools/ui/list-components.test.ts +10 -0
  228. package/src/worker/tools/ui/list-tool-profiles.test.ts +42 -0
  229. package/src/worker/tools/ui/manage-tools.test.ts +34 -0
  230. package/src/worker/tools/ui/render-component.test.ts +346 -0
  231. package/src/worker/tools/ui/send-notification.test.ts +40 -0
  232. package/src/worker/tools/ui/utils/getAllowFullInternetAccess.test.ts +31 -0
  233. package/src/worker/tools/ui/utils/parseBooleanConfig.test.ts +25 -0
  234. package/src/worker/tools/workspace/attach-file.test.ts +84 -0
  235. package/src/worker/tools/workspace/copy-file.test.ts +69 -0
  236. package/src/worker/tools/workspace/create-directory.test.ts +40 -0
  237. package/src/worker/tools/workspace/delete-file.test.ts +73 -0
  238. package/src/worker/tools/workspace/diff-files.test.ts +94 -0
  239. package/src/worker/tools/workspace/list-files.test.ts +36 -0
  240. package/src/worker/tools/workspace/move-file.test.ts +66 -0
  241. package/src/worker/tools/workspace/open-file.test.ts +57 -0
  242. package/src/worker/tools/workspace/patch-file.test.ts +81 -0
  243. package/src/worker/tools/workspace/read-file.test.ts +116 -0
  244. package/src/worker/tools/workspace/send-file.test.ts +82 -0
  245. package/src/worker/tools/workspace/update-memory.test.ts +29 -0
  246. package/src/worker/tools/workspace/write-file.test.ts +28 -0
  247. package/src/worker/utils/rate-limit.test.ts +199 -0
  248. package/src/worker/utils/tool-state.test.ts +29 -0
  249. package/src/worker/utils/transformers-js-utils.test.ts +59 -0
  250. package/dist/public/config-BYAuxx3j.js +0 -1
  251. package/dist/public/downloadGroupFile-Drdvg1Ev.js +0 -1
  252. package/dist/public/file-viewer-CcZYw-PS.js +0 -1
  253. package/dist/public/orchestrator-o9Fj67BN.js +0 -15
  254. package/dist/public/shadow-claw-dialog-ByyHgxM9.js +0 -1
  255. package/dist/public/shadow-claw-files-BpfpJIuP.js +0 -840
@@ -0,0 +1,158 @@
1
+ # Control Plane & Client Bridge
2
+
3
+ > Bidirectional communication gateway connecting CLI commands, external MCP clients, and connected browser/Electron tabs.
4
+
5
+ **Source:** `src/server/control-plane.ts` · `src/server/client-registry.ts` · `src/core/control-plane-client.ts` · `bin/commands/`
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ ShadowClaw provides a real-time **Control Plane** and client bridge that enables external tools and CLI commands to interact directly with connected browser or Electron clients. Connected clients act as persistent execution surfaces with access to local browser features, WebMCP tools, and IndexedDB/OPFS storage.
12
+
13
+ ---
14
+
15
+ ## Architecture & Transports
16
+
17
+ The Control Plane supports three interchangeable communication transports:
18
+
19
+ 1. **Server-Sent Events + HTTP POST (`sse`, Default & Recommended):**
20
+ - **Server $\rightarrow$ Client:** Persistent HTTP SSE stream (`GET /api/control/events?clientId=...`).
21
+ - **Client $\rightarrow$ Server:** Standard HTTP JSON requests (`POST /api/control/messages`).
22
+ - **Why Default:** Extremely resilient across corporate firewalls, reverse proxies (Nginx, Caddy, Traefik), and cloud edge/CDN setups that do not support or maintain WebSocket connection upgrades.
23
+ 2. **WebSocket (`websocket`, Opt-in):**
24
+ - Full-duplex WebSocket connection to `/ws/control?token=...`.
25
+ - Useful for low-latency direct connections or environments where WebSockets are preferred.
26
+ 3. **WebRTC DataChannel (`webrtc`, Peer-to-Peer):**
27
+ - Direct P2P browser-to-CLI communication via PeerJS DataChannels.
28
+ - CLI coordinates via `shadow-claw webrtc listen` and the local IPC socket bridge (`.cache/webrtc-ipc.sock`).
29
+ - Allows headless commands and automation without requiring an active control-plane HTTP/SSE session.
30
+
31
+ ```mermaid
32
+ graph TB
33
+ subgraph CLI ["CLI & External Tools"]
34
+ CLI_BIN["shadow-claw CLI (clients, send, backup, tasks)"]
35
+ CLI_LISTEN["webrtc listen (IPC Socket: .cache/webrtc-ipc.sock)"]
36
+ EXT_MCP["External MCP Clients (Hermes, Claude, Cursor)"]
37
+ end
38
+
39
+ subgraph Server ["Express Server & Control Plane"]
40
+ SSE_ENDPOINT["/api/control/events (SSE Stream)"]
41
+ MSG_ENDPOINT["/api/control/messages (HTTP POST)"]
42
+ WS_ENDPOINT["/ws/control (WebSocket Server)"]
43
+ PEERJS_SERVER["/peerjs/* (PeerJS Signaling Server)"]
44
+ REST_ROUTES["/api/control/* (REST API)"]
45
+ REGISTRY["Client Registry (SQLite: clients.db)"]
46
+ TOKEN_STORE["Control Token Auth"]
47
+ PUSH_WAKE["Web Push Remote Command Wakeup"]
48
+ end
49
+
50
+ subgraph Browser ["Connected Browser Client"]
51
+ CP_CLIENT["src/core/control-plane-client.ts<br>(SSE Default / WS Configurable)"]
52
+ PEERJS_CHANNEL["src/subsystems/channels/peerjs.ts<br>(WebRTC DataChannel)"]
53
+ ORCH["Orchestrator"]
54
+ WEBMCP["WebMCP (document.modelContext)"]
55
+ STORAGE["OPFS Storage"]
56
+ end
57
+
58
+ CLI_BIN <-->|REST & WebSocket| WS_ENDPOINT
59
+ CLI_BIN <-->|Local IPC Socket| CLI_LISTEN
60
+ CLI_LISTEN <-->|WebRTC DataChannel| PEERJS_CHANNEL
61
+ EXT_MCP <-->|Standard MCP| REST_ROUTES
62
+ CP_CLIENT -->|SSE Stream (Commands)| SSE_ENDPOINT
63
+ CP_CLIENT -->|HTTP POST (Results)| MSG_ENDPOINT
64
+ CP_CLIENT -.->|Optional WebSocket| WS_ENDPOINT
65
+ PEERJS_CHANNEL <-->|Signaling| PEERJS_SERVER
66
+ WS_ENDPOINT --> REGISTRY
67
+ SSE_ENDPOINT --> REGISTRY
68
+ REGISTRY --> TOKEN_STORE
69
+ WS_ENDPOINT --> PUSH_WAKE
70
+
71
+ CP_CLIENT --> ORCH
72
+ CP_CLIENT --> WEBMCP
73
+ CP_CLIENT --> STORAGE
74
+ PEERJS_CHANNEL --> ORCH
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Protocol Specification
80
+
81
+ All messages are JSON objects matching the `ControlMessage` envelope:
82
+
83
+ ```ts
84
+ interface ControlMessage {
85
+ id: string; // ULID for message/response correlation
86
+ type: string; // Message discriminator
87
+ replyTo?: string; // Request ID when responding
88
+ payload: unknown; // Type-specific payload
89
+ }
90
+ ```
91
+
92
+ ### Message Types
93
+
94
+ | Message Type | Direction | Description |
95
+ | :------------------ | :-------------- | :-------------------------------------------------------------------------------------------------------- |
96
+ | `client:register` | Client → Server | Client registration with ID, device label, version, transport, and capabilities |
97
+ | `server:registered` | Server → Client | Acknowledgement of successful registration |
98
+ | `client:heartbeat` | Client → Server | Periodic heartbeat to refresh active client timestamp |
99
+ | `command:execute` | Server → Client | Dispatches a command action (`send-message`, `list-tasks`, `invoke-tool`, `read-state`, `trigger-backup`) |
100
+ | `command:result` | Client → Server | Response containing execution status, result data, or error message |
101
+
102
+ ### Command Payloads
103
+
104
+ #### `command:execute` Payload
105
+
106
+ ```ts
107
+ interface CommandExecutePayload {
108
+ commandId: string;
109
+ action:
110
+ | "send-message"
111
+ | "list-tasks"
112
+ | "trigger-backup"
113
+ | "invoke-tool"
114
+ | "read-state";
115
+ args: Record<string, unknown>;
116
+ }
117
+ ```
118
+
119
+ #### `command:result` Payload
120
+
121
+ ```ts
122
+ interface CommandResultPayload {
123
+ commandId: string;
124
+ success: boolean;
125
+ data?: unknown;
126
+ error?: string;
127
+ }
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Authentication & Security
133
+
134
+ 1. **Control Token:** Connections must supply a secret control token via query parameter (`?token=...`), header (`x-control-token`), or Bearer authorization header (`Authorization: Bearer <token>`).
135
+ 2. **Token Generation:** On server startup, a token is read from `SHADOWCLAW_CONTROL_TOKEN` or generated and persisted in SQLite (`clients.db` metadata table) and `.cache/control-token.json`.
136
+ 3. **WebRTC CLI Peer Identity & Trust:** CLI clients connecting over WebRTC identify using a persistent peer ID in `.cache/cli-peer-id`. This ID can be retrieved or renewed using `npx shadow-claw peer-id [--renew|--set <id>]` and must be added in the browser under **Settings → WebRTC/PeerJS → Trusted Peer IDs** when peer restrictions are enabled.
137
+ 4. **Localhost Binding:** Binds to `127.0.0.1` by default.
138
+
139
+ ---
140
+
141
+ ## Browser Integration & Settings
142
+
143
+ Browser tabs connect automatically to the Control Plane on startup using `createDefaultControlPlaneClient` when served from a server environment.
144
+
145
+ ### Transport Configuration
146
+
147
+ Users can toggle between transports in **Settings $\rightarrow$ Integrations $\rightarrow$ Control Plane $\rightarrow$ Control Plane Transport**:
148
+
149
+ - `Server-Sent Events (SSE + HTTP POST)` — Default
150
+ - `WebSocket (Full Duplex)` — Opt-in
151
+
152
+ ### Registered Command Handlers
153
+
154
+ - `send-message`: Submits message/prompt into Orchestrator queue (supports optional `groupId` e.g. `br:main` or `peer:<peerId>`).
155
+ - `list-tasks`: Queries scheduled tasks from IndexedDB.
156
+ - `read-state`: Returns current active conversation group and state.
157
+ - `invoke-tool`: Invokes WebMCP (`document.modelContext`) tools directly.
158
+ - `trigger-backup`: Initiates workspace upload via `BackupController`.
@@ -3,7 +3,7 @@
3
3
  > Web Push notifications delivered via VAPID, a server-side SQLite task scheduler,
4
4
  > and a client-side cron evaluator — so tasks fire even when no browser tab is open.
5
5
 
6
- **Source:** `src/subsystems/notifications/` · `src/subsystems/tools/task-scheduler.ts` · `src/subsystems/tools/cron.ts` · `src/service-worker/push-handler.ts`
6
+ **Source:** `src/subsystems/notifications/` · `src/subsystems/notifications/push-routes.ts` · `src/subsystems/tools/task-scheduler.ts` · `src/subsystems/tools/cron.ts` · `src/service-worker/push-handler.ts`
7
7
 
8
8
  ## System Overview
9
9
 
@@ -14,6 +14,7 @@ graph TD
14
14
  SW["Service Worker<br>push-handler.ts"]
15
15
  CS["ClientTaskScheduler<br>task-scheduler.ts"]
16
16
  PC["push-client.ts<br>Subscribe / Unsubscribe"]
17
+ OW["Open Windows<br>(postMessage)"]
17
18
  end
18
19
 
19
20
  subgraph Server ["Express / Electron Server"]
@@ -22,6 +23,7 @@ graph TD
22
23
  TSR["task-schedule-routes.ts<br>/schedule/tasks/* endpoints"]
23
24
  TSS["task-schedule-store.ts<br>SQLite scheduled tasks"]
24
25
  Sched["ServerTaskScheduler<br>60s cron ticks"]
26
+ CP["Control Plane<br>/push/command"]
25
27
  end
26
28
 
27
29
  subgraph Agent ["Agent"]
@@ -35,9 +37,11 @@ graph TD
35
37
  TT --> TSR
36
38
  TSR --> TSS
37
39
  TSS --> Sched
38
- Sched -->|Web Push| SW
40
+ Sched -->|Web Push: scheduled-task| SW
39
41
  SW -->|relay to window| CS
40
42
  CS --> TT
43
+ CP -->|Web Push: remote-command| SW
44
+ SW -->|postMessage: remote-command-trigger| OW
41
45
  ```
42
46
 
43
47
  ## Push Notifications
@@ -75,6 +79,54 @@ On `push` event:
75
79
 
76
80
  The service worker uses a `BroadcastChannel("shadowclaw-push")` to relay push events to any open windows. The orchestrator listens on this channel and triggers the agent invocation for scheduled tasks.
77
81
 
82
+ ---
83
+
84
+ ## Remote Command Push Wakeup
85
+
86
+ **Files:** `src/service-worker/push-handler.ts` · `src/subsystems/notifications/push-routes.ts`
87
+
88
+ ### Purpose
89
+
90
+ The control plane may need to dispatch a command to a specific ShadowClaw client that is currently dormant — i.e. not connected to the WebSocket. Rather than dropping the command or waiting for the client to reconnect, the server sends a Web Push notification of type `remote-command` to wake the client up and relay the payload.
91
+
92
+ ### How it works
93
+
94
+ ```mermaid
95
+ sequenceDiagram
96
+ participant CP as Control Plane
97
+ participant PR as push-routes.ts<br>POST /push/command
98
+ participant PS as push-store.ts<br>getAllSubscriptions()
99
+ participant WP as web-push
100
+ participant SW as Service Worker<br>push-handler.ts
101
+ participant OW as Open Windows
102
+
103
+ CP->>PR: POST /push/command { clientId, action, args, prompt }
104
+ PR->>PS: getAllSubscriptions() [ORDER BY … id DESC]
105
+ PS-->>PR: subscriptions[]
106
+ loop each subscription
107
+ PR->>WP: sendNotification(subscription, { type: "remote-command", clientId, action, args, prompt })
108
+ end
109
+ WP-->>SW: push event
110
+ SW->>OW: client.postMessage({ type: "remote-command-trigger", clientId, action, args, prompt })
111
+ SW->>SW: showNotification("ShadowClaw — Remote Command")
112
+ ```
113
+
114
+ 1. **Server:** The control plane POSTs to `/push/command` with `{ clientId, action, args, prompt }`.
115
+ 2. **`push-routes.ts`:** Calls `getAllSubscriptions()` (results are stably ordered by `id DESC`) and fans out a Web Push to every subscribed endpoint with payload `{ type: "remote-command", clientId, action, args, prompt }`.
116
+ 3. **Service worker (`push-handler.ts`):** On receiving a push whose `data.type === 'remote-command'`, the handler:
117
+ - Iterates all open `WindowClient`s and calls `client.postMessage({ type: 'remote-command-trigger', clientId, action, args, prompt })`.
118
+ - Shows an OS notification titled **`ShadowClaw — Remote Command`** so the user is informed even if no window catches the message.
119
+
120
+ ### Client-side handling
121
+
122
+ Open windows listen for `message` events from the service worker. When a message of type `'remote-command-trigger'` arrives, the orchestrator can act on `action`, `args`, and `prompt` in the same way as a locally-initiated command. The `clientId` field lets the handler route the command only to the intended client and ignore it otherwise.
123
+
124
+ ### Push subscription ordering
125
+
126
+ `push-store.ts`'s `getAllSubscriptions()` query now applies a secondary `ORDER BY … id DESC` clause, ensuring that subscriptions are returned in a stable, deterministic order. This prevents non-deterministic fan-out behaviour when multiple subscriptions exist for the same browser profile.
127
+
128
+ ---
129
+
78
130
  ## Client-Side Task Scheduler
79
131
 
80
132
  **File:** `src/subsystems/tools/task-scheduler.ts`
@@ -216,6 +216,7 @@ _profiles (State) ──────────┘
216
216
  | Page Header | `<shadow-claw-page-header>` | Reusable mobile-first header |
217
217
  | Settings — Accounts | `<shadow-claw-accounts>` | General account connection and OAuth flows |
218
218
  | Settings — Channels | `<shadow-claw-channel-config>` | Third-party messaging channels configuration |
219
+ | Settings — Control Plane | `<shadow-claw-control-plane>` | CLI and remote control plane client configuration |
219
220
  | Settings — Git | `<shadow-claw-git>` | Git token and proxy settings |
220
221
  | Settings — Integrations | `<shadow-claw-integrations>` | Special API integrations and Web Search |
221
222
  | Settings — LLM | `<shadow-claw-llm>` | Provider and model settings |
@@ -0,0 +1 @@
1
+ import{xn as e}from"./orchestrator-3h9xxHUm.js";import{t}from"./getGroupDir-P1h9wl6S.js";async function n(n,r,i){let a=await t(n,r),{dirs:o,filename:s}=e(i),c=a;for(let e of o)c=await c.getDirectoryHandle(e);let l=await(await c.getFileHandle(s)).getFile(),u=URL.createObjectURL(l),d=document.createElement(`a`);d.href=u,d.download=s,document.body.appendChild(d),d.click(),document.body.removeChild(d),URL.revokeObjectURL(u)}export{n as t};
@@ -380,6 +380,7 @@ Settings panel controller.
380
380
  - `sidebarTasksHiddenToggle()` — Toggle visibility for Tasks navigation item in sidebar
381
381
  - `sidebarFilesHiddenToggle()` — Toggle visibility for Files navigation item in sidebar
382
382
  - `taskServerConfig()` — Task server configuration component (`<shadow-claw-task-server>`)
383
+ - `controlPlaneConfig()` — Control plane configuration component (`<shadow-claw-control-plane>`)
383
384
 
384
385
  ## Shared Utilities (`shared/index.ts`)
385
386
 
@@ -454,11 +455,14 @@ The build, CLI, and prerender test suites live under `bin/` and run through the
454
455
  project Jest configuration. The build integration test covers both the normal
455
456
  `pages/` layout and a project with no `pages/` directory (which publishes the
456
457
  built-in `index.html` and `MEMORY.md` defaults), as well as the unified `shadow-claw`
457
- CLI tool commands (`build`, `dev`, `run`, `serve`, `init`, `--version`).
458
+ CLI tool commands (`build`, `dev`, `run`, `serve`, `init`, `clients`, `send`, `backup`, `tasks`, `webrtc`, `peer-id`, `--version`).
458
459
 
459
460
  ```bash
460
461
  NODE_OPTIONS="--no-warnings --experimental-vm-modules" \
461
462
  npx jest --runInBand bin/cli.test.mjs \
463
+ bin/commands/peer-id.test.mjs \
464
+ bin/utils/control-client.test.mjs \
465
+ bin/utils/webrtc-control-client.test.mjs \
462
466
  bin/build/build.test.mjs \
463
467
  bin/prerender-dsd-shell/prerender-dsd-shell.test.mjs \
464
468
  bin/prerender-pretty-paths/prerender-pretty-paths.test.mjs
@@ -1 +1 @@
1
- import{W as e,it as t,tt as n,v as r}from"./orchestrator-DRfEudXk.js";function i(i,a){globalThis.__SHADOWCLAW_E2E__={isReady(){return i.ready},getDb(){return a.db},getActiveGroupId(){return i.activeGroupId},async createConversation(e){let t=a.db;if(!t)throw Error(`[E2E Bridge] DB not ready`);return i.createConversation(t,e)},async switchConversation(e){let t=a.db;if(!t)throw Error(`[E2E Bridge] DB not ready`);await i.switchConversation(t,e)},async loadTasks(){let e=a.db;if(!e)throw Error(`[E2E Bridge] DB not ready`);await i.loadTasks(e)},async configureProvider(o,s,c,l){let u=i.orchestrator,d=a.db;if(!u||!d)throw Error(`[E2E Bridge] Orchestrator or DB not ready`);await t(u,d,o,{loadApiKeyForProvider:u.loadApiKeyForProvider.bind(u),getApiKeyForHeaders:()=>e(u)}),await u.setApiKey(d,s),await n(u,d,c),await r(u,d,l)}}}function a(){return globalThis.__SHADOWCLAW_E2E_ENABLE__===!0}export{a as n,i as t};
1
+ import{W as e,it as t,tt as n,v as r}from"./orchestrator-3h9xxHUm.js";function i(i,a){globalThis.__SHADOWCLAW_E2E__={isReady(){return i.ready},getDb(){return a.db},getActiveGroupId(){return i.activeGroupId},async createConversation(e){let t=a.db;if(!t)throw Error(`[E2E Bridge] DB not ready`);return i.createConversation(t,e)},async switchConversation(e){let t=a.db;if(!t)throw Error(`[E2E Bridge] DB not ready`);await i.switchConversation(t,e)},async loadTasks(){let e=a.db;if(!e)throw Error(`[E2E Bridge] DB not ready`);await i.loadTasks(e)},async configureProvider(o,s,c,l){let u=i.orchestrator,d=a.db;if(!u||!d)throw Error(`[E2E Bridge] Orchestrator or DB not ready`);await t(u,d,o,{loadApiKeyForProvider:u.loadApiKeyForProvider.bind(u),getApiKeyForHeaders:()=>e(u)}),await u.setApiKey(d,s),await n(u,d,c),await r(u,d,l)}}}function a(){return globalThis.__SHADOWCLAW_E2E_ENABLE__===!0}export{a as n,i as t};
@@ -0,0 +1 @@
1
+ import{o as e}from"./config-CrHchneq.js";import{n as t}from"./toast-60iDlgiH.js";import{_n as n,bn as r,xn as i}from"./orchestrator-3h9xxHUm.js";import{t as a}from"./getGroupDir-P1h9wl6S.js";async function o(e,t,n){let r=await a(e,t),{dirs:o,filename:s}=i(n),c=r;for(let e of o)c=await c.getDirectoryHandle(e);return await(await c.getFileHandle(s)).getFile()}const s=new class{_file;constructor(){this._file=new t.State(null)}closeFile(){this._file.set(null)}get file(){return this._file.get()}getFile(){return this.file}getPreviewBinaryMimeType(e){return{"3gp":`video/3gpp`,"7z":`application/x-7z-compressed`,aac:`audio/aac`,apng:`image/apng`,avi:`video/x-msvideo`,avif:`image/avif`,bin:`application/octet-stream`,bmp:`image/bmp`,db:`application/vnd.sqlite3`,dll:`application/octet-stream`,dmg:`application/octet-stream`,dylib:`application/octet-stream`,exe:`application/vnd.microsoft.portable-executable`,flac:`audio/flac`,flv:`video/x-flv`,gif:`image/gif`,gz:`application/gzip`,heic:`image/heic`,heif:`image/heif`,ico:`image/x-icon`,iso:`application/octet-stream`,jpeg:`image/jpeg`,jpg:`image/jpeg`,m4a:`audio/mp4`,m4v:`video/mp4`,mkv:`video/x-matroska`,mov:`video/mp4`,mp3:`audio/mpeg`,mp4:`video/mp4`,oga:`audio/ogg`,ogg:`audio/ogg`,ogv:`video/ogg`,png:`image/png`,rar:`application/vnd.rar`,so:`application/octet-stream`,sqlite:`application/vnd.sqlite3`,sqlite3:`application/vnd.sqlite3`,tar:`application/x-tar`,tif:`image/tiff`,tiff:`image/tiff`,wasm:`application/wasm`,wav:`audio/wav`,weba:`audio/webm`,webm:`video/webm`,webp:`image/webp`,wmv:`video/x-ms-wmv`,zip:`application/zip`}[e.toLowerCase().split(`.`).pop()||``]||``}async openFile(t,i,a=e){try{let e=i.split(`/`).pop()||i,s=/\.pdf$/i.test(e),c=this.getPreviewBinaryMimeType(e);if(s){let n=await r(t,a,i);this._file.set({name:e,path:i,groupId:a,content:``,kind:`pdf`,binaryContent:n,mimeType:`application/pdf`});return}if(c){let n=await o(t,a,i);this._file.set({name:e,path:i,groupId:a,content:``,kind:`binary`,binaryContent:null,nativeFile:n,mimeType:c});return}let l=await n(t,a,i);this._file.set({name:e,path:i,groupId:a,content:l,kind:`text`,binaryContent:null,mimeType:`text/plain`})}catch(e){throw console.error(`Failed to open file:`,i,e),e}}};export{s as t};
@@ -0,0 +1 @@
1
+ import{t as e}from"./txPromise-EBECky1b.js";function t(t){return e(t,`tasks`,`readonly`,e=>e.getAll()).then(e=>e.map(e=>({...e,enabled:!!e.enabled})))}export{t};
@@ -0,0 +1 @@
1
+ import{n as e}from"./rolldown-runtime-aKtaBQYM.js";import{r as t,x as n}from"./config-CrHchneq.js";import{i as r}from"./app-routes-Bs-riIvr.js";import{t as i}from"./txPromise-EBECky1b.js";import{t as a}from"./getConfig-D89uJgo5.js";import{t as o}from"./memoryStorage-C0KvLNUp.js";async function s(e,...t){let n=e;for(let e of t){let t=await n.getDirectoryHandle(e,{create:!0});if(typeof n.isSameEntry==`function`&&await n.isSameEntry(t))throw Error(`Directory navigation failed: "${e}" resolved to the same directory as its parent. The stored directory handle may be stale — try re-selecting the storage folder.`);n=t}return n}function c(e,t){return i(e,`config`,`readwrite`,e=>e.delete(t)).then(()=>void 0)}let l=null;async function u(e,t){try{return await e.entries().next(),`granted`}catch{return t}}function d(){let e=r();return e?`${n}-${e}`:n}async function f(){try{if(typeof navigator<`u`&&navigator.storage&&typeof navigator.storage.getDirectory==`function`){let e=await navigator.storage.getDirectory(),t=d();return await e.getDirectoryHandle(t,{create:!0})}}catch(e){console.warn(`OPFS is unavailable (e.g. Firefox Private Browsing). Using in-memory storage fallback:`,e)}return o()}function p(e){if(!e||typeof e!=`object`)return!1;let t=globalThis.FileSystemDirectoryHandle;return t!==void 0&&e instanceof t||typeof e.getDirectoryHandle==`function`&&typeof e.getFileHandle==`function`}async function m(e){if(l)return l;try{let n=await a(e,t.STORAGE_HANDLE);if(p(n)){let e=n;if(typeof e.queryPermission==`function`&&await e.queryPermission({mode:`readwrite`})!==`granted`&&typeof e.requestPermission==`function`)try{await e.requestPermission({mode:`readwrite`})}catch{}return e}}catch(e){console.warn(`Failed to retrieve local storage handle from DB:`,e)}return f()}function h(e){l=e}function g(){l=null}function _(e){return e instanceof DOMException&&e.name===`InvalidStateError`}async function v(e){await c(e,t.STORAGE_HANDLE),l=null}async function y(e){if(l)try{let e=await l.queryPermission({mode:`readwrite`});return{type:`local`,permission:e===`granted`?`granted`:await u(l,e),name:l.name}}catch{console.warn(`Getting storage status failed for explicit storage root.`)}try{let n=await a(e,t.STORAGE_HANDLE);if(p(n)){let e=n,t=await e.queryPermission({mode:`readwrite`});return{type:`local`,permission:t===`granted`?`granted`:await u(e,t),name:e.name}}}catch(e){console.warn(`Failed to check storage status:`,e)}return{type:`opfs`,permission:`granted`,name:`OPFS`}}var b=e({getGroupDir:()=>x});async function x(e,t){return s(await m(e),`groups`,t.replace(/:/g,`-`))}export{y as a,v as c,m as i,h as l,b as n,g as o,d as r,_ as s,x as t};