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,699 @@
1
+ /**
2
+ * ShadowClaw CLI — WebRTC DataChannel Control Client
3
+ *
4
+ * Provides a stateless WebRTC transport client for CLI commands to execute
5
+ * commands directly against browser tabs or server peers over RTCDataChannels.
6
+ */
7
+
8
+ import http from "node:http";
9
+ import path from "node:path";
10
+ import fs from "node:fs";
11
+ import { ulid } from "./ulid.mjs";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // IPC socket helpers — allow `send` to talk to a running `webrtc listen`
15
+ // process via a Unix domain socket (.cache/webrtc-ipc.sock).
16
+ // No port selection, no TCP, no collision.
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export function getIpcSocketPath(cacheDir) {
20
+ const dir = cacheDir || path.join(process.cwd(), ".cache");
21
+ return path.join(dir, "webrtc-ipc.sock");
22
+ }
23
+
24
+ // Keep getIpcFilePath as an alias pointing at the socket for test compatibility
25
+ export function getIpcFilePath(cacheDir) {
26
+ return getIpcSocketPath(cacheDir);
27
+ }
28
+
29
+ export function writeIpcFile(socketPath, cacheDir) {
30
+ // Nothing to write — the socket file IS the IPC endpoint.
31
+ // This function is kept for API compatibility; callers pass the socket path.
32
+ }
33
+
34
+ export function clearIpcFile(cacheDir) {
35
+ try {
36
+ fs.unlinkSync(getIpcSocketPath(cacheDir));
37
+ } catch (_) {}
38
+ }
39
+
40
+ /**
41
+ * Returns the socket path if a live listener is reachable, else null.
42
+ */
43
+ export async function resolveListenerIpc(cacheDir) {
44
+ const socketPath = getIpcSocketPath(cacheDir);
45
+ try {
46
+ if (!fs.existsSync(socketPath)) return null;
47
+
48
+ // Verify the socket actually responds
49
+ await new Promise((resolve, reject) => {
50
+ const req = http.request(
51
+ { socketPath, path: "/ping", method: "GET" },
52
+ (res) => {
53
+ res.resume();
54
+ res.statusCode === 200
55
+ ? resolve()
56
+ : reject(new Error(`status ${res.statusCode}`));
57
+ },
58
+ );
59
+ req.setTimeout(1000, () => {
60
+ req.destroy();
61
+ reject(new Error("timeout"));
62
+ });
63
+ req.on("error", reject);
64
+ req.end();
65
+ });
66
+
67
+ return { socketPath };
68
+ } catch (_) {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Send a command to a running listener via its Unix socket.
75
+ */
76
+ export function sendCommandViaIpc(ipc, targetPeerId, action, args, timeoutMs) {
77
+ const socketPath = ipc.socketPath || ipc.port; // backwards compat
78
+ return new Promise((resolve, reject) => {
79
+ const body = JSON.stringify({ targetPeerId, action, args, timeoutMs });
80
+ const req = http.request(
81
+ {
82
+ socketPath,
83
+ path: "/command",
84
+ method: "POST",
85
+ headers: {
86
+ "Content-Type": "application/json",
87
+ "Content-Length": Buffer.byteLength(body),
88
+ },
89
+ },
90
+ (res) => {
91
+ let data = "";
92
+ res.on("data", (c) => (data += c));
93
+ res.on("end", () => {
94
+ try {
95
+ const parsed = JSON.parse(data);
96
+ if (res.statusCode >= 200 && res.statusCode < 300) {
97
+ resolve(parsed);
98
+ } else {
99
+ reject(new Error(parsed?.error || `IPC error ${res.statusCode}`));
100
+ }
101
+ } catch (_) {
102
+ reject(new Error(`Bad IPC response: ${data}`));
103
+ }
104
+ });
105
+ },
106
+ );
107
+ req.setTimeout(timeoutMs + 2000, () => {
108
+ req.destroy();
109
+ reject(new Error("IPC request timed out"));
110
+ });
111
+ req.on("error", (err) =>
112
+ reject(new Error(`IPC connection failed: ${err.message}`)),
113
+ );
114
+ req.write(body);
115
+ req.end();
116
+ });
117
+ }
118
+
119
+ export function getCliPeerIdFilePath(cacheDir) {
120
+ const dir = cacheDir || path.join(process.cwd(), ".cache");
121
+ return path.join(dir, "cli-peer-id");
122
+ }
123
+
124
+ export function readCliPeerId(cacheDir) {
125
+ if (process.env.SHADOWCLAW_CLI_PEER_ID) {
126
+ return process.env.SHADOWCLAW_CLI_PEER_ID.trim();
127
+ }
128
+ const filePath = getCliPeerIdFilePath(cacheDir);
129
+ try {
130
+ if (fs.existsSync(filePath)) {
131
+ const stored = fs.readFileSync(filePath, "utf8").trim();
132
+ if (stored) {
133
+ return stored;
134
+ }
135
+ }
136
+ } catch (_) {}
137
+ return "";
138
+ }
139
+
140
+ export function renewCliPeerId(customId, cacheDir) {
141
+ const filePath = getCliPeerIdFilePath(cacheDir);
142
+ const dir = path.dirname(filePath);
143
+ const newId =
144
+ customId && typeof customId === "string" && customId.trim()
145
+ ? customId.trim()
146
+ : `cli-${ulid().toLowerCase()}`;
147
+
148
+ try {
149
+ if (!fs.existsSync(dir)) {
150
+ fs.mkdirSync(dir, { recursive: true });
151
+ }
152
+ fs.writeFileSync(filePath, newId + "\n", "utf8");
153
+ } catch (_) {}
154
+
155
+ return newId;
156
+ }
157
+
158
+ export function getOrCreateCliPeerId(customId, cacheDir) {
159
+ if (customId && typeof customId === "string" && customId.trim()) {
160
+ return customId.trim();
161
+ }
162
+ if (process.env.SHADOWCLAW_CLI_PEER_ID) {
163
+ return process.env.SHADOWCLAW_CLI_PEER_ID.trim();
164
+ }
165
+ const existing = readCliPeerId(cacheDir);
166
+ if (existing) {
167
+ return existing;
168
+ }
169
+ return renewCliPeerId(undefined, cacheDir);
170
+ }
171
+
172
+ export async function ensureWebRtcPolyfill() {
173
+ if (
174
+ typeof globalThis !== "undefined" &&
175
+ typeof globalThis.RTCPeerConnection !== "undefined"
176
+ ) {
177
+ return true;
178
+ }
179
+
180
+ try {
181
+ const wrtc = await import("node-datachannel/polyfill");
182
+ if (wrtc && wrtc.RTCPeerConnection) {
183
+ Object.assign(globalThis, {
184
+ RTCPeerConnection: wrtc.RTCPeerConnection,
185
+ RTCSessionDescription: wrtc.RTCSessionDescription,
186
+ RTCIceCandidate: wrtc.RTCIceCandidate,
187
+ });
188
+ return true;
189
+ }
190
+ } catch (err) {
191
+ console.warn(
192
+ "[webrtc-cli] Failed to load node-datachannel polyfill:",
193
+ err.message || String(err),
194
+ );
195
+ }
196
+ return false;
197
+ }
198
+
199
+ export class CliWebRtcControlClient {
200
+ constructor(options = {}) {
201
+ this.host = options.host || process.env.SHADOWCLAW_HOST || "127.0.0.1";
202
+ this.port =
203
+ options.port || parseInt(process.env.SHADOWCLAW_PORT || "8888", 10);
204
+ this.path = options.path || "/";
205
+ this.secure = options.secure ?? false;
206
+ this.peer = null;
207
+ this.cacheDir = options.cacheDir;
208
+ this.cliPeerId =
209
+ options.peerId && options.peerId.trim()
210
+ ? options.peerId.trim()
211
+ : getOrCreateCliPeerId(undefined, options.cacheDir);
212
+ }
213
+
214
+ async _getPeer() {
215
+ if (this.peer && !this.peer.destroyed) {
216
+ return this.peer;
217
+ }
218
+
219
+ await ensureWebRtcPolyfill();
220
+ const mod = await import("peerjs");
221
+ const Peer = mod.default?.Peer || mod.default || mod.Peer;
222
+
223
+ return new Promise((resolve, reject) => {
224
+ const peer = new Peer(this.cliPeerId, {
225
+ host: this.host,
226
+ port: this.port,
227
+ path: this.path,
228
+ secure: this.secure,
229
+ });
230
+
231
+ this.peer = peer;
232
+
233
+ peer.on("open", () => {
234
+ resolve(peer);
235
+ });
236
+
237
+ peer.on("error", (err) => {
238
+ reject(new Error(`PeerJS error: ${err.type || err.message || err}`));
239
+ });
240
+ });
241
+ }
242
+
243
+ async sendCommand(targetPeerId, action, args = {}, timeoutMs = 30000) {
244
+ // If a `webrtc listen` process is running, route through its local IPC
245
+ // socket so we don't try to register the same peer ID on the PeerJS server.
246
+ const ipc = await resolveListenerIpc(this.cacheDir);
247
+ if (ipc) {
248
+ return sendCommandViaIpc(ipc, targetPeerId, action, args, timeoutMs);
249
+ }
250
+
251
+ const peer = await this._getPeer();
252
+
253
+ return new Promise((resolve, reject) => {
254
+ let timeoutTimer = null;
255
+ let conn = null;
256
+
257
+ const cleanup = () => {
258
+ if (timeoutTimer) clearTimeout(timeoutTimer);
259
+ if (conn) {
260
+ try {
261
+ conn.close();
262
+ } catch (_) {}
263
+ }
264
+ if (this.peer) {
265
+ try {
266
+ this.peer.destroy();
267
+ } catch (_) {}
268
+ this.peer = null;
269
+ }
270
+ };
271
+
272
+ timeoutTimer = setTimeout(() => {
273
+ cleanup();
274
+ reject(
275
+ new Error(
276
+ `WebRTC command '${action}' to peer '${targetPeerId}' timed out after ${timeoutMs}ms`,
277
+ ),
278
+ );
279
+ }, timeoutMs);
280
+
281
+ try {
282
+ conn = peer.connect(targetPeerId, {
283
+ reliable: true,
284
+ serialization: "binary",
285
+ });
286
+
287
+ conn.on("open", () => {
288
+ const commandId = ulid();
289
+ const message = {
290
+ id: ulid(),
291
+ type: "command:execute",
292
+ payload: {
293
+ commandId,
294
+ action,
295
+ args,
296
+ },
297
+ };
298
+ conn.send(message);
299
+ });
300
+
301
+ conn.on("data", (rawData) => {
302
+ let msg = rawData;
303
+ if (typeof rawData === "string") {
304
+ try {
305
+ msg = JSON.parse(rawData);
306
+ } catch (_) {}
307
+ }
308
+
309
+ // Respond to A2A AgentCard discovery if requested
310
+ if (msg && msg.method === "GetAgentCard" && msg.id) {
311
+ try {
312
+ conn.send({
313
+ jsonrpc: "2.0",
314
+ id: msg.id,
315
+ result: {
316
+ name: "ShadowClaw CLI",
317
+ description: "ShadowClaw Command Line Interface",
318
+ version: "1.0.0",
319
+ },
320
+ });
321
+ } catch (_) {}
322
+ return;
323
+ }
324
+
325
+ if (msg && msg.type === "command:result") {
326
+ cleanup();
327
+ if (msg.payload?.error) {
328
+ reject(new Error(msg.payload.error));
329
+ } else {
330
+ resolve(msg.payload);
331
+ }
332
+ }
333
+ });
334
+
335
+ conn.on("error", (err) => {
336
+ cleanup();
337
+ reject(
338
+ new Error(
339
+ `WebRTC connection error to peer '${targetPeerId}': ${err.message || String(err)}`,
340
+ ),
341
+ );
342
+ });
343
+ } catch (err) {
344
+ cleanup();
345
+ reject(err);
346
+ }
347
+ });
348
+ }
349
+
350
+ async listClients() {
351
+ const ipc = await resolveListenerIpc(this.cacheDir);
352
+ if (!ipc) {
353
+ return [];
354
+ }
355
+
356
+ const socketPath = ipc.socketPath || ipc.port;
357
+ return new Promise((resolve) => {
358
+ const req = http.request(
359
+ { socketPath, path: "/clients", method: "GET" },
360
+ (res) => {
361
+ let data = "";
362
+ res.on("data", (c) => (data += c));
363
+ res.on("end", () => {
364
+ try {
365
+ const parsed = JSON.parse(data);
366
+ resolve(parsed.clients || []);
367
+ } catch (_) {
368
+ resolve([]);
369
+ }
370
+ });
371
+ },
372
+ );
373
+ req.setTimeout(2000, () => {
374
+ req.destroy();
375
+ resolve([]);
376
+ });
377
+ req.on("error", () => resolve([]));
378
+ req.end();
379
+ });
380
+ }
381
+
382
+ close() {
383
+ if (this.peer) {
384
+ try {
385
+ this.peer.destroy();
386
+ } catch (_) {}
387
+ this.peer = null;
388
+ }
389
+ }
390
+ }
391
+
392
+ /**
393
+ * CliWebRtcListener — Keeps the CLI registered as a live PeerJS peer on the
394
+ * signaling server so that browser peers can initiate connections to it without
395
+ * needing a control-plane (SSE/WebSocket) connection.
396
+ *
397
+ * Usage:
398
+ * const listener = new CliWebRtcListener({ host, port, path, secure });
399
+ * await listener.start(); // registers on signaling server and blocks
400
+ * // Ctrl-C / SIGINT cleanly calls listener.close()
401
+ */
402
+ export class CliWebRtcListener {
403
+ static _RECONNECT_BASE_MS = 1000;
404
+ static _RECONNECT_MAX_MS = 30000;
405
+
406
+ constructor(options = {}) {
407
+ this.host = options.host || process.env.SHADOWCLAW_HOST || "127.0.0.1";
408
+ this.port =
409
+ options.port || parseInt(process.env.SHADOWCLAW_PORT || "8888", 10);
410
+ this.path = options.path || "/";
411
+ this.secure = options.secure ?? false;
412
+ this.cacheDir = options.cacheDir;
413
+ this.verbose = options.verbose ?? false;
414
+ this.handlers = options.handlers || {};
415
+ this.trustedPeerIds = new Set(
416
+ (options.trustedPeerIds || []).map((id) => id.trim()).filter(Boolean),
417
+ );
418
+ this.cliPeerId = options.renewPeerId
419
+ ? renewCliPeerId(options.peerId, options.cacheDir)
420
+ : getOrCreateCliPeerId(options.peerId, options.cacheDir);
421
+
422
+ this._peer = null;
423
+ this._connections = new Map();
424
+ this._peerCards = new Map();
425
+ this._running = false;
426
+ this._reconnectAttempts = 0;
427
+ this._reconnectTimer = null;
428
+ }
429
+
430
+ _log(msg) {
431
+ if (this.verbose) {
432
+ console.log(`[webrtc-listen] ${msg}`);
433
+ }
434
+ }
435
+
436
+ _isTrusted(remotePeerId) {
437
+ if (this.trustedPeerIds.size === 0) return true;
438
+ if (this.trustedPeerIds.has(remotePeerId)) return true;
439
+ for (const t of this.trustedPeerIds) {
440
+ if (t.endsWith("*") && remotePeerId.startsWith(t.slice(0, -1)))
441
+ return true;
442
+ }
443
+ return false;
444
+ }
445
+
446
+ /**
447
+ * Schedule a reconnection attempt with exponential backoff.
448
+ * Delay doubles each attempt: 1s, 2s, 4s, 8s, ... up to 30s max.
449
+ */
450
+ _scheduleReconnect() {
451
+ if (!this._running || this._reconnectTimer !== null) {
452
+ return;
453
+ }
454
+
455
+ const delay = Math.min(
456
+ CliWebRtcListener._RECONNECT_BASE_MS *
457
+ Math.pow(2, this._reconnectAttempts),
458
+ CliWebRtcListener._RECONNECT_MAX_MS,
459
+ );
460
+
461
+ this._reconnectAttempts++;
462
+
463
+ console.log(
464
+ `[webrtc-listen] Disconnected from signaling server — reconnecting in ${(delay / 1000).toFixed(1)}s (attempt ${this._reconnectAttempts})…`,
465
+ );
466
+
467
+ this._reconnectTimer = setTimeout(() => {
468
+ this._reconnectTimer = null;
469
+ if (!this._running || !this._peer || this._peer.destroyed) {
470
+ return;
471
+ }
472
+
473
+ try {
474
+ this._peer.reconnect();
475
+ } catch {
476
+ this._scheduleReconnect();
477
+ }
478
+ }, delay);
479
+ }
480
+
481
+ async _handleCommand(conn, remotePeerId, msg) {
482
+ const payload = msg.payload || {};
483
+ const { commandId, action, args } = payload;
484
+
485
+ const handler = this.handlers[action];
486
+ if (!handler) {
487
+ conn.send({
488
+ id: ulid(),
489
+ type: "command:result",
490
+ payload: {
491
+ commandId,
492
+ success: false,
493
+ error: `Unknown action: ${action}`,
494
+ },
495
+ });
496
+ return;
497
+ }
498
+
499
+ try {
500
+ const data = await handler(args || {});
501
+ conn.send({
502
+ id: ulid(),
503
+ type: "command:result",
504
+ payload: { commandId, success: true, data },
505
+ });
506
+ } catch (err) {
507
+ conn.send({
508
+ id: ulid(),
509
+ type: "command:result",
510
+ payload: {
511
+ commandId,
512
+ success: false,
513
+ error: err?.message || String(err),
514
+ },
515
+ });
516
+ }
517
+ }
518
+
519
+ _setupConnection(conn, remotePeerId) {
520
+ conn.on("open", () => {
521
+ this._connections.set(remotePeerId, conn);
522
+ console.log(
523
+ `[webrtc-listen] Connection opened from peer: ${remotePeerId}`,
524
+ );
525
+
526
+ // Request remote peer's agent card for client discovery
527
+ try {
528
+ conn.send({
529
+ jsonrpc: "2.0",
530
+ id: ulid(),
531
+ method: "GetAgentCard",
532
+ params: {},
533
+ });
534
+ } catch (_) {}
535
+ });
536
+
537
+ conn.on("data", async (rawData) => {
538
+ let msg = rawData;
539
+ if (typeof rawData === "string") {
540
+ try {
541
+ msg = JSON.parse(rawData);
542
+ } catch (_) {
543
+ return;
544
+ }
545
+ }
546
+ if (!msg || typeof msg !== "object") return;
547
+
548
+ // Store AgentCard response from browser peer
549
+ if (msg.result && typeof msg.result === "object" && msg.result.name) {
550
+ this._peerCards.set(remotePeerId, msg.result);
551
+ return;
552
+ }
553
+
554
+ // Respond to GetAgentCard request from browser peer
555
+ if (msg.method === "GetAgentCard" && msg.id) {
556
+ try {
557
+ conn.send({
558
+ jsonrpc: "2.0",
559
+ id: msg.id,
560
+ result: {
561
+ name: "ShadowClaw CLI",
562
+ description: "ShadowClaw Command Line Interface",
563
+ version: "1.0.0",
564
+ capabilities: ["cli", "webrtc", "control"],
565
+ },
566
+ });
567
+ } catch (_) {}
568
+ return;
569
+ }
570
+
571
+ if (msg.type === "command:execute") {
572
+ await this._handleCommand(conn, remotePeerId, msg);
573
+ }
574
+ });
575
+
576
+ conn.on("close", () => {
577
+ this._connections.delete(remotePeerId);
578
+ this._peerCards.delete(remotePeerId);
579
+ console.log(
580
+ `[webrtc-listen] Connection closed from peer: ${remotePeerId}`,
581
+ );
582
+ });
583
+
584
+ conn.on("error", (err) => {
585
+ this._connections.delete(remotePeerId);
586
+ this._peerCards.delete(remotePeerId);
587
+ console.error(
588
+ `[webrtc-listen] Connection error from ${remotePeerId}: ${err?.message || err}`,
589
+ );
590
+ });
591
+ }
592
+
593
+ /**
594
+ * Start listening. Resolves when registered on the signaling server.
595
+ * The process stays alive until close() is called or SIGINT/SIGTERM.
596
+ */
597
+ async start() {
598
+ await ensureWebRtcPolyfill();
599
+ const mod = await import("peerjs");
600
+ const Peer = mod.default?.Peer || mod.default || mod.Peer;
601
+
602
+ return new Promise((resolve, reject) => {
603
+ let isOpened = false;
604
+
605
+ const peer = new Peer(this.cliPeerId, {
606
+ host: this.host,
607
+ port: this.port,
608
+ path: this.path,
609
+ secure: this.secure,
610
+ });
611
+
612
+ this._peer = peer;
613
+ this._running = true;
614
+
615
+ peer.on("open", (id) => {
616
+ isOpened = true;
617
+ this._reconnectAttempts = 0;
618
+ if (this._reconnectTimer !== null) {
619
+ clearTimeout(this._reconnectTimer);
620
+ this._reconnectTimer = null;
621
+ }
622
+
623
+ console.log(
624
+ `[webrtc-listen] Registered on signaling server as peer: ${id}`,
625
+ );
626
+ console.log(
627
+ `[webrtc-listen] Waiting for connections from browser peers…`,
628
+ );
629
+ resolve(id);
630
+ });
631
+
632
+ peer.on("connection", (conn) => {
633
+ const remotePeerId = conn.peer;
634
+
635
+ if (!this._isTrusted(remotePeerId)) {
636
+ console.warn(
637
+ `[webrtc-listen] Rejecting untrusted peer: ${remotePeerId}`,
638
+ );
639
+ try {
640
+ conn.close();
641
+ } catch (_) {}
642
+ return;
643
+ }
644
+
645
+ this._setupConnection(conn, remotePeerId);
646
+ });
647
+
648
+ peer.on("error", (err) => {
649
+ if (!this._running) return;
650
+ const errStr = err?.type || err?.message || String(err);
651
+ console.error(`[webrtc-listen] PeerJS error: ${errStr}`);
652
+ if (!isOpened) {
653
+ reject(new Error(`PeerJS error: ${errStr}`));
654
+ } else if (!peer.open) {
655
+ this._scheduleReconnect();
656
+ }
657
+ });
658
+
659
+ peer.on("disconnected", () => {
660
+ if (this._running) {
661
+ this._scheduleReconnect();
662
+ }
663
+ });
664
+
665
+ peer.on("close", () => {
666
+ this._running = false;
667
+ if (this._reconnectTimer !== null) {
668
+ clearTimeout(this._reconnectTimer);
669
+ this._reconnectTimer = null;
670
+ }
671
+ console.log("[webrtc-listen] Peer closed.");
672
+ });
673
+ });
674
+ }
675
+
676
+ close() {
677
+ this._running = false;
678
+ if (this._reconnectTimer !== null) {
679
+ clearTimeout(this._reconnectTimer);
680
+ this._reconnectTimer = null;
681
+ }
682
+ this._reconnectAttempts = 0;
683
+
684
+ for (const conn of this._connections.values()) {
685
+ try {
686
+ conn.close();
687
+ } catch (_) {}
688
+ }
689
+ this._connections.clear();
690
+ this._peerCards.clear();
691
+
692
+ if (this._peer) {
693
+ try {
694
+ this._peer.destroy();
695
+ } catch (_) {}
696
+ this._peer = null;
697
+ }
698
+ }
699
+ }