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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shadow-claw",
3
- "version": "1.23.4",
3
+ "version": "1.24.0",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0",
6
6
  "description": "Browser-native personal AI assistant.",
@@ -92,6 +92,7 @@
92
92
  "imapflow": "^1.3.3",
93
93
  "isomorphic-git": "^1.37.6",
94
94
  "marked": "^18.0.3",
95
+ "node-datachannel": "^0.33.2",
95
96
  "nodemailer": "^8.0.7",
96
97
  "peer": "^1.0.2",
97
98
  "peerjs": "github:kherrick/peerjs#v1.5.4-custom-events.0",
@@ -1,11 +1,21 @@
1
- module.exports = class JSZip {
1
+ class JSZip {
2
2
  constructor() {}
3
3
 
4
+ static async loadAsync(_file) {
5
+ return new JSZip();
6
+ }
7
+
8
+ file(_name) {
9
+ return {
10
+ async: async (_type) =>
11
+ JSON.stringify({ messages: [{ id: "1", content: "hello" }] }),
12
+ };
13
+ }
14
+
4
15
  async generateAsync() {
5
16
  return new Blob([]);
6
17
  }
18
+ }
7
19
 
8
- file() {
9
- return this;
10
- }
11
- };
20
+ module.exports = JSZip;
21
+ module.exports.default = JSZip;
@@ -388,4 +388,39 @@ describe("shadow-claw-accounts", () => {
388
388
 
389
389
  document.body.removeChild(el);
390
390
  });
391
+
392
+ it("handles setDefaultAccount and deleteAccount", async () => {
393
+ const el = new ShadowClawAccounts();
394
+ document.body.appendChild(el);
395
+ await new Promise((resolve) => setTimeout(resolve, 50));
396
+ await el.render();
397
+ el.db = {} as any;
398
+
399
+ el.accounts = [
400
+ {
401
+ id: "acct-1",
402
+ label: "Account 1",
403
+ service: "Figma",
404
+ hostPattern: "api.figma.com",
405
+ authMode: "token",
406
+ } as any,
407
+ {
408
+ id: "acct-2",
409
+ label: "Account 2",
410
+ service: "Linear",
411
+ hostPattern: "api.linear.app",
412
+ authMode: "token",
413
+ } as any,
414
+ ];
415
+ el.defaultAccountId = "acct-1";
416
+
417
+ await el.setDefaultAccount("acct-2");
418
+ expect(el.defaultAccountId).toBe("acct-2");
419
+
420
+ await el.deleteAccount("acct-1");
421
+ expect(el.accounts.find((a) => a.id === "acct-1")).toBeUndefined();
422
+ expect(el.defaultAccountId).toBe("acct-2");
423
+
424
+ document.body.removeChild(el);
425
+ });
391
426
  });
@@ -0,0 +1,117 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ font-family: var(--shadow-claw-font-sans);
5
+ }
6
+
7
+ :host {
8
+ display: block;
9
+ }
10
+
11
+ .settings-section {
12
+ margin-bottom: 1.75rem;
13
+ }
14
+
15
+ .settings-section h3 {
16
+ align-items: center;
17
+ display: flex;
18
+ font-size: 1rem;
19
+ font-weight: 600;
20
+ gap: 0.5rem;
21
+ margin: 0 0 0.75rem;
22
+ }
23
+
24
+ .form-group {
25
+ margin-bottom: 1rem;
26
+ }
27
+
28
+ .form-label {
29
+ color: var(--shadow-claw-text-primary);
30
+ display: block;
31
+ font-size: 0.8125rem;
32
+ font-weight: 500;
33
+ margin-bottom: 0.375rem;
34
+ }
35
+
36
+ .form-input,
37
+ .form-select {
38
+ background-color: var(--shadow-claw-bg-primary);
39
+ border: 0.0625rem solid var(--shadow-claw-border-color);
40
+ border-radius: var(--shadow-claw-radius-s, 0.625rem);
41
+ box-sizing: border-box;
42
+ color: var(--shadow-claw-text-primary);
43
+ font-family: var(--shadow-claw-font-sans);
44
+ font-size: var(--shadow-claw-font-size-sm);
45
+ padding: 0.625rem 0.75rem;
46
+ transition:
47
+ border-color 0.15s,
48
+ box-shadow 0.15s;
49
+ width: 100%;
50
+ }
51
+
52
+ .form-input:focus,
53
+ .form-select:focus {
54
+ border-color: var(--shadow-claw-accent-primary);
55
+ box-shadow: 0 0 0 0.125rem rgba(0, 0, 0, 0.06);
56
+ outline: none;
57
+ }
58
+
59
+ .form-toggle {
60
+ align-items: center;
61
+ display: flex;
62
+ gap: 0.75rem;
63
+ }
64
+
65
+ .form-toggle input[type="checkbox"] {
66
+ accent-color: var(--shadow-claw-accent-primary);
67
+ cursor: pointer;
68
+ height: 1.125rem;
69
+ width: 1.125rem;
70
+ }
71
+
72
+ .form-toggle .form-label {
73
+ cursor: pointer;
74
+ display: inline;
75
+ margin-bottom: 0;
76
+ }
77
+
78
+ .form-helper {
79
+ color: var(--shadow-claw-text-tertiary);
80
+ font-size: 0.75rem;
81
+ line-height: 1.45;
82
+ margin-top: 0.375rem;
83
+ max-width: 48rem;
84
+ }
85
+
86
+ .form-helper code {
87
+ background-color: var(--shadow-claw-bg-secondary);
88
+ border: 0.0625rem solid var(--shadow-claw-border-color);
89
+ border-radius: 0.375rem;
90
+ color: var(--shadow-claw-text-primary);
91
+ font-family: var(--shadow-claw-font-mono, monospace);
92
+ font-size: 0.72rem;
93
+ padding: 0.08rem 0.3rem;
94
+ }
95
+
96
+ .save-btn {
97
+ background-color: var(--shadow-claw-text-primary);
98
+ border: none;
99
+ border-radius: 62.5rem;
100
+ color: var(--shadow-claw-bg-primary);
101
+ cursor: pointer;
102
+ font-size: var(--shadow-claw-font-size-sm);
103
+ font-weight: 600;
104
+ margin-top: 0.75rem;
105
+ padding: 0.625rem 1.5rem;
106
+ transition: background-color 150ms cubic-bezier(0.33, 1, 0.68, 1);
107
+ }
108
+
109
+ .save-btn:hover {
110
+ background-color: var(--shadow-claw-accent-primary);
111
+ color: var(--shadow-claw-on-primary);
112
+ }
113
+
114
+ .save-btn:focus-visible {
115
+ outline: 0.125rem solid var(--shadow-claw-accent-primary);
116
+ outline-offset: 0.125rem;
117
+ }
@@ -0,0 +1,81 @@
1
+ <template>
2
+ <div class="settings-section">
3
+ <h3>🎛️ Control Plane</h3>
4
+
5
+ <div class="form-group">
6
+ <div class="form-helper">
7
+ Configure the remote bridge allowing external tools, CLI commands
8
+ (<code>shadow-claw</code>), and MCP clients to discover, query, and
9
+ communicate with this client instance.
10
+ </div>
11
+ </div>
12
+
13
+ <div class="form-group">
14
+ <div class="form-toggle">
15
+ <input
16
+ data-setting="control-plane-enabled-toggle"
17
+ id="control-plane-enabled-toggle"
18
+ type="checkbox"
19
+ />
20
+ <label class="form-label" for="control-plane-enabled-toggle">
21
+ Enable Remote Control Plane Client
22
+ </label>
23
+ </div>
24
+ <div class="form-helper">
25
+ Allow the <code>shadow-claw</code> CLI and remote server commands to
26
+ discover and communicate with this client. When disabled, this client
27
+ operates in standalone/serverless mode without opening a control plane
28
+ connection.
29
+ </div>
30
+ </div>
31
+
32
+ <div class="form-group">
33
+ <label class="form-label" for="control-plane-url">
34
+ Control Plane Server URL
35
+ </label>
36
+ <input
37
+ class="form-input"
38
+ data-setting="control-plane-url-input"
39
+ id="control-plane-url"
40
+ placeholder="http://127.0.0.1:8888"
41
+ spellcheck="false"
42
+ type="url"
43
+ />
44
+ <div class="form-helper">
45
+ The base URL of the ShadowClaw server or local dev instance for the CLI
46
+ and remote control plane (e.g. <code>http://127.0.0.1:8888</code> or
47
+ <code>https://my-server.example.com</code>). Defaults to
48
+ <code>127.0.0.1:8888</code> when hosted on static sites (like GitHub
49
+ Pages), or same-origin on server deployments.
50
+ </div>
51
+ <button class="save-btn" data-action="save-control-plane-url">
52
+ 💾 Save Control Plane URL
53
+ </button>
54
+ </div>
55
+
56
+ <div class="form-group">
57
+ <label class="form-label" for="control-plane-transport">
58
+ Control Plane Transport
59
+ </label>
60
+ <select
61
+ class="form-select"
62
+ id="control-plane-transport"
63
+ data-setting="control-plane-transport-select"
64
+ >
65
+ <option value="sse">
66
+ Server-Sent Events (SSE + HTTP POST) — Recommended / Proxy Friendly
67
+ (Default)
68
+ </option>
69
+ <option value="websocket">WebSocket (Full Duplex)</option>
70
+ </select>
71
+ <div class="form-helper">
72
+ Select the protocol used for the CLI and remote client control plane.
73
+ SSE + HTTP POST works through standard reverse proxies and firewalls
74
+ without requiring connection upgrades.
75
+ </div>
76
+ <button class="save-btn" data-action="save-control-plane-transport">
77
+ 💾 Save Transport
78
+ </button>
79
+ </div>
80
+ </div>
81
+ </template>
@@ -0,0 +1,127 @@
1
+ import { jest } from "@jest/globals";
2
+
3
+ jest.unstable_mockModule("../../../core/effect.js", () => ({
4
+ effect: jest.fn((cb: any) => {
5
+ cb();
6
+ return () => {};
7
+ }),
8
+ }));
9
+
10
+ jest.unstable_mockModule("../../../db/db.js", () => ({
11
+ getDb: jest.fn(() => Promise.resolve({})),
12
+ }));
13
+
14
+ const mockSetConfig = jest.fn(() => Promise.resolve());
15
+ const mockGetConfig = jest.fn(() => Promise.resolve("sse"));
16
+ jest.unstable_mockModule("../../../db/setConfig.js", () => ({
17
+ setConfig: mockSetConfig,
18
+ }));
19
+ jest.unstable_mockModule("../../../db/getConfig.js", () => ({
20
+ getConfig: mockGetConfig,
21
+ }));
22
+
23
+ jest.unstable_mockModule("../../../stores/orchestrator.js", () => ({
24
+ orchestratorStore: {
25
+ orchestrator: {},
26
+ ready: true,
27
+ },
28
+ }));
29
+
30
+ jest.unstable_mockModule("../../../ui/toast.js", () => ({
31
+ showError: jest.fn(),
32
+ showSuccess: jest.fn(),
33
+ }));
34
+
35
+ const { ShadowClawControlPlane } =
36
+ await import("./shadow-claw-control-plane.js");
37
+ const { showSuccess } = (await import("../../../ui/toast.js")) as any;
38
+
39
+ describe("shadow-claw-control-plane", () => {
40
+ beforeEach(() => {
41
+ jest.clearAllMocks();
42
+ document.body.innerHTML = "";
43
+ });
44
+
45
+ it("should render correctly", async () => {
46
+ const el = new ShadowClawControlPlane();
47
+ document.body.appendChild(el);
48
+ await el.connectedCallback();
49
+
50
+ expect(el.shadowRoot).toBeTruthy();
51
+ const toggle = el.shadowRoot?.querySelector(
52
+ '[data-setting="control-plane-enabled-toggle"]',
53
+ ) as HTMLInputElement;
54
+ expect(toggle).toBeTruthy();
55
+ });
56
+
57
+ it("should save control plane transport", async () => {
58
+ const el = new ShadowClawControlPlane();
59
+ document.body.appendChild(el);
60
+ await el.connectedCallback();
61
+
62
+ const select = el.shadowRoot?.querySelector(
63
+ '[data-setting="control-plane-transport-select"]',
64
+ ) as HTMLSelectElement;
65
+ if (select) select.value = "websocket";
66
+
67
+ await el.saveControlPlaneTransport();
68
+
69
+ expect(showSuccess).toHaveBeenCalledWith(
70
+ expect.stringContaining("WEBSOCKET"),
71
+ expect.any(Number),
72
+ );
73
+ });
74
+
75
+ it("should save control plane enabled state", async () => {
76
+ const el = new ShadowClawControlPlane();
77
+ document.body.appendChild(el);
78
+ await el.connectedCallback();
79
+
80
+ await el.saveControlPlaneEnabled(true);
81
+
82
+ expect(mockSetConfig).toHaveBeenCalledWith(
83
+ expect.anything(),
84
+ "control_plane_enabled",
85
+ "true",
86
+ );
87
+ expect(showSuccess).toHaveBeenCalledWith(
88
+ expect.stringContaining("enabled"),
89
+ expect.any(Number),
90
+ );
91
+
92
+ await el.saveControlPlaneEnabled(false);
93
+ expect(mockSetConfig).toHaveBeenCalledWith(
94
+ expect.anything(),
95
+ "control_plane_enabled",
96
+ "false",
97
+ );
98
+ expect(showSuccess).toHaveBeenCalledWith(
99
+ expect.stringContaining("disabled"),
100
+ expect.any(Number),
101
+ );
102
+ });
103
+
104
+ it("should save control plane url", async () => {
105
+ const el = new ShadowClawControlPlane();
106
+ document.body.appendChild(el);
107
+ await el.connectedCallback();
108
+
109
+ const input = el.shadowRoot?.querySelector(
110
+ '[data-setting="control-plane-url-input"]',
111
+ ) as HTMLInputElement;
112
+ expect(input).toBeTruthy();
113
+ input.value = "https://custom-control.example.com";
114
+
115
+ await el.saveControlPlaneUrl();
116
+
117
+ expect(mockSetConfig).toHaveBeenCalledWith(
118
+ expect.anything(),
119
+ "control_plane_url",
120
+ "https://custom-control.example.com",
121
+ );
122
+ expect(showSuccess).toHaveBeenCalledWith(
123
+ expect.stringContaining("https://custom-control.example.com"),
124
+ expect.any(Number),
125
+ );
126
+ });
127
+ });
@@ -0,0 +1,292 @@
1
+ import { effect } from "../../../core/effect.js";
2
+ import { getDb } from "../../../db/db.js";
3
+ import { getConfig } from "../../../db/getConfig.js";
4
+ import { setConfig } from "../../../db/setConfig.js";
5
+ import {
6
+ CONFIG_KEYS,
7
+ DEFAULT_CONTROL_PLANE_ENABLED,
8
+ DEFAULT_CONTROL_PLANE_TRANSPORT,
9
+ } from "../../../config/config.js";
10
+ import { orchestratorStore } from "../../../stores/orchestrator.js";
11
+ import { showError, showSuccess } from "../../../ui/toast.js";
12
+
13
+ import type { Orchestrator } from "../../../core/orchestrator/orchestrator.js";
14
+ import type { ShadowClawDatabase } from "../../../db/types.js";
15
+
16
+ import ShadowClawElement from "../../shadow-claw-element.js";
17
+ import shadowClawControlPlaneStyles from "./shadow-claw-control-plane.css" with { type: "css" };
18
+ import shadowClawControlPlaneTemplate from "./shadow-claw-control-plane.html" with { type: "html" };
19
+
20
+ const elementName = "shadow-claw-control-plane";
21
+
22
+ /**
23
+ * Settings sub-component for Control Plane Client URL, enabled toggle, and transport configuration.
24
+ */
25
+ export class ShadowClawControlPlane extends ShadowClawElement {
26
+ static styles = shadowClawControlPlaneStyles;
27
+ static template = shadowClawControlPlaneTemplate;
28
+
29
+ db: ShadowClawDatabase | null;
30
+ orchestrator: Orchestrator | null;
31
+
32
+ constructor() {
33
+ super();
34
+
35
+ this.db = null;
36
+ this.orchestrator = null;
37
+ }
38
+
39
+ async connectedCallback() {
40
+ const root = this.shadowRoot;
41
+ if (!root) {
42
+ throw new Error("shadowRoot not found");
43
+ }
44
+
45
+ this.db = await getDb();
46
+ this.orchestrator = orchestratorStore.orchestrator;
47
+
48
+ this.bindEventListeners();
49
+ this.setupEffects();
50
+ await this.render();
51
+ }
52
+
53
+ bindEventListeners() {
54
+ const root = this.shadowRoot;
55
+ if (!root) {
56
+ return;
57
+ }
58
+
59
+ root
60
+ .querySelector('[data-action="save-control-plane-transport"]')
61
+ ?.addEventListener("click", () => {
62
+ void this.saveControlPlaneTransport();
63
+ });
64
+
65
+ root
66
+ .querySelector('[data-action="save-control-plane-url"]')
67
+ ?.addEventListener("click", () => {
68
+ void this.saveControlPlaneUrl();
69
+ });
70
+
71
+ root
72
+ .querySelector('[data-setting="control-plane-enabled-toggle"]')
73
+ ?.addEventListener("change", (e) => {
74
+ const target = e.target as HTMLInputElement;
75
+ void this.saveControlPlaneEnabled(target.checked);
76
+ });
77
+ }
78
+
79
+ setupEffects() {
80
+ effect(() => {
81
+ if (orchestratorStore.ready) {
82
+ this.orchestrator = orchestratorStore.orchestrator;
83
+ this.render();
84
+ }
85
+ });
86
+ }
87
+
88
+ async render() {
89
+ const root = this.shadowRoot;
90
+ if (!root) {
91
+ return;
92
+ }
93
+
94
+ const cpEnabledToggle = root.querySelector(
95
+ '[data-setting="control-plane-enabled-toggle"]',
96
+ ) as HTMLInputElement | null;
97
+ if (cpEnabledToggle && this.db) {
98
+ try {
99
+ const { isControlPlaneEnabled } =
100
+ await import("../../../core/utils/initControlPlane.js");
101
+ cpEnabledToggle.checked = await isControlPlaneEnabled(this.db);
102
+ } catch (_) {
103
+ cpEnabledToggle.checked = DEFAULT_CONTROL_PLANE_ENABLED;
104
+ }
105
+ }
106
+
107
+ const cpUrlInput = root.querySelector(
108
+ '[data-setting="control-plane-url-input"]',
109
+ ) as HTMLInputElement | null;
110
+ if (cpUrlInput && this.db) {
111
+ try {
112
+ const stored = await getConfig(this.db, CONFIG_KEYS.CONTROL_PLANE_URL);
113
+ cpUrlInput.value = stored || "";
114
+ } catch (_) {
115
+ cpUrlInput.value = "";
116
+ }
117
+ }
118
+
119
+ const transportSelect = root.querySelector(
120
+ '[data-setting="control-plane-transport-select"]',
121
+ ) as HTMLSelectElement | null;
122
+ if (transportSelect && this.db) {
123
+ try {
124
+ const stored = await getConfig(
125
+ this.db,
126
+ CONFIG_KEYS.CONTROL_PLANE_TRANSPORT,
127
+ );
128
+ transportSelect.value = stored || DEFAULT_CONTROL_PLANE_TRANSPORT;
129
+ } catch (_) {
130
+ transportSelect.value = DEFAULT_CONTROL_PLANE_TRANSPORT;
131
+ }
132
+ }
133
+ }
134
+
135
+ async saveControlPlaneTransport() {
136
+ if (!this.db) {
137
+ return;
138
+ }
139
+
140
+ const root = this.shadowRoot;
141
+ if (!root) {
142
+ return;
143
+ }
144
+
145
+ const select = root.querySelector(
146
+ '[data-setting="control-plane-transport-select"]',
147
+ ) as HTMLSelectElement | null;
148
+ if (!select) {
149
+ return;
150
+ }
151
+
152
+ const transport = select.value === "websocket" ? "websocket" : "sse";
153
+
154
+ try {
155
+ await setConfig(this.db, CONFIG_KEYS.CONTROL_PLANE_TRANSPORT, transport);
156
+ if (typeof localStorage !== "undefined") {
157
+ try {
158
+ localStorage.setItem(CONFIG_KEYS.CONTROL_PLANE_TRANSPORT, transport);
159
+ } catch (_) {}
160
+ }
161
+
162
+ const {
163
+ isControlPlaneEnabled,
164
+ createDefaultControlPlaneClient,
165
+ shouldConnectControlPlane,
166
+ } = await import("../../../core/utils/initControlPlane.js");
167
+
168
+ if (
169
+ (await isControlPlaneEnabled(this.db)) &&
170
+ shouldConnectControlPlane()
171
+ ) {
172
+ createDefaultControlPlaneClient({
173
+ orchestrator: this.orchestrator || undefined,
174
+ transport,
175
+ });
176
+ }
177
+
178
+ showSuccess(
179
+ `Control Plane transport saved: ${transport.toUpperCase()}`,
180
+ 3000,
181
+ );
182
+ } catch (err) {
183
+ const errorMsg = err instanceof Error ? err.message : String(err);
184
+ showError("Error saving Control Plane transport: " + errorMsg, 6000);
185
+ }
186
+ }
187
+
188
+ async saveControlPlaneUrl() {
189
+ if (!this.db) {
190
+ return;
191
+ }
192
+
193
+ const root = this.shadowRoot;
194
+ if (!root) {
195
+ return;
196
+ }
197
+
198
+ const input = root.querySelector(
199
+ '[data-setting="control-plane-url-input"]',
200
+ ) as HTMLInputElement | null;
201
+ if (!input) {
202
+ return;
203
+ }
204
+
205
+ const url = input.value.trim();
206
+
207
+ try {
208
+ await setConfig(this.db, CONFIG_KEYS.CONTROL_PLANE_URL, url);
209
+ if (typeof localStorage !== "undefined") {
210
+ try {
211
+ if (url) {
212
+ localStorage.setItem(CONFIG_KEYS.CONTROL_PLANE_URL, url);
213
+ } else {
214
+ localStorage.removeItem(CONFIG_KEYS.CONTROL_PLANE_URL);
215
+ }
216
+ } catch (_) {}
217
+ }
218
+
219
+ const {
220
+ isControlPlaneEnabled,
221
+ createDefaultControlPlaneClient,
222
+ shouldConnectControlPlane,
223
+ } = await import("../../../core/utils/initControlPlane.js");
224
+
225
+ if (
226
+ (await isControlPlaneEnabled(this.db)) &&
227
+ shouldConnectControlPlane()
228
+ ) {
229
+ createDefaultControlPlaneClient({
230
+ orchestrator: this.orchestrator || undefined,
231
+ });
232
+ }
233
+
234
+ showSuccess(
235
+ url
236
+ ? `Control Plane URL saved: ${url}`
237
+ : "Control Plane URL reset to default",
238
+ 3000,
239
+ );
240
+ } catch (err) {
241
+ const errorMsg = err instanceof Error ? err.message : String(err);
242
+ showError("Error saving Control Plane URL: " + errorMsg, 6000);
243
+ }
244
+ }
245
+
246
+ async saveControlPlaneEnabled(enabled: boolean) {
247
+ if (!this.db) {
248
+ return;
249
+ }
250
+
251
+ try {
252
+ await setConfig(
253
+ this.db,
254
+ CONFIG_KEYS.CONTROL_PLANE_ENABLED,
255
+ enabled ? "true" : "false",
256
+ );
257
+ if (typeof localStorage !== "undefined") {
258
+ try {
259
+ localStorage.setItem(
260
+ CONFIG_KEYS.CONTROL_PLANE_ENABLED,
261
+ enabled ? "true" : "false",
262
+ );
263
+ } catch (_) {}
264
+ }
265
+
266
+ const {
267
+ createDefaultControlPlaneClient,
268
+ stopControlPlaneClient,
269
+ shouldConnectControlPlane,
270
+ } = await import("../../../core/utils/initControlPlane.js");
271
+
272
+ if (enabled) {
273
+ if (shouldConnectControlPlane()) {
274
+ createDefaultControlPlaneClient({
275
+ orchestrator: this.orchestrator || undefined,
276
+ });
277
+ }
278
+ showSuccess("Control plane client enabled and connected", 3000);
279
+ } else {
280
+ stopControlPlaneClient();
281
+ showSuccess("Control plane client disabled", 3000);
282
+ }
283
+ } catch (err) {
284
+ const errorMsg = err instanceof Error ? err.message : String(err);
285
+ showError("Error saving Control Plane setting: " + errorMsg, 6000);
286
+ }
287
+ }
288
+ }
289
+
290
+ if (!customElements.get(elementName)) {
291
+ customElements.define(elementName, ShadowClawControlPlane);
292
+ }