shadow-claw 1.27.0 → 1.28.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 (240) hide show
  1. package/.well-known/ai-catalog.json +11 -0
  2. package/.well-known/mcp/index.html +19 -0
  3. package/.well-known/mcp/server-card.json +52 -0
  4. package/.well-known/mcp/servers.json +15 -0
  5. package/.well-known/mcp.json +52 -0
  6. package/.well-known/oauth-protected-resource +10 -0
  7. package/.well-known/oauth-protected-resource.json +10 -0
  8. package/bin/assert-version-bump.mjs +61 -3
  9. package/bin/assert-version-bump.test.mjs +77 -1
  10. package/bin/build/build.mjs +116 -49
  11. package/bin/build/build.test.mjs +103 -0
  12. package/bin/cli.mjs +299 -237
  13. package/bin/cli.test.mjs +27 -0
  14. package/bin/commands/mcp.mjs +132 -2
  15. package/bin/commands/mcp.test.mjs +166 -0
  16. package/bin/commands/skills-index.mjs +139 -20
  17. package/bin/commands/skills-index.test.mjs +82 -0
  18. package/bin/utils/control-client.mjs +17 -0
  19. package/bin/utils/control-client.test.mjs +46 -0
  20. package/bin/version.mjs +208 -0
  21. package/bin/version.test.mjs +138 -0
  22. package/dist/electron/main.cjs +195 -185
  23. package/dist/lib/components/index.js +21 -21
  24. package/dist/lib/components/settings/shadow-claw-channel-config.js +1 -1
  25. package/dist/lib/components/settings/shadow-claw-control-plane.js +1 -1
  26. package/dist/lib/components/settings/shadow-claw-git.js +1 -1
  27. package/dist/lib/components/settings/shadow-claw-llm.js +1 -1
  28. package/dist/lib/components/settings/shadow-claw-networking.js +1 -1
  29. package/dist/lib/components/settings/shadow-claw-notifications.js +1 -1
  30. package/dist/lib/components/settings/shadow-claw-peerjs.js +1 -1
  31. package/dist/lib/components/settings/shadow-claw-storage.js +1 -1
  32. package/dist/lib/components/settings/shadow-claw-task-server.js +1 -1
  33. package/dist/lib/components/settings/shadow-claw-webvm.js +1 -1
  34. package/dist/lib/components/shadow-claw/shadow-claw.d.ts +1 -0
  35. package/dist/lib/components/shadow-claw/utils/setupAppLifecycle.d.ts +23 -0
  36. package/dist/lib/components/shadow-claw-channel-config.js +1 -1
  37. package/dist/lib/components/shadow-claw-channels.js +1 -1
  38. package/dist/lib/components/shadow-claw-chat.js +1 -1
  39. package/dist/lib/components/shadow-claw-control-plane.js +1 -1
  40. package/dist/lib/components/shadow-claw-conversations.js +1 -1
  41. package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
  42. package/dist/lib/components/shadow-claw-files.js +1 -1
  43. package/dist/lib/components/shadow-claw-git.js +1 -1
  44. package/dist/lib/components/shadow-claw-llm.js +1 -1
  45. package/dist/lib/components/shadow-claw-networking.js +1 -1
  46. package/dist/lib/components/shadow-claw-notifications.js +1 -1
  47. package/dist/lib/components/shadow-claw-pages.js +1 -1
  48. package/dist/lib/components/shadow-claw-peerjs.js +1 -1
  49. package/dist/lib/components/shadow-claw-settings.js +1 -1
  50. package/dist/lib/components/shadow-claw-storage.js +1 -1
  51. package/dist/lib/components/shadow-claw-task-server.js +1 -1
  52. package/dist/lib/components/shadow-claw-tasks.js +1 -1
  53. package/dist/lib/components/shadow-claw-terminal.js +1 -1
  54. package/dist/lib/components/shadow-claw-tools.js +1 -1
  55. package/dist/lib/components/shadow-claw-webvm.js +1 -1
  56. package/dist/lib/components/shadow-claw.js +1 -1
  57. package/dist/lib/{configurePeerJs-oODzltaC.js → configurePeerJs-D8Ph0-A9.js} +2 -2
  58. package/dist/lib/{configurePeerJs-oODzltaC.js.map → configurePeerJs-D8Ph0-A9.js.map} +1 -1
  59. package/dist/lib/core/control-plane-client.d.ts +2 -0
  60. package/dist/lib/core/orchestrator/orchestrator.d.ts +10 -0
  61. package/dist/lib/core/orchestrator/utils/operations/channel.d.ts +2 -2
  62. package/dist/lib/core/utils/initControlPlane.d.ts +6 -0
  63. package/dist/lib/index.js +21 -21
  64. package/dist/lib/{initControlPlane-C2zURVIS.js → initControlPlane-BIrWmpzc.js} +76 -9
  65. package/dist/lib/initControlPlane-BIrWmpzc.js.map +1 -0
  66. package/dist/lib/{markdown-C6EHl9zs.js → markdown-DEUoM1c6.js} +2 -2
  67. package/dist/lib/{markdown-C6EHl9zs.js.map → markdown-DEUoM1c6.js.map} +1 -1
  68. package/dist/lib/{orchestrator-KRjx0eba.js → orchestrator-BtVj9bBB.js} +11 -10
  69. package/dist/lib/{orchestrator-KRjx0eba.js.map → orchestrator-BtVj9bBB.js.map} +1 -1
  70. package/dist/lib/{orchestrator-CACLa_Ro.js → orchestrator-CFCW-l1O.js} +52 -4
  71. package/dist/lib/orchestrator-CFCW-l1O.js.map +1 -0
  72. package/dist/lib/{peerjs-DFWOBcXf.js → peerjs-BoBkx9DJ.js} +23 -3
  73. package/dist/lib/peerjs-BoBkx9DJ.js.map +1 -0
  74. package/dist/lib/{push-client-_tiWQsCT.js → push-client-D6hbzDNV.js} +10 -4
  75. package/dist/lib/push-client-D6hbzDNV.js.map +1 -0
  76. package/dist/lib/{shadow-claw-6UHdiy4a.js → shadow-claw-Cuf2O5bZ.js} +71 -4
  77. package/dist/lib/shadow-claw-Cuf2O5bZ.js.map +1 -0
  78. package/dist/lib/{shadow-claw-channel-config-DmfwJc6G.js → shadow-claw-channel-config-BDPKt6Bd.js} +3 -3
  79. package/dist/lib/{shadow-claw-channel-config-DmfwJc6G.js.map → shadow-claw-channel-config-BDPKt6Bd.js.map} +1 -1
  80. package/dist/lib/{shadow-claw-channels-1N6Jl-By.js → shadow-claw-channels-BsP5JABM.js} +2 -2
  81. package/dist/lib/{shadow-claw-channels-1N6Jl-By.js.map → shadow-claw-channels-BsP5JABM.js.map} +1 -1
  82. package/dist/lib/{shadow-claw-chat-BMoqQJRK.js → shadow-claw-chat-Cam2lX4k.js} +4 -4
  83. package/dist/lib/{shadow-claw-chat-BMoqQJRK.js.map → shadow-claw-chat-Cam2lX4k.js.map} +1 -1
  84. package/dist/lib/{shadow-claw-control-plane-UG0o7pCz.js → shadow-claw-control-plane-DB2qcCFs.js} +20 -10
  85. package/dist/lib/shadow-claw-control-plane-DB2qcCFs.js.map +1 -0
  86. package/dist/lib/{shadow-claw-conversations-BwXep3iu.js → shadow-claw-conversations-C9Vf4XGx.js} +2 -2
  87. package/dist/lib/{shadow-claw-conversations-BwXep3iu.js.map → shadow-claw-conversations-C9Vf4XGx.js.map} +1 -1
  88. package/dist/lib/{shadow-claw-file-viewer-DUxzVxDT.js → shadow-claw-file-viewer-B4HYiI0v.js} +3 -3
  89. package/dist/lib/{shadow-claw-file-viewer-DUxzVxDT.js.map → shadow-claw-file-viewer-B4HYiI0v.js.map} +1 -1
  90. package/dist/lib/{shadow-claw-files-DIREEnT2.js → shadow-claw-files-DVROUESO.js} +2 -2
  91. package/dist/lib/{shadow-claw-files-DIREEnT2.js.map → shadow-claw-files-DVROUESO.js.map} +1 -1
  92. package/dist/lib/{shadow-claw-git-Bg1SWgsF.js → shadow-claw-git-BqrO7cHJ.js} +3 -3
  93. package/dist/lib/{shadow-claw-git-Bg1SWgsF.js.map → shadow-claw-git-BqrO7cHJ.js.map} +1 -1
  94. package/dist/lib/{shadow-claw-llm-CazBrVpM.js → shadow-claw-llm-BexUQdYo.js} +2 -2
  95. package/dist/lib/{shadow-claw-llm-CazBrVpM.js.map → shadow-claw-llm-BexUQdYo.js.map} +1 -1
  96. package/dist/lib/{shadow-claw-networking-Df85tCbO.js → shadow-claw-networking-CfXRGpUD.js} +2 -2
  97. package/dist/lib/{shadow-claw-networking-Df85tCbO.js.map → shadow-claw-networking-CfXRGpUD.js.map} +1 -1
  98. package/dist/lib/{shadow-claw-notifications-v62FryTc.js → shadow-claw-notifications-C9KPMgx_.js} +2 -2
  99. package/dist/lib/{shadow-claw-notifications-v62FryTc.js.map → shadow-claw-notifications-C9KPMgx_.js.map} +1 -1
  100. package/dist/lib/{shadow-claw-pages-BgPyIEZO.js → shadow-claw-pages-CGzGXmrI.js} +3 -3
  101. package/dist/lib/{shadow-claw-pages-BgPyIEZO.js.map → shadow-claw-pages-CGzGXmrI.js.map} +1 -1
  102. package/dist/lib/{shadow-claw-peerjs-xkBXILed.js → shadow-claw-peerjs-B0ZB7yhB.js} +3 -3
  103. package/dist/lib/{shadow-claw-peerjs-xkBXILed.js.map → shadow-claw-peerjs-B0ZB7yhB.js.map} +1 -1
  104. package/dist/lib/{shadow-claw-settings-CN89xRVU.js → shadow-claw-settings-DBfMZdDS.js} +2 -2
  105. package/dist/lib/{shadow-claw-settings-CN89xRVU.js.map → shadow-claw-settings-DBfMZdDS.js.map} +1 -1
  106. package/dist/lib/{shadow-claw-storage-VnhcObCe.js → shadow-claw-storage-CM-w6mgG.js} +2 -2
  107. package/dist/lib/{shadow-claw-storage-VnhcObCe.js.map → shadow-claw-storage-CM-w6mgG.js.map} +1 -1
  108. package/dist/lib/{shadow-claw-task-server-jgEC2hHB.js → shadow-claw-task-server-DV4b1g2-.js} +4 -2
  109. package/dist/lib/{shadow-claw-task-server-jgEC2hHB.js.map → shadow-claw-task-server-DV4b1g2-.js.map} +1 -1
  110. package/dist/lib/{shadow-claw-tasks-i_evIufy.js → shadow-claw-tasks-K-foOHSE.js} +3 -3
  111. package/dist/lib/{shadow-claw-tasks-i_evIufy.js.map → shadow-claw-tasks-K-foOHSE.js.map} +1 -1
  112. package/dist/lib/{shadow-claw-terminal-CwScROlu.js → shadow-claw-terminal-CpAknKyA.js} +2 -2
  113. package/dist/lib/{shadow-claw-terminal-CwScROlu.js.map → shadow-claw-terminal-CpAknKyA.js.map} +1 -1
  114. package/dist/lib/{shadow-claw-tools-DD3fyXP4.js → shadow-claw-tools-D98XZ0Ep.js} +3 -3
  115. package/dist/lib/{shadow-claw-tools-DD3fyXP4.js.map → shadow-claw-tools-D98XZ0Ep.js.map} +1 -1
  116. package/dist/lib/{shadow-claw-webvm-DbJq4I3-.js → shadow-claw-webvm-DLOb8dMI.js} +2 -2
  117. package/dist/lib/{shadow-claw-webvm-DbJq4I3-.js.map → shadow-claw-webvm-DLOb8dMI.js.map} +1 -1
  118. package/dist/lib/subsystems/channels/imessage.d.ts +1 -0
  119. package/dist/lib/subsystems/channels/peerjs.d.ts +1 -0
  120. package/dist/lib/subsystems/channels/telegram.d.ts +1 -0
  121. package/dist/lib/subsystems/channels/types.d.ts +2 -0
  122. package/dist/lib/subsystems/notifications/push-client.d.ts +1 -1
  123. package/dist/lib/{syncWebMcpRegistration-DNLnQ-jz.js → syncWebMcpRegistration-NspKefOI.js} +2 -2
  124. package/dist/lib/{syncWebMcpRegistration-DNLnQ-jz.js.map → syncWebMcpRegistration-NspKefOI.js.map} +1 -1
  125. package/dist/public/.well-known/ai-catalog.json +11 -0
  126. package/dist/public/.well-known/mcp/index.html +19 -0
  127. package/dist/public/.well-known/mcp/server-card.json +52 -0
  128. package/dist/public/.well-known/mcp/servers.json +15 -0
  129. package/dist/public/.well-known/mcp.json +52 -0
  130. package/dist/public/.well-known/oauth-protected-resource +10 -0
  131. package/dist/public/.well-known/oauth-protected-resource.json +10 -0
  132. package/dist/public/404.html +28 -1
  133. package/dist/public/AGENTS.md +1 -1
  134. package/dist/public/agent.worker.js +1 -1
  135. package/dist/public/{configurePeerJs-BqgeBjP7.js → configurePeerJs-BhBokQlC.js} +1 -1
  136. package/dist/public/docs/example/article/index.html +3 -3
  137. package/dist/public/docs/publishing/index.html +3 -3
  138. package/dist/public/docs/skill-creator/index.html +3 -3
  139. package/dist/public/docs/subsystems/cli.md +11 -8
  140. package/dist/public/docs/subsystems/mcp-server.md +1 -0
  141. package/dist/public/docs/subsystems/notifications.md +10 -0
  142. package/dist/public/{e2e-bridge-Ceqr5kGc.js → e2e-bridge-DcBoeFvd.js} +1 -1
  143. package/dist/public/{file-viewer-C-6Jc_tb.js → file-viewer-CKtVC9Ix.js} +1 -1
  144. package/dist/public/index.css +1 -1
  145. package/dist/public/index.html +3 -3
  146. package/dist/public/index.js +1 -1
  147. package/dist/public/initControlPlane-Bi8ouFEb.js +1 -0
  148. package/dist/public/llms.txt +36 -1
  149. package/dist/public/main/index.html +3 -3
  150. package/dist/public/main/memory/index.html +3 -3
  151. package/dist/public/{markdown-C3M4OW_A.js → markdown-C-hCVJtc.js} +1 -1
  152. package/dist/public/orchestrator-8qNGfw8r.js +15 -0
  153. package/dist/public/{orchestrator-DHjdLtqa.js → orchestrator-BT3Ks5ip.js} +2 -2
  154. package/dist/public/peerjs-DfhwEiiq.js +1 -0
  155. package/dist/public/push-client-BVLrE9pK.js +1 -0
  156. package/dist/public/service-worker/fetch-proxy.js +1 -1
  157. package/dist/public/service-worker.js +1 -1
  158. package/dist/public/{shadow-claw-EAt2ml5Y.js → shadow-claw-DCjxNDCX.js} +3 -3
  159. package/dist/public/{shadow-claw-channels-XsfuWFF8.js → shadow-claw-channels-CITDrqMl.js} +1 -1
  160. package/dist/public/{shadow-claw-chat-CbSaOZ4q.js → shadow-claw-chat-BQrSgSKF.js} +1 -1
  161. package/dist/public/{shadow-claw-control-plane-DHvN3Ukg.js → shadow-claw-control-plane-Cp001OY0.js} +2 -2
  162. package/dist/public/{shadow-claw-conversations-BWs5VVfs.js → shadow-claw-conversations-EhX4RvIj.js} +1 -1
  163. package/dist/public/{shadow-claw-file-viewer-BWsoW5QS.js → shadow-claw-file-viewer-DiQXIjZD.js} +1 -1
  164. package/dist/public/{shadow-claw-files-CG7MqWiV.js → shadow-claw-files-lkTnSJsn.js} +1 -1
  165. package/dist/public/{shadow-claw-git-DBRPZNav.js → shadow-claw-git-BlcA_z8x.js} +2 -2
  166. package/dist/public/{shadow-claw-llm-DMvLgKXo.js → shadow-claw-llm-CW16X9Rg.js} +1 -1
  167. package/dist/public/{shadow-claw-networking-Dd8Rw31D.js → shadow-claw-networking-ySsMBY_i.js} +1 -1
  168. package/dist/public/{shadow-claw-notifications-Dbevj-lq.js → shadow-claw-notifications-CyV2zCQ8.js} +1 -1
  169. package/dist/public/{shadow-claw-pages-3_lQAv0t.js → shadow-claw-pages-Cx7cF9ah.js} +1 -1
  170. package/dist/public/{shadow-claw-settings-t8TIlgrM.js → shadow-claw-settings-zm4PHHGi.js} +2 -2
  171. package/dist/public/{shadow-claw-storage-CfEXoSIE.js → shadow-claw-storage-dy9JAEGd.js} +1 -1
  172. package/dist/public/{shadow-claw-task-server-DxQSsgRZ.js → shadow-claw-task-server-Bleww48s.js} +2 -2
  173. package/dist/public/{shadow-claw-tasks-DNpicQwz.js → shadow-claw-tasks-CTI5oaLN.js} +1 -1
  174. package/dist/public/{shadow-claw-terminal-DpY6zU_r.js → shadow-claw-terminal-mZ3i4HaQ.js} +1 -1
  175. package/dist/public/{shadow-claw-tools-WSwWohnk.js → shadow-claw-tools-B_nuk8Zl.js} +1 -1
  176. package/dist/public/{shadow-claw-webvm-CP6oFjg5.js → shadow-claw-webvm-Vl_jH5YL.js} +1 -1
  177. package/dist/public/{syncWebMcpRegistration-DOn9gLnD.js → syncWebMcpRegistration-n2vAMJ4w.js} +1 -1
  178. package/dist/server.js +197 -187
  179. package/package.json +2 -1
  180. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.test.ts +6 -0
  181. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.ts +19 -9
  182. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +14 -0
  183. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.ts +12 -0
  184. package/src/components/shadow-claw/shadow-claw.ts +6 -0
  185. package/src/components/shadow-claw/utils/bindEventListeners.test.ts +57 -0
  186. package/src/components/shadow-claw/utils/bindEventListeners.ts +14 -0
  187. package/src/components/shadow-claw/utils/setupAppLifecycle.test.ts +201 -0
  188. package/src/components/shadow-claw/utils/setupAppLifecycle.ts +101 -0
  189. package/src/core/control-plane-client.test.ts +82 -1
  190. package/src/core/control-plane-client.ts +35 -1
  191. package/src/core/orchestrator/orchestrator.test.ts +49 -0
  192. package/src/core/orchestrator/orchestrator.ts +43 -0
  193. package/src/core/orchestrator/utils/operations/channel.test.ts +17 -0
  194. package/src/core/orchestrator/utils/operations/channel.ts +11 -5
  195. package/src/core/orchestrator/utils/operations/task.test.ts +26 -0
  196. package/src/core/orchestrator/utils/operations/task.ts +8 -1
  197. package/src/core/utils/initControlPlane.test.ts +138 -0
  198. package/src/core/utils/initControlPlane.ts +82 -0
  199. package/src/core/utils/initializeApp.test.ts +27 -1
  200. package/src/core/utils/initializeApp.ts +6 -13
  201. package/src/server/mcp/mcp-server.test.ts +6 -0
  202. package/src/server/mcp/mcp-server.ts +2 -1
  203. package/src/server/mcp/tools/built-in-tools.ts +88 -1
  204. package/src/server/mcp/tools/client-tool-relay.ts +6 -0
  205. package/src/server/mcp/tools/mcp-tools.test.ts +128 -0
  206. package/src/server/openapi/openapi-coverage.test.ts +2 -1
  207. package/src/server/openapi/openapi-spec.ts +2 -1
  208. package/src/server/openapi/paths/mcp.ts +104 -0
  209. package/src/server/openapi/paths/push.ts +3 -0
  210. package/src/server/routes/mcp.test.ts +92 -0
  211. package/src/server/routes/mcp.ts +138 -2
  212. package/src/server/utils/packageVersion.test.ts +10 -0
  213. package/src/server/utils/packageVersion.ts +32 -0
  214. package/src/service-worker/fetch-proxy-rules.test.ts +73 -0
  215. package/src/service-worker/fetch-proxy-rules.ts +44 -0
  216. package/src/service-worker/fetch-proxy.ts +1 -1
  217. package/src/subsystems/channels/imessage.test.ts +36 -0
  218. package/src/subsystems/channels/imessage.ts +11 -0
  219. package/src/subsystems/channels/peerjs.test.ts +56 -0
  220. package/src/subsystems/channels/peerjs.ts +29 -0
  221. package/src/subsystems/channels/telegram.test.ts +38 -0
  222. package/src/subsystems/channels/telegram.ts +13 -0
  223. package/src/subsystems/channels/types.ts +2 -0
  224. package/src/subsystems/notifications/push-client.test.ts +70 -0
  225. package/src/subsystems/notifications/push-client.ts +28 -3
  226. package/src/subsystems/notifications/push-routes.test.ts +82 -0
  227. package/src/subsystems/notifications/push-routes.ts +70 -16
  228. package/src/subsystems/notifications/push-store.test.ts +59 -0
  229. package/src/subsystems/notifications/push-store.ts +113 -19
  230. package/src/testing/jest-setup.ts +15 -0
  231. package/dist/lib/initControlPlane-C2zURVIS.js.map +0 -1
  232. package/dist/lib/orchestrator-CACLa_Ro.js.map +0 -1
  233. package/dist/lib/peerjs-DFWOBcXf.js.map +0 -1
  234. package/dist/lib/push-client-_tiWQsCT.js.map +0 -1
  235. package/dist/lib/shadow-claw-6UHdiy4a.js.map +0 -1
  236. package/dist/lib/shadow-claw-control-plane-UG0o7pCz.js.map +0 -1
  237. package/dist/public/initControlPlane-BN9cWyO0.js +0 -1
  238. package/dist/public/orchestrator-CMbCTjBf.js +0 -15
  239. package/dist/public/peerjs-DUCKH-m9.js +0 -1
  240. package/dist/public/push-client-D0lkwrK0.js +0 -1
package/bin/cli.test.mjs CHANGED
@@ -60,6 +60,12 @@ describe("shadow-claw CLI", () => {
60
60
  "--help",
61
61
  ]);
62
62
  expect(stdout).toContain("Usage: shadow-claw");
63
+ expect(stdout).toContain("Development:");
64
+ expect(stdout).toContain("Server:");
65
+ expect(stdout).toContain("Control Plane & Client Bridge:");
66
+ expect(stdout).toContain("WebRTC:");
67
+ expect(stdout).toContain("Project:");
68
+ expect(stdout).toContain("Help & Utility:");
63
69
  expect(stdout).toContain("build");
64
70
  expect(stdout).toContain("dev");
65
71
  expect(stdout).toContain("run");
@@ -75,6 +81,27 @@ describe("shadow-claw CLI", () => {
75
81
  expect(stdout).toContain("mcp");
76
82
  });
77
83
 
84
+ it("groups subcommand options into logical categories in help", async () => {
85
+ const { stdout: devHelp } = await execFileAsync(process.execPath, [
86
+ cliPath,
87
+ "dev",
88
+ "--help",
89
+ ]);
90
+ expect(devHelp).toContain("Server & Network Options:");
91
+ expect(devHelp).toContain("Site & Build Options:");
92
+ expect(devHelp).toContain("Security & Proxy Options:");
93
+ expect(devHelp).toContain("TLS / HTTPS Options:");
94
+ expect(devHelp).toContain("Storage & Database Options:");
95
+
96
+ const { stdout: webrtcHelp } = await execFileAsync(process.execPath, [
97
+ cliPath,
98
+ "webrtc",
99
+ "--help",
100
+ ]);
101
+ expect(webrtcHelp).toContain("Identity Options:");
102
+ expect(webrtcHelp).toContain("Signaling & Listener Options");
103
+ });
104
+
78
105
  it("outputs server command help with server --help and aliases", async () => {
79
106
  const { stdout: outServer } = await execFileAsync(process.execPath, [
80
107
  cliPath,
@@ -8,8 +8,23 @@
8
8
 
9
9
  import readline from "node:readline";
10
10
  import http from "node:http";
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import { fileURLToPath } from "node:url";
11
14
  import { CliControlClient } from "../utils/control-client.mjs";
12
15
 
16
+ const __mcp_dirname = path.dirname(fileURLToPath(import.meta.url));
17
+ function getCliVersion() {
18
+ try {
19
+ const pkgPath = path.resolve(__mcp_dirname, "../../package.json");
20
+ if (fs.existsSync(pkgPath)) {
21
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
22
+ if (pkg.version) return pkg.version;
23
+ }
24
+ } catch {}
25
+ return "1.27.1";
26
+ }
27
+
13
28
  export const CLI_BUILTIN_TOOLS = [
14
29
  {
15
30
  name: "shadowclaw_list_clients",
@@ -105,6 +120,30 @@ export const CLI_BUILTIN_TOOLS = [
105
120
  },
106
121
  },
107
122
  },
123
+ {
124
+ name: "shadowclaw_send_notification",
125
+ description:
126
+ "Broadcast an OS-level push notification to subscribed devices via Web Push (VAPID), or send to a specific registered client. Works even when the client browser tab is closed, asleep, or running in the background.",
127
+ inputSchema: {
128
+ type: "object",
129
+ properties: {
130
+ title: {
131
+ type: "string",
132
+ description: "Notification title (default: 'ShadowClaw').",
133
+ },
134
+ body: {
135
+ type: "string",
136
+ description: "The notification body message text.",
137
+ },
138
+ clientId: {
139
+ type: "string",
140
+ description:
141
+ "Target client ID, prefix, or device label of a specific client that has registered in the past. If omitted, broadcasts to all subscribed devices.",
142
+ },
143
+ },
144
+ required: ["body"],
145
+ },
146
+ },
108
147
  ];
109
148
 
110
149
  export function createCliMcpEngine(options = {}) {
@@ -115,7 +154,7 @@ export function createCliMcpEngine(options = {}) {
115
154
 
116
155
  const serverInfo = {
117
156
  name: "shadow-claw",
118
- version: "1.25.0",
157
+ version: options.version || getCliVersion(),
119
158
  };
120
159
 
121
160
  const capabilities = {
@@ -193,6 +232,12 @@ export function createCliMcpEngine(options = {}) {
193
232
  Array.isArray(res.data.tools)
194
233
  ) {
195
234
  for (const t of res.data.tools) {
235
+ if (
236
+ t.name === "send_notification" ||
237
+ t.name.startsWith("shadowclaw_")
238
+ ) {
239
+ continue;
240
+ }
196
241
  if (!toolDefMap.has(t.name)) {
197
242
  toolDefMap.set(t.name, t);
198
243
  toolSupportingClientsMap.set(t.name, []);
@@ -316,7 +361,9 @@ export function createCliMcpEngine(options = {}) {
316
361
  combined.set(t.name, t);
317
362
  }
318
363
  for (const t of relayed) {
319
- combined.set(t.name, t);
364
+ if (!combined.has(t.name)) {
365
+ combined.set(t.name, t);
366
+ }
320
367
  }
321
368
 
322
369
  const sortedTools = Array.from(combined.values()).sort((a, b) =>
@@ -541,6 +588,89 @@ export function createCliMcpEngine(options = {}) {
541
588
  };
542
589
  }
543
590
 
591
+ if (
592
+ toolName === "shadowclaw_send_notification" ||
593
+ toolName === "send_notification"
594
+ ) {
595
+ const body = String(args.body || "").trim();
596
+ if (!body) {
597
+ return {
598
+ jsonrpc: "2.0",
599
+ id: reqId,
600
+ result: {
601
+ resultType: "complete",
602
+ isError: true,
603
+ content: [
604
+ {
605
+ type: "text",
606
+ text: "Error: Notification 'body' parameter cannot be empty.",
607
+ },
608
+ ],
609
+ _meta: { "io.modelcontextprotocol/serverInfo": serverInfo },
610
+ },
611
+ };
612
+ }
613
+
614
+ const title = String(args.title || "ShadowClaw").trim();
615
+ const targetClientId =
616
+ typeof args.clientId === "string" && args.clientId.trim()
617
+ ? args.clientId.trim()
618
+ : undefined;
619
+
620
+ try {
621
+ const res = await client.broadcastNotification({
622
+ title,
623
+ body,
624
+ ...(targetClientId ? { clientId: targetClientId } : {}),
625
+ });
626
+ const sentCount = res?.sent ?? 0;
627
+ const failedCount = res?.failed ?? 0;
628
+ let msg;
629
+
630
+ if (res?.notFound) {
631
+ msg = `Warning: No push subscriptions found for client '${targetClientId}'. Ensure push notifications were registered by this client in ShadowClaw Settings.`;
632
+ } else if (res?.noSubscribers) {
633
+ msg =
634
+ "Warning: Push notification broadcast completed, but no devices are currently subscribed to push notifications. Ensure push notifications are enabled in ShadowClaw Settings on the client first.";
635
+ } else if (targetClientId) {
636
+ msg = `Push notification sent to client '${targetClientId}': ${sentCount} recipient(s) delivered, ${failedCount} failed.`;
637
+ } else {
638
+ msg = `Push notification broadcast sent: ${sentCount} recipient(s) delivered, ${failedCount} failed.`;
639
+ }
640
+
641
+ return {
642
+ jsonrpc: "2.0",
643
+ id: reqId,
644
+ result: {
645
+ resultType: "complete",
646
+ isError: false,
647
+ content: [{ type: "text", text: msg }],
648
+ _meta: {
649
+ ...res,
650
+ ...(targetClientId ? { targetClientId } : {}),
651
+ "io.modelcontextprotocol/serverInfo": serverInfo,
652
+ },
653
+ },
654
+ };
655
+ } catch (err) {
656
+ return {
657
+ jsonrpc: "2.0",
658
+ id: reqId,
659
+ result: {
660
+ resultType: "complete",
661
+ isError: true,
662
+ content: [
663
+ {
664
+ type: "text",
665
+ text: `Failed to send push notification: ${err.message}`,
666
+ },
667
+ ],
668
+ _meta: { "io.modelcontextprotocol/serverInfo": serverInfo },
669
+ },
670
+ };
671
+ }
672
+ }
673
+
544
674
  // Relayed interactive ask_user tool with MRTR response fulfillment if provided
545
675
  if (
546
676
  toolName === "ask_user" &&
@@ -1,3 +1,6 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
1
4
  import {
2
5
  describe,
3
6
  it,
@@ -8,6 +11,11 @@ import {
8
11
  } from "@jest/globals";
9
12
  import { createCliMcpEngine } from "./mcp.mjs";
10
13
 
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ const pkg = JSON.parse(
16
+ fs.readFileSync(path.resolve(__dirname, "../../package.json"), "utf8"),
17
+ );
18
+
11
19
  describe("CLI MCP Engine (createCliMcpEngine)", () => {
12
20
  let errorSpy;
13
21
 
@@ -41,6 +49,7 @@ describe("CLI MCP Engine (createCliMcpEngine)", () => {
41
49
  expect(res.id).toBe(1);
42
50
  expect(res.result.protocolVersion).toBe("2026-07-28");
43
51
  expect(res.result.serverInfo.name).toBe("shadow-claw");
52
+ expect(res.result.serverInfo.version).toBe(pkg.version);
44
53
  });
45
54
 
46
55
  it("processes tools/list including built-in and client tools", async () => {
@@ -82,7 +91,164 @@ describe("CLI MCP Engine (createCliMcpEngine)", () => {
82
91
  const toolNames = res.result.tools.map((t) => t.name);
83
92
  expect(toolNames).toContain("shadowclaw_list_clients");
84
93
  expect(toolNames).toContain("shadowclaw_send_message");
94
+ expect(toolNames).toContain("shadowclaw_send_notification");
85
95
  expect(toolNames).toContain("read_file");
96
+
97
+ const notifTool = res.result.tools.find(
98
+ (t) => t.name === "shadowclaw_send_notification",
99
+ );
100
+ expect(notifTool.inputSchema.properties).toHaveProperty("clientId");
101
+ });
102
+
103
+ it("processes tools/call for shadowclaw_send_notification directly via broadcastNotification", async () => {
104
+ const mockClient = {
105
+ listClients: jest.fn().mockResolvedValue([]),
106
+ broadcastNotification: jest.fn().mockResolvedValue({
107
+ sent: 2,
108
+ failed: 0,
109
+ }),
110
+ };
111
+
112
+ const engine = createCliMcpEngine({ client: mockClient });
113
+ const res = await engine.handleMessage({
114
+ jsonrpc: "2.0",
115
+ id: 30,
116
+ method: "tools/call",
117
+ params: {
118
+ name: "shadowclaw_send_notification",
119
+ arguments: { title: "Custom Alert", body: "Server push message" },
120
+ },
121
+ });
122
+
123
+ expect(res).toBeDefined();
124
+ expect(res.result.resultType).toBe("complete");
125
+ expect(res.result.isError).toBe(false);
126
+ expect(res.result.content[0].text).toContain(
127
+ "2 recipient(s) delivered, 0 failed",
128
+ );
129
+ expect(mockClient.broadcastNotification).toHaveBeenCalledWith({
130
+ title: "Custom Alert",
131
+ body: "Server push message",
132
+ });
133
+ });
134
+
135
+ it("processes tools/call for send_notification alias directly via broadcastNotification", async () => {
136
+ const mockClient = {
137
+ listClients: jest.fn().mockResolvedValue([]),
138
+ broadcastNotification: jest.fn().mockResolvedValue({
139
+ sent: 1,
140
+ failed: 0,
141
+ }),
142
+ };
143
+
144
+ const engine = createCliMcpEngine({ client: mockClient });
145
+ const res = await engine.handleMessage({
146
+ jsonrpc: "2.0",
147
+ id: 31,
148
+ method: "tools/call",
149
+ params: {
150
+ name: "send_notification",
151
+ arguments: { body: "Fallback push" },
152
+ },
153
+ });
154
+
155
+ expect(res).toBeDefined();
156
+ expect(res.result.resultType).toBe("complete");
157
+ expect(res.result.isError).toBe(false);
158
+ expect(mockClient.broadcastNotification).toHaveBeenCalledWith({
159
+ title: "ShadowClaw",
160
+ body: "Fallback push",
161
+ });
162
+ });
163
+
164
+ it("handles empty body validation for shadowclaw_send_notification", async () => {
165
+ const mockClient = {
166
+ listClients: jest.fn().mockResolvedValue([]),
167
+ broadcastNotification: jest.fn(),
168
+ };
169
+
170
+ const engine = createCliMcpEngine({ client: mockClient });
171
+ const res = await engine.handleMessage({
172
+ jsonrpc: "2.0",
173
+ id: 32,
174
+ method: "tools/call",
175
+ params: {
176
+ name: "shadowclaw_send_notification",
177
+ arguments: { body: "" },
178
+ },
179
+ });
180
+
181
+ expect(res).toBeDefined();
182
+ expect(res.result.isError).toBe(true);
183
+ expect(res.result.content[0].text).toContain("cannot be empty");
184
+ expect(mockClient.broadcastNotification).not.toHaveBeenCalled();
185
+ });
186
+
187
+ it("targets a specific client in shadowclaw_send_notification", async () => {
188
+ const mockClient = {
189
+ listClients: jest.fn().mockResolvedValue([]),
190
+ broadcastNotification: jest.fn().mockResolvedValue({
191
+ sent: 1,
192
+ failed: 0,
193
+ }),
194
+ };
195
+
196
+ const engine = createCliMcpEngine({ client: mockClient });
197
+ const res = await engine.handleMessage({
198
+ jsonrpc: "2.0",
199
+ id: 33,
200
+ method: "tools/call",
201
+ params: {
202
+ name: "shadowclaw_send_notification",
203
+ arguments: {
204
+ title: "Direct Alert",
205
+ body: "Direct push message",
206
+ clientId: "client-target-99",
207
+ },
208
+ },
209
+ });
210
+
211
+ expect(res).toBeDefined();
212
+ expect(res.result.isError).toBe(false);
213
+ expect(res.result.content[0].text).toContain(
214
+ "Push notification sent to client 'client-target-99': 1 recipient(s) delivered, 0 failed.",
215
+ );
216
+ expect(mockClient.broadcastNotification).toHaveBeenCalledWith({
217
+ title: "Direct Alert",
218
+ body: "Direct push message",
219
+ clientId: "client-target-99",
220
+ });
221
+ });
222
+
223
+ it("handles notFound warning when targeted client has no push subscriptions", async () => {
224
+ const mockClient = {
225
+ listClients: jest.fn().mockResolvedValue([]),
226
+ broadcastNotification: jest.fn().mockResolvedValue({
227
+ sent: 0,
228
+ failed: 0,
229
+ notFound: true,
230
+ }),
231
+ };
232
+
233
+ const engine = createCliMcpEngine({ client: mockClient });
234
+ const res = await engine.handleMessage({
235
+ jsonrpc: "2.0",
236
+ id: 34,
237
+ method: "tools/call",
238
+ params: {
239
+ name: "shadowclaw_send_notification",
240
+ arguments: {
241
+ body: "Alert",
242
+ clientId: "missing-client-id",
243
+ },
244
+ },
245
+ });
246
+
247
+ expect(res).toBeDefined();
248
+ expect(res.result.isError).toBe(false);
249
+ expect(res.result.content[0].text).toContain(
250
+ "No push subscriptions found for client 'missing-client-id'",
251
+ );
86
252
  });
87
253
 
88
254
  it("processes tools/call for shadowclaw_send_message", async () => {
@@ -8,8 +8,12 @@ import crypto from "node:crypto";
8
8
  import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
9
9
  import path from "node:path";
10
10
  import process from "node:process";
11
+ import { fileURLToPath } from "node:url";
11
12
  import matter from "gray-matter";
12
13
 
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+
13
17
  /**
14
18
  * Computes a standardized sha256:{hex} digest from Buffer or string.
15
19
  */
@@ -64,30 +68,86 @@ export async function generateSkillsIndex(
64
68
  const wellKnownDirRel = options.outDir || ".well-known/agent-skills";
65
69
 
66
70
  // 1. Read metadata from shadow-claw.config.json or package.json
67
- let siteName = path.basename(resolvedRoot);
68
- let siteDescription =
69
- "Agent skills and tools collection, powered by ShadowClaw.";
70
-
71
- const configPath = path.join(resolvedRoot, "shadow-claw.config.json");
72
- if (await pathExists(configPath)) {
73
- try {
74
- const configStr = await readFile(configPath, "utf8");
75
- const config = JSON.parse(configStr);
76
- if (config.site?.title) siteName = config.site.title;
77
- if (config.site?.description) siteDescription = config.site.description;
78
- } catch {}
79
- } else {
80
- const pkgPath = path.join(resolvedRoot, "package.json");
81
- if (await pathExists(pkgPath)) {
71
+ const metadataRoot = options.metadataRoot
72
+ ? path.resolve(options.metadataRoot)
73
+ : resolvedRoot;
74
+
75
+ let siteName = options.siteName;
76
+ let siteDescription = options.siteDescription;
77
+
78
+ const parent1 = path.dirname(metadataRoot);
79
+ const parent2 = path.dirname(parent1);
80
+
81
+ const configCandidates = [
82
+ options.configPath,
83
+ path.join(metadataRoot, "shadow-claw.config.json"),
84
+ path.join(metadataRoot, "shadow-claw-config.json"),
85
+ path.join(metadataRoot, "site-config.json"),
86
+ path.join(parent1, "shadow-claw.config.json"),
87
+ path.join(parent1, "shadow-claw-config.json"),
88
+ path.join(parent1, "site-config.json"),
89
+ path.join(parent2, "shadow-claw.config.json"),
90
+ path.join(parent2, "shadow-claw-config.json"),
91
+ path.join(parent2, "site-config.json"),
92
+ ].filter(Boolean);
93
+
94
+ let loadedConfig = null;
95
+ for (const candidate of configCandidates) {
96
+ if (await pathExists(candidate)) {
82
97
  try {
83
- const pkgStr = await readFile(pkgPath, "utf8");
84
- const pkg = JSON.parse(pkgStr);
85
- if (pkg.name) siteName = pkg.name;
86
- if (pkg.description) siteDescription = pkg.description;
98
+ const configStr = await readFile(candidate, "utf8");
99
+ loadedConfig = JSON.parse(configStr);
100
+ break;
87
101
  } catch {}
88
102
  }
89
103
  }
90
104
 
105
+ if (loadedConfig) {
106
+ if (!siteName && loadedConfig.site?.title)
107
+ siteName = loadedConfig.site.title;
108
+ if (!siteDescription && loadedConfig.site?.description) {
109
+ siteDescription = loadedConfig.site.description;
110
+ }
111
+ }
112
+
113
+ if (!siteName || !siteDescription) {
114
+ const pkgCandidates = [
115
+ path.join(metadataRoot, "package.json"),
116
+ path.join(parent1, "package.json"),
117
+ path.join(parent2, "package.json"),
118
+ ];
119
+ for (const pkgPath of pkgCandidates) {
120
+ if (await pathExists(pkgPath)) {
121
+ try {
122
+ const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
123
+ if (!siteName && pkg.name) siteName = pkg.name;
124
+ if (!siteDescription && pkg.description) {
125
+ siteDescription = pkg.description;
126
+ }
127
+ break;
128
+ } catch {}
129
+ }
130
+ }
131
+ }
132
+
133
+ if (!siteName) {
134
+ siteName = path.basename(metadataRoot);
135
+ if (siteName === "public" || siteName === "dist") {
136
+ siteName = path.basename(parent1);
137
+ if (
138
+ (siteName === "public" || siteName === "dist") &&
139
+ path.basename(parent2)
140
+ ) {
141
+ siteName = path.basename(parent2);
142
+ }
143
+ }
144
+ }
145
+
146
+ if (!siteDescription) {
147
+ siteDescription =
148
+ "Agent skills and tools collection, powered by ShadowClaw.";
149
+ }
150
+
91
151
  // 2. Discover Tools (.agents/tools/**/*.json)
92
152
  const toolsDir = path.join(resolvedRoot, ".agents", "tools");
93
153
  const toolFiles = await findFilesRecursively(toolsDir, (name) =>
@@ -249,6 +309,61 @@ export async function generateSkillsIndex(
249
309
  });
250
310
  }
251
311
 
312
+ // 4b. Discover Bundled Skills from Toolchain (if requested or enabled)
313
+ const defaultToolchainRoot = path.resolve(__dirname, "../..");
314
+ const bundledSkillsDir = options.bundledSkillsDir
315
+ ? path.resolve(options.bundledSkillsDir)
316
+ : options.toolchainRoot
317
+ ? path.join(path.resolve(options.toolchainRoot), ".agents", "skills")
318
+ : options.includeBundled
319
+ ? path.join(defaultToolchainRoot, ".agents", "skills")
320
+ : null;
321
+
322
+ if (
323
+ bundledSkillsDir &&
324
+ (await pathExists(bundledSkillsDir)) &&
325
+ bundledSkillsDir !== skillsDir
326
+ ) {
327
+ const bundledSkillFiles = await findFilesRecursively(
328
+ bundledSkillsDir,
329
+ (name) => name === "SKILL.md",
330
+ );
331
+ for (const fullSkillPath of bundledSkillFiles) {
332
+ const relFromBundled = path.relative(bundledSkillsDir, fullSkillPath);
333
+ const relFromRoot = path.join(".agents", "skills", relFromBundled);
334
+ const relUrl = path.posix.normalize(
335
+ path.posix.relative(
336
+ wellKnownDirRel,
337
+ relFromRoot.split(path.sep).join(path.posix.sep),
338
+ ),
339
+ );
340
+ const rawContent = await readFile(fullSkillPath);
341
+ const digest = computeSha256(rawContent);
342
+
343
+ let frontmatter = {};
344
+ try {
345
+ const parsed = matter(rawContent.toString("utf8"));
346
+ frontmatter = parsed.data || {};
347
+ } catch {}
348
+
349
+ const parentDirName = path.basename(path.dirname(fullSkillPath));
350
+ const skillName = frontmatter.name || parentDirName;
351
+ const skillDesc = frontmatter.description || "";
352
+
353
+ if (discoveredSkills.some((s) => s.name === skillName)) {
354
+ continue;
355
+ }
356
+
357
+ discoveredSkills.push({
358
+ name: skillName,
359
+ type: "skill-md",
360
+ description: skillDesc,
361
+ url: relUrl,
362
+ digest,
363
+ });
364
+ }
365
+ }
366
+
252
367
  // 5. Construct Index Document
253
368
  const indexDoc = {
254
369
  $schema: "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
@@ -319,7 +434,11 @@ export async function runSkillsIndexCommand(dir, options = {}) {
319
434
  const targetDir = dir ? path.resolve(dir) : process.cwd();
320
435
  console.log(`Generating Agent Skills Discovery index for ${targetDir}...`);
321
436
 
322
- const result = await generateSkillsIndex(targetDir, options);
437
+ const result = await generateSkillsIndex(targetDir, {
438
+ includeBundled: options.bundled !== false,
439
+ ...options,
440
+ configPath: options.config || options.configPath,
441
+ });
323
442
  const count = result.skills ? result.skills.length : 0;
324
443
  console.log(
325
444
  `Indexed ${count} skill(s) into .well-known/agent-skills/index.json`,
@@ -158,4 +158,86 @@ metadata:
158
158
  await generateSkillsIndex(tempDir, { outFile: customOut });
159
159
  expect(fs.existsSync(customOut)).toBe(true);
160
160
  });
161
+
162
+ it("resolves site metadata from parent directory when indexing a subfolder like dist/public", async () => {
163
+ await writeFile(
164
+ path.join(tempDir, "shadow-claw.config.json"),
165
+ JSON.stringify({
166
+ site: {
167
+ title: "My Custom Knowledge Hub",
168
+ description: "A custom description for testing metadata resolution.",
169
+ },
170
+ }),
171
+ "utf8",
172
+ );
173
+
174
+ const distPublicDir = path.join(tempDir, "dist", "public");
175
+ const skillDir = path.join(
176
+ distPublicDir,
177
+ ".agents",
178
+ "skills",
179
+ "main",
180
+ "custom",
181
+ );
182
+ await mkdir(skillDir, { recursive: true });
183
+ await writeFile(
184
+ path.join(skillDir, "SKILL.md"),
185
+ "---\nname: custom\ndescription: Custom skill.\n---\n",
186
+ "utf8",
187
+ );
188
+
189
+ const res = await generateSkillsIndex(distPublicDir, { write: false });
190
+ expect(res.name).toBe("My Custom Knowledge Hub");
191
+ expect(res.description).toBe(
192
+ "A custom description for testing metadata resolution.",
193
+ );
194
+ expect(res.skills).toHaveLength(1);
195
+ expect(res.skills[0].name).toBe("custom");
196
+ expect(res.skills[0].url).toBe("../../.agents/skills/main/custom/SKILL.md");
197
+ });
198
+
199
+ it("discovers bundled skills when toolchainRoot is provided and merges them without duplicates", async () => {
200
+ const mockToolchain = path.join(tempDir, "toolchain");
201
+ const bundledSkillDir = path.join(
202
+ mockToolchain,
203
+ ".agents",
204
+ "skills",
205
+ "main",
206
+ "skill-creator",
207
+ );
208
+ await mkdir(bundledSkillDir, { recursive: true });
209
+ await writeFile(
210
+ path.join(bundledSkillDir, "SKILL.md"),
211
+ "---\nname: skill-creator\ndescription: Bundled skill creator.\n---\n",
212
+ "utf8",
213
+ );
214
+
215
+ const mockContent = path.join(tempDir, "content");
216
+ const contentSkillDir = path.join(
217
+ mockContent,
218
+ ".agents",
219
+ "skills",
220
+ "main",
221
+ "my-skill",
222
+ );
223
+ await mkdir(contentSkillDir, { recursive: true });
224
+ await writeFile(
225
+ path.join(contentSkillDir, "SKILL.md"),
226
+ "---\nname: my-skill\ndescription: Custom skill.\n---\n",
227
+ "utf8",
228
+ );
229
+
230
+ const res = await generateSkillsIndex(mockContent, {
231
+ toolchainRoot: mockToolchain,
232
+ write: false,
233
+ });
234
+ expect(res.skills).toHaveLength(2);
235
+ expect(res.skills.map((s) => s.name)).toEqual(
236
+ expect.arrayContaining(["my-skill", "skill-creator"]),
237
+ );
238
+ const creatorSkill = res.skills.find((s) => s.name === "skill-creator");
239
+ expect(creatorSkill.url).toBe(
240
+ "../../.agents/skills/main/skill-creator/SKILL.md",
241
+ );
242
+ });
161
243
  });