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
@@ -17,25 +17,35 @@ describe("TransformersRuntimeService", () => {
17
17
  jest.resetModules();
18
18
  jest.useFakeTimers();
19
19
 
20
- mkdirMock = jest.fn();
20
+ mkdirMock = jest.fn().mockResolvedValue(undefined as never);
21
21
  readFileMock = jest.fn();
22
- writeFileMock = jest.fn();
22
+ writeFileMock = jest.fn().mockResolvedValue(undefined as never);
23
23
  statMock = jest.fn();
24
- unlinkMock = jest.fn();
25
- readdirMock = jest.fn();
24
+ unlinkMock = jest.fn().mockResolvedValue(undefined as never);
25
+ readdirMock = jest
26
+ .fn()
27
+ .mockResolvedValue(["model1.onnx", "model2.onnx"] as never);
26
28
 
27
- processorMock = Object.assign(jest.fn(), {
28
- apply_chat_template: jest.fn(() => "prompt"),
29
- tokenizer: {},
30
- dispose: jest.fn(),
31
- });
29
+ processorMock = Object.assign(
30
+ jest.fn().mockImplementation(() => ({
31
+ input_ids: { dims: [1, 5] },
32
+ })),
33
+ {
34
+ apply_chat_template: jest.fn(() => "prompt"),
35
+ tokenizer: {
36
+ apply_chat_template: jest.fn(() => "prompt"),
37
+ },
38
+ batch_decode: jest.fn(() => ["Generated response"]),
39
+ dispose: jest.fn(),
40
+ },
41
+ );
32
42
 
33
43
  modelMock = {
34
44
  generate: jest.fn(async ({ streamer }: any) => {
35
45
  streamer?.callback_function?.("Hello");
36
46
 
37
47
  return {
38
- input_ids: { dims: [1, 5] },
48
+ slice: jest.fn(() => [1, 2, 3]),
39
49
  };
40
50
  }),
41
51
  dispose: jest.fn(),
@@ -52,10 +62,16 @@ describe("TransformersRuntimeService", () => {
52
62
  Gemma4ForConditionalGeneration: {
53
63
  from_pretrained: jest.fn(async () => modelMock),
54
64
  },
65
+ AutoModelForCausalLM: {
66
+ from_pretrained: jest.fn(async () => modelMock),
67
+ },
68
+ AutoModelForImageTextToText: {
69
+ from_pretrained: jest.fn(async () => modelMock),
70
+ },
55
71
  TextStreamer: class {
56
72
  callback_function: any;
57
73
  constructor(_tokenizer: any, opts: any) {
58
- this.callback_function = opts.callback_function;
74
+ this.callback_function = opts?.callback_function;
59
75
  }
60
76
  },
61
77
  };
@@ -86,7 +102,7 @@ describe("TransformersRuntimeService", () => {
86
102
  }
87
103
 
88
104
  describe("runChatCompletion", () => {
89
- it("loads model and generates text", async () => {
105
+ it("loads Gemma 4 model and generates text", async () => {
90
106
  const service = await getService();
91
107
  const onToken = jest.fn();
92
108
 
@@ -98,7 +114,6 @@ describe("TransformersRuntimeService", () => {
98
114
  onToken,
99
115
  });
100
116
 
101
- // Advance timers to handle internal awaitOperation timeout poll
102
117
  await jest.advanceTimersByTimeAsync(0);
103
118
 
104
119
  const result = await promise;
@@ -109,10 +124,44 @@ describe("TransformersRuntimeService", () => {
109
124
  ).toHaveBeenCalled();
110
125
  });
111
126
 
127
+ it("loads standard non-Gemma model (Qwen 3)", async () => {
128
+ const service = await getService();
129
+ const onToken = jest.fn();
130
+
131
+ const promise = service.runChatCompletion({
132
+ modelId: "onnx-community/Qwen3-0.6B-ONNX",
133
+ messages: [{ role: "user", content: "hi" }],
134
+ maxCompletionTokens: 10,
135
+ verbose: false,
136
+ onToken,
137
+ });
138
+
139
+ await jest.advanceTimersByTimeAsync(0);
140
+
141
+ const result = await promise;
142
+ expect(result.text).toBe("Hello");
143
+ expect(
144
+ transformersMock.AutoModelForImageTextToText.from_pretrained,
145
+ ).toHaveBeenCalled();
146
+ });
147
+
148
+ it("throws error for unsupported model ID", async () => {
149
+ const service = await getService();
150
+
151
+ await expect(
152
+ service.runChatCompletion({
153
+ modelId: "unsupported/model",
154
+ messages: [{ role: "user", content: "hi" }],
155
+ maxCompletionTokens: 10,
156
+ verbose: false,
157
+ }),
158
+ ).rejects.toThrow(/not supported by Transformers.js/);
159
+ });
160
+
112
161
  it("times out if model loading takes too long", async () => {
113
162
  process.env.TRANSFORMERS_JS_REQUEST_TIMEOUT_MS = "100";
114
163
  transformersMock.Gemma4ForConditionalGeneration.from_pretrained.mockImplementation(
115
- () => new Promise(() => {}), // Never resolve
164
+ () => new Promise(() => {}),
116
165
  );
117
166
 
118
167
  const service = await getService();
@@ -130,14 +179,122 @@ describe("TransformersRuntimeService", () => {
130
179
  await wrappedPromise;
131
180
  delete process.env.TRANSFORMERS_JS_REQUEST_TIMEOUT_MS;
132
181
  });
182
+
183
+ it("handles batch_decode fallback when streamer produces empty string", async () => {
184
+ modelMock.generate.mockImplementationOnce(async () => ({
185
+ slice: jest.fn(() => [1, 2, 3]),
186
+ }));
187
+
188
+ const service = await getService();
189
+ const promise = service.runChatCompletion({
190
+ modelId: "onnx-community/gemma-4-E2B-it-ONNX",
191
+ messages: [{ role: "user", content: "hi" }],
192
+ maxCompletionTokens: 10,
193
+ verbose: false,
194
+ });
195
+
196
+ await jest.advanceTimersByTimeAsync(0);
197
+
198
+ const result = await promise;
199
+ expect(result.text).toBe("Generated response");
200
+ });
201
+ });
202
+
203
+ describe("fetchDynamicModels", () => {
204
+ it("returns models from cached models.json if present", async () => {
205
+ readFileMock.mockResolvedValueOnce(
206
+ JSON.stringify({
207
+ models: [
208
+ {
209
+ id: "onnx-community/custom-model",
210
+ name: "Custom (ONNX)",
211
+ context_length: 32000,
212
+ max_completion_tokens: 8192,
213
+ supports_tools: true,
214
+ },
215
+ ],
216
+ }),
217
+ );
218
+
219
+ const service = await getService();
220
+ const models = await service.fetchDynamicModels();
221
+ expect(models.length).toBe(1);
222
+ expect(models[0].id).toBe("onnx-community/custom-model");
223
+ });
224
+
225
+ it("fetches from discovery URL when cache file is missing", async () => {
226
+ readFileMock
227
+ .mockRejectedValueOnce(new Error("ENOENT")) // cache file missing
228
+ .mockResolvedValueOnce(
229
+ JSON.stringify([
230
+ { id: "onnx-community/gemma-4-26B-it-ONNX" },
231
+ { id: "onnx-community/gemma-4-E4B-it-ONNX" },
232
+ ]),
233
+ ); // discovery part file content
234
+
235
+ statMock.mockRejectedValue(new Error("ENOENT"));
236
+
237
+ (globalThis as any).fetch = (jest.fn() as any).mockResolvedValue({
238
+ ok: true,
239
+ status: 200,
240
+ body: {
241
+ getReader: () => ({
242
+ read: (jest.fn() as any)
243
+ .mockResolvedValueOnce({
244
+ done: false,
245
+ value: new Uint8Array([123]),
246
+ })
247
+ .mockResolvedValueOnce({ done: true, value: undefined }),
248
+ releaseLock: jest.fn(),
249
+ }),
250
+ },
251
+ });
252
+
253
+ const service = await getService();
254
+ const models = await service.fetchDynamicModels();
255
+ expect(models.length).toBeGreaterThan(0);
256
+ expect(models.some((m) => m.id.includes("gemma-4"))).toBe(true);
257
+ });
133
258
  });
134
259
 
135
- describe("disposeRuntime", () => {
260
+ describe("getDownloadStatus & getDiskCacheStatus & prewarmModel", () => {
261
+ it("returns download status and allows testing status injection", async () => {
262
+ const service = await getService();
263
+ expect(service.getDownloadStatus().status).toBe("idle");
264
+
265
+ service.__setDownloadStatusForTests({ status: "running", progress: 0.5 });
266
+ expect(service.getDownloadStatus().status).toBe("running");
267
+ expect(service.getDownloadStatus().progress).toBe(0.5);
268
+ });
269
+
270
+ it("returns disk cache status", async () => {
271
+ statMock.mockResolvedValue({ isFile: () => true });
272
+ readdirMock.mockResolvedValue(["entry1", "entry2"]);
273
+
274
+ const service = await getService();
275
+ const status = await service.getDiskCacheStatus();
276
+ expect(status.modelsCatalogExists).toBe(true);
277
+ expect(status.runtimeCacheDirExists).toBe(true);
278
+ expect(status.runtimeCacheEntryCount).toBe(2);
279
+ });
280
+
281
+ it("prewarms model and returns metadata", async () => {
282
+ const service = await getService();
283
+ const info = await service.prewarmModel({
284
+ modelId: "onnx-community/gemma-4-E2B-it-ONNX",
285
+ verbose: false,
286
+ });
287
+
288
+ expect(info.modelId).toBe("onnx-community/gemma-4-E2B-it-ONNX");
289
+ expect(info.loader).toBeTruthy();
290
+ });
291
+ });
292
+
293
+ describe("disposeRuntime and reset", () => {
136
294
  it("disposes model and processor", async () => {
137
295
  const service = await getService();
138
296
  const modelId = "onnx-community/gemma-4-E2B-it-ONNX";
139
297
 
140
- // Load it first
141
298
  await service.runChatCompletion({
142
299
  modelId,
143
300
  messages: [{ role: "user", content: "hi" }],
@@ -150,6 +307,19 @@ describe("TransformersRuntimeService", () => {
150
307
  expect(modelMock.dispose).toHaveBeenCalled();
151
308
  expect(processorMock.dispose).toHaveBeenCalled();
152
309
  });
310
+
311
+ it("resets runtime for tests", async () => {
312
+ const service = await getService();
313
+ service.__setRuntimeForTests("onnx-community/gemma-4-E2B-it-ONNX", {
314
+ model: modelMock,
315
+ processor: processorMock,
316
+ TextStreamer: class {},
317
+ modelLoaderName: "test",
318
+ });
319
+
320
+ await service.__resetRuntimeForTests();
321
+ expect(modelMock.dispose).toHaveBeenCalled();
322
+ });
153
323
  });
154
324
 
155
325
  describe("idle cleanup", () => {
@@ -164,10 +334,8 @@ describe("TransformersRuntimeService", () => {
164
334
  verbose: false,
165
335
  });
166
336
 
167
- // Should still be alive
168
337
  expect(modelMock.dispose).not.toHaveBeenCalled();
169
338
 
170
- // Wait for idle timeout (10s)
171
339
  await jest.advanceTimersByTimeAsync(11000);
172
340
 
173
341
  expect(modelMock.dispose).toHaveBeenCalled();
@@ -0,0 +1,21 @@
1
+ import {
2
+ ensureServerWebRtc,
3
+ isWebRtcAvailable,
4
+ getPeerClass,
5
+ } from "./webrtc.js";
6
+
7
+ describe("server webrtc", () => {
8
+ it("polyfs WebRTC APIs on globalThis in Node environment", async () => {
9
+ const available = await ensureServerWebRtc();
10
+ expect(available).toBe(true);
11
+ expect(isWebRtcAvailable()).toBe(true);
12
+ expect((globalThis as any).RTCPeerConnection).toBeDefined();
13
+ expect((globalThis as any).RTCSessionDescription).toBeDefined();
14
+ expect((globalThis as any).RTCIceCandidate).toBeDefined();
15
+ });
16
+
17
+ it("loads PeerJS constructor configured for Node", async () => {
18
+ const PeerClass = await getPeerClass();
19
+ expect(typeof PeerClass).toBe("function");
20
+ });
21
+ });
@@ -0,0 +1,72 @@
1
+ /**
2
+ * ShadowClaw — Server-Side WebRTC Initialization & PeerJS Loader
3
+ *
4
+ * Polyfills WebRTC (RTCPeerConnection, RTCSessionDescription, RTCIceCandidate)
5
+ * in Node.js environments using node-datachannel, allowing Node servers and CLI
6
+ * tools to establish direct WebRTC RTCDataChannel connections.
7
+ */
8
+
9
+ let isAvailable: boolean | null = null;
10
+
11
+ /**
12
+ * Ensure WebRTC APIs are present in globalThis for Node.js environments.
13
+ * Returns true if WebRTC is available (either natively or via polyfill).
14
+ */
15
+ export async function ensureServerWebRtc(): Promise<boolean> {
16
+ if (
17
+ typeof globalThis !== "undefined" &&
18
+ typeof (globalThis as any).RTCPeerConnection !== "undefined"
19
+ ) {
20
+ isAvailable = true;
21
+ return true;
22
+ }
23
+
24
+ if (isAvailable !== null) {
25
+ return isAvailable;
26
+ }
27
+
28
+ try {
29
+ const wrtc = await import("node-datachannel/polyfill");
30
+ if (wrtc && wrtc.RTCPeerConnection) {
31
+ Object.assign(globalThis, {
32
+ RTCPeerConnection: wrtc.RTCPeerConnection,
33
+ RTCSessionDescription: wrtc.RTCSessionDescription,
34
+ RTCIceCandidate: wrtc.RTCIceCandidate,
35
+ });
36
+ isAvailable = true;
37
+ return true;
38
+ }
39
+ } catch (err) {
40
+ console.warn(
41
+ "[webrtc] Failed to load node-datachannel WebRTC polyfill:",
42
+ err instanceof Error ? err.message : String(err),
43
+ );
44
+ }
45
+
46
+ isAvailable = false;
47
+ return false;
48
+ }
49
+
50
+ /**
51
+ * Check whether WebRTC is available in the current environment.
52
+ */
53
+ export function isWebRtcAvailable(): boolean {
54
+ if (
55
+ typeof globalThis !== "undefined" &&
56
+ typeof (globalThis as any).RTCPeerConnection !== "undefined"
57
+ ) {
58
+ return true;
59
+ }
60
+ return isAvailable === true;
61
+ }
62
+
63
+ /**
64
+ * Dynamically loads and returns the PeerJS Peer constructor configured for Node.
65
+ */
66
+ export async function getPeerClass(): Promise<any> {
67
+ await ensureServerWebRtc();
68
+ const mod = await import("peerjs");
69
+ return (
70
+ (mod as any).default?.Peer || (mod as any).default || (mod as any).Peer
71
+ );
72
+ }
@@ -2,8 +2,9 @@
2
2
 
3
3
  import { beforeEach, describe, expect, it, jest } from "@jest/globals";
4
4
 
5
- describe("service-worker fetch proxy workspace routes", () => {
5
+ describe("service-worker fetch proxy", () => {
6
6
  let fetchListener: ((event: any) => void) | null = null;
7
+ let messageListener: ((event: any) => void) | null = null;
7
8
  let networkFetch: jest.Mock;
8
9
 
9
10
  const imageBytes = new Uint8Array([255, 216, 255, 217]);
@@ -82,6 +83,7 @@ describe("service-worker fetch proxy workspace routes", () => {
82
83
  beforeEach(async () => {
83
84
  jest.resetModules();
84
85
  fetchListener = null;
86
+ messageListener = null;
85
87
 
86
88
  Object.defineProperty(globalThis, "Response", {
87
89
  configurable: true,
@@ -168,6 +170,8 @@ describe("service-worker fetch proxy workspace routes", () => {
168
170
  (type: string, handler: (event: any) => void) => {
169
171
  if (type === "fetch") {
170
172
  fetchListener = handler;
173
+ } else if (type === "message") {
174
+ messageListener = handler;
171
175
  }
172
176
  },
173
177
  ),
@@ -292,4 +296,63 @@ describe("service-worker fetch proxy workspace routes", () => {
292
296
  expect(response).toBeNull();
293
297
  expect(networkFetch).not.toHaveBeenCalled();
294
298
  });
299
+
300
+ it("handles proxy configuration messages and proxies cross-origin requests", async () => {
301
+ expect(messageListener).toBeTruthy();
302
+
303
+ // Enable proxy via message
304
+ messageListener!({
305
+ data: {
306
+ type: "set-proxy-config",
307
+ payload: {
308
+ useProxy: true,
309
+ proxyUrl: "/proxy",
310
+ },
311
+ },
312
+ } as any);
313
+
314
+ const crossOriginPostRequest = {
315
+ url: "https://api.external.com/v1/resource",
316
+ method: "POST",
317
+ mode: "cors",
318
+ destination: "fetch",
319
+ headers: new TestHeaders({ Authorization: "Bearer token" }),
320
+ clone: () => ({
321
+ text: async () => JSON.stringify({ key: "val" }),
322
+ }),
323
+ };
324
+
325
+ const response = await dispatchFetch(crossOriginPostRequest);
326
+ expect(response.status).toBe(200);
327
+ expect(networkFetch).toHaveBeenCalledWith(
328
+ `${globalThis.location.origin}/proxy/https://api.external.com/v1/resource`,
329
+ expect.objectContaining({
330
+ method: "POST",
331
+ headers: { "Content-Type": "application/json" },
332
+ }),
333
+ );
334
+ });
335
+
336
+ it("handles legacy set-use-proxy message and direct fetch when proxy disabled", async () => {
337
+ messageListener!({
338
+ data: {
339
+ type: "set-use-proxy",
340
+ payload: {
341
+ useProxy: false,
342
+ },
343
+ },
344
+ } as any);
345
+
346
+ const crossOriginGetRequest = {
347
+ url: "https://api.external.com/v1/data",
348
+ method: "GET",
349
+ mode: "cors",
350
+ destination: "fetch",
351
+ headers: new TestHeaders(),
352
+ };
353
+
354
+ const response = await dispatchFetch(crossOriginGetRequest);
355
+ expect(response.status).toBe(200);
356
+ expect(networkFetch).toHaveBeenCalledWith(crossOriginGetRequest);
357
+ });
295
358
  });
@@ -12,6 +12,9 @@ interface PushData {
12
12
  type?: string;
13
13
  taskId?: string;
14
14
  groupId?: string;
15
+ clientId?: string;
16
+ action?: string;
17
+ args?: Record<string, unknown>;
15
18
  prompt?: string;
16
19
  taskType?: string;
17
20
  tools?: unknown[];
@@ -29,6 +32,42 @@ self.addEventListener("push", (event: PushEvent) => {
29
32
  }
30
33
  }
31
34
 
35
+ // Remote command trigger — relay to client windows and notify
36
+ if (data.type === "remote-command") {
37
+ event.waitUntil(
38
+ self.clients
39
+ .matchAll({ type: "window", includeUncontrolled: true })
40
+ .then((clientList) => {
41
+ for (const client of clientList) {
42
+ client.postMessage({
43
+ type: "remote-command-trigger",
44
+ clientId: data.clientId,
45
+ action: data.action,
46
+ args: data.args,
47
+ prompt: data.prompt,
48
+ });
49
+ }
50
+
51
+ return self.registration.showNotification(
52
+ "ShadowClaw — Remote Command",
53
+ {
54
+ body: data.prompt || `Remote command received: ${data.action}`,
55
+ icon: "/assets/icons/512.png",
56
+ badge: "/assets/icons/192.png",
57
+ data: {
58
+ type: "remote-command",
59
+ clientId: data.clientId,
60
+ action: data.action,
61
+ args: data.args,
62
+ },
63
+ },
64
+ );
65
+ }),
66
+ );
67
+
68
+ return;
69
+ }
70
+
32
71
  // Scheduled task trigger — relay to any open client windows, then show notification
33
72
  if (data.type === "scheduled-task") {
34
73
  event.waitUntil(