shadow-claw 1.27.1 → 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 (236) 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 +99 -34
  11. package/bin/build/build.test.mjs +32 -0
  12. package/bin/cli.mjs +290 -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/utils/control-client.mjs +17 -0
  17. package/bin/utils/control-client.test.mjs +46 -0
  18. package/bin/version.mjs +208 -0
  19. package/bin/version.test.mjs +138 -0
  20. package/dist/electron/main.cjs +195 -185
  21. package/dist/lib/components/index.js +21 -21
  22. package/dist/lib/components/settings/shadow-claw-channel-config.js +1 -1
  23. package/dist/lib/components/settings/shadow-claw-control-plane.js +1 -1
  24. package/dist/lib/components/settings/shadow-claw-git.js +1 -1
  25. package/dist/lib/components/settings/shadow-claw-llm.js +1 -1
  26. package/dist/lib/components/settings/shadow-claw-networking.js +1 -1
  27. package/dist/lib/components/settings/shadow-claw-notifications.js +1 -1
  28. package/dist/lib/components/settings/shadow-claw-peerjs.js +1 -1
  29. package/dist/lib/components/settings/shadow-claw-storage.js +1 -1
  30. package/dist/lib/components/settings/shadow-claw-task-server.js +1 -1
  31. package/dist/lib/components/settings/shadow-claw-webvm.js +1 -1
  32. package/dist/lib/components/shadow-claw/shadow-claw.d.ts +1 -0
  33. package/dist/lib/components/shadow-claw/utils/setupAppLifecycle.d.ts +23 -0
  34. package/dist/lib/components/shadow-claw-channel-config.js +1 -1
  35. package/dist/lib/components/shadow-claw-channels.js +1 -1
  36. package/dist/lib/components/shadow-claw-chat.js +1 -1
  37. package/dist/lib/components/shadow-claw-control-plane.js +1 -1
  38. package/dist/lib/components/shadow-claw-conversations.js +1 -1
  39. package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
  40. package/dist/lib/components/shadow-claw-files.js +1 -1
  41. package/dist/lib/components/shadow-claw-git.js +1 -1
  42. package/dist/lib/components/shadow-claw-llm.js +1 -1
  43. package/dist/lib/components/shadow-claw-networking.js +1 -1
  44. package/dist/lib/components/shadow-claw-notifications.js +1 -1
  45. package/dist/lib/components/shadow-claw-pages.js +1 -1
  46. package/dist/lib/components/shadow-claw-peerjs.js +1 -1
  47. package/dist/lib/components/shadow-claw-settings.js +1 -1
  48. package/dist/lib/components/shadow-claw-storage.js +1 -1
  49. package/dist/lib/components/shadow-claw-task-server.js +1 -1
  50. package/dist/lib/components/shadow-claw-tasks.js +1 -1
  51. package/dist/lib/components/shadow-claw-terminal.js +1 -1
  52. package/dist/lib/components/shadow-claw-tools.js +1 -1
  53. package/dist/lib/components/shadow-claw-webvm.js +1 -1
  54. package/dist/lib/components/shadow-claw.js +1 -1
  55. package/dist/lib/{configurePeerJs-oODzltaC.js → configurePeerJs-D8Ph0-A9.js} +2 -2
  56. package/dist/lib/{configurePeerJs-oODzltaC.js.map → configurePeerJs-D8Ph0-A9.js.map} +1 -1
  57. package/dist/lib/core/control-plane-client.d.ts +2 -0
  58. package/dist/lib/core/orchestrator/orchestrator.d.ts +10 -0
  59. package/dist/lib/core/orchestrator/utils/operations/channel.d.ts +2 -2
  60. package/dist/lib/core/utils/initControlPlane.d.ts +6 -0
  61. package/dist/lib/index.js +21 -21
  62. package/dist/lib/{initControlPlane-C2zURVIS.js → initControlPlane-BIrWmpzc.js} +76 -9
  63. package/dist/lib/initControlPlane-BIrWmpzc.js.map +1 -0
  64. package/dist/lib/{markdown-C6EHl9zs.js → markdown-DEUoM1c6.js} +2 -2
  65. package/dist/lib/{markdown-C6EHl9zs.js.map → markdown-DEUoM1c6.js.map} +1 -1
  66. package/dist/lib/{orchestrator-KRjx0eba.js → orchestrator-BtVj9bBB.js} +11 -10
  67. package/dist/lib/{orchestrator-KRjx0eba.js.map → orchestrator-BtVj9bBB.js.map} +1 -1
  68. package/dist/lib/{orchestrator-CACLa_Ro.js → orchestrator-CFCW-l1O.js} +52 -4
  69. package/dist/lib/orchestrator-CFCW-l1O.js.map +1 -0
  70. package/dist/lib/{peerjs-DFWOBcXf.js → peerjs-BoBkx9DJ.js} +23 -3
  71. package/dist/lib/peerjs-BoBkx9DJ.js.map +1 -0
  72. package/dist/lib/{push-client-_tiWQsCT.js → push-client-D6hbzDNV.js} +10 -4
  73. package/dist/lib/push-client-D6hbzDNV.js.map +1 -0
  74. package/dist/lib/{shadow-claw-6UHdiy4a.js → shadow-claw-Cuf2O5bZ.js} +71 -4
  75. package/dist/lib/shadow-claw-Cuf2O5bZ.js.map +1 -0
  76. package/dist/lib/{shadow-claw-channel-config-DmfwJc6G.js → shadow-claw-channel-config-BDPKt6Bd.js} +3 -3
  77. package/dist/lib/{shadow-claw-channel-config-DmfwJc6G.js.map → shadow-claw-channel-config-BDPKt6Bd.js.map} +1 -1
  78. package/dist/lib/{shadow-claw-channels-1N6Jl-By.js → shadow-claw-channels-BsP5JABM.js} +2 -2
  79. package/dist/lib/{shadow-claw-channels-1N6Jl-By.js.map → shadow-claw-channels-BsP5JABM.js.map} +1 -1
  80. package/dist/lib/{shadow-claw-chat-BMoqQJRK.js → shadow-claw-chat-Cam2lX4k.js} +4 -4
  81. package/dist/lib/{shadow-claw-chat-BMoqQJRK.js.map → shadow-claw-chat-Cam2lX4k.js.map} +1 -1
  82. package/dist/lib/{shadow-claw-control-plane-UG0o7pCz.js → shadow-claw-control-plane-DB2qcCFs.js} +20 -10
  83. package/dist/lib/shadow-claw-control-plane-DB2qcCFs.js.map +1 -0
  84. package/dist/lib/{shadow-claw-conversations-BwXep3iu.js → shadow-claw-conversations-C9Vf4XGx.js} +2 -2
  85. package/dist/lib/{shadow-claw-conversations-BwXep3iu.js.map → shadow-claw-conversations-C9Vf4XGx.js.map} +1 -1
  86. package/dist/lib/{shadow-claw-file-viewer-DUxzVxDT.js → shadow-claw-file-viewer-B4HYiI0v.js} +3 -3
  87. package/dist/lib/{shadow-claw-file-viewer-DUxzVxDT.js.map → shadow-claw-file-viewer-B4HYiI0v.js.map} +1 -1
  88. package/dist/lib/{shadow-claw-files-DIREEnT2.js → shadow-claw-files-DVROUESO.js} +2 -2
  89. package/dist/lib/{shadow-claw-files-DIREEnT2.js.map → shadow-claw-files-DVROUESO.js.map} +1 -1
  90. package/dist/lib/{shadow-claw-git-Bg1SWgsF.js → shadow-claw-git-BqrO7cHJ.js} +3 -3
  91. package/dist/lib/{shadow-claw-git-Bg1SWgsF.js.map → shadow-claw-git-BqrO7cHJ.js.map} +1 -1
  92. package/dist/lib/{shadow-claw-llm-CazBrVpM.js → shadow-claw-llm-BexUQdYo.js} +2 -2
  93. package/dist/lib/{shadow-claw-llm-CazBrVpM.js.map → shadow-claw-llm-BexUQdYo.js.map} +1 -1
  94. package/dist/lib/{shadow-claw-networking-Df85tCbO.js → shadow-claw-networking-CfXRGpUD.js} +2 -2
  95. package/dist/lib/{shadow-claw-networking-Df85tCbO.js.map → shadow-claw-networking-CfXRGpUD.js.map} +1 -1
  96. package/dist/lib/{shadow-claw-notifications-v62FryTc.js → shadow-claw-notifications-C9KPMgx_.js} +2 -2
  97. package/dist/lib/{shadow-claw-notifications-v62FryTc.js.map → shadow-claw-notifications-C9KPMgx_.js.map} +1 -1
  98. package/dist/lib/{shadow-claw-pages-BgPyIEZO.js → shadow-claw-pages-CGzGXmrI.js} +3 -3
  99. package/dist/lib/{shadow-claw-pages-BgPyIEZO.js.map → shadow-claw-pages-CGzGXmrI.js.map} +1 -1
  100. package/dist/lib/{shadow-claw-peerjs-xkBXILed.js → shadow-claw-peerjs-B0ZB7yhB.js} +3 -3
  101. package/dist/lib/{shadow-claw-peerjs-xkBXILed.js.map → shadow-claw-peerjs-B0ZB7yhB.js.map} +1 -1
  102. package/dist/lib/{shadow-claw-settings-CN89xRVU.js → shadow-claw-settings-DBfMZdDS.js} +2 -2
  103. package/dist/lib/{shadow-claw-settings-CN89xRVU.js.map → shadow-claw-settings-DBfMZdDS.js.map} +1 -1
  104. package/dist/lib/{shadow-claw-storage-VnhcObCe.js → shadow-claw-storage-CM-w6mgG.js} +2 -2
  105. package/dist/lib/{shadow-claw-storage-VnhcObCe.js.map → shadow-claw-storage-CM-w6mgG.js.map} +1 -1
  106. package/dist/lib/{shadow-claw-task-server-jgEC2hHB.js → shadow-claw-task-server-DV4b1g2-.js} +4 -2
  107. package/dist/lib/{shadow-claw-task-server-jgEC2hHB.js.map → shadow-claw-task-server-DV4b1g2-.js.map} +1 -1
  108. package/dist/lib/{shadow-claw-tasks-i_evIufy.js → shadow-claw-tasks-K-foOHSE.js} +3 -3
  109. package/dist/lib/{shadow-claw-tasks-i_evIufy.js.map → shadow-claw-tasks-K-foOHSE.js.map} +1 -1
  110. package/dist/lib/{shadow-claw-terminal-CwScROlu.js → shadow-claw-terminal-CpAknKyA.js} +2 -2
  111. package/dist/lib/{shadow-claw-terminal-CwScROlu.js.map → shadow-claw-terminal-CpAknKyA.js.map} +1 -1
  112. package/dist/lib/{shadow-claw-tools-DD3fyXP4.js → shadow-claw-tools-D98XZ0Ep.js} +3 -3
  113. package/dist/lib/{shadow-claw-tools-DD3fyXP4.js.map → shadow-claw-tools-D98XZ0Ep.js.map} +1 -1
  114. package/dist/lib/{shadow-claw-webvm-DbJq4I3-.js → shadow-claw-webvm-DLOb8dMI.js} +2 -2
  115. package/dist/lib/{shadow-claw-webvm-DbJq4I3-.js.map → shadow-claw-webvm-DLOb8dMI.js.map} +1 -1
  116. package/dist/lib/subsystems/channels/imessage.d.ts +1 -0
  117. package/dist/lib/subsystems/channels/peerjs.d.ts +1 -0
  118. package/dist/lib/subsystems/channels/telegram.d.ts +1 -0
  119. package/dist/lib/subsystems/channels/types.d.ts +2 -0
  120. package/dist/lib/subsystems/notifications/push-client.d.ts +1 -1
  121. package/dist/lib/{syncWebMcpRegistration-DNLnQ-jz.js → syncWebMcpRegistration-NspKefOI.js} +2 -2
  122. package/dist/lib/{syncWebMcpRegistration-DNLnQ-jz.js.map → syncWebMcpRegistration-NspKefOI.js.map} +1 -1
  123. package/dist/public/.well-known/ai-catalog.json +11 -0
  124. package/dist/public/.well-known/mcp/index.html +19 -0
  125. package/dist/public/.well-known/mcp/server-card.json +52 -0
  126. package/dist/public/.well-known/mcp/servers.json +15 -0
  127. package/dist/public/.well-known/mcp.json +52 -0
  128. package/dist/public/.well-known/oauth-protected-resource +10 -0
  129. package/dist/public/.well-known/oauth-protected-resource.json +10 -0
  130. package/dist/public/404.html +28 -1
  131. package/dist/public/agent.worker.js +1 -1
  132. package/dist/public/{configurePeerJs-BqgeBjP7.js → configurePeerJs-BhBokQlC.js} +1 -1
  133. package/dist/public/docs/example/article/index.html +3 -3
  134. package/dist/public/docs/publishing/index.html +3 -3
  135. package/dist/public/docs/skill-creator/index.html +3 -3
  136. package/dist/public/docs/subsystems/mcp-server.md +1 -0
  137. package/dist/public/docs/subsystems/notifications.md +10 -0
  138. package/dist/public/{e2e-bridge-Ceqr5kGc.js → e2e-bridge-DcBoeFvd.js} +1 -1
  139. package/dist/public/{file-viewer-C-6Jc_tb.js → file-viewer-CKtVC9Ix.js} +1 -1
  140. package/dist/public/index.css +1 -1
  141. package/dist/public/index.html +3 -3
  142. package/dist/public/index.js +1 -1
  143. package/dist/public/initControlPlane-Bi8ouFEb.js +1 -0
  144. package/dist/public/llms.txt +36 -1
  145. package/dist/public/main/index.html +3 -3
  146. package/dist/public/main/memory/index.html +3 -3
  147. package/dist/public/{markdown-C3M4OW_A.js → markdown-C-hCVJtc.js} +1 -1
  148. package/dist/public/orchestrator-8qNGfw8r.js +15 -0
  149. package/dist/public/{orchestrator-DHjdLtqa.js → orchestrator-BT3Ks5ip.js} +2 -2
  150. package/dist/public/peerjs-DfhwEiiq.js +1 -0
  151. package/dist/public/push-client-BVLrE9pK.js +1 -0
  152. package/dist/public/service-worker/fetch-proxy.js +1 -1
  153. package/dist/public/service-worker.js +1 -1
  154. package/dist/public/{shadow-claw-EAt2ml5Y.js → shadow-claw-DCjxNDCX.js} +3 -3
  155. package/dist/public/{shadow-claw-channels-XsfuWFF8.js → shadow-claw-channels-CITDrqMl.js} +1 -1
  156. package/dist/public/{shadow-claw-chat-CbSaOZ4q.js → shadow-claw-chat-BQrSgSKF.js} +1 -1
  157. package/dist/public/{shadow-claw-control-plane-DHvN3Ukg.js → shadow-claw-control-plane-Cp001OY0.js} +2 -2
  158. package/dist/public/{shadow-claw-conversations-BWs5VVfs.js → shadow-claw-conversations-EhX4RvIj.js} +1 -1
  159. package/dist/public/{shadow-claw-file-viewer-BWsoW5QS.js → shadow-claw-file-viewer-DiQXIjZD.js} +1 -1
  160. package/dist/public/{shadow-claw-files-CG7MqWiV.js → shadow-claw-files-lkTnSJsn.js} +1 -1
  161. package/dist/public/{shadow-claw-git-DBRPZNav.js → shadow-claw-git-BlcA_z8x.js} +2 -2
  162. package/dist/public/{shadow-claw-llm-DMvLgKXo.js → shadow-claw-llm-CW16X9Rg.js} +1 -1
  163. package/dist/public/{shadow-claw-networking-Dd8Rw31D.js → shadow-claw-networking-ySsMBY_i.js} +1 -1
  164. package/dist/public/{shadow-claw-notifications-Dbevj-lq.js → shadow-claw-notifications-CyV2zCQ8.js} +1 -1
  165. package/dist/public/{shadow-claw-pages-3_lQAv0t.js → shadow-claw-pages-Cx7cF9ah.js} +1 -1
  166. package/dist/public/{shadow-claw-settings-t8TIlgrM.js → shadow-claw-settings-zm4PHHGi.js} +2 -2
  167. package/dist/public/{shadow-claw-storage-CfEXoSIE.js → shadow-claw-storage-dy9JAEGd.js} +1 -1
  168. package/dist/public/{shadow-claw-task-server-DxQSsgRZ.js → shadow-claw-task-server-Bleww48s.js} +2 -2
  169. package/dist/public/{shadow-claw-tasks-DNpicQwz.js → shadow-claw-tasks-CTI5oaLN.js} +1 -1
  170. package/dist/public/{shadow-claw-terminal-DpY6zU_r.js → shadow-claw-terminal-mZ3i4HaQ.js} +1 -1
  171. package/dist/public/{shadow-claw-tools-WSwWohnk.js → shadow-claw-tools-B_nuk8Zl.js} +1 -1
  172. package/dist/public/{shadow-claw-webvm-CP6oFjg5.js → shadow-claw-webvm-Vl_jH5YL.js} +1 -1
  173. package/dist/public/{syncWebMcpRegistration-DOn9gLnD.js → syncWebMcpRegistration-n2vAMJ4w.js} +1 -1
  174. package/dist/server.js +197 -187
  175. package/package.json +2 -1
  176. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.test.ts +6 -0
  177. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.ts +19 -9
  178. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +14 -0
  179. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.ts +12 -0
  180. package/src/components/shadow-claw/shadow-claw.ts +6 -0
  181. package/src/components/shadow-claw/utils/bindEventListeners.test.ts +57 -0
  182. package/src/components/shadow-claw/utils/bindEventListeners.ts +14 -0
  183. package/src/components/shadow-claw/utils/setupAppLifecycle.test.ts +201 -0
  184. package/src/components/shadow-claw/utils/setupAppLifecycle.ts +101 -0
  185. package/src/core/control-plane-client.test.ts +82 -1
  186. package/src/core/control-plane-client.ts +35 -1
  187. package/src/core/orchestrator/orchestrator.test.ts +49 -0
  188. package/src/core/orchestrator/orchestrator.ts +43 -0
  189. package/src/core/orchestrator/utils/operations/channel.test.ts +17 -0
  190. package/src/core/orchestrator/utils/operations/channel.ts +11 -5
  191. package/src/core/orchestrator/utils/operations/task.test.ts +26 -0
  192. package/src/core/orchestrator/utils/operations/task.ts +8 -1
  193. package/src/core/utils/initControlPlane.test.ts +138 -0
  194. package/src/core/utils/initControlPlane.ts +82 -0
  195. package/src/core/utils/initializeApp.test.ts +27 -1
  196. package/src/core/utils/initializeApp.ts +6 -13
  197. package/src/server/mcp/mcp-server.test.ts +6 -0
  198. package/src/server/mcp/mcp-server.ts +2 -1
  199. package/src/server/mcp/tools/built-in-tools.ts +88 -1
  200. package/src/server/mcp/tools/client-tool-relay.ts +6 -0
  201. package/src/server/mcp/tools/mcp-tools.test.ts +128 -0
  202. package/src/server/openapi/openapi-coverage.test.ts +2 -1
  203. package/src/server/openapi/openapi-spec.ts +2 -1
  204. package/src/server/openapi/paths/mcp.ts +104 -0
  205. package/src/server/openapi/paths/push.ts +3 -0
  206. package/src/server/routes/mcp.test.ts +92 -0
  207. package/src/server/routes/mcp.ts +138 -2
  208. package/src/server/utils/packageVersion.test.ts +10 -0
  209. package/src/server/utils/packageVersion.ts +32 -0
  210. package/src/service-worker/fetch-proxy-rules.test.ts +73 -0
  211. package/src/service-worker/fetch-proxy-rules.ts +44 -0
  212. package/src/service-worker/fetch-proxy.ts +1 -1
  213. package/src/subsystems/channels/imessage.test.ts +36 -0
  214. package/src/subsystems/channels/imessage.ts +11 -0
  215. package/src/subsystems/channels/peerjs.test.ts +56 -0
  216. package/src/subsystems/channels/peerjs.ts +29 -0
  217. package/src/subsystems/channels/telegram.test.ts +38 -0
  218. package/src/subsystems/channels/telegram.ts +13 -0
  219. package/src/subsystems/channels/types.ts +2 -0
  220. package/src/subsystems/notifications/push-client.test.ts +70 -0
  221. package/src/subsystems/notifications/push-client.ts +28 -3
  222. package/src/subsystems/notifications/push-routes.test.ts +82 -0
  223. package/src/subsystems/notifications/push-routes.ts +70 -16
  224. package/src/subsystems/notifications/push-store.test.ts +59 -0
  225. package/src/subsystems/notifications/push-store.ts +113 -19
  226. package/src/testing/jest-setup.ts +2 -0
  227. package/dist/lib/initControlPlane-C2zURVIS.js.map +0 -1
  228. package/dist/lib/orchestrator-CACLa_Ro.js.map +0 -1
  229. package/dist/lib/peerjs-DFWOBcXf.js.map +0 -1
  230. package/dist/lib/push-client-_tiWQsCT.js.map +0 -1
  231. package/dist/lib/shadow-claw-6UHdiy4a.js.map +0 -1
  232. package/dist/lib/shadow-claw-control-plane-UG0o7pCz.js.map +0 -1
  233. package/dist/public/initControlPlane-BN9cWyO0.js +0 -1
  234. package/dist/public/orchestrator-CMbCTjBf.js +0 -15
  235. package/dist/public/peerjs-DUCKH-m9.js +0 -1
  236. 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 () => {
@@ -271,4 +271,21 @@ export class CliControlClient {
271
271
  });
272
272
  return data;
273
273
  }
274
+
275
+ async broadcastNotification({ title = "ShadowClaw", body, clientId } = {}) {
276
+ const data = await this._request({
277
+ path: "/push/broadcast",
278
+ method: "POST",
279
+ body: {
280
+ title,
281
+ body,
282
+ ...(clientId ? { clientId } : {}),
283
+ },
284
+ });
285
+ return data;
286
+ }
287
+
288
+ async sendNotification(options) {
289
+ return this.broadcastNotification(options);
290
+ }
274
291
  }
@@ -35,6 +35,10 @@ describe("CliControlClient", () => {
35
35
  token,
36
36
  });
37
37
 
38
+ app.post("/push/broadcast", (req, res) => {
39
+ res.json({ sent: 1, failed: 0, received: req.body });
40
+ });
41
+
38
42
  await new Promise((resolve) => {
39
43
  server.listen(0, "127.0.0.1", () => {
40
44
  port = server.address().port;
@@ -90,4 +94,46 @@ describe("CliControlClient", () => {
90
94
  expect(client2.protocol).toBe("https");
91
95
  delete process.env.SHADOWCLAW_HTTPS;
92
96
  });
97
+
98
+ it("broadcasts push notifications via broadcastNotification", async () => {
99
+ const client = new CliControlClient({
100
+ host: "127.0.0.1",
101
+ port,
102
+ token,
103
+ });
104
+
105
+ const result = await client.broadcastNotification({
106
+ title: "Test Title",
107
+ body: "Test Body",
108
+ });
109
+
110
+ expect(result).toBeDefined();
111
+ expect(result.sent).toBe(1);
112
+ expect(result.received).toEqual({
113
+ title: "Test Title",
114
+ body: "Test Body",
115
+ });
116
+ });
117
+
118
+ it("sends targeted push notification with clientId via sendNotification", async () => {
119
+ const client = new CliControlClient({
120
+ host: "127.0.0.1",
121
+ port,
122
+ token,
123
+ });
124
+
125
+ const result = await client.sendNotification({
126
+ title: "Targeted Alert",
127
+ body: "Hello specific client",
128
+ clientId: "client-target-42",
129
+ });
130
+
131
+ expect(result).toBeDefined();
132
+ expect(result.sent).toBe(1);
133
+ expect(result.received).toEqual({
134
+ title: "Targeted Alert",
135
+ body: "Hello specific client",
136
+ clientId: "client-target-42",
137
+ });
138
+ });
93
139
  });
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from "node:child_process";
4
+ import { readFile, writeFile, stat } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const rootDir = path.resolve(__dirname, "..");
11
+
12
+ export const DISALLOWED_KEYWORDS = new Set([
13
+ "major",
14
+ "minor",
15
+ "patch",
16
+ "premajor",
17
+ "preminor",
18
+ "prepatch",
19
+ "prerelease",
20
+ "from-git",
21
+ ]);
22
+
23
+ export const USAGE_NOTICE = `
24
+ Usage: npm run version [<newversion>]
25
+
26
+ Note: Only explicit semantic version strings (e.g. 1.28.0) are supported.
27
+ Keyword commands (major, minor, patch, premajor, preminor, prepatch, prerelease, from-git) are not supported.
28
+
29
+ Standard npm version syntax for reference:
30
+ npm version [<newversion> | major | minor | patch | premajor | preminor | prepatch | prerelease | from-git]
31
+ `.trim();
32
+
33
+ export async function pathExists(p) {
34
+ try {
35
+ await stat(p);
36
+ return true;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ export function parseSemverString(v) {
43
+ const clean = String(v || "")
44
+ .trim()
45
+ .replace(/^v/, "");
46
+ const match = clean.match(
47
+ /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/,
48
+ );
49
+ if (!match) return null;
50
+ return clean;
51
+ }
52
+
53
+ export function validateVersionArg(arg) {
54
+ if (!arg) return null;
55
+ const lower = arg.toLowerCase().trim();
56
+ if (lower === "--help" || lower === "-h") {
57
+ return { isHelp: true };
58
+ }
59
+ if (DISALLOWED_KEYWORDS.has(lower)) {
60
+ return {
61
+ error: `Error: Keyword command "${arg}" is not supported by npm run version.\n\n${USAGE_NOTICE}`,
62
+ };
63
+ }
64
+ const clean = parseSemverString(arg);
65
+ if (!clean) {
66
+ return {
67
+ error: `Error: Invalid version string "${arg}". Expected a valid semantic version (e.g. 1.28.0).\n\n${USAGE_NOTICE}`,
68
+ };
69
+ }
70
+ return { version: clean };
71
+ }
72
+
73
+ export async function getVersionMap(targetRoot = rootDir) {
74
+ const pkg = JSON.parse(
75
+ await readFile(path.join(targetRoot, "package.json"), "utf8"),
76
+ );
77
+ let npmVer = "";
78
+ try {
79
+ npmVer = execSync("npm -v", { encoding: "utf8" }).trim();
80
+ } catch {}
81
+
82
+ return {
83
+ [pkg.name]: pkg.version,
84
+ ...(npmVer ? { npm: npmVer } : {}),
85
+ ...process.versions,
86
+ };
87
+ }
88
+
89
+ export async function syncWellKnownVersions(newVersion, targetRoot = rootDir) {
90
+ const filesToSync = [
91
+ path.join(targetRoot, ".well-known/mcp.json"),
92
+ path.join(targetRoot, ".well-known/mcp/server-card.json"),
93
+ path.join(targetRoot, "dist/public/.well-known/mcp.json"),
94
+ path.join(targetRoot, "dist/public/.well-known/mcp/server-card.json"),
95
+ ];
96
+
97
+ const updatedFiles = [];
98
+ for (const filePath of filesToSync) {
99
+ if (await pathExists(filePath)) {
100
+ try {
101
+ const content = await readFile(filePath, "utf8");
102
+ const json = JSON.parse(content);
103
+ if (json.version !== newVersion) {
104
+ json.version = newVersion;
105
+ await writeFile(
106
+ filePath,
107
+ JSON.stringify(json, null, 2) + "\n",
108
+ "utf8",
109
+ );
110
+ updatedFiles.push(path.relative(targetRoot, filePath));
111
+ }
112
+ } catch (err) {
113
+ console.warn(`Warning: Could not update version in ${filePath}:`, err);
114
+ }
115
+ }
116
+ }
117
+ return updatedFiles;
118
+ }
119
+
120
+ export async function runVersion(args = process.argv.slice(2), options = {}) {
121
+ const isLifecycle =
122
+ options.isLifecycle ??
123
+ (process.env.npm_command === "version" &&
124
+ process.env.npm_lifecycle_event === "version");
125
+ const targetRoot = options.rootDir || rootDir;
126
+ const execOptions = options.execOptions || {
127
+ cwd: targetRoot,
128
+ stdio: options.stdio || "inherit",
129
+ };
130
+
131
+ // 1. If running under npm's lifecycle hook (npm version <newver> triggered this script)
132
+ if (isLifecycle) {
133
+ const pkg = JSON.parse(
134
+ await readFile(path.join(targetRoot, "package.json"), "utf8"),
135
+ );
136
+ const newVersion = pkg.version;
137
+ const updated = await syncWellKnownVersions(newVersion, targetRoot);
138
+ if (
139
+ updated.length > 0 &&
140
+ (await pathExists(path.join(targetRoot, ".git")))
141
+ ) {
142
+ try {
143
+ execSync(
144
+ `git add .well-known/mcp.json .well-known/mcp/server-card.json`,
145
+ { cwd: targetRoot, stdio: "pipe" },
146
+ );
147
+ } catch {}
148
+ }
149
+ return { version: newVersion, updated };
150
+ }
151
+
152
+ // 2. Direct CLI invocation
153
+ const targetArg = args.find((a) => !a.startsWith("--")) || args[0];
154
+
155
+ // No arguments passed -> list versions mimicking `npm version`
156
+ if (!targetArg) {
157
+ const versionMap = await getVersionMap(targetRoot);
158
+ if (execOptions.stdio === "inherit") {
159
+ console.log(versionMap);
160
+ }
161
+ return { action: "list", versions: versionMap };
162
+ }
163
+
164
+ const validation = validateVersionArg(targetArg);
165
+ if (validation.isHelp) {
166
+ if (execOptions.stdio === "inherit") {
167
+ console.log(USAGE_NOTICE);
168
+ }
169
+ return { action: "help", message: USAGE_NOTICE };
170
+ }
171
+
172
+ if (validation.error) {
173
+ throw new Error(validation.error);
174
+ }
175
+
176
+ const cleanVersion = validation.version;
177
+
178
+ // Run npm version --no-git-tag-version --allow-same-version --ignore-scripts
179
+ execSync(
180
+ `npm version ${cleanVersion} --no-git-tag-version --allow-same-version --ignore-scripts`,
181
+ execOptions,
182
+ );
183
+
184
+ // Sync .well-known JSON discovery files
185
+ const updated = await syncWellKnownVersions(cleanVersion, targetRoot);
186
+
187
+ if (execOptions.stdio === "inherit") {
188
+ console.log(`v${cleanVersion}`);
189
+ if (updated.length > 0) {
190
+ console.log(
191
+ `Updated .well-known discovery versions: ${updated.join(", ")}`,
192
+ );
193
+ }
194
+ }
195
+
196
+ return { version: cleanVersion, updated };
197
+ }
198
+
199
+ const isMainModule =
200
+ process.argv[1] &&
201
+ import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
202
+
203
+ if (isMainModule) {
204
+ runVersion().catch((err) => {
205
+ console.error(`\n${err.message}\n`);
206
+ process.exit(1);
207
+ });
208
+ }