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,59 @@
1
+ /**
2
+ * ShadowClaw CLI — `tasks` command
3
+ * Lists scheduled / active tasks on a connected client.
4
+ */
5
+
6
+ import { CliControlClient } from "../utils/control-client.mjs";
7
+
8
+ export async function runTasksCommand(options = {}) {
9
+ const client = new CliControlClient(options);
10
+
11
+ try {
12
+ let targetClientId = options.client;
13
+ if (!targetClientId) {
14
+ const clients = await client.listClients();
15
+ if (!clients || clients.length === 0) {
16
+ console.error("Error: No clients connected.");
17
+ process.exitCode = 1;
18
+ return;
19
+ }
20
+ targetClientId = clients[0].clientId;
21
+ }
22
+
23
+ console.log(
24
+ `Fetching tasks from client ${targetClientId}${options.group ? ` (group: ${options.group})` : ""}...`,
25
+ );
26
+ const result = await client.sendCommand(targetClientId, "list-tasks", {
27
+ groupId: options.group,
28
+ });
29
+
30
+ if (result.success) {
31
+ const tasks = result.data?.tasks || [];
32
+ if (tasks.length === 0) {
33
+ console.log(
34
+ options.group
35
+ ? `No tasks configured on client for group "${options.group}".`
36
+ : "No tasks configured on client.",
37
+ );
38
+ return;
39
+ }
40
+
41
+ console.log(
42
+ `Tasks on ${targetClientId}${options.group ? ` (group: ${options.group})` : ""} (${tasks.length}):\n`,
43
+ );
44
+ tasks.forEach((t, idx) => {
45
+ const status = t.enabled ? "[enabled]" : "[disabled]";
46
+ const name = t.name || t.prompt?.slice(0, 30) || "Untitled task";
47
+ console.log(
48
+ ` ${idx + 1}. ${status} ${name} (${t.schedule || "manual"})`,
49
+ );
50
+ });
51
+ } else {
52
+ console.error(`Failed to list tasks: ${result.error}`);
53
+ process.exitCode = 1;
54
+ }
55
+ } catch (err) {
56
+ console.error(`Error executing tasks command: ${err.message}`);
57
+ process.exitCode = 1;
58
+ }
59
+ }
@@ -0,0 +1,254 @@
1
+ /**
2
+ * ShadowClaw CLI — `webrtc listen` command
3
+ *
4
+ * Registers the CLI as a live PeerJS peer on the signaling server so that
5
+ * browser tabs can initiate WebRTC DataChannel connections to the CLI without
6
+ * needing a control-plane (SSE/WebSocket) connection.
7
+ *
8
+ * Also starts a local IPC HTTP server on a Unix domain socket
9
+ * (.cache/webrtc-ipc.sock) so that concurrent `send --transport webrtc`
10
+ * invocations can route through the already-established DataChannel instead
11
+ * of trying to register the same peer ID a second time.
12
+ * No port selection, no TCP, no collision.
13
+ */
14
+
15
+ import fs from "node:fs";
16
+ import http from "node:http";
17
+ import path from "node:path";
18
+ import {
19
+ CliWebRtcListener,
20
+ getOrCreateCliPeerId,
21
+ getIpcSocketPath,
22
+ clearIpcFile,
23
+ } from "../utils/webrtc-control-client.mjs";
24
+
25
+ function startIpcServer(listener, cacheDir) {
26
+ return new Promise((resolve, reject) => {
27
+ const socketPath = getIpcSocketPath(cacheDir);
28
+
29
+ // Remove stale socket from a previous run
30
+ try {
31
+ fs.unlinkSync(socketPath);
32
+ } catch (_) {}
33
+
34
+ // Ensure .cache dir exists
35
+ try {
36
+ fs.mkdirSync(path.dirname(socketPath), { recursive: true });
37
+ } catch (_) {}
38
+
39
+ const server = http.createServer(async (req, res) => {
40
+ // Health check
41
+ if (req.method === "GET" && req.url === "/ping") {
42
+ res.writeHead(200, { "Content-Type": "application/json" });
43
+ res.end(JSON.stringify({ ok: true, peerId: listener.cliPeerId }));
44
+ return;
45
+ }
46
+
47
+ // List connected WebRTC peers
48
+ if (req.method === "GET" && req.url === "/clients") {
49
+ const connectedPeers = Array.from(listener._connections.keys());
50
+ const clients = connectedPeers.map((peerId) => {
51
+ const card = listener._peerCards?.get(peerId);
52
+ return {
53
+ clientId: peerId,
54
+ peerId,
55
+ deviceLabel: card?.name || `Browser Peer (${peerId})`,
56
+ capabilities: card?.capabilities || ["webrtc", "peerjs"],
57
+ version: card?.version || "1.0.0",
58
+ transport: "webrtc",
59
+ connectedAt: Date.now(),
60
+ lastSeen: Date.now(),
61
+ };
62
+ });
63
+ res.writeHead(200, { "Content-Type": "application/json" });
64
+ res.end(JSON.stringify({ ok: true, clients }));
65
+ return;
66
+ }
67
+
68
+ // Forward command over the established DataChannel
69
+ if (req.method === "POST" && req.url === "/command") {
70
+ let body = "";
71
+ req.on("data", (c) => (body += c));
72
+ req.on("end", async () => {
73
+ try {
74
+ const { targetPeerId, action, args, timeoutMs } = JSON.parse(body);
75
+
76
+ if (!targetPeerId) {
77
+ res.writeHead(400, { "Content-Type": "application/json" });
78
+ res.end(JSON.stringify({ error: "targetPeerId is required" }));
79
+ return;
80
+ }
81
+
82
+ const conn = listener._connections.get(targetPeerId);
83
+ if (!conn) {
84
+ const connectedPeers = Array.from(listener._connections.keys());
85
+ res.writeHead(503, { "Content-Type": "application/json" });
86
+ res.end(
87
+ JSON.stringify({
88
+ error:
89
+ `No DataChannel open to peer "${targetPeerId}". ` +
90
+ `Make sure the browser has connected to this listener first.` +
91
+ (connectedPeers.length
92
+ ? ` Connected peers: ${connectedPeers.join(", ")}`
93
+ : " No peers are currently connected."),
94
+ }),
95
+ );
96
+ return;
97
+ }
98
+
99
+ const commandId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
100
+ const message = {
101
+ id: commandId,
102
+ type: "command:execute",
103
+ payload: { commandId, action, args: args || {} },
104
+ };
105
+
106
+ const result = await new Promise((cmdResolve, cmdReject) => {
107
+ const timer = setTimeout(() => {
108
+ listener._pendingIpc.delete(commandId);
109
+ cmdReject(
110
+ new Error(
111
+ `Command "${action}" timed out after ${timeoutMs || 30000}ms`,
112
+ ),
113
+ );
114
+ }, timeoutMs || 30000);
115
+
116
+ listener._pendingIpc.set(commandId, (payload) => {
117
+ clearTimeout(timer);
118
+ cmdResolve(payload);
119
+ });
120
+
121
+ try {
122
+ conn.send(message);
123
+ } catch (err) {
124
+ clearTimeout(timer);
125
+ listener._pendingIpc.delete(commandId);
126
+ cmdReject(err);
127
+ }
128
+ });
129
+
130
+ res.writeHead(200, { "Content-Type": "application/json" });
131
+ res.end(JSON.stringify(result));
132
+ } catch (err) {
133
+ res.writeHead(500, { "Content-Type": "application/json" });
134
+ res.end(JSON.stringify({ error: err.message }));
135
+ }
136
+ });
137
+ return;
138
+ }
139
+
140
+ res.writeHead(404);
141
+ res.end();
142
+ });
143
+
144
+ server.listen(socketPath, () => {
145
+ console.log(`[webrtc-listen] IPC socket: ${socketPath}`);
146
+ resolve(server);
147
+ });
148
+
149
+ server.on("error", reject);
150
+ });
151
+ }
152
+
153
+ export async function runWebRtcListenCommand(options = {}) {
154
+ const host = options.host || process.env.SHADOWCLAW_HOST || "127.0.0.1";
155
+ const port = options.port
156
+ ? parseInt(options.port, 10)
157
+ : parseInt(process.env.SHADOWCLAW_PORT || "8888", 10);
158
+ const peerPath = options.path || "/";
159
+ const secure = Boolean(options.secure);
160
+ const trustedPeerIds = options.trustedPeer
161
+ ? Array.isArray(options.trustedPeer)
162
+ ? options.trustedPeer
163
+ : [options.trustedPeer]
164
+ : [];
165
+
166
+ const cliPeerId = getOrCreateCliPeerId(options.peerId, options.cacheDir);
167
+
168
+ console.log(`WebRTC CLI Peer ID : ${cliPeerId}`);
169
+ console.log(
170
+ `Signaling server : ${secure ? "wss" : "ws"}://${host}:${port}${peerPath}`,
171
+ );
172
+ if (trustedPeerIds.length > 0) {
173
+ console.log(`Trusted peers : ${trustedPeerIds.join(", ")}`);
174
+ } else {
175
+ console.log(
176
+ `Trusted peers : (any — add --trusted-peer <id> to restrict)`,
177
+ );
178
+ }
179
+ console.log("");
180
+ console.log("Add this CLI Peer ID as a trusted peer in the browser:");
181
+ console.log(` Settings → WebRTC/PeerJS → Trusted Peer IDs → "${cliPeerId}"`);
182
+ console.log("");
183
+
184
+ const listener = new CliWebRtcListener({
185
+ host,
186
+ port,
187
+ path: peerPath,
188
+ secure,
189
+ trustedPeerIds,
190
+ peerId: options.peerId,
191
+ cacheDir: options.cacheDir,
192
+ renewPeerId: Boolean(options.renewPeerId),
193
+ verbose: Boolean(options.verbose),
194
+ });
195
+
196
+ // Map for pending IPC-dispatched commands awaiting DataChannel responses
197
+ listener._pendingIpc = new Map();
198
+
199
+ // Intercept _setupConnection to wire up IPC command result handling
200
+ const originalSetup = listener._setupConnection.bind(listener);
201
+ listener._setupConnection = function (conn, remotePeerId) {
202
+ originalSetup(conn, remotePeerId);
203
+ conn.on("data", (rawData) => {
204
+ let msg = rawData;
205
+ if (typeof rawData === "string") {
206
+ try {
207
+ msg = JSON.parse(rawData);
208
+ } catch (_) {
209
+ return;
210
+ }
211
+ }
212
+ if (!msg || typeof msg !== "object") return;
213
+ if (msg.type === "command:result") {
214
+ const payload = msg.payload || {};
215
+ const cb = listener._pendingIpc.get(payload.commandId);
216
+ if (cb) {
217
+ listener._pendingIpc.delete(payload.commandId);
218
+ cb(payload);
219
+ }
220
+ }
221
+ });
222
+ };
223
+
224
+ const shutdown = (ipcServer) => {
225
+ console.log("\n[webrtc-listen] Shutting down…");
226
+ clearIpcFile(options.cacheDir);
227
+ if (ipcServer) {
228
+ try {
229
+ ipcServer.close();
230
+ } catch (_) {}
231
+ }
232
+ listener.close();
233
+ process.exit(0);
234
+ };
235
+
236
+ try {
237
+ await listener.start();
238
+ const ipcServer = await startIpcServer(listener, options.cacheDir);
239
+
240
+ process.on("SIGINT", () => shutdown(ipcServer));
241
+ process.on("SIGTERM", () => shutdown(ipcServer));
242
+
243
+ console.log(
244
+ `[webrtc-listen] Ready. Run commands with:\n` +
245
+ ` node bin/cli.mjs send --transport webrtc --client <browser-peer-id> "message"`,
246
+ );
247
+
248
+ await new Promise(() => {}); // keep alive
249
+ } catch (err) {
250
+ clearIpcFile(options.cacheDir);
251
+ console.error(`Error starting WebRTC listener: ${err.message}`);
252
+ process.exitCode = 1;
253
+ }
254
+ }
@@ -1,15 +1,19 @@
1
1
  import {
2
+ applySidebarVisibilityToTemplate,
2
3
  applyStaticPagesContent,
3
4
  collectPageSources,
4
5
  escapeJsonForHtmlScript,
6
+ extractTemplateContent,
5
7
  injectPageHeaderDsd,
6
8
  injectStaticManifestScript,
9
+ isPageFile,
7
10
  markNoSeedPrerenderHost,
8
11
  minifyDsdTemplateHtml,
9
12
  normalizePrerenderPagesOption,
10
13
  prerenderDsdShell,
11
14
  renderPageHtml,
12
15
  sortPagePaths,
16
+ splitFrontmatterWithGrayMatter,
13
17
  } from "./prerender-dsd-shell.mjs";
14
18
 
15
19
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
@@ -478,4 +482,46 @@ describe("prerenderDsdShell purge flag pages", () => {
478
482
  expect(manifestJson.purgeId).toBe("build-test-999");
479
483
  expect(manifestJson.preRenderedStaticPages["br-main"]).toBeDefined();
480
484
  });
485
+
486
+ describe("prerender helper functions", () => {
487
+ it("splits frontmatter with gray-matter", () => {
488
+ const src = "---\ntitle: Hello\n---\n# Content";
489
+ const res = splitFrontmatterWithGrayMatter(src);
490
+ expect(res.data.title).toBe("Hello");
491
+ expect(res.content.trim()).toBe("# Content");
492
+ });
493
+
494
+ it("extracts template content and throws on missing template", () => {
495
+ const html = "<template><div>Hello</div></template>";
496
+ expect(extractTemplateContent(html)).toBe("<div>Hello</div>");
497
+ expect(() => extractTemplateContent("<div>No template</div>")).toThrow(
498
+ "Template wrapper not found",
499
+ );
500
+ });
501
+
502
+ it("identifies page files by extension", () => {
503
+ expect(isPageFile("index.md")).toBe(true);
504
+ expect(isPageFile("page.html")).toBe(true);
505
+ expect(isPageFile("script.js")).toBe(false);
506
+ });
507
+
508
+ it("applies sidebar visibility config to template", () => {
509
+ const tpl =
510
+ '<li class="nav-item" data-page="chat">Chat</li><li class="nav-item" data-page="files">Files</li>';
511
+ const modified = applySidebarVisibilityToTemplate(tpl, {
512
+ chatHidden: true,
513
+ });
514
+ expect(modified).toContain('data-page="chat" hidden aria-hidden="true"');
515
+ expect(modified).not.toContain('data-page="files" hidden');
516
+ });
517
+
518
+ it("minifies DSD template HTML removing excess whitespace and comments", () => {
519
+ const html =
520
+ '<template shadowrootmode="open">\n<div>\n <span>Text</span>\n</div>\n</template>';
521
+ const minified = minifyDsdTemplateHtml(html);
522
+ expect(minified).toBe(
523
+ '<template shadowrootmode="open"><div><span>Text</span></div></template>',
524
+ );
525
+ });
526
+ });
481
527
  });