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
@@ -0,0 +1,251 @@
1
+ import { jest } from "@jest/globals";
2
+
3
+ const mockGetGroupDir = jest.fn() as any;
4
+ const mockReadGroupFile = jest.fn() as any;
5
+
6
+ jest.unstable_mockModule("../../storage/getGroupDir.js", () => ({
7
+ getGroupDir: mockGetGroupDir,
8
+ }));
9
+
10
+ jest.unstable_mockModule("../../storage/readGroupFile.js", () => ({
11
+ readGroupFile: mockReadGroupFile,
12
+ }));
13
+
14
+ const { discoverSkills, loadSkill } = await import("./discoverSkills.js");
15
+ const { DEFAULT_GROUP_ID } = await import("../../config/config.js");
16
+
17
+ function createMockDirectoryHandle(
18
+ name: string,
19
+ children: Record<string, any> = {},
20
+ ) {
21
+ return {
22
+ kind: "directory",
23
+ name,
24
+ getDirectoryHandle: jest.fn(async (childName: string) => {
25
+ const child = children[childName];
26
+ if (!child || child.kind !== "directory") {
27
+ throw new Error(`Directory not found: ${childName}`);
28
+ }
29
+ return child;
30
+ }),
31
+ entries: async function* () {
32
+ for (const [childName, child] of Object.entries(children)) {
33
+ yield [childName, child];
34
+ }
35
+ },
36
+ };
37
+ }
38
+
39
+ function createMockFileHandle(name: string) {
40
+ return {
41
+ kind: "file",
42
+ name,
43
+ };
44
+ }
45
+
46
+ describe("discoverSkills and loadSkill", () => {
47
+ beforeEach(() => {
48
+ jest.clearAllMocks();
49
+ });
50
+
51
+ it("returns empty results when .agents or .agents/skills does not exist", async () => {
52
+ const root = createMockDirectoryHandle("root", {});
53
+ mockGetGroupDir.mockResolvedValue(root);
54
+
55
+ const result = await discoverSkills({} as any, DEFAULT_GROUP_ID);
56
+ expect(result.skills).toEqual([]);
57
+ expect(result.diagnostics).toEqual([]);
58
+ });
59
+
60
+ it("discovers skills from .agents/skills directory hierarchy", async () => {
61
+ const skillA = createMockDirectoryHandle("skill-a", {
62
+ "SKILL.md": createMockFileHandle("SKILL.md"),
63
+ });
64
+ const skillsDir = createMockDirectoryHandle("skills", {
65
+ "skill-a": skillA,
66
+ });
67
+ const agentsDir = createMockDirectoryHandle(".agents", {
68
+ skills: skillsDir,
69
+ });
70
+ const root = createMockDirectoryHandle("root", {
71
+ ".agents": agentsDir,
72
+ });
73
+
74
+ mockGetGroupDir.mockResolvedValue(root);
75
+ mockReadGroupFile.mockResolvedValue(`---
76
+ name: skill-a
77
+ description: First test skill
78
+ ---
79
+ # Skill A Content
80
+ `);
81
+
82
+ const result = await discoverSkills({} as any, DEFAULT_GROUP_ID);
83
+ expect(result.skills).toHaveLength(1);
84
+ expect(result.skills[0].name).toBe("skill-a");
85
+ expect(result.skills[0].description).toBe("First test skill");
86
+ expect(result.skills[0].path).toBe(".agents/skills/skill-a/SKILL.md");
87
+ expect(result.diagnostics).toEqual([]);
88
+ });
89
+
90
+ it("records diagnostic when duplicate skill names exist in current group", async () => {
91
+ const skillA1 = createMockDirectoryHandle("skill-a1", {
92
+ "SKILL.md": createMockFileHandle("SKILL.md"),
93
+ });
94
+ const skillA2 = createMockDirectoryHandle("skill-a2", {
95
+ "SKILL.md": createMockFileHandle("SKILL.md"),
96
+ });
97
+ const skillsDir = createMockDirectoryHandle("skills", {
98
+ "skill-a1": skillA1,
99
+ "skill-a2": skillA2,
100
+ });
101
+ const agentsDir = createMockDirectoryHandle(".agents", {
102
+ skills: skillsDir,
103
+ });
104
+ const root = createMockDirectoryHandle("root", {
105
+ ".agents": agentsDir,
106
+ });
107
+
108
+ mockGetGroupDir.mockResolvedValue(root);
109
+ // Both return the same name "duplicate-skill"
110
+ mockReadGroupFile.mockResolvedValue(`---
111
+ name: duplicate-skill
112
+ description: Dup skill
113
+ ---
114
+ Body
115
+ `);
116
+
117
+ const result = await discoverSkills({} as any, DEFAULT_GROUP_ID);
118
+ expect(result.skills).toHaveLength(1);
119
+ expect(result.diagnostics).toHaveLength(1);
120
+ expect(result.diagnostics[0].message).toContain(
121
+ "Duplicate skill name: duplicate-skill",
122
+ );
123
+ });
124
+
125
+ it("records diagnostic when skill parsing fails (invalid frontmatter)", async () => {
126
+ const brokenSkill = createMockDirectoryHandle("broken", {
127
+ "SKILL.md": createMockFileHandle("SKILL.md"),
128
+ });
129
+ const skillsDir = createMockDirectoryHandle("skills", {
130
+ broken: brokenSkill,
131
+ });
132
+ const agentsDir = createMockDirectoryHandle(".agents", {
133
+ skills: skillsDir,
134
+ });
135
+ const root = createMockDirectoryHandle("root", {
136
+ ".agents": agentsDir,
137
+ });
138
+
139
+ mockGetGroupDir.mockResolvedValue(root);
140
+ mockReadGroupFile.mockResolvedValue(`invalid content without frontmatter`);
141
+
142
+ const result = await discoverSkills({} as any, DEFAULT_GROUP_ID);
143
+ expect(result.skills).toHaveLength(0);
144
+ expect(result.diagnostics).toHaveLength(1);
145
+ expect(result.diagnostics[0].message).toBeTruthy();
146
+ });
147
+
148
+ it("stops and adds diagnostic when visited directories exceeds limit", async () => {
149
+ const deepChildren: Record<string, any> = {};
150
+ for (let i = 0; i <= 2001; i++) {
151
+ deepChildren[`dir_${i}`] = createMockDirectoryHandle(`dir_${i}`, {});
152
+ }
153
+
154
+ const skillsDir = createMockDirectoryHandle("skills", deepChildren);
155
+ const agentsDir = createMockDirectoryHandle(".agents", {
156
+ skills: skillsDir,
157
+ });
158
+ const root = createMockDirectoryHandle("root", {
159
+ ".agents": agentsDir,
160
+ });
161
+
162
+ mockGetGroupDir.mockResolvedValue(root);
163
+
164
+ const result = await discoverSkills({} as any, DEFAULT_GROUP_ID);
165
+ expect(
166
+ result.diagnostics.some((d) =>
167
+ d.message.includes("stopped after 2000 directories"),
168
+ ),
169
+ ).toBe(true);
170
+ });
171
+
172
+ it("scans custom group first then fallback main group", async () => {
173
+ const rootCustom = createMockDirectoryHandle("rootCustom", {});
174
+ const skillDefault = createMockDirectoryHandle("default-skill", {
175
+ "SKILL.md": createMockFileHandle("SKILL.md"),
176
+ });
177
+ const skillsDir = createMockDirectoryHandle("skills", {
178
+ "default-skill": skillDefault,
179
+ });
180
+ const agentsDir = createMockDirectoryHandle(".agents", {
181
+ skills: skillsDir,
182
+ });
183
+ const rootDefault = createMockDirectoryHandle("rootDefault", {
184
+ ".agents": agentsDir,
185
+ });
186
+
187
+ mockGetGroupDir.mockImplementation(async (_db: any, gId: string) => {
188
+ if (gId === "custom-group") return rootCustom;
189
+ return rootDefault;
190
+ });
191
+
192
+ mockReadGroupFile.mockResolvedValue(`---
193
+ name: default-skill
194
+ description: Default fallback
195
+ ---
196
+ Body
197
+ `);
198
+
199
+ const result = await discoverSkills({} as any, "custom-group");
200
+ expect(result.skills).toHaveLength(1);
201
+ expect(result.skills[0].name).toBe("default-skill");
202
+ });
203
+
204
+ describe("loadSkill", () => {
205
+ it("returns null if skill is not found", async () => {
206
+ const root = createMockDirectoryHandle("root", {});
207
+ mockGetGroupDir.mockResolvedValue(root);
208
+
209
+ const skill = await loadSkill({} as any, DEFAULT_GROUP_ID, "nonexistent");
210
+ expect(skill).toBeNull();
211
+ });
212
+
213
+ it("loads and refreshes skill with resources", async () => {
214
+ const helperFile = createMockFileHandle("helper.py");
215
+ const subFolder = createMockDirectoryHandle("scripts", {
216
+ "run.sh": createMockFileHandle("run.sh"),
217
+ });
218
+ const skillDir = createMockDirectoryHandle("my-skill", {
219
+ "SKILL.md": createMockFileHandle("SKILL.md"),
220
+ "helper.py": helperFile,
221
+ scripts: subFolder,
222
+ });
223
+ const skillsDir = createMockDirectoryHandle("skills", {
224
+ "my-skill": skillDir,
225
+ });
226
+ const agentsDir = createMockDirectoryHandle(".agents", {
227
+ skills: skillsDir,
228
+ });
229
+ const root = createMockDirectoryHandle("root", {
230
+ ".agents": agentsDir,
231
+ });
232
+
233
+ mockGetGroupDir.mockResolvedValue(root);
234
+ mockReadGroupFile.mockResolvedValue(`---
235
+ name: my-skill
236
+ description: Full loaded skill
237
+ ---
238
+ Loaded body instructions
239
+ `);
240
+
241
+ const loaded = await loadSkill({} as any, DEFAULT_GROUP_ID, "my-skill");
242
+ expect(loaded).not.toBeNull();
243
+ expect(loaded?.name).toBe("my-skill");
244
+ expect(loaded?.body).toBe("Loaded body instructions");
245
+ expect(loaded?.resources).toEqual([
246
+ ".agents/skills/my-skill/helper.py",
247
+ ".agents/skills/my-skill/scripts/run.sh",
248
+ ]);
249
+ });
250
+ });
251
+ });
@@ -0,0 +1,64 @@
1
+ import { matchesCron } from "./cron.js";
2
+
3
+ describe("matchesCron", () => {
4
+ it("returns false for invalid or non-5-part expressions", () => {
5
+ expect(matchesCron("", new Date())).toBe(false);
6
+ expect(matchesCron(null as any, new Date())).toBe(false);
7
+ expect(matchesCron("* * *", new Date())).toBe(false);
8
+ expect(matchesCron("* * * * * *", new Date())).toBe(false);
9
+ });
10
+
11
+ it("matches wildcard expression", () => {
12
+ expect(matchesCron("* * * * *", new Date())).toBe(true);
13
+ });
14
+
15
+ it("matches exact numbers", () => {
16
+ // 2026-08-30 14:15:00, Sunday (day 0), August (month 8), day of month 30
17
+ const testDate = new Date(2026, 7, 30, 14, 15, 0); // Month is 0-indexed in JS Date: 7 = August
18
+
19
+ expect(matchesCron("15 14 30 8 0", testDate)).toBe(true);
20
+ expect(matchesCron("16 14 30 8 0", testDate)).toBe(false);
21
+ });
22
+
23
+ it("matches lists of numbers", () => {
24
+ const testDate = new Date(2026, 7, 30, 14, 15, 0);
25
+
26
+ expect(matchesCron("10,15,20 * * * *", testDate)).toBe(true);
27
+ expect(matchesCron("10,20,30 * * * *", testDate)).toBe(false);
28
+ });
29
+
30
+ it("matches ranges (N-M)", () => {
31
+ const testDate = new Date(2026, 7, 30, 14, 15, 0);
32
+
33
+ expect(matchesCron("10-20 * * * *", testDate)).toBe(true);
34
+ expect(matchesCron("20-30 * * * *", testDate)).toBe(false);
35
+ });
36
+
37
+ it("matches steps (*/N)", () => {
38
+ const testDate = new Date(2026, 7, 30, 14, 15, 0);
39
+
40
+ expect(matchesCron("*/5 * * * *", testDate)).toBe(true);
41
+ expect(matchesCron("*/7 * * * *", testDate)).toBe(false);
42
+ });
43
+
44
+ it("matches range with step (N-M/S)", () => {
45
+ const testDate = new Date(2026, 7, 30, 14, 15, 0);
46
+
47
+ expect(matchesCron("10-20/5 * * * *", testDate)).toBe(true); // 10, 15, 20 -> 15 matches
48
+ expect(matchesCron("10-20/4 * * * *", testDate)).toBe(false); // 10, 14, 18
49
+ });
50
+
51
+ it("matches start with step (N/S)", () => {
52
+ const testDate = new Date(2026, 7, 30, 14, 15, 0);
53
+
54
+ expect(matchesCron("5/10 * * * *", testDate)).toBe(true); // 5, 15, 25... -> 15 matches
55
+ expect(matchesCron("6/10 * * * *", testDate)).toBe(false);
56
+ });
57
+
58
+ it("handles invalid step strings", () => {
59
+ const testDate = new Date(2026, 7, 30, 14, 15, 0);
60
+
61
+ expect(matchesCron("*/0 * * * *", testDate)).toBe(false);
62
+ expect(matchesCron("*/abc * * * *", testDate)).toBe(false);
63
+ });
64
+ });
@@ -1,11 +1,45 @@
1
- import { describe, expect, it } from "@jest/globals";
1
+ import { describe, expect, it, jest, beforeEach } from "@jest/globals";
2
2
 
3
- import { parseDeclarativeTool } from "./declarative.js";
3
+ jest.unstable_mockModule("../../db/getConfig.js", () => ({
4
+ getConfig: jest.fn(),
5
+ }));
4
6
 
5
- describe("parseDeclarativeTool", () => {
6
- it("parses a sandboxed javascript tool definition", () => {
7
- expect(
8
- parseDeclarativeTool(".agents/tools/main/echo.json", {
7
+ jest.unstable_mockModule("../../storage/getGroupDir.js", () => ({
8
+ getGroupDir: jest.fn(),
9
+ }));
10
+
11
+ jest.unstable_mockModule("../../storage/readGroupFile.js", () => ({
12
+ readGroupFile: jest.fn(),
13
+ }));
14
+
15
+ const { parseDeclarativeTool, loadDeclarativeTools, findDeclarativeTool } =
16
+ await import("./declarative.js");
17
+ const { getConfig } = await import("../../db/getConfig.js");
18
+ const { getGroupDir } = await import("../../storage/getGroupDir.js");
19
+ const { readGroupFile } = await import("../../storage/readGroupFile.js");
20
+
21
+ describe("declarative tools", () => {
22
+ beforeEach(() => {
23
+ jest.clearAllMocks();
24
+ });
25
+
26
+ describe("parseDeclarativeTool", () => {
27
+ it("parses a sandboxed javascript tool definition", () => {
28
+ expect(
29
+ parseDeclarativeTool(".agents/tools/main/echo.json", {
30
+ name: "echo",
31
+ description: "Echo structured input.",
32
+ input_schema: {
33
+ type: "object",
34
+ properties: { value: { type: "string" } },
35
+ required: ["value"],
36
+ },
37
+ execution: {
38
+ type: "javascript",
39
+ code: "return JSON.parse(data).value;",
40
+ },
41
+ }),
42
+ ).toEqual({
9
43
  name: "echo",
10
44
  description: "Echo structured input.",
11
45
  input_schema: {
@@ -17,101 +51,233 @@ describe("parseDeclarativeTool", () => {
17
51
  type: "javascript",
18
52
  code: "return JSON.parse(data).value;",
19
53
  },
20
- }),
21
- ).toEqual({
22
- name: "echo",
23
- description: "Echo structured input.",
24
- input_schema: {
25
- type: "object",
26
- properties: { value: { type: "string" } },
27
- required: ["value"],
28
- },
29
- execution: {
30
- type: "javascript",
31
- code: "return JSON.parse(data).value;",
32
- },
33
- path: ".agents/tools/main/echo.json",
54
+ path: ".agents/tools/main/echo.json",
55
+ });
34
56
  });
35
- });
36
57
 
37
- it("rejects tools without a sandbox execution recipe", () => {
38
- expect(() =>
39
- parseDeclarativeTool(".agents/tools/main/echo.json", {
40
- name: "echo",
41
- description: "Echo structured input.",
58
+ it("parses a bash tool definition", () => {
59
+ const tool = parseDeclarativeTool(".agents/tools/main/run_sh.json", {
60
+ name: "run_sh",
61
+ description: "Run bash command.",
42
62
  input_schema: { type: "object" },
43
- }),
44
- ).toThrow("execution");
45
- });
63
+ execution: {
64
+ type: "bash",
65
+ command: "echo hello",
66
+ },
67
+ });
46
68
 
47
- it("parses a tool executor that delegates to an existing tool", () => {
48
- expect(
49
- parseDeclarativeTool(".agents/tools/main/repo_status.json", {
69
+ expect(tool.execution).toEqual({
70
+ type: "bash",
71
+ command: "echo hello",
72
+ });
73
+ });
74
+
75
+ it("parses a tool executor that delegates to an existing tool with inputs", () => {
76
+ const tool = parseDeclarativeTool(".agents/tools/main/repo_status.json", {
50
77
  name: "repo_status",
51
78
  description: "Show repository status.",
52
79
  input_schema: { type: "object" },
53
- execution: { type: "tool", name: "git_status" },
54
- }).execution,
55
- ).toEqual({ type: "tool", name: "git_status" });
80
+ execution: {
81
+ type: "tool",
82
+ name: "git_status",
83
+ input: { verbose: true },
84
+ },
85
+ });
86
+
87
+ expect(tool.execution).toEqual({
88
+ type: "tool",
89
+ name: "git_status",
90
+ input: { verbose: true },
91
+ });
92
+ });
93
+
94
+ it("rejects invalid value shapes", () => {
95
+ expect(() => parseDeclarativeTool("test.json", null)).toThrow(
96
+ "must be a JSON object",
97
+ );
98
+ expect(() => parseDeclarativeTool("test.json", "string")).toThrow(
99
+ "must be a JSON object",
100
+ );
101
+ });
102
+
103
+ it("rejects invalid tool names", () => {
104
+ expect(() =>
105
+ parseDeclarativeTool("test.json", {
106
+ name: "Invalid-Name!",
107
+ description: "desc",
108
+ input_schema: {},
109
+ execution: {},
110
+ }),
111
+ ).toThrow("Tool name is invalid");
112
+
113
+ expect(() =>
114
+ parseDeclarativeTool("test.json", {
115
+ name: "",
116
+ description: "desc",
117
+ input_schema: {},
118
+ execution: {},
119
+ }),
120
+ ).toThrow("Tool name is invalid");
121
+ });
122
+
123
+ it("rejects missing description or input schema", () => {
124
+ expect(() =>
125
+ parseDeclarativeTool("test.json", {
126
+ name: "valid_name",
127
+ description: "",
128
+ input_schema: {},
129
+ execution: {},
130
+ }),
131
+ ).toThrow("requires a description");
132
+
133
+ expect(() =>
134
+ parseDeclarativeTool("test.json", {
135
+ name: "valid_name",
136
+ description: "desc",
137
+ input_schema: null,
138
+ execution: {},
139
+ }),
140
+ ).toThrow("requires input_schema");
141
+ });
142
+
143
+ it("rejects invalid execution specifications", () => {
144
+ expect(() =>
145
+ parseDeclarativeTool("test.json", {
146
+ name: "valid_name",
147
+ description: "desc",
148
+ input_schema: { type: "object" },
149
+ execution: null,
150
+ }),
151
+ ).toThrow("requires execution");
152
+
153
+ expect(() =>
154
+ parseDeclarativeTool("test.json", {
155
+ name: "valid_name",
156
+ description: "desc",
157
+ input_schema: { type: "object" },
158
+ execution: { type: "unsupported" },
159
+ }),
160
+ ).toThrow("execution type must be bash, javascript, or tool");
161
+
162
+ expect(() =>
163
+ parseDeclarativeTool("test.json", {
164
+ name: "valid_name",
165
+ description: "desc",
166
+ input_schema: { type: "object" },
167
+ execution: { type: "tool", name: "INVALID-DELEGATE!" },
168
+ }),
169
+ ).toThrow("execution requires a valid tool name");
170
+
171
+ expect(() =>
172
+ parseDeclarativeTool("test.json", {
173
+ name: "valid_name",
174
+ description: "desc",
175
+ input_schema: { type: "object" },
176
+ execution: { type: "bash", command: " " },
177
+ }),
178
+ ).toThrow("execution requires command");
179
+ });
56
180
  });
57
181
 
58
- it("parses generate_random_number declarative tool definition", () => {
59
- const raw = {
60
- name: "generate_random_number",
61
- description:
62
- "Generate a random integer within a specified range (min through max, inclusive).",
63
- input_schema: {
64
- type: "object",
65
- properties: {
66
- min: { type: "number", description: "Minimum integer value" },
67
- max: { type: "number", description: "Maximum integer value" },
182
+ describe("loadDeclarativeTools & findDeclarativeTool", () => {
183
+ it("loads declarative tools from directory tree", async () => {
184
+ const mockFileHandle = { kind: "file" };
185
+ const mockToolsDir = {
186
+ entries: async function* () {
187
+ yield ["calc.json", mockFileHandle];
188
+ yield ["invalid.json", mockFileHandle];
189
+ },
190
+ };
191
+ const mockAgentsDir = {
192
+ getDirectoryHandle: jest.fn().mockImplementation(async (name) => {
193
+ if (name === "tools") return mockToolsDir;
194
+ throw new Error("Not found");
195
+ }),
196
+ };
197
+ const mockRoot = {
198
+ getDirectoryHandle: jest.fn().mockImplementation(async (name) => {
199
+ if (name === ".agents") return mockAgentsDir;
200
+ throw new Error("Not found");
201
+ }),
202
+ };
203
+
204
+ (getGroupDir as jest.Mock<any>).mockResolvedValue(mockRoot);
205
+ (readGroupFile as jest.Mock<any>).mockImplementation(
206
+ async (_db: any, _gid: any, path: any) => {
207
+ if (path === ".agents/tools/calc.json") {
208
+ return JSON.stringify({
209
+ name: "calc",
210
+ description: "Calculate value",
211
+ input_schema: { type: "object" },
212
+ execution: { type: "javascript", code: "return 1;" },
213
+ });
214
+ }
215
+ return "{ invalid json }";
68
216
  },
69
- },
70
- execution: {
71
- type: "javascript",
72
- code: "const input = data ? JSON.parse(data) : {}; const min = typeof input.min === 'number' ? input.min : 1; const max = typeof input.max === 'number' ? input.max : 1000000; return Math.floor(Math.random() * (max - min + 1)) + min;",
73
- },
74
- };
75
-
76
- expect(
77
- parseDeclarativeTool(
78
- ".agents/tools/main/generate_random_number.json",
79
- raw,
80
- ),
81
- ).toEqual({
82
- ...raw,
83
- path: ".agents/tools/main/generate_random_number.json",
217
+ );
218
+
219
+ const { tools, diagnostics } = await loadDeclarativeTools({} as any);
220
+ expect(tools).toHaveLength(1);
221
+ expect(tools[0].name).toBe("calc");
222
+ expect(diagnostics).toHaveLength(1);
84
223
  });
85
- });
86
224
 
87
- it("parses tools using parameters and evaluation expression aliases", () => {
88
- const raw = {
89
- name: "calculate_ore_depth",
90
- description: "Calculates ore depth.",
91
- parameters: {
92
- type: "object",
93
- properties: { y: { type: "number" } },
94
- },
95
- evaluation: {
96
- type: "javascript",
97
- expression: "return { y: data ? JSON.parse(data).y : 0 };",
98
- },
99
- };
100
-
101
- expect(
102
- parseDeclarativeTool(".agents/tools/main/calculate_ore_depth.json", raw),
103
- ).toEqual({
104
- name: "calculate_ore_depth",
105
- description: "Calculates ore depth.",
106
- input_schema: {
107
- type: "object",
108
- properties: { y: { type: "number" } },
109
- },
110
- execution: {
111
- type: "javascript",
112
- code: "return { y: data ? JSON.parse(data).y : 0 };",
113
- },
114
- path: ".agents/tools/main/calculate_ore_depth.json",
225
+ it("returns empty tools when agents directory does not exist", async () => {
226
+ (getGroupDir as jest.Mock<any>).mockResolvedValue({
227
+ getDirectoryHandle: (jest.fn() as any).mockRejectedValue(
228
+ new Error("No .agents dir"),
229
+ ),
230
+ });
231
+
232
+ const { tools, diagnostics } = await loadDeclarativeTools({} as any);
233
+ expect(tools).toEqual([]);
234
+ expect(diagnostics).toEqual([]);
235
+ });
236
+
237
+ it("finds declarative tool by name when enabled", async () => {
238
+ const mockFileHandle = { kind: "file" };
239
+ const mockToolsDir = {
240
+ entries: async function* () {
241
+ yield ["my_tool.json", mockFileHandle];
242
+ },
243
+ };
244
+ const mockAgentsDir = {
245
+ getDirectoryHandle: (jest.fn() as any).mockResolvedValue(mockToolsDir),
246
+ };
247
+ const mockRoot = {
248
+ getDirectoryHandle: (jest.fn() as any).mockResolvedValue(mockAgentsDir),
249
+ };
250
+
251
+ (getGroupDir as jest.Mock<any>).mockResolvedValue(mockRoot);
252
+ (readGroupFile as jest.Mock<any>).mockResolvedValue(
253
+ JSON.stringify({
254
+ name: "my_tool",
255
+ description: "My custom tool",
256
+ input_schema: { type: "object" },
257
+ execution: { type: "javascript", code: "return true;" },
258
+ }),
259
+ );
260
+ (getConfig as jest.Mock<any>).mockResolvedValue(["my_tool"]);
261
+
262
+ const tool = await findDeclarativeTool({} as any, "main", "my_tool");
263
+ expect(tool?.name).toBe("my_tool");
264
+
265
+ // When tool is not in enabled list
266
+ (getConfig as jest.Mock<any>).mockResolvedValue(["other_tool"]);
267
+ const disabledTool = await findDeclarativeTool(
268
+ {} as any,
269
+ "main",
270
+ "my_tool",
271
+ );
272
+ expect(disabledTool).toBeNull();
273
+
274
+ // When searching for non-existent tool
275
+ const missing = await findDeclarativeTool(
276
+ {} as any,
277
+ "main",
278
+ "non_existent",
279
+ );
280
+ expect(missing).toBeNull();
115
281
  });
116
282
  });
117
283
  });