shadow-claw 1.23.3 → 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 (286) hide show
  1. package/README.md +42 -5
  2. package/bin/assert-version-bump.mjs +60 -38
  3. package/bin/assert-version-bump.test.mjs +74 -11
  4. package/bin/cli.mjs +166 -0
  5. package/bin/cli.test.mjs +60 -0
  6. package/bin/commands/backup.mjs +70 -0
  7. package/bin/commands/clients.mjs +41 -0
  8. package/bin/commands/peer-id.mjs +76 -0
  9. package/bin/commands/peer-id.test.mjs +104 -0
  10. package/bin/commands/send.mjs +51 -0
  11. package/bin/commands/tasks.mjs +59 -0
  12. package/bin/commands/webrtc-listen.mjs +254 -0
  13. package/bin/prerender-dsd-shell/prerender-dsd-shell.test.mjs +46 -0
  14. package/bin/rolldown.config.test.mjs +401 -8
  15. package/bin/utils/control-client.mjs +243 -0
  16. package/bin/utils/control-client.test.mjs +83 -0
  17. package/bin/utils/ulid.mjs +45 -0
  18. package/bin/utils/webrtc-control-client.mjs +699 -0
  19. package/bin/utils/webrtc-control-client.test.mjs +318 -0
  20. package/dist/electron/main.cjs +1 -1
  21. package/dist/public/AGENTS.md +5 -3
  22. package/dist/public/README.md +42 -5
  23. package/dist/public/agent.worker.js +6 -6
  24. package/dist/public/{app-routes-B0ePT49v.js → app-routes-Bs-riIvr.js} +1 -1
  25. package/dist/public/backup-controller-C_s9in2t.js +1 -0
  26. package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +1 -0
  27. package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +1 -0
  28. package/dist/public/components/settings/shadow-claw-storage/shadow-claw-storage.html +1 -1
  29. package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +1 -1
  30. package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
  31. package/dist/public/components/shadow-claw-files/shadow-claw-files.css +1 -1
  32. package/dist/public/components/shadow-claw-files/shadow-claw-files.html +1 -1
  33. package/dist/public/components/shadow-claw-settings/shadow-claw-settings.html +1 -1
  34. package/dist/public/config-CrHchneq.js +1 -0
  35. package/dist/public/{configurePeerJs-B7kNU8AQ.js → configurePeerJs-CxpZeDJJ.js} +1 -1
  36. package/dist/public/{connections-xd_66YVU.js → connections-DvbgdkaB.js} +1 -1
  37. package/dist/public/{custom-element-security-CZ92anB8.js → custom-element-security-NCqQ5lmC.js} +1 -1
  38. package/dist/public/docs/README.md +3 -1
  39. package/dist/public/docs/architecture/overview.md +12 -12
  40. package/dist/public/docs/architecture/storage.md +5 -2
  41. package/dist/public/docs/decisions/bundled-typescript-architecture.md +1 -1
  42. package/dist/public/docs/decisions/indexeddb-and-opfs-storage.md +6 -3
  43. package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
  44. package/dist/public/docs/example/article/index.html +2 -2
  45. package/dist/public/docs/guides/configuring-messaging-channels.md +41 -6
  46. package/dist/public/docs/guides/server-development-configuration.md +24 -0
  47. package/dist/public/docs/publishing/index.html +19 -76
  48. package/dist/public/docs/skill-creator/index.html +2 -2
  49. package/dist/public/docs/subsystems/backup.md +56 -0
  50. package/dist/public/docs/subsystems/cli.md +124 -0
  51. package/dist/public/docs/subsystems/control-plane.md +158 -0
  52. package/dist/public/docs/subsystems/notifications.md +54 -2
  53. package/dist/public/docs/subsystems/reactive-ui.md +1 -0
  54. package/dist/public/downloadGroupFile-By0GR6eV.js +1 -0
  55. package/dist/public/e2e/README.md +5 -1
  56. package/dist/public/{e2e-bridge-DkS5xFGX.js → e2e-bridge-IGWsRl4i.js} +1 -1
  57. package/dist/public/file-viewer-D0eflgt6.js +1 -0
  58. package/dist/public/files/main/~/docs/publishing-to-github-pages.md +17 -82
  59. package/dist/public/getAllTasks-vpBlgGsc.js +1 -0
  60. package/dist/public/getGroupDir-P1h9wl6S.js +1 -0
  61. package/dist/public/{git-BkjrsRXX.js → git-7Vq_SJSp.js} +2 -2
  62. package/dist/public/{iframe-sanitizer-L6Jyu__Z.js → iframe-sanitizer-y2Z1pPwY.js} +1 -1
  63. package/dist/public/index.html +2 -2
  64. package/dist/public/index.js +1 -1
  65. package/dist/public/{initChatSplitResize-BslPXAXS.js → initChatSplitResize-CzV2E7Ul.js} +1 -1
  66. package/dist/public/initControlPlane-C9jpnWev.js +1 -0
  67. package/dist/public/llms.txt +1 -1
  68. package/dist/public/main/index.html +2 -2
  69. package/dist/public/main/memory/index.html +2 -2
  70. package/dist/public/{markdown-6MBrIo-S.js → markdown-BnpKxSxl.js} +1 -1
  71. package/dist/public/mcp-reconnect-9emsqGRe.js +1 -0
  72. package/dist/public/{memoryStorage-CjGseKI1.js → memoryStorage-C0KvLNUp.js} +1 -1
  73. package/dist/public/{orchestrator-B9KmIj8Q.js → orchestrator-3h9xxHUm.js} +676 -676
  74. package/dist/public/orchestrator-nJj59RqD.js +15 -0
  75. package/dist/public/pages/main/~/docs/publishing-to-github-pages.md +17 -82
  76. package/dist/public/{peerjs-Dv8JNm7v.js → peerjs-BP4laMXj.js} +1 -1
  77. package/dist/public/{push-client-Dqq06wbB.js → push-client-CrjEXK3U.js} +1 -1
  78. package/dist/public/service-worker/fetch-proxy.js +1 -1
  79. package/dist/public/service-worker/push-handler.js +1 -1
  80. package/dist/public/service-worker.js +1 -1
  81. package/dist/public/{shadow-claw-Bd1rfuVC.js → shadow-claw-CbwETBWf.js} +2 -2
  82. package/dist/public/{shadow-claw-accounts-DL2l3oM0.js → shadow-claw-accounts-B4sBTQEj.js} +3 -3
  83. package/dist/public/{shadow-claw-channels-BGXDsQ38.js → shadow-claw-channels-BDWG5Qa3.js} +1 -1
  84. package/dist/public/{shadow-claw-chat-NXg6zz_l.js → shadow-claw-chat-Crv0DTCY.js} +6 -6
  85. package/dist/public/shadow-claw-control-plane-CX1IGbh9.js +199 -0
  86. package/dist/public/{shadow-claw-conversations-C8eqGpP9.js → shadow-claw-conversations-_9qAaRXk.js} +1 -1
  87. package/dist/public/shadow-claw-dialog-BrOwAOdk.js +1 -0
  88. package/dist/public/shadow-claw-file-viewer-Blf6yGVD.js +577 -0
  89. package/dist/public/shadow-claw-files-CYySBDCl.js +840 -0
  90. package/dist/public/{shadow-claw-git-C0ntz7aX.js → shadow-claw-git-v8S0JjuR.js} +3 -3
  91. package/dist/public/{shadow-claw-integrations-DCXV2r69.js → shadow-claw-integrations-D7ufpwhX.js} +1 -1
  92. package/dist/public/{shadow-claw-llm-CudgFQsS.js → shadow-claw-llm-GWgoVfhI.js} +2 -2
  93. package/dist/public/{shadow-claw-mcp-remote-B8RkpX28.js → shadow-claw-mcp-remote-kY4LMFNN.js} +1 -1
  94. package/dist/public/{shadow-claw-networking-BLiRsrsK.js → shadow-claw-networking-qHbP95z9.js} +1 -1
  95. package/dist/public/{shadow-claw-notifications-CQhQcjtL.js → shadow-claw-notifications-YyHOK2Fr.js} +1 -1
  96. package/dist/public/{shadow-claw-page-header-NeRD0tKU.js → shadow-claw-page-header-DMKbkv3s.js} +1 -1
  97. package/dist/public/{shadow-claw-pages-DCHnFh6r.js → shadow-claw-pages-DFZHhzFZ.js} +3 -3
  98. package/dist/public/{shadow-claw-settings-VAfjKFQg.js → shadow-claw-settings-DhxrKIfA.js} +10 -3
  99. package/dist/public/shadow-claw-storage-BFynbP0j.js +219 -0
  100. package/dist/public/{shadow-claw-task-server-Dk33N_1G.js → shadow-claw-task-server-3BIeb5oZ.js} +25 -4
  101. package/dist/public/{shadow-claw-tasks-xA1VfmWN.js → shadow-claw-tasks-Caxc9atL.js} +4 -4
  102. package/dist/public/{shadow-claw-terminal-Du2r0ciq.js → shadow-claw-terminal-Beb5qF-U.js} +1 -1
  103. package/dist/public/{shadow-claw-tools-CZj4rq7l.js → shadow-claw-tools-BfJx3A9W.js} +2 -2
  104. package/dist/public/{shadow-claw-webvm-JYUkWICG.js → shadow-claw-webvm-BCS8ykeP.js} +1 -1
  105. package/dist/public/static-main/~/docs/publishing-to-github-pages.md +17 -82
  106. package/dist/public/static-main-manifest.json +1 -1
  107. package/dist/public/{syncWebMcpRegistration-DfpvASnh.js → syncWebMcpRegistration-B_M2fCjH.js} +1 -1
  108. package/dist/server.js +286 -189
  109. package/package.json +2 -1
  110. package/src/__mocks__/jszip.cjs +15 -5
  111. package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +35 -0
  112. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +117 -0
  113. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +81 -0
  114. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.test.ts +127 -0
  115. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.ts +292 -0
  116. package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +72 -0
  117. package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +20 -30
  118. package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +116 -0
  119. package/src/components/settings/shadow-claw-llm/model-ranking.test.ts +143 -51
  120. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +307 -4
  121. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.ts +8 -8
  122. package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +31 -0
  123. package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +186 -0
  124. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.html +17 -0
  125. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +120 -0
  126. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.ts +56 -0
  127. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +23 -2
  128. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
  129. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +19 -0
  130. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.ts +2 -1
  131. package/src/components/shadow-claw/utils/initChatSplitResize.test.ts +180 -13
  132. package/src/components/shadow-claw/utils/setupEffects.test.ts +39 -0
  133. package/src/components/shadow-claw-a2ui/catalog/basic/shared.test.ts +117 -0
  134. package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +1159 -1
  135. package/src/components/shadow-claw-chat/shadow-claw-chat.test.ts +1523 -19
  136. package/src/components/shadow-claw-chat/utils/escapeHtml.test.ts +13 -0
  137. package/src/components/shadow-claw-chat/utils/getPeerChatDisplayStatus.test.ts +20 -0
  138. package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +494 -0
  139. package/src/components/shadow-claw-dialog/shadow-claw-dialog.ts +13 -2
  140. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +378 -0
  141. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +11 -9
  142. package/src/components/shadow-claw-files/shadow-claw-files.css +41 -17
  143. package/src/components/shadow-claw-files/shadow-claw-files.html +35 -0
  144. package/src/components/shadow-claw-files/shadow-claw-files.test.ts +627 -1
  145. package/src/components/shadow-claw-files/shadow-claw-files.ts +170 -6
  146. package/src/components/shadow-claw-pages/utils/resolveWorkspaceLinkPath.test.ts +31 -0
  147. package/src/components/shadow-claw-pages/utils/resolveWorkspaceLinkPath.ts +2 -1
  148. package/src/components/shadow-claw-pages/utils/rewriteWorkspacePreviewHtml.test.ts +15 -0
  149. package/src/components/shadow-claw-pdf-viewer/utils/installGetOrInsertComputedPolyfill.test.ts +65 -0
  150. package/src/components/shadow-claw-settings/shadow-claw-settings.html +7 -0
  151. package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +221 -0
  152. package/src/components/shadow-claw-settings/shadow-claw-settings.ts +2 -0
  153. package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +407 -0
  154. package/src/components/shadow-claw-terminal/utils/consumeEscapeSequence.test.ts +58 -0
  155. package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +80 -0
  156. package/src/config/config.test.ts +9 -0
  157. package/src/config/config.ts +15 -0
  158. package/src/core/app-routes.test.ts +56 -0
  159. package/src/core/app-routes.ts +8 -1
  160. package/src/core/backup-controller.test.ts +101 -0
  161. package/src/core/backup-controller.ts +137 -0
  162. package/src/core/control-plane-client.test.ts +408 -0
  163. package/src/core/control-plane-client.ts +425 -0
  164. package/src/core/orchestrator/orchestrator.ts +4 -0
  165. package/src/core/orchestrator/utils/compactContext.test.ts +78 -0
  166. package/src/core/orchestrator/utils/operations/channel.test.ts +254 -9
  167. package/src/core/orchestrator/utils/operations/provider.test.ts +326 -34
  168. package/src/core/orchestrator/utils/operations/vm.test.ts +114 -2
  169. package/src/core/orchestrator/utils/syncProxyConfigToServiceWorker.test.ts +60 -0
  170. package/src/core/utils/initControlPlane.test.ts +276 -0
  171. package/src/core/utils/initControlPlane.ts +437 -0
  172. package/src/core/utils/initializeApp.ts +18 -0
  173. package/src/db/getSession.test.ts +13 -1
  174. package/src/db/openDatabase.test.ts +10 -1
  175. package/src/db/openDatabase.ts +3 -1
  176. package/src/server/app.test.ts +12 -0
  177. package/src/server/app.ts +18 -8
  178. package/src/server/client-registry.test.ts +250 -0
  179. package/src/server/client-registry.ts +477 -0
  180. package/src/server/config.ts +15 -0
  181. package/src/server/control-plane-types.ts +120 -0
  182. package/src/server/control-plane.test.ts +472 -0
  183. package/src/server/control-plane.ts +581 -0
  184. package/src/server/routes/backup.test.ts +263 -0
  185. package/src/server/routes/backup.ts +262 -0
  186. package/src/server/routes/integrations-email.test.ts +532 -0
  187. package/src/server/routes/llamafile.test.ts +172 -48
  188. package/src/server/routes/mesh-llm.test.ts +192 -0
  189. package/src/server/routes/ollama.test.ts +49 -0
  190. package/src/server/server-peer.test.ts +86 -0
  191. package/src/server/server-peer.ts +330 -0
  192. package/src/server/server.ts +35 -0
  193. package/src/server/services/llamafile-manager.test.ts +230 -6
  194. package/src/server/services/transformers-runtime.test.ts +186 -18
  195. package/src/server/webrtc.test.ts +21 -0
  196. package/src/server/webrtc.ts +72 -0
  197. package/src/service-worker/fetch-proxy.test.ts +64 -1
  198. package/src/service-worker/push-handler.ts +39 -0
  199. package/src/shell/vm.test.ts +744 -12
  200. package/src/storage/ensureMainGroupIndex.test.ts +153 -0
  201. package/src/storage/getGroupFile.test.ts +66 -0
  202. package/src/storage/memoryStorage.test.ts +82 -2
  203. package/src/storage/migrateLegacyOpfs.test.ts +327 -0
  204. package/src/storage/migrateLegacyOpfs.ts +117 -0
  205. package/src/storage/writeGroupFileBytes.test.ts +143 -0
  206. package/src/stores/chat-ui.test.ts +20 -0
  207. package/src/stores/orchestrator.test.ts +134 -0
  208. package/src/stores/toast.test.ts +50 -0
  209. package/src/stores/tools.test.ts +147 -10
  210. package/src/subsystems/channels/imessage.test.ts +59 -0
  211. package/src/subsystems/channels/peerjs.test.ts +500 -62
  212. package/src/subsystems/channels/peerjs.ts +71 -4
  213. package/src/subsystems/channels/telegram.test.ts +177 -0
  214. package/src/subsystems/email/catalog.test.ts +1 -0
  215. package/src/subsystems/email/connections.test.ts +39 -1
  216. package/src/subsystems/git/credentials.test.ts +160 -825
  217. package/src/subsystems/git/git.test.ts +463 -0
  218. package/src/subsystems/notifications/__mocks__/node-sqlite.cjs +12 -1
  219. package/src/subsystems/notifications/push-routes.test.ts +33 -0
  220. package/src/subsystems/notifications/push-routes.ts +23 -0
  221. package/src/subsystems/notifications/push-store.ts +1 -1
  222. package/src/subsystems/providers/builtin-ai-tasks.test.ts +74 -0
  223. package/src/subsystems/providers/litert-lm-provider.test.ts +212 -51
  224. package/src/subsystems/providers/model-registry.test.ts +115 -0
  225. package/src/subsystems/providers/providers.test.ts +94 -1
  226. package/src/subsystems/providers/transformers-js-provider.test.ts +279 -0
  227. package/src/subsystems/providers/utils/createModelCacheFetch.test.ts +2 -2
  228. package/src/subsystems/providers/utils/downloadModelToCache.test.ts +2 -2
  229. package/src/subsystems/providers/utils/loadModelStream.test.ts +2 -2
  230. package/src/subsystems/skills/activateSkill.test.ts +101 -0
  231. package/src/subsystems/skills/discoverSkills.test.ts +251 -0
  232. package/src/subsystems/tools/cron.test.ts +64 -0
  233. package/src/subsystems/tools/declarative.test.ts +255 -89
  234. package/src/testing/assets/v86.9pfs/libv86.mjs +63 -0
  235. package/src/testing/assets/v86.ext2/libv86.mjs +3 -0
  236. package/src/testing/assets_missing_v86/assets/v86.9pfs/libv86.mjs +1 -0
  237. package/src/testing/jest-setup.ts +328 -40
  238. package/src/testing/mock-v86.mjs +49 -0
  239. package/src/ui/a2ui/types.ts +4 -3
  240. package/src/ui/a2ui/utils/normaliseComponentsToMap.test.ts +102 -0
  241. package/src/ui/a2ui/utils/resolveChildIds.test.ts +63 -0
  242. package/src/ui/a2ui/utils/resolveDynamicNumber.test.ts +13 -1
  243. package/src/worker/tools/bash/utils/waitForVMReady.test.ts +40 -0
  244. package/src/worker/tools/email/email.test.ts +193 -8
  245. package/src/worker/tools/email/utils/asEmailAttachmentInputs.test.ts +47 -0
  246. package/src/worker/tools/git/git.test.ts +214 -3
  247. package/src/worker/tools/rooms/invite-to-room.test.ts +54 -0
  248. package/src/worker/tools/rooms/leave-room.test.ts +40 -0
  249. package/src/worker/tools/rooms/list-room-members.test.ts +75 -0
  250. package/src/worker/tools/tasks/tasks-utils.test.ts +22 -0
  251. package/src/worker/tools/ui/ask-user.test.ts +50 -0
  252. package/src/worker/tools/ui/clear-chat.test.ts +19 -0
  253. package/src/worker/tools/ui/get-current-time.test.ts +19 -0
  254. package/src/worker/tools/ui/javascript.test.ts +116 -0
  255. package/src/worker/tools/ui/list-components.test.ts +10 -0
  256. package/src/worker/tools/ui/list-tool-profiles.test.ts +42 -0
  257. package/src/worker/tools/ui/manage-tools.test.ts +34 -0
  258. package/src/worker/tools/ui/render-component.test.ts +346 -0
  259. package/src/worker/tools/ui/send-notification.test.ts +40 -0
  260. package/src/worker/tools/ui/utils/getAllowFullInternetAccess.test.ts +31 -0
  261. package/src/worker/tools/ui/utils/parseBooleanConfig.test.ts +25 -0
  262. package/src/worker/tools/workspace/attach-file.test.ts +84 -0
  263. package/src/worker/tools/workspace/copy-file.test.ts +69 -0
  264. package/src/worker/tools/workspace/create-directory.test.ts +40 -0
  265. package/src/worker/tools/workspace/delete-file.test.ts +73 -0
  266. package/src/worker/tools/workspace/diff-files.test.ts +94 -0
  267. package/src/worker/tools/workspace/list-files.test.ts +36 -0
  268. package/src/worker/tools/workspace/move-file.test.ts +66 -0
  269. package/src/worker/tools/workspace/open-file.test.ts +57 -0
  270. package/src/worker/tools/workspace/patch-file.test.ts +81 -0
  271. package/src/worker/tools/workspace/read-file.test.ts +116 -0
  272. package/src/worker/tools/workspace/send-file.test.ts +82 -0
  273. package/src/worker/tools/workspace/update-memory.test.ts +29 -0
  274. package/src/worker/tools/workspace/write-file.test.ts +28 -0
  275. package/src/worker/utils/rate-limit.test.ts +199 -0
  276. package/src/worker/utils/tool-state.test.ts +29 -0
  277. package/src/worker/utils/transformers-js-utils.test.ts +59 -0
  278. package/dist/public/config-AL77QAx4.js +0 -1
  279. package/dist/public/downloadGroupFile-BABv_1mJ.js +0 -1
  280. package/dist/public/file-viewer-CCSABMsE.js +0 -1
  281. package/dist/public/mcp-reconnect-DoicazQd.js +0 -1
  282. package/dist/public/orchestrator-4su7iyWj.js +0 -15
  283. package/dist/public/shadow-claw-dialog-ByyHgxM9.js +0 -1
  284. package/dist/public/shadow-claw-file-viewer-9Qkwc30T.js +0 -577
  285. package/dist/public/shadow-claw-files-aDyq6Hv7.js +0 -781
  286. package/dist/public/shadow-claw-storage-Cf31np81.js +0 -202
package/README.md CHANGED
@@ -2,23 +2,43 @@
2
2
 
3
3
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/xt-ml/shadow-claw)
4
4
 
5
- A browser-native, fully-featured AI assistant. TypeScript + Web Components + IndexedDB + File System API.
5
+ A browser-native, fully-featured AI assistant — runs as a PWA, a native desktop app, a Node.js server, and via the `shadow-claw` CLI. TypeScript + Web Components + IndexedDB + File System API.
6
6
 
7
7
  [![ShadowClaw Screenshot](https://xt-ml.github.io/shadow-claw/assets/screenshots/shadow-claw-screenshot-1920x1052.png)](https://xt-ml.github.io/shadow-claw/)
8
8
 
9
9
  ## Quick Start
10
10
 
11
+ ### 1. Launch ShadowClaw instantly without cloning the repo
12
+
11
13
  ```bash
12
- npm install && npm run dev # Dev server → http://localhost:8888
14
+ npx shadow-claw dev --open
13
15
  ```
14
16
 
15
- Open Settings, select a provider (or use browser Prompt API), and start chatting.
17
+ Open Settings, select your AI provider (or use the in-browser Prompt API / local models), and start chatting.
18
+
19
+ ### 2. Scaffold a Project or Knowledge Hub
20
+
21
+ Create a custom template with pages and site configuration:
22
+
23
+ ```bash
24
+ npx shadow-claw init my-assistant
25
+ cd my-assistant
26
+ npx shadow-claw dev --open
27
+ ```
28
+
29
+ ### 3. From Source (Contributing / Development)
30
+
31
+ ```bash
32
+ git clone https://github.com/xt-ml/shadow-claw.git
33
+ cd shadow-claw
34
+ npm install && npm run dev # Dev server → http://localhost:8888
35
+ ```
16
36
 
17
- **Desktop:** `npm run electron` or `npm run electron:build` for distributable.
37
+ **Desktop App:** `npm run electron` or `npm run electron:build` for distributable installer.
18
38
 
19
39
  ## What is ShadowClaw?
20
40
 
21
- A fully-functional agent runtime that runs entirely in the browser—no AI processing on a server. Built with TypeScript, it provides:
41
+ A fully-functional agent runtime built on browser-native technology the orchestration and tool-use loop run client-side, with AI inference routing to remote APIs, local servers, or in-browser models depending on your configuration. Built with TypeScript, it provides:
22
42
 
23
43
  - **Multi-model support**: OpenRouter, Anthropic, Google Gemini, AWS Bedrock, Ollama, Llamafile, Mesh LLM, Transformers.js, and browser-native Prompt API
24
44
  - **Web Components UI**: Native Custom Elements + TC39 Signals for reactive updates
@@ -378,6 +398,23 @@ npm run electron:build:win # Build Electron for Windows
378
398
  npm run electron:build:mac # Build Electron for macOS
379
399
  ```
380
400
 
401
+ ### CLI Runtime Commands
402
+
403
+ The `shadow-claw` CLI connects to a running server or browser clients to interact with active sessions:
404
+
405
+ ```bash
406
+ npx shadow-claw clients # List connected browser/Electron clients
407
+ npx shadow-claw send "your prompt" --client <id> # Send a message to a connected client
408
+ npx shadow-claw backup # Trigger OPFS workspace backup
409
+ npx shadow-claw backup list # List available backup snapshots
410
+ npx shadow-claw backup delete --backup-id <id> # Delete a backup snapshot
411
+ npx shadow-claw tasks --client <id> # List scheduled tasks on a client
412
+ npx shadow-claw webrtc listen # Start headless WebRTC DataChannel daemon
413
+ npx shadow-claw peer-id # Get or generate persistent CLI Peer ID
414
+ ```
415
+
416
+ Commands support `--transport webrtc` for direct peer-to-peer DataChannel execution with connected browser clients. Control plane authentication uses `SHADOWCLAW_CONTROL_TOKEN` (env) or `--token` flag. The control plane endpoint and token are printed to the console on server start.
417
+
381
418
  ## License
382
419
 
383
420
  AGPLv3. Core logic derived from [openbrowserclaw](https://github.com/sachaa/openbrowserclaw) (MIT).
@@ -4,12 +4,12 @@ import { execSync } from "node:child_process";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import process from "node:process";
7
- import { fileURLToPath } from "node:url";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
8
 
9
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
10
  const rootDir = path.resolve(__dirname, "..");
11
11
 
12
- function parseSemver(v) {
12
+ export function parseSemver(v) {
13
13
  const clean = String(v || "")
14
14
  .trim()
15
15
  .replace(/^[v^~]/, "");
@@ -26,7 +26,7 @@ function parseSemver(v) {
26
26
  };
27
27
  }
28
28
 
29
- function compareSemver(a, b) {
29
+ export function compareSemver(a, b) {
30
30
  const pa = parseSemver(a);
31
31
  const pb = parseSemver(b);
32
32
  if (!pa || !pb) return String(a).localeCompare(String(b));
@@ -43,6 +43,47 @@ function compareSemver(a, b) {
43
43
  return 0;
44
44
  }
45
45
 
46
+ export async function assertVersionBump({
47
+ localPkgVersion,
48
+ localLockVersion,
49
+ pkgName,
50
+ publishedVersion,
51
+ silent = false,
52
+ }) {
53
+ if (!silent) {
54
+ console.log(`Checking version bump for package "${pkgName}"...`);
55
+ console.log(`- Local package.json version : ${localPkgVersion}`);
56
+ }
57
+
58
+ if (localLockVersion) {
59
+ if (!silent) {
60
+ console.log(`- Local package-lock.json version: ${localLockVersion}`);
61
+ }
62
+ if (localPkgVersion !== localLockVersion) {
63
+ throw new Error(
64
+ `Version mismatch: package.json (${localPkgVersion}) does not match package-lock.json (${localLockVersion})!`,
65
+ );
66
+ }
67
+ }
68
+
69
+ if (publishedVersion) {
70
+ if (!silent) {
71
+ console.log(`- Published npm registry version : ${publishedVersion}`);
72
+ }
73
+ const cmp = compareSemver(localPkgVersion, publishedVersion);
74
+ if (cmp <= 0) {
75
+ throw new Error(
76
+ `Version assertion failed: Local version (${localPkgVersion}) must be greater than published npm version (${publishedVersion}). Please bump the version before publishing.`,
77
+ );
78
+ }
79
+ if (!silent) {
80
+ console.log(
81
+ `✓ Version assertion passed: ${localPkgVersion} > ${publishedVersion}`,
82
+ );
83
+ }
84
+ }
85
+ }
86
+
46
87
  async function main() {
47
88
  const pkgJsonPath = path.join(rootDir, "package.json");
48
89
  const lockJsonPath = path.join(rootDir, "package-lock.json");
@@ -57,21 +98,6 @@ async function main() {
57
98
  const localPkgVersion = pkg.version;
58
99
  const localLockVersion = lock?.version;
59
100
 
60
- console.log(`Checking version bump for package "${pkgName}"...`);
61
- console.log(`- Local package.json version : ${localPkgVersion}`);
62
- if (localLockVersion) {
63
- console.log(`- Local package-lock.json version: ${localLockVersion}`);
64
- if (localPkgVersion !== localLockVersion) {
65
- console.error(
66
- `\n[FATAL] Version mismatch: package.json (${localPkgVersion}) does not match package-lock.json (${localLockVersion})!`,
67
- );
68
- console.error(
69
- `Run "npm install" or "npm version" to synchronize package-lock.json.`,
70
- );
71
- process.exit(1);
72
- }
73
- }
74
-
75
101
  let publishedVersion = null;
76
102
  try {
77
103
  const rawOut = execSync(`npm view ${pkgName} version --json`, {
@@ -98,25 +124,21 @@ async function main() {
98
124
  );
99
125
  }
100
126
 
101
- if (publishedVersion) {
102
- console.log(`- Published npm registry version : ${publishedVersion}`);
103
- const cmp = compareSemver(localPkgVersion, publishedVersion);
104
- if (cmp <= 0) {
105
- console.error(
106
- `\n[FATAL] Version assertion failed: Local version (${localPkgVersion}) must be greater than published npm version (${publishedVersion}).`,
107
- );
108
- console.error(
109
- `Please bump the version in package.json and package-lock.json (e.g. "npm version patch|minor|major") before publishing.\n`,
110
- );
111
- process.exit(1);
112
- }
113
- console.log(
114
- `✓ Version assertion passed: ${localPkgVersion} > ${publishedVersion}`,
115
- );
116
- }
127
+ await assertVersionBump({
128
+ localPkgVersion,
129
+ localLockVersion,
130
+ pkgName,
131
+ publishedVersion,
132
+ });
117
133
  }
118
134
 
119
- main().catch((err) => {
120
- console.error("Version assertion check failed:", err);
121
- process.exit(1);
122
- });
135
+ const isMainModule =
136
+ process.argv[1] &&
137
+ import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
138
+
139
+ if (isMainModule) {
140
+ main().catch((err) => {
141
+ console.error(`\n[FATAL] ${err.message}\n`);
142
+ process.exit(1);
143
+ });
144
+ }
@@ -1,16 +1,79 @@
1
1
  import { describe, it, expect } from "@jest/globals";
2
- import { execFile } from "node:child_process";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import { promisify } from "node:util";
6
-
7
- const execFileAsync = promisify(execFile);
8
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
- const scriptPath = path.join(__dirname, "assert-version-bump.mjs");
2
+ import {
3
+ parseSemver,
4
+ compareSemver,
5
+ assertVersionBump,
6
+ } from "./assert-version-bump.mjs";
10
7
 
11
8
  describe("assert-version-bump", () => {
12
- it("runs successfully when local version is greater than published version", async () => {
13
- const { stdout } = await execFileAsync(process.execPath, [scriptPath]);
14
- expect(stdout).toContain("Checking version bump for package");
9
+ describe("parseSemver", () => {
10
+ it("parses valid semver strings correctly", () => {
11
+ expect(parseSemver("1.23.4")).toEqual({
12
+ major: 1,
13
+ minor: 23,
14
+ patch: 4,
15
+ prerelease: null,
16
+ raw: "1.23.4",
17
+ });
18
+ expect(parseSemver("v2.0.0-beta.1")).toEqual({
19
+ major: 2,
20
+ minor: 0,
21
+ patch: 0,
22
+ prerelease: "beta.1",
23
+ raw: "2.0.0-beta.1",
24
+ });
25
+ });
26
+
27
+ it("returns null for invalid semver strings", () => {
28
+ expect(parseSemver("invalid")).toBeNull();
29
+ expect(parseSemver("")).toBeNull();
30
+ });
31
+ });
32
+
33
+ describe("compareSemver", () => {
34
+ it("correctly compares semver versions", () => {
35
+ expect(compareSemver("1.23.4", "1.23.3")).toBe(1);
36
+ expect(compareSemver("2.0.0", "1.99.99")).toBe(1);
37
+ expect(compareSemver("1.23.3", "1.23.3")).toBe(0);
38
+ expect(compareSemver("1.23.2", "1.23.3")).toBe(-1);
39
+ });
40
+ });
41
+
42
+ describe("assertVersionBump", () => {
43
+ it("passes when local version is greater than published version", async () => {
44
+ await expect(
45
+ assertVersionBump({
46
+ localPkgVersion: "1.23.4",
47
+ localLockVersion: "1.23.4",
48
+ pkgName: "shadow-claw",
49
+ publishedVersion: "1.23.3",
50
+ silent: true,
51
+ }),
52
+ ).resolves.toBeUndefined();
53
+ });
54
+
55
+ it("throws when local version is equal to published version", async () => {
56
+ await expect(
57
+ assertVersionBump({
58
+ localPkgVersion: "1.23.3",
59
+ localLockVersion: "1.23.3",
60
+ pkgName: "shadow-claw",
61
+ publishedVersion: "1.23.3",
62
+ silent: true,
63
+ }),
64
+ ).rejects.toThrow("Version assertion failed");
65
+ });
66
+
67
+ it("throws when package.json does not match package-lock.json", async () => {
68
+ await expect(
69
+ assertVersionBump({
70
+ localPkgVersion: "1.23.4",
71
+ localLockVersion: "1.23.3",
72
+ pkgName: "shadow-claw",
73
+ publishedVersion: "1.23.2",
74
+ silent: true,
75
+ }),
76
+ ).rejects.toThrow("Version mismatch");
77
+ });
15
78
  });
16
79
  });
package/bin/cli.mjs CHANGED
@@ -8,6 +8,12 @@ import { fileURLToPath } from "node:url";
8
8
  import { Command } from "commander";
9
9
 
10
10
  import { runBuild } from "./build/build.mjs";
11
+ import { runClientsCommand } from "./commands/clients.mjs";
12
+ import { runSendCommand } from "./commands/send.mjs";
13
+ import { runBackupCommand } from "./commands/backup.mjs";
14
+ import { runTasksCommand } from "./commands/tasks.mjs";
15
+ import { runPeerIdCommand } from "./commands/peer-id.mjs";
16
+ import { runWebRtcListenCommand } from "./commands/webrtc-listen.mjs";
11
17
 
12
18
  const __filename = fileURLToPath(import.meta.url);
13
19
  const __dirname = path.dirname(__filename);
@@ -421,4 +427,164 @@ slug: "home"
421
427
  }
422
428
  });
423
429
 
430
+ // ---------------------------------------------------------------------------
431
+ // PEER-ID / WEBRTC COMMAND
432
+ // ---------------------------------------------------------------------------
433
+ program
434
+ .command("peer-id [action] [customId]")
435
+ .alias("webrtc-peer-id")
436
+ .description(
437
+ "Get, generate, or renew the WebRTC CLI peer ID stored in .cache/cli-peer-id",
438
+ )
439
+ .option(
440
+ "-r, --renew",
441
+ "Force renewal / generation of a new WebRTC CLI peer ID",
442
+ )
443
+ .option("--set <id>", "Set a specific custom WebRTC CLI peer ID")
444
+ .option("--cache-dir <dir>", "Custom cache directory (defaults to .cache)")
445
+ .option("-q, --quiet", "Output only the peer ID string")
446
+ .option("--json", "Output peer ID and file path as JSON")
447
+ .action(async (action, customId, options) => {
448
+ const opts = { ...options };
449
+ if (customId && !opts.set) {
450
+ opts.set = customId;
451
+ }
452
+ await runPeerIdCommand(action, opts);
453
+ });
454
+
455
+ program
456
+ .command("webrtc [action] [customId]")
457
+ .description(
458
+ "Manage WebRTC CLI configuration and peer identity (.cache/cli-peer-id)\n\n" +
459
+ " Actions:\n" +
460
+ " (default) Show or generate the CLI peer ID\n" +
461
+ " listen Register as a live PeerJS peer — browser tabs can then\n" +
462
+ " connect directly without a control-plane connection",
463
+ )
464
+ .option(
465
+ "-r, --renew",
466
+ "Force renewal / generation of a new WebRTC CLI peer ID",
467
+ )
468
+ .option("--set <id>", "Set a specific custom WebRTC CLI peer ID")
469
+ .option("--cache-dir <dir>", "Custom cache directory (defaults to .cache)")
470
+ .option("-q, --quiet", "Output only the peer ID string")
471
+ .option("--json", "Output peer ID and file path as JSON")
472
+ // listen-specific options
473
+ .option("--host <host>", "PeerJS signaling server host (for listen)")
474
+ .option("--port <port>", "PeerJS signaling server port (for listen)", "8888")
475
+ .option("--path <path>", "PeerJS signaling server path (for listen)", "/")
476
+ .option(
477
+ "--secure",
478
+ "Use TLS (wss://) for the signaling server (for listen)",
479
+ false,
480
+ )
481
+ .option(
482
+ "--trusted-peer <id>",
483
+ "Accept connections only from this peer ID (repeatable, for listen)",
484
+ (v, prev) => (prev ? [...prev, v] : [v]),
485
+ [],
486
+ )
487
+ .option("--verbose", "Verbose connection logging (for listen)", false)
488
+ .option("--renew-peer-id", "Renew CLI peer ID before listening", false)
489
+ .action(async (action, customId, options) => {
490
+ if (action === "listen") {
491
+ await runWebRtcListenCommand(options);
492
+ return;
493
+ }
494
+ const opts = { ...options };
495
+ if (customId && !opts.set) {
496
+ opts.set = customId;
497
+ }
498
+ await runPeerIdCommand(action, opts);
499
+ });
500
+
501
+ // ---------------------------------------------------------------------------
502
+ // CLIENTS COMMAND
503
+ // ---------------------------------------------------------------------------
504
+ program
505
+ .command("clients")
506
+ .description("List connected / registered browser and Electron clients")
507
+ .option("--host <host>", "Control plane host")
508
+ .option("--port <port>", "Control plane port")
509
+ .option("--token <token>", "Control token")
510
+ .option("--transport <transport>", "Transport to use: http | webrtc", "http")
511
+ .option("--peer-id <id>", "Custom WebRTC CLI peer ID")
512
+ .option(
513
+ "--renew-peer-id",
514
+ "Renew WebRTC CLI peer ID before connecting",
515
+ false,
516
+ )
517
+ .option("--cache-dir <dir>", "Custom cache directory")
518
+ .action(async (options) => {
519
+ await runClientsCommand(options);
520
+ });
521
+
522
+ // ---------------------------------------------------------------------------
523
+ // SEND COMMAND
524
+ // ---------------------------------------------------------------------------
525
+ program
526
+ .command("send <message>")
527
+ .description("Send a message/prompt to a connected client")
528
+ .option("--client <id>", "Target client ID (defaults to first available)")
529
+ .option("--group <groupId>", "Target conversation group ID")
530
+ .option("--host <host>", "Control plane host")
531
+ .option("--port <port>", "Control plane port")
532
+ .option("--token <token>", "Control token")
533
+ .option("--transport <transport>", "Transport to use: http | webrtc", "http")
534
+ .option("--peer-id <id>", "Custom WebRTC CLI peer ID")
535
+ .option("--renew-peer-id", "Renew WebRTC CLI peer ID before sending", false)
536
+ .option("--cache-dir <dir>", "Custom cache directory")
537
+ .action(async (message, options) => {
538
+ await runSendCommand(message, options);
539
+ });
540
+
541
+ // ---------------------------------------------------------------------------
542
+ // BACKUP COMMAND
543
+ // ---------------------------------------------------------------------------
544
+ program
545
+ .command("backup [action]")
546
+ .description(
547
+ "Trigger or manage client workspace backups (trigger | list | delete)",
548
+ )
549
+ .option("--client <id>", "Client ID")
550
+ .option("--backup-id <id>", "Backup ID (for delete)")
551
+ .option("--group <groupId>", "Specific workspace group ID")
552
+ .option("--host <host>", "Control plane host")
553
+ .option("--port <port>", "Control plane port")
554
+ .option("--token <token>", "Control token")
555
+ .option("--transport <transport>", "Transport to use: http | webrtc", "http")
556
+ .option("--peer-id <id>", "Custom WebRTC CLI peer ID")
557
+ .option(
558
+ "--renew-peer-id",
559
+ "Renew WebRTC CLI peer ID before connecting",
560
+ false,
561
+ )
562
+ .option("--cache-dir <dir>", "Custom cache directory")
563
+ .action(async (action, options) => {
564
+ await runBackupCommand(action || "trigger", options);
565
+ });
566
+
567
+ // ---------------------------------------------------------------------------
568
+ // TASKS COMMAND
569
+ // ---------------------------------------------------------------------------
570
+ program
571
+ .command("tasks")
572
+ .description("List scheduled tasks on a connected client")
573
+ .option("--client <id>", "Target client ID")
574
+ .option("--group <groupId>", "Filter tasks by conversation group ID")
575
+ .option("--host <host>", "Control plane host")
576
+ .option("--port <port>", "Control plane port")
577
+ .option("--token <token>", "Control token")
578
+ .option("--transport <transport>", "Transport to use: http | webrtc", "http")
579
+ .option("--peer-id <id>", "Custom WebRTC CLI peer ID")
580
+ .option(
581
+ "--renew-peer-id",
582
+ "Renew WebRTC CLI peer ID before connecting",
583
+ false,
584
+ )
585
+ .option("--cache-dir <dir>", "Custom cache directory")
586
+ .action(async (options) => {
587
+ await runTasksCommand(options);
588
+ });
589
+
424
590
  program.parse(process.argv);
package/bin/cli.test.mjs CHANGED
@@ -49,6 +49,66 @@ describe("shadow-claw CLI", () => {
49
49
  expect(stdout).toContain("run");
50
50
  expect(stdout).toContain("serve");
51
51
  expect(stdout).toContain("init");
52
+ expect(stdout).toContain("clients");
53
+ expect(stdout).toContain("send");
54
+ expect(stdout).toContain("backup");
55
+ expect(stdout).toContain("tasks");
56
+ expect(stdout).toContain("peer-id");
57
+ expect(stdout).toContain("webrtc");
58
+ });
59
+
60
+ it("manages WebRTC CLI peer ID with peer-id command", async () => {
61
+ const cacheDir = path.join(tempDir, ".cache");
62
+
63
+ // 1. Get/generate new peer ID
64
+ const { stdout: out1 } = await execFileAsync(process.execPath, [
65
+ cliPath,
66
+ "peer-id",
67
+ "--cache-dir",
68
+ cacheDir,
69
+ "--json",
70
+ ]);
71
+ const json1 = JSON.parse(out1);
72
+ expect(json1.peerId).toMatch(/^cli-[0-9a-z]+$/);
73
+ expect(json1.renewed).toBe(true);
74
+
75
+ // 2. Subsequent call returns existing peer ID
76
+ const { stdout: out2 } = await execFileAsync(process.execPath, [
77
+ cliPath,
78
+ "peer-id",
79
+ "--cache-dir",
80
+ cacheDir,
81
+ "-q",
82
+ ]);
83
+ expect(out2.trim()).toBe(json1.peerId);
84
+
85
+ // 3. Renew peer ID
86
+ const { stdout: out3 } = await execFileAsync(process.execPath, [
87
+ cliPath,
88
+ "peer-id",
89
+ "--renew",
90
+ "--cache-dir",
91
+ cacheDir,
92
+ "--json",
93
+ ]);
94
+ const json3 = JSON.parse(out3);
95
+ expect(json3.peerId).not.toBe(json1.peerId);
96
+ expect(json3.renewed).toBe(true);
97
+
98
+ // 4. Set custom peer ID
99
+ const { stdout: out4 } = await execFileAsync(process.execPath, [
100
+ cliPath,
101
+ "peer-id",
102
+ "--set",
103
+ "my-custom-cli-peer",
104
+ "--cache-dir",
105
+ cacheDir,
106
+ "-q",
107
+ ]);
108
+ expect(out4.trim()).toBe("my-custom-cli-peer");
109
+
110
+ const saved = await readFile(path.join(cacheDir, "cli-peer-id"), "utf8");
111
+ expect(saved.trim()).toBe("my-custom-cli-peer");
52
112
  });
53
113
 
54
114
  it("initializes a new template with init command", async () => {
@@ -0,0 +1,70 @@
1
+ /**
2
+ * ShadowClaw CLI — `backup` command
3
+ * Triggers or lists workspace backups.
4
+ */
5
+
6
+ import { CliControlClient } from "../utils/control-client.mjs";
7
+
8
+ export async function runBackupCommand(action = "trigger", options = {}) {
9
+ const client = new CliControlClient(options);
10
+
11
+ try {
12
+ if (action === "list") {
13
+ const backups = await client.listBackups(options.client);
14
+ if (!backups || backups.length === 0) {
15
+ console.log("No backups found.");
16
+ return;
17
+ }
18
+
19
+ console.log(`Available backups (${backups.length}):\n`);
20
+ backups.forEach((b, idx) => {
21
+ const dateStr = new Date(b.timestamp).toISOString();
22
+ const sizeMb = (b.totalBytes / (1024 * 1024)).toFixed(2);
23
+ console.log(` ${idx + 1}. [${b.id}] Client: ${b.clientId}`);
24
+ console.log(` Date: ${dateStr}`);
25
+ console.log(` Files: ${b.fileCount} (${sizeMb} MB)\n`);
26
+ });
27
+ return;
28
+ }
29
+
30
+ if (action === "delete") {
31
+ if (!options.backupId) {
32
+ console.error("Error: --backup-id is required for delete action.");
33
+ process.exitCode = 1;
34
+ return;
35
+ }
36
+ await client.deleteBackup(options.backupId, options.client);
37
+ console.log(`Backup ${options.backupId} deleted.`);
38
+ return;
39
+ }
40
+
41
+ // Default: Trigger backup
42
+ let targetClientId = options.client;
43
+ if (!targetClientId) {
44
+ const clients = await client.listClients();
45
+ if (!clients || clients.length === 0) {
46
+ console.error("Error: No clients are connected to back up.");
47
+ process.exitCode = 1;
48
+ return;
49
+ }
50
+ targetClientId = clients[0].clientId;
51
+ }
52
+
53
+ console.log(`Requesting backup from client ${targetClientId}...`);
54
+ const result = await client.sendCommand(targetClientId, "trigger-backup", {
55
+ groupId: options.group,
56
+ token: client.token,
57
+ });
58
+
59
+ if (result.success) {
60
+ console.log("Backup completed successfully.");
61
+ console.log(JSON.stringify(result.data, null, 2));
62
+ } else {
63
+ console.error(`Backup failed: ${result.error}`);
64
+ process.exitCode = 1;
65
+ }
66
+ } catch (err) {
67
+ console.error(`Error executing backup command: ${err.message}`);
68
+ process.exitCode = 1;
69
+ }
70
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * ShadowClaw CLI — `clients` command
3
+ * Lists connected clients and their capabilities.
4
+ */
5
+
6
+ import { CliControlClient } from "../utils/control-client.mjs";
7
+
8
+ export async function runClientsCommand(options = {}) {
9
+ const client = new CliControlClient(options);
10
+ try {
11
+ const clients = await client.listClients();
12
+
13
+ if (!clients || clients.length === 0) {
14
+ console.log("No clients currently registered or connected.");
15
+ return;
16
+ }
17
+
18
+ console.log(`Connected / registered clients (${clients.length}):\n`);
19
+ clients.forEach((c, idx) => {
20
+ const lastSeenSec = Math.max(
21
+ 0,
22
+ Math.floor((Date.now() - c.lastSeen) / 1000),
23
+ );
24
+ const lastSeenStr =
25
+ lastSeenSec < 60
26
+ ? `${lastSeenSec}s ago`
27
+ : `${Math.floor(lastSeenSec / 60)}m ago`;
28
+
29
+ console.log(` ${idx + 1}. ${c.deviceLabel} (id: ${c.clientId})`);
30
+ if (c.peerId) {
31
+ console.log(` Peer ID: ${c.peerId}`);
32
+ }
33
+ console.log(` Capabilities: ${c.capabilities.join(", ") || "none"}`);
34
+ console.log(` Version: ${c.version}`);
35
+ console.log(` Last seen: ${lastSeenStr}\n`);
36
+ });
37
+ } catch (err) {
38
+ console.error(`Error fetching clients: ${err.message}`);
39
+ process.exitCode = 1;
40
+ }
41
+ }