shadow-claw 1.23.4 → 1.24.1

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 (267) 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 +12 -3
  20. package/dist/public/README.md +42 -5
  21. package/dist/public/agent.worker.js +50 -50
  22. package/dist/public/backup-controller-CvzDOq4T.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-UfQ6HUlv.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 +169 -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-9niHBj9n.js +1 -0
  48. package/dist/public/e2e/README.md +5 -1
  49. package/dist/public/{e2e-bridge-BQ4KzUt5.js → e2e-bridge-D28tOggc.js} +1 -1
  50. package/dist/public/file-viewer-BVg3B_JH.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-DKNht5ph.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-SiFq0PTy.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-8w9-exfN.js +15 -0
  66. package/dist/public/{orchestrator-DRfEudXk.js → orchestrator-tHWrqi_F.js} +676 -676
  67. package/dist/public/{peerjs-CurBuKvk.js → peerjs-D8hck8Ih.js} +1 -1
  68. package/dist/public/push-client-D0lkwrK0.js +1 -0
  69. package/dist/public/service-worker/fetch-proxy.js +1 -1
  70. package/dist/public/service-worker/push-handler.js +1 -1
  71. package/dist/public/service-worker.js +1 -1
  72. package/dist/public/{shadow-claw-5IHaDDMJ.js → shadow-claw-Cs8wbk3d.js} +2 -2
  73. package/dist/public/{shadow-claw-accounts-CZvz4iQz.js → shadow-claw-accounts-B4sBTQEj.js} +1 -1
  74. package/dist/public/{shadow-claw-channels-BKXFyRS3.js → shadow-claw-channels-mY-JMvib.js} +1 -1
  75. package/dist/public/{shadow-claw-chat-CCLuhWdI.js → shadow-claw-chat-DGPKpmU5.js} +6 -6
  76. package/dist/public/shadow-claw-control-plane-B1F0jnPk.js +199 -0
  77. package/dist/public/{shadow-claw-conversations-DV-87UUh.js → shadow-claw-conversations-0Q-VC_7N.js} +1 -1
  78. package/dist/public/shadow-claw-dialog-BrOwAOdk.js +1 -0
  79. package/dist/public/{shadow-claw-file-viewer-Dk2dbAUd.js → shadow-claw-file-viewer-AZfG4D-u.js} +1 -1
  80. package/dist/public/shadow-claw-files-Bx4v0Ggp.js +840 -0
  81. package/dist/public/{shadow-claw-git-DRA7_qck.js → shadow-claw-git-DD7iQRxq.js} +2 -2
  82. package/dist/public/{shadow-claw-integrations-1Xu5snuV.js → shadow-claw-integrations-D7ufpwhX.js} +1 -1
  83. package/dist/public/{shadow-claw-llm-6Aci1BDP.js → shadow-claw-llm-CfeDBZKB.js} +2 -2
  84. package/dist/public/{shadow-claw-mcp-remote-BUjAeVXC.js → shadow-claw-mcp-remote-kY4LMFNN.js} +1 -1
  85. package/dist/public/{shadow-claw-networking-FQe4nCp3.js → shadow-claw-networking-e0EarDVs.js} +1 -1
  86. package/dist/public/{shadow-claw-notifications-DV2wibL3.js → shadow-claw-notifications-BkxuDEQI.js} +3 -3
  87. package/dist/public/{shadow-claw-page-header-B63S4HTU.js → shadow-claw-page-header-DMKbkv3s.js} +1 -1
  88. package/dist/public/{shadow-claw-pages-DE3jWj5K.js → shadow-claw-pages-BWSE91I1.js} +1 -1
  89. package/dist/public/{shadow-claw-settings-S6DAKfYx.js → shadow-claw-settings-D6lPBn6m.js} +10 -3
  90. package/dist/public/{shadow-claw-storage-DaacA3_K.js → shadow-claw-storage-Crs6LY15.js} +2 -2
  91. package/dist/public/{shadow-claw-task-server-IjKX042p.js → shadow-claw-task-server-CKm_Ennl.js} +25 -4
  92. package/dist/public/{shadow-claw-tasks-Cy0JYmGU.js → shadow-claw-tasks-Czx6Ucf5.js} +1 -1
  93. package/dist/public/{shadow-claw-terminal-znAt7HyP.js → shadow-claw-terminal-Dmtr9KY1.js} +1 -1
  94. package/dist/public/{shadow-claw-tools-Cutv9Si5.js → shadow-claw-tools-t3Qq4TDO.js} +1 -1
  95. package/dist/public/{shadow-claw-webvm-BH8mPDEu.js → shadow-claw-webvm-CIuSywD_.js} +1 -1
  96. package/dist/public/{syncWebMcpRegistration-BqJxaMGR.js → syncWebMcpRegistration-DzC80wVG.js} +1 -1
  97. package/dist/server.js +286 -189
  98. package/package.json +2 -1
  99. package/src/__mocks__/jszip.cjs +15 -5
  100. package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +35 -0
  101. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +117 -0
  102. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +81 -0
  103. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.test.ts +127 -0
  104. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.ts +292 -0
  105. package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +72 -0
  106. package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +20 -30
  107. package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +116 -0
  108. package/src/components/settings/shadow-claw-llm/model-ranking.test.ts +143 -51
  109. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +307 -4
  110. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.ts +8 -8
  111. package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +31 -0
  112. package/src/components/settings/shadow-claw-notifications/shadow-claw-notifications.test.ts +3 -0
  113. package/src/components/settings/shadow-claw-notifications/shadow-claw-notifications.ts +43 -9
  114. package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +186 -0
  115. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +76 -0
  116. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +23 -2
  117. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
  118. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +19 -0
  119. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.ts +2 -1
  120. package/src/components/shadow-claw/utils/initChatSplitResize.test.ts +180 -13
  121. package/src/components/shadow-claw/utils/setupEffects.test.ts +39 -0
  122. package/src/components/shadow-claw-a2ui/catalog/basic/shared.test.ts +117 -0
  123. package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +1159 -1
  124. package/src/components/shadow-claw-chat/shadow-claw-chat.test.ts +1523 -19
  125. package/src/components/shadow-claw-chat/utils/escapeHtml.test.ts +13 -0
  126. package/src/components/shadow-claw-chat/utils/getPeerChatDisplayStatus.test.ts +20 -0
  127. package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +494 -0
  128. package/src/components/shadow-claw-dialog/shadow-claw-dialog.ts +13 -2
  129. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +273 -0
  130. package/src/components/shadow-claw-files/shadow-claw-files.test.ts +408 -1
  131. package/src/components/shadow-claw-pdf-viewer/utils/installGetOrInsertComputedPolyfill.test.ts +65 -0
  132. package/src/components/shadow-claw-settings/shadow-claw-settings.html +7 -0
  133. package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +221 -0
  134. package/src/components/shadow-claw-settings/shadow-claw-settings.ts +2 -0
  135. package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +407 -0
  136. package/src/components/shadow-claw-terminal/utils/consumeEscapeSequence.test.ts +58 -0
  137. package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +80 -0
  138. package/src/config/config.ts +10 -0
  139. package/src/core/backup-controller.test.ts +101 -0
  140. package/src/core/backup-controller.ts +166 -0
  141. package/src/core/control-plane-client.test.ts +415 -0
  142. package/src/core/control-plane-client.ts +441 -0
  143. package/src/core/orchestrator/orchestrator.ts +4 -0
  144. package/src/core/orchestrator/utils/compactContext.test.ts +78 -0
  145. package/src/core/orchestrator/utils/operations/channel.test.ts +254 -9
  146. package/src/core/orchestrator/utils/operations/provider.test.ts +326 -34
  147. package/src/core/orchestrator/utils/operations/task.ts +46 -15
  148. package/src/core/orchestrator/utils/operations/vm.test.ts +114 -2
  149. package/src/core/orchestrator/utils/syncProxyConfigToServiceWorker.test.ts +60 -0
  150. package/src/core/utils/initControlPlane.test.ts +276 -0
  151. package/src/core/utils/initControlPlane.ts +437 -0
  152. package/src/core/utils/initializeApp.ts +18 -0
  153. package/src/db/getSession.test.ts +13 -1
  154. package/src/server/app.test.ts +12 -0
  155. package/src/server/app.ts +20 -8
  156. package/src/server/client-registry.test.ts +250 -0
  157. package/src/server/client-registry.ts +477 -0
  158. package/src/server/config.ts +15 -0
  159. package/src/server/control-plane-types.ts +120 -0
  160. package/src/server/control-plane.test.ts +508 -0
  161. package/src/server/control-plane.ts +607 -0
  162. package/src/server/middleware/pna.test.ts +15 -3
  163. package/src/server/middleware/pna.ts +6 -2
  164. package/src/server/routes/backup.test.ts +277 -0
  165. package/src/server/routes/backup.ts +291 -0
  166. package/src/server/routes/integrations-email.test.ts +532 -0
  167. package/src/server/routes/llamafile.test.ts +172 -48
  168. package/src/server/routes/mesh-llm.test.ts +192 -0
  169. package/src/server/routes/ollama.test.ts +49 -0
  170. package/src/server/server-peer.test.ts +86 -0
  171. package/src/server/server-peer.ts +330 -0
  172. package/src/server/server.ts +37 -0
  173. package/src/server/services/llamafile-manager.test.ts +230 -6
  174. package/src/server/services/transformers-runtime.test.ts +186 -18
  175. package/src/server/webrtc.test.ts +21 -0
  176. package/src/server/webrtc.ts +72 -0
  177. package/src/service-worker/fetch-proxy-rules.test.ts +41 -0
  178. package/src/service-worker/fetch-proxy-rules.ts +15 -8
  179. package/src/service-worker/fetch-proxy.test.ts +64 -1
  180. package/src/service-worker/fetch-proxy.ts +18 -2
  181. package/src/service-worker/push-handler.ts +39 -0
  182. package/src/shell/vm.test.ts +744 -12
  183. package/src/storage/ensureMainGroupIndex.test.ts +153 -0
  184. package/src/storage/getGroupFile.test.ts +66 -0
  185. package/src/storage/memoryStorage.test.ts +82 -2
  186. package/src/storage/writeGroupFileBytes.test.ts +143 -0
  187. package/src/stores/chat-ui.test.ts +20 -0
  188. package/src/stores/orchestrator.test.ts +134 -0
  189. package/src/stores/toast.test.ts +50 -0
  190. package/src/stores/tools.test.ts +147 -10
  191. package/src/subsystems/channels/imessage.test.ts +59 -0
  192. package/src/subsystems/channels/peerjs.test.ts +500 -62
  193. package/src/subsystems/channels/peerjs.ts +71 -4
  194. package/src/subsystems/channels/telegram.test.ts +177 -0
  195. package/src/subsystems/email/catalog.test.ts +1 -0
  196. package/src/subsystems/email/connections.test.ts +39 -1
  197. package/src/subsystems/git/credentials.test.ts +160 -825
  198. package/src/subsystems/git/git.test.ts +463 -0
  199. package/src/subsystems/notifications/__mocks__/node-sqlite.cjs +12 -1
  200. package/src/subsystems/notifications/push-client.test.ts +5 -1
  201. package/src/subsystems/notifications/push-client.ts +50 -12
  202. package/src/subsystems/notifications/push-routes.test.ts +33 -0
  203. package/src/subsystems/notifications/push-routes.ts +23 -0
  204. package/src/subsystems/notifications/push-store.ts +1 -1
  205. package/src/subsystems/providers/builtin-ai-tasks.test.ts +74 -0
  206. package/src/subsystems/providers/litert-lm-provider.test.ts +212 -51
  207. package/src/subsystems/providers/model-registry.test.ts +115 -0
  208. package/src/subsystems/providers/providers.test.ts +94 -1
  209. package/src/subsystems/providers/transformers-js-provider.test.ts +279 -0
  210. package/src/subsystems/providers/utils/createModelCacheFetch.test.ts +2 -2
  211. package/src/subsystems/providers/utils/downloadModelToCache.test.ts +2 -2
  212. package/src/subsystems/providers/utils/loadModelStream.test.ts +2 -2
  213. package/src/subsystems/skills/activateSkill.test.ts +101 -0
  214. package/src/subsystems/skills/discoverSkills.test.ts +251 -0
  215. package/src/subsystems/tools/cron.test.ts +64 -0
  216. package/src/subsystems/tools/declarative.test.ts +255 -89
  217. package/src/testing/assets/v86.9pfs/libv86.mjs +63 -0
  218. package/src/testing/assets/v86.ext2/libv86.mjs +3 -0
  219. package/src/testing/assets_missing_v86/assets/v86.9pfs/libv86.mjs +1 -0
  220. package/src/testing/jest-setup.ts +328 -40
  221. package/src/testing/mock-v86.mjs +49 -0
  222. package/src/ui/a2ui/types.ts +4 -3
  223. package/src/ui/a2ui/utils/normaliseComponentsToMap.test.ts +102 -0
  224. package/src/ui/a2ui/utils/resolveChildIds.test.ts +63 -0
  225. package/src/ui/a2ui/utils/resolveDynamicNumber.test.ts +13 -1
  226. package/src/worker/tools/bash/utils/waitForVMReady.test.ts +40 -0
  227. package/src/worker/tools/email/email.test.ts +193 -8
  228. package/src/worker/tools/email/utils/asEmailAttachmentInputs.test.ts +47 -0
  229. package/src/worker/tools/git/git.test.ts +214 -3
  230. package/src/worker/tools/rooms/invite-to-room.test.ts +54 -0
  231. package/src/worker/tools/rooms/leave-room.test.ts +40 -0
  232. package/src/worker/tools/rooms/list-room-members.test.ts +75 -0
  233. package/src/worker/tools/tasks/tasks-utils.test.ts +22 -0
  234. package/src/worker/tools/ui/ask-user.test.ts +50 -0
  235. package/src/worker/tools/ui/clear-chat.test.ts +19 -0
  236. package/src/worker/tools/ui/get-current-time.test.ts +19 -0
  237. package/src/worker/tools/ui/javascript.test.ts +116 -0
  238. package/src/worker/tools/ui/list-components.test.ts +10 -0
  239. package/src/worker/tools/ui/list-tool-profiles.test.ts +42 -0
  240. package/src/worker/tools/ui/manage-tools.test.ts +34 -0
  241. package/src/worker/tools/ui/render-component.test.ts +346 -0
  242. package/src/worker/tools/ui/send-notification.test.ts +40 -0
  243. package/src/worker/tools/ui/utils/getAllowFullInternetAccess.test.ts +31 -0
  244. package/src/worker/tools/ui/utils/parseBooleanConfig.test.ts +25 -0
  245. package/src/worker/tools/workspace/attach-file.test.ts +84 -0
  246. package/src/worker/tools/workspace/copy-file.test.ts +69 -0
  247. package/src/worker/tools/workspace/create-directory.test.ts +40 -0
  248. package/src/worker/tools/workspace/delete-file.test.ts +73 -0
  249. package/src/worker/tools/workspace/diff-files.test.ts +94 -0
  250. package/src/worker/tools/workspace/list-files.test.ts +36 -0
  251. package/src/worker/tools/workspace/move-file.test.ts +66 -0
  252. package/src/worker/tools/workspace/open-file.test.ts +57 -0
  253. package/src/worker/tools/workspace/patch-file.test.ts +81 -0
  254. package/src/worker/tools/workspace/read-file.test.ts +116 -0
  255. package/src/worker/tools/workspace/send-file.test.ts +82 -0
  256. package/src/worker/tools/workspace/update-memory.test.ts +29 -0
  257. package/src/worker/tools/workspace/write-file.test.ts +28 -0
  258. package/src/worker/utils/rate-limit.test.ts +199 -0
  259. package/src/worker/utils/tool-state.test.ts +29 -0
  260. package/src/worker/utils/transformers-js-utils.test.ts +59 -0
  261. package/dist/public/config-BYAuxx3j.js +0 -1
  262. package/dist/public/downloadGroupFile-Drdvg1Ev.js +0 -1
  263. package/dist/public/file-viewer-CcZYw-PS.js +0 -1
  264. package/dist/public/orchestrator-o9Fj67BN.js +0 -15
  265. package/dist/public/push-client-Dii-vPEh.js +0 -1
  266. package/dist/public/shadow-claw-dialog-ByyHgxM9.js +0 -1
  267. package/dist/public/shadow-claw-files-BpfpJIuP.js +0 -840
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).
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
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * ShadowClaw CLI — `peer-id` command
3
+ * Manages the WebRTC CLI peer ID stored in .cache/cli-peer-id.
4
+ */
5
+
6
+ import {
7
+ getCliPeerIdFilePath,
8
+ getOrCreateCliPeerId,
9
+ readCliPeerId,
10
+ renewCliPeerId,
11
+ } from "../utils/webrtc-control-client.mjs";
12
+
13
+ export async function runPeerIdCommand(action = "get", options = {}) {
14
+ const cacheDir = options.cacheDir;
15
+ const filePath = getCliPeerIdFilePath(cacheDir);
16
+
17
+ const shouldRenew = Boolean(
18
+ options.renew ||
19
+ action === "renew" ||
20
+ action === "generate" ||
21
+ action === "new",
22
+ );
23
+ const customSetId =
24
+ options.set || (action === "set" && options.id ? options.id : null);
25
+
26
+ let peerId = "";
27
+ let isRenewed = false;
28
+
29
+ if (customSetId) {
30
+ peerId = renewCliPeerId(customSetId, cacheDir);
31
+ isRenewed = true;
32
+ } else if (shouldRenew) {
33
+ peerId = renewCliPeerId(undefined, cacheDir);
34
+ isRenewed = true;
35
+ } else {
36
+ const existing = readCliPeerId(cacheDir);
37
+ if (existing) {
38
+ peerId = existing;
39
+ isRenewed = false;
40
+ } else {
41
+ peerId = getOrCreateCliPeerId(undefined, cacheDir);
42
+ isRenewed = true;
43
+ }
44
+ }
45
+
46
+ if (options.quiet) {
47
+ console.log(peerId);
48
+ return { peerId, filePath, isRenewed };
49
+ }
50
+
51
+ if (options.json) {
52
+ console.log(
53
+ JSON.stringify(
54
+ {
55
+ peerId,
56
+ filePath,
57
+ renewed: isRenewed,
58
+ },
59
+ null,
60
+ 2,
61
+ ),
62
+ );
63
+ return { peerId, filePath, isRenewed };
64
+ }
65
+
66
+ if (customSetId) {
67
+ console.log(`WebRTC CLI Peer ID set to: ${peerId}`);
68
+ } else if (isRenewed) {
69
+ console.log(`WebRTC CLI Peer ID created/renewed: ${peerId}`);
70
+ } else {
71
+ console.log(`WebRTC CLI Peer ID: ${peerId}`);
72
+ }
73
+ console.log(`Stored in: ${filePath}`);
74
+
75
+ return { peerId, filePath, isRenewed };
76
+ }
@@ -0,0 +1,104 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import {
5
+ jest,
6
+ describe,
7
+ it,
8
+ expect,
9
+ beforeEach,
10
+ afterEach,
11
+ } from "@jest/globals";
12
+ import { runPeerIdCommand } from "./peer-id.mjs";
13
+ import {
14
+ getCliPeerIdFilePath,
15
+ readCliPeerId,
16
+ } from "../utils/webrtc-control-client.mjs";
17
+
18
+ describe("runPeerIdCommand", () => {
19
+ let tempDir;
20
+ let logSpy;
21
+
22
+ beforeEach(() => {
23
+ tempDir = path.join(
24
+ os.tmpdir(),
25
+ "test-cli-peer-id-" +
26
+ Date.now() +
27
+ "-" +
28
+ Math.random().toString(36).slice(2),
29
+ );
30
+ logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
31
+ });
32
+
33
+ afterEach(() => {
34
+ if (logSpy) logSpy.mockRestore();
35
+ try {
36
+ if (fs.existsSync(tempDir)) {
37
+ fs.rmSync(tempDir, { recursive: true, force: true });
38
+ }
39
+ } catch (_) {}
40
+ });
41
+
42
+ it("creates and retrieves a new peer ID if none exists", async () => {
43
+ const result = await runPeerIdCommand("get", { cacheDir: tempDir });
44
+ expect(result.peerId).toMatch(/^cli-[0-9a-z]+$/);
45
+ expect(result.isRenewed).toBe(true);
46
+
47
+ const saved = readCliPeerId(tempDir);
48
+ expect(saved).toBe(result.peerId);
49
+ });
50
+
51
+ it("returns existing peer ID without changing it on subsequent calls", async () => {
52
+ const res1 = await runPeerIdCommand("get", { cacheDir: tempDir });
53
+ const res2 = await runPeerIdCommand("get", { cacheDir: tempDir });
54
+
55
+ expect(res2.peerId).toBe(res1.peerId);
56
+ expect(res2.isRenewed).toBe(false);
57
+ });
58
+
59
+ it("renews peer ID when action is 'renew'", async () => {
60
+ const res1 = await runPeerIdCommand("get", { cacheDir: tempDir });
61
+ const res2 = await runPeerIdCommand("renew", { cacheDir: tempDir });
62
+
63
+ expect(res2.peerId).not.toBe(res1.peerId);
64
+ expect(res2.isRenewed).toBe(true);
65
+ expect(readCliPeerId(tempDir)).toBe(res2.peerId);
66
+ });
67
+
68
+ it("renews peer ID when --renew flag is passed", async () => {
69
+ const res1 = await runPeerIdCommand("get", { cacheDir: tempDir });
70
+ const res2 = await runPeerIdCommand("get", {
71
+ cacheDir: tempDir,
72
+ renew: true,
73
+ });
74
+
75
+ expect(res2.peerId).not.toBe(res1.peerId);
76
+ expect(res2.isRenewed).toBe(true);
77
+ });
78
+
79
+ it("sets a custom peer ID when --set is passed", async () => {
80
+ const res = await runPeerIdCommand("get", {
81
+ cacheDir: tempDir,
82
+ set: "custom-my-peer",
83
+ });
84
+
85
+ expect(res.peerId).toBe("custom-my-peer");
86
+ expect(res.isRenewed).toBe(true);
87
+ expect(readCliPeerId(tempDir)).toBe("custom-my-peer");
88
+ });
89
+
90
+ it("supports quiet mode and json mode", async () => {
91
+ const resQuiet = await runPeerIdCommand("get", {
92
+ cacheDir: tempDir,
93
+ quiet: true,
94
+ });
95
+ expect(resQuiet.peerId).toBeDefined();
96
+
97
+ const resJson = await runPeerIdCommand("get", {
98
+ cacheDir: tempDir,
99
+ json: true,
100
+ });
101
+ expect(resJson.peerId).toBe(resQuiet.peerId);
102
+ expect(resJson.filePath).toBe(getCliPeerIdFilePath(tempDir));
103
+ });
104
+ });
@@ -0,0 +1,51 @@
1
+ /**
2
+ * ShadowClaw CLI — `send` command
3
+ * Dispatches a prompt / message to a connected client.
4
+ */
5
+
6
+ import { CliControlClient } from "../utils/control-client.mjs";
7
+
8
+ export async function runSendCommand(message, options = {}) {
9
+ if (!message || typeof message !== "string" || !message.trim()) {
10
+ console.error("Error: Message cannot be empty.");
11
+ process.exitCode = 1;
12
+ return;
13
+ }
14
+
15
+ const client = new CliControlClient(options);
16
+ try {
17
+ let targetClientId = options.client;
18
+
19
+ if (!targetClientId) {
20
+ const clients = await client.listClients();
21
+ if (!clients || clients.length === 0) {
22
+ console.error("Error: No clients are connected.");
23
+ process.exitCode = 1;
24
+ return;
25
+ }
26
+ targetClientId = clients[0].clientId;
27
+ console.log(
28
+ `Targeting client: ${clients[0].deviceLabel} (${targetClientId})`,
29
+ );
30
+ }
31
+
32
+ console.log(`Sending message to ${targetClientId}...`);
33
+ const result = await client.sendCommand(targetClientId, "send-message", {
34
+ text: message.trim(),
35
+ groupId: options.group,
36
+ });
37
+
38
+ if (result.success) {
39
+ console.log("Message successfully dispatched.");
40
+ if (result.data) {
41
+ console.log(JSON.stringify(result.data, null, 2));
42
+ }
43
+ } else {
44
+ console.error(`Error from client: ${result.error}`);
45
+ process.exitCode = 1;
46
+ }
47
+ } catch (err) {
48
+ console.error(`Error sending message: ${err.message}`);
49
+ process.exitCode = 1;
50
+ }
51
+ }