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
@@ -53,6 +53,7 @@ const {
53
53
  unsubscribeFromPush,
54
54
  getCurrentSubscription,
55
55
  urlBase64ToUint8Array,
56
+ getPushFetchOptions,
56
57
  } = await import("./push-client.js");
57
58
 
58
59
  describe("push-client", () => {
@@ -62,6 +63,60 @@ describe("push-client", () => {
62
63
  mockPushManager.getSubscription.mockResolvedValue(null);
63
64
  });
64
65
 
66
+ describe("getPushFetchOptions", () => {
67
+ it("sets targetAddressSpace loopback for localhost", () => {
68
+ expect(
69
+ getPushFetchOptions("http://localhost:8888/push/vapid-public-key"),
70
+ ).toEqual({
71
+ targetAddressSpace: "loopback",
72
+ });
73
+ expect(
74
+ getPushFetchOptions("http://127.0.0.1:8888/push/vapid-public-key"),
75
+ ).toEqual({
76
+ targetAddressSpace: "loopback",
77
+ });
78
+ });
79
+
80
+ it("sets targetAddressSpace private for private IP addresses", () => {
81
+ expect(
82
+ getPushFetchOptions("https://10.9.8.226:8888/push/vapid-public-key"),
83
+ ).toEqual({
84
+ targetAddressSpace: "private",
85
+ });
86
+ expect(
87
+ getPushFetchOptions("https://192.168.1.50:8888/push/vapid-public-key"),
88
+ ).toEqual({
89
+ targetAddressSpace: "private",
90
+ });
91
+ });
92
+
93
+ it("sets targetAddressSpace private for local single-label hostnames and .local domains", () => {
94
+ expect(
95
+ getPushFetchOptions("https://hostname:8888/push/vapid-public-key"),
96
+ ).toEqual({
97
+ targetAddressSpace: "private",
98
+ });
99
+ expect(
100
+ getPushFetchOptions(
101
+ "https://hostname.local:8888/push/vapid-public-key",
102
+ ),
103
+ ).toEqual({
104
+ targetAddressSpace: "private",
105
+ });
106
+ expect(
107
+ getPushFetchOptions("https://my-nas.lan:8888/push/vapid-public-key"),
108
+ ).toEqual({
109
+ targetAddressSpace: "private",
110
+ });
111
+ });
112
+
113
+ it("does not set targetAddressSpace for public domains", () => {
114
+ expect(
115
+ getPushFetchOptions("https://example.com/push/vapid-public-key"),
116
+ ).toEqual({});
117
+ });
118
+ });
119
+
65
120
  describe("urlBase64ToUint8Array", () => {
66
121
  it("converts a URL-safe base64 string to Uint8Array", () => {
67
122
  // "AQAB" in standard base64 = [1, 0, 1]
@@ -117,6 +172,21 @@ describe("push-client", () => {
117
172
  expect(postCall[1].method).toBe("POST");
118
173
  expect(postCall[1].targetAddressSpace).toBe("loopback");
119
174
  });
175
+
176
+ it("includes clientId and deviceLabel in the request body", async () => {
177
+ (fetch as any)
178
+ .mockResolvedValueOnce({
179
+ ok: true,
180
+ json: () => Promise.resolve({ publicKey: "test-vapid-key" }),
181
+ })
182
+ .mockResolvedValueOnce({ ok: true });
183
+
184
+ await subscribeToPush("client-explicit-123", "Pixel 9 Pro");
185
+ const postCall = (fetch as any).mock.calls[1];
186
+ const body = JSON.parse(postCall[1].body);
187
+ expect(body.clientId).toBe("client-explicit-123");
188
+ expect(body.deviceLabel).toBe("Pixel 9 Pro");
189
+ });
120
190
  });
121
191
 
122
192
  describe("unsubscribeFromPush", () => {
@@ -53,7 +53,14 @@ export function getPushFetchOptions(
53
53
  host === "[::1]"
54
54
  ) {
55
55
  opts.targetAddressSpace = "loopback";
56
- } else if (/^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/.test(host)) {
56
+ } else if (
57
+ /^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/.test(host) ||
58
+ !host.includes(".") ||
59
+ host.endsWith(".local") ||
60
+ host.endsWith(".lan") ||
61
+ host.endsWith(".home") ||
62
+ host.endsWith(".internal")
63
+ ) {
57
64
  opts.targetAddressSpace = "private";
58
65
  }
59
66
  } catch (_) {}
@@ -87,7 +94,10 @@ export async function getVapidPublicKey(): Promise<string> {
87
94
  * Subscribe to push notifications.
88
95
  * Requests permission, creates a push subscription, and sends it to the server.
89
96
  */
90
- export async function subscribeToPush(): Promise<PushSubscription> {
97
+ export async function subscribeToPush(
98
+ clientId?: string,
99
+ deviceLabel?: string,
100
+ ): Promise<PushSubscription> {
91
101
  const registration = await navigator.serviceWorker.ready;
92
102
  const publicKey = await getVapidPublicKey();
93
103
 
@@ -96,13 +106,28 @@ export async function subscribeToPush(): Promise<PushSubscription> {
96
106
  applicationServerKey: urlBase64ToUint8Array(publicKey) as any,
97
107
  });
98
108
 
109
+ const resolvedClientId =
110
+ clientId ||
111
+ (typeof localStorage !== "undefined"
112
+ ? localStorage.getItem(CONFIG_KEYS.CONTROL_PLANE_CLIENT_ID) || undefined
113
+ : undefined);
114
+
115
+ const subJson =
116
+ typeof subscription.toJSON === "function"
117
+ ? subscription.toJSON()
118
+ : JSON.parse(JSON.stringify(subscription));
119
+
99
120
  const url = await getPushUrl("/push/subscribe");
100
121
  await fetch(
101
122
  url,
102
123
  getPushFetchOptions(url, {
103
124
  method: "POST",
104
125
  headers: { "Content-Type": "application/json" },
105
- body: JSON.stringify(subscription),
126
+ body: JSON.stringify({
127
+ ...subJson,
128
+ ...(resolvedClientId ? { clientId: resolvedClientId } : {}),
129
+ ...(deviceLabel ? { deviceLabel } : {}),
130
+ }),
106
131
  }),
107
132
  );
108
133
 
@@ -23,6 +23,8 @@ jest.unstable_mockModule("./push-store.js", () => ({
23
23
  removeSubscriptionById: jest.fn(),
24
24
  getSubscription: jest.fn(),
25
25
  getAllSubscriptions: jest.fn(() => []),
26
+ getSubscriptionsByClientId: jest.fn(() => []),
27
+ findSubscriptionsForClient: jest.fn(() => []),
26
28
  }));
27
29
 
28
30
  const { registerPushRoutes, broadcastPush } = await import("./push-routes.js");
@@ -112,6 +114,24 @@ describe("push-routes", () => {
112
114
  const req = createMockReq(subscription);
113
115
  const res = createMockRes();
114
116
  await app.routes.post["/push/subscribe"](req, res);
117
+ expect(store.saveSubscription).toHaveBeenCalledWith({
118
+ ...subscription,
119
+ clientId: undefined,
120
+ deviceLabel: undefined,
121
+ });
122
+ expect(res.statusCode).toBe(201);
123
+ });
124
+
125
+ it("saves the subscription with clientId and deviceLabel", async () => {
126
+ const subscription: any = {
127
+ endpoint: "https://fcm.example.com/abc-with-client",
128
+ keys: { p256dh: "key1", auth: "key2" },
129
+ clientId: "client-test-777",
130
+ deviceLabel: "Test Device 777",
131
+ };
132
+ const req = createMockReq(subscription);
133
+ const res = createMockRes();
134
+ await app.routes.post["/push/subscribe"](req, res);
115
135
  expect(store.saveSubscription).toHaveBeenCalledWith(subscription);
116
136
  expect(res.statusCode).toBe(201);
117
137
  });
@@ -308,6 +328,33 @@ describe("push-routes", () => {
308
328
  await app.routes.post["/push/broadcast"](req, res);
309
329
  expect(res.statusCode).toBe(400);
310
330
  });
331
+
332
+ it("targets a specific client when clientId is provided in POST /push/broadcast", async () => {
333
+ (store.findSubscriptionsForClient as any).mockReturnValue([
334
+ {
335
+ id: 1,
336
+ endpoint: "https://fcm.example.com/client-specific",
337
+ keys_p256dh: "k1",
338
+ keys_auth: "k2",
339
+ client_id: "client-target-99",
340
+ },
341
+ ]);
342
+ webpush.sendNotification.mockResolvedValueOnce({
343
+ statusCode: 201,
344
+ } as any);
345
+
346
+ const req = createMockReq({
347
+ body: "Specific client alert",
348
+ clientId: "client-target-99",
349
+ });
350
+ const res = createMockRes();
351
+ await app.routes.post["/push/broadcast"](req, res);
352
+ expect(res.statusCode).toBe(200);
353
+ expect(store.findSubscriptionsForClient).toHaveBeenCalledWith(
354
+ "client-target-99",
355
+ );
356
+ expect(res._json.sent).toBe(1);
357
+ });
311
358
  });
312
359
 
313
360
  describe("POST /push/command", () => {
@@ -367,5 +414,40 @@ describe("push-routes", () => {
367
414
  expect(result.noSubscribers).toBeUndefined();
368
415
  expect(result.sent).toBe(1);
369
416
  });
417
+
418
+ it("targets a specific client and returns notFound when no subscription matches", async () => {
419
+ (store.findSubscriptionsForClient as any).mockReturnValue([]);
420
+ const result = await broadcastPush(
421
+ { title: "Test", body: "Hello" },
422
+ { clientId: "unknown-client" },
423
+ );
424
+ expect(result).toEqual({ sent: 0, failed: 0, notFound: true });
425
+ expect(store.findSubscriptionsForClient).toHaveBeenCalledWith(
426
+ "unknown-client",
427
+ );
428
+ });
429
+
430
+ it("targets a specific client and sends push when matching subscription exists", async () => {
431
+ (store.findSubscriptionsForClient as any).mockReturnValue([
432
+ {
433
+ id: 1,
434
+ endpoint: "https://fcm.example.com/target-sub",
435
+ keys_p256dh: "k1",
436
+ keys_auth: "k2",
437
+ client_id: "client-target-123",
438
+ },
439
+ ]);
440
+ webpush.sendNotification.mockResolvedValueOnce({
441
+ statusCode: 201,
442
+ } as any);
443
+ const result = await broadcastPush(
444
+ { title: "Test", body: "Hello" },
445
+ { clientId: "client-target-123" },
446
+ );
447
+ expect(result).toEqual({ sent: 1, failed: 0 });
448
+ expect(store.findSubscriptionsForClient).toHaveBeenCalledWith(
449
+ "client-target-123",
450
+ );
451
+ });
370
452
  });
371
453
  });
@@ -15,25 +15,54 @@ import {
15
15
  removeSubscriptionById,
16
16
  getSubscription,
17
17
  getAllSubscriptions,
18
+ findSubscriptionsForClient,
19
+ type PushSubscriptionRow,
18
20
  } from "./push-store.js";
19
21
  import type { Express } from "express";
20
22
 
23
+ export interface BroadcastPushOptions {
24
+ clientId?: string;
25
+ }
26
+
27
+ export interface BroadcastPushResult {
28
+ sent: number;
29
+ failed: number;
30
+ noSubscribers?: true;
31
+ notFound?: true;
32
+ }
33
+
21
34
  /**
22
- * Broadcast a push notification payload to all subscriptions.
23
- * Used by both the /push/broadcast route and the server-side task scheduler.
35
+ * Broadcast a push notification payload to all subscriptions, or send to a specific client.
36
+ * Used by /push/broadcast, MCP send_notification tool, and the server-side task scheduler.
24
37
  */
25
38
  export async function broadcastPush(
26
39
  payload: any,
27
- ): Promise<{ sent: number; failed: number; noSubscribers?: true }> {
28
- const subs = getAllSubscriptions();
29
-
30
- if (subs.length === 0) {
31
- return { sent: 0, failed: 0, noSubscribers: true };
40
+ options?: BroadcastPushOptions,
41
+ ): Promise<BroadcastPushResult> {
42
+ const targetClientId =
43
+ options?.clientId ||
44
+ (typeof payload?.clientId === "string" &&
45
+ payload.type !== "remote-command" &&
46
+ payload.clientId.trim()
47
+ ? payload.clientId.trim()
48
+ : undefined);
49
+
50
+ let subs: PushSubscriptionRow[];
51
+ if (targetClientId) {
52
+ subs = findSubscriptionsForClient(targetClientId);
53
+ if (subs.length === 0) {
54
+ return { sent: 0, failed: 0, notFound: true };
55
+ }
56
+ } else {
57
+ subs = getAllSubscriptions();
58
+ if (subs.length === 0) {
59
+ return { sent: 0, failed: 0, noSubscribers: true };
60
+ }
32
61
  }
33
62
 
34
63
  const keys = getOrCreateVapidKeys();
35
64
  const notification = JSON.stringify(payload);
36
- const options = {
65
+ const sendOptions = {
37
66
  TTL: 60 * 60,
38
67
  vapidDetails: {
39
68
  subject: keys.subject,
@@ -53,7 +82,11 @@ export async function broadcastPush(
53
82
  };
54
83
 
55
84
  try {
56
- await webpush.sendNotification(pushSubscription, notification, options);
85
+ await webpush.sendNotification(
86
+ pushSubscription,
87
+ notification,
88
+ sendOptions,
89
+ );
57
90
  sent++;
58
91
  } catch (err: any) {
59
92
  failed++;
@@ -86,7 +119,25 @@ export function registerPushRoutes(app: Express): void {
86
119
  return res.status(400).json({ error: "Invalid subscription object" });
87
120
  }
88
121
 
89
- saveSubscription(subscription);
122
+ const clientId =
123
+ subscription.clientId ||
124
+ subscription.client_id ||
125
+ (typeof req.query?.clientId === "string"
126
+ ? req.query.clientId
127
+ : undefined);
128
+
129
+ const deviceLabel =
130
+ subscription.deviceLabel ||
131
+ subscription.device_label ||
132
+ (typeof req.query?.deviceLabel === "string"
133
+ ? req.query.deviceLabel
134
+ : undefined);
135
+
136
+ saveSubscription({
137
+ ...subscription,
138
+ clientId,
139
+ deviceLabel,
140
+ });
90
141
  res.sendStatus(201);
91
142
  });
92
143
 
@@ -177,19 +228,22 @@ export function registerPushRoutes(app: Express): void {
177
228
  res.sendStatus(200);
178
229
  });
179
230
 
180
- // Broadcast a notification to all subscriptions
231
+ // Broadcast a notification to all subscriptions (or to a specific client if clientId is provided)
181
232
  app.post("/push/broadcast", async (req, res) => {
182
- const { title, body } = req.body || {};
233
+ const { title, body, clientId } = req.body || {};
183
234
 
184
235
  if (!body) {
185
236
  return res.status(400).json({ error: "Missing body" });
186
237
  }
187
238
 
188
239
  try {
189
- const result = await broadcastPush({
190
- title: title || "ShadowClaw",
191
- body,
192
- });
240
+ const result = await broadcastPush(
241
+ {
242
+ title: title || "ShadowClaw",
243
+ body,
244
+ },
245
+ clientId ? { clientId } : undefined,
246
+ );
193
247
  res.json(result);
194
248
  } catch (err: any) {
195
249
  res.status(500).json({ error: err.message });
@@ -7,6 +7,8 @@ import {
7
7
  getSubscription,
8
8
  getAllSubscriptions,
9
9
  removeSubscriptionById,
10
+ getSubscriptionsByClientId,
11
+ findSubscriptionsForClient,
10
12
  } from "./push-store.js";
11
13
 
12
14
  // Use in-memory DB for tests
@@ -148,4 +150,61 @@ describe("push-store", () => {
148
150
  expect(all[0].endpoint).toBe(MOCK_SUBSCRIPTION_2.endpoint);
149
151
  });
150
152
  });
153
+
154
+ describe("client-targeted subscriptions", () => {
155
+ it("stores and retrieves subscriptions with clientId and deviceLabel", () => {
156
+ saveSubscription({
157
+ ...MOCK_SUBSCRIPTION,
158
+ clientId: "client-01jtest123",
159
+ deviceLabel: "Pixel 9 Pro",
160
+ });
161
+
162
+ const sub = getSubscription(MOCK_SUBSCRIPTION.endpoint);
163
+ expect(sub).toBeDefined();
164
+ expect(sub?.client_id).toBe("client-01jtest123");
165
+ expect(sub?.device_label).toBe("Pixel 9 Pro");
166
+
167
+ const byClient = getSubscriptionsByClientId("client-01jtest123");
168
+ expect(byClient).toHaveLength(1);
169
+ expect(byClient[0].endpoint).toBe(MOCK_SUBSCRIPTION.endpoint);
170
+ });
171
+
172
+ it("finds subscriptions by exact clientId, prefix, device label, and row ID", () => {
173
+ saveSubscription({
174
+ ...MOCK_SUBSCRIPTION,
175
+ clientId: "client-01jtest123",
176
+ deviceLabel: "Pixel 9 Pro",
177
+ });
178
+ saveSubscription({
179
+ ...MOCK_SUBSCRIPTION_2,
180
+ clientId: "client-02ktest456",
181
+ deviceLabel: "MacBook Air",
182
+ });
183
+
184
+ // Exact clientId
185
+ const exact = findSubscriptionsForClient("client-01jtest123");
186
+ expect(exact).toHaveLength(1);
187
+ expect(exact[0].endpoint).toBe(MOCK_SUBSCRIPTION.endpoint);
188
+
189
+ // Prefix clientId without 'client-'
190
+ const prefix = findSubscriptionsForClient("01jtest");
191
+ expect(prefix).toHaveLength(1);
192
+ expect(prefix[0].endpoint).toBe(MOCK_SUBSCRIPTION.endpoint);
193
+
194
+ // Device label substring (case-insensitive)
195
+ const byLabel = findSubscriptionsForClient("macbook");
196
+ expect(byLabel).toHaveLength(1);
197
+ expect(byLabel[0].endpoint).toBe(MOCK_SUBSCRIPTION_2.endpoint);
198
+
199
+ // Row ID
200
+ const all = getAllSubscriptions();
201
+ const byId = findSubscriptionsForClient(String(all[0].id));
202
+ expect(byId).toHaveLength(1);
203
+ expect(byId[0].id).toBe(all[0].id);
204
+
205
+ // Non-existent target
206
+ const notFound = findSubscriptionsForClient("nonexistent-device");
207
+ expect(notFound).toHaveLength(0);
208
+ });
209
+ });
151
210
  });
@@ -24,6 +24,8 @@ export interface PushSubscriptionRow {
24
24
  endpoint: string;
25
25
  keys_p256dh: string;
26
26
  keys_auth: string;
27
+ client_id?: string;
28
+ device_label?: string;
27
29
  created_at: string;
28
30
  }
29
31
 
@@ -33,6 +35,10 @@ export interface PushSubscriptionInput {
33
35
  p256dh: string;
34
36
  auth: string;
35
37
  };
38
+ clientId?: string;
39
+ client_id?: string;
40
+ deviceLabel?: string;
41
+ device_label?: string;
36
42
  }
37
43
 
38
44
  /**
@@ -62,10 +68,20 @@ export function openPushStore(
62
68
  endpoint TEXT NOT NULL UNIQUE,
63
69
  keys_p256dh TEXT NOT NULL,
64
70
  keys_auth TEXT NOT NULL,
71
+ client_id TEXT,
72
+ device_label TEXT,
65
73
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
66
74
  )
67
75
  `);
68
76
 
77
+ try {
78
+ db.exec(`ALTER TABLE subscriptions ADD COLUMN client_id TEXT`);
79
+ } catch (_) {}
80
+
81
+ try {
82
+ db.exec(`ALTER TABLE subscriptions ADD COLUMN device_label TEXT`);
83
+ } catch (_) {}
84
+
69
85
  return db;
70
86
  }
71
87
 
@@ -112,6 +128,18 @@ export function getOrCreateVapidKeys(
112
128
  };
113
129
  }
114
130
 
131
+ function rowToSubscription(row: any): PushSubscriptionRow {
132
+ return {
133
+ id: Number(row.id),
134
+ endpoint: `${row.endpoint}`,
135
+ keys_p256dh: `${row.keys_p256dh}`,
136
+ keys_auth: `${row.keys_auth}`,
137
+ client_id: row.client_id ? `${row.client_id}` : undefined,
138
+ device_label: row.device_label ? `${row.device_label}` : undefined,
139
+ created_at: `${row.created_at}`,
140
+ };
141
+ }
142
+
115
143
  /**
116
144
  * Save a push subscription (upsert by endpoint).
117
145
  */
@@ -120,12 +148,26 @@ export function saveSubscription(subscription: PushSubscriptionInput): void {
120
148
  throw new Error("Push store not opened.");
121
149
  }
122
150
 
151
+ const clientId = subscription.clientId || subscription.client_id || null;
152
+ const deviceLabel =
153
+ subscription.deviceLabel || subscription.device_label || null;
154
+
123
155
  db.prepare(
124
- "INSERT OR REPLACE INTO subscriptions (endpoint, keys_p256dh, keys_auth) VALUES (?, ?, ?)",
156
+ `
157
+ INSERT INTO subscriptions (endpoint, keys_p256dh, keys_auth, client_id, device_label)
158
+ VALUES (?, ?, ?, ?, ?)
159
+ ON CONFLICT(endpoint) DO UPDATE SET
160
+ keys_p256dh = excluded.keys_p256dh,
161
+ keys_auth = excluded.keys_auth,
162
+ client_id = COALESCE(excluded.client_id, subscriptions.client_id),
163
+ device_label = COALESCE(excluded.device_label, subscriptions.device_label)
164
+ `,
125
165
  ).run(
126
166
  subscription.endpoint,
127
167
  subscription.keys.p256dh,
128
168
  subscription.keys.auth,
169
+ clientId,
170
+ deviceLabel,
129
171
  );
130
172
  }
131
173
 
@@ -165,15 +207,7 @@ export function getSubscription(
165
207
  .prepare("SELECT * FROM subscriptions WHERE endpoint = ?")
166
208
  .get(endpoint);
167
209
 
168
- return result
169
- ? {
170
- id: Number(result.id),
171
- endpoint: `${result.endpoint}`,
172
- keys_p256dh: `${result.keys_p256dh}`,
173
- keys_auth: `${result.keys_auth}`,
174
- created_at: `${result.created_at}`,
175
- }
176
- : undefined;
210
+ return result ? rowToSubscription(result) : undefined;
177
211
  }
178
212
 
179
213
  /**
@@ -188,13 +222,73 @@ export function getAllSubscriptions(): PushSubscriptionRow[] {
188
222
  .prepare("SELECT * FROM subscriptions ORDER BY created_at DESC, id DESC")
189
223
  .all();
190
224
 
191
- return result
192
- ? result.map((row) => ({
193
- id: Number(row.id),
194
- endpoint: `${row.endpoint}`,
195
- keys_p256dh: `${row.keys_p256dh}`,
196
- keys_auth: `${row.keys_auth}`,
197
- created_at: `${row.created_at}`,
198
- }))
199
- : [];
225
+ return result ? result.map(rowToSubscription) : [];
226
+ }
227
+
228
+ /**
229
+ * Get subscriptions matching a specific client ID.
230
+ */
231
+ export function getSubscriptionsByClientId(
232
+ clientId: string,
233
+ ): PushSubscriptionRow[] {
234
+ if (!db) {
235
+ throw new Error("Push store not opened.");
236
+ }
237
+
238
+ const result = db
239
+ .prepare(
240
+ "SELECT * FROM subscriptions WHERE client_id = ? ORDER BY created_at DESC, id DESC",
241
+ )
242
+ .all(clientId);
243
+
244
+ return result ? result.map(rowToSubscription) : [];
245
+ }
246
+
247
+ /**
248
+ * Find subscriptions for a client by exact ID, row ID, ID prefix, or device label.
249
+ */
250
+ export function findSubscriptionsForClient(
251
+ target: string,
252
+ ): PushSubscriptionRow[] {
253
+ if (!db) {
254
+ throw new Error("Push store not opened.");
255
+ }
256
+
257
+ const all = getAllSubscriptions();
258
+ if (!target || !target.trim()) {
259
+ return all;
260
+ }
261
+
262
+ const trimmed = target.trim();
263
+
264
+ // 1. Exact match on client_id
265
+ const exact = all.filter((s) => s.client_id === trimmed);
266
+ if (exact.length > 0) return exact;
267
+
268
+ // 2. Exact match on row ID if target is numeric
269
+ const numericId = parseInt(trimmed, 10);
270
+ if (!isNaN(numericId) && numericId.toString() === trimmed) {
271
+ const byId = all.filter((s) => s.id === numericId);
272
+ if (byId.length > 0) return byId;
273
+ }
274
+
275
+ // 3. Prefix match on client_id (e.g. ULID prefix or client- prefix)
276
+ const prefix = all.filter(
277
+ (s) =>
278
+ s.client_id &&
279
+ (s.client_id.startsWith(trimmed) ||
280
+ s.client_id.replace(/^client-/, "").startsWith(trimmed) ||
281
+ trimmed.startsWith(s.client_id)),
282
+ );
283
+ if (prefix.length > 0) return prefix;
284
+
285
+ // 4. Case-insensitive substring match on device_label
286
+ const byLabel = all.filter(
287
+ (s) =>
288
+ s.device_label &&
289
+ s.device_label.toLowerCase().includes(trimmed.toLowerCase()),
290
+ );
291
+ if (byLabel.length > 0) return byLabel;
292
+
293
+ return [];
200
294
  }
@@ -514,9 +514,11 @@ const expectedLogs = [
514
514
  "PeerTaskManager: task",
515
515
  "[ShadowClaw] Failed to load models from",
516
516
  "PeerJsChannel: connected to signaling server",
517
+ "PeerJsChannel: disconnected from signaling server",
517
518
  "PeerJsChannel: connection opened with",
518
519
  "PeerJsChannel: rejecting connection from untrusted peer:",
519
520
  "PeerJsChannel: connection closed with",
521
+ "[ControlPlaneClient] SSE connection error:",
520
522
  "[WebVM] Ignoring invalid VM boot host:",
521
523
  "[WebVM] Ignoring invalid VM relay URL:",
522
524
  "[WebVM]",
@@ -566,6 +568,8 @@ const expectedLogs = [
566
568
  "Failed to check Prompt API onboarding:",
567
569
  "[webrtc-listen]",
568
570
  "[ShadowClaw MCP]",
571
+ "fatal: not a git repository",
572
+ "Stopping at filesystem boundary",
569
573
  ];
570
574
 
571
575
  function isExpectedLog(...args: any[]) {
@@ -577,6 +581,17 @@ function isExpectedLog(...args: any[]) {
577
581
  return expectedLogs.some((expected) => str.includes(expected));
578
582
  }
579
583
 
584
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
585
+ (process.stderr as any).write = (chunk: any, ...args: any[]): boolean => {
586
+ const str = typeof chunk === "string" ? chunk : (chunk?.toString?.() ?? "");
587
+ if (isExpectedLog(str)) {
588
+ const cb = args.find((a) => typeof a === "function");
589
+ if (cb) cb();
590
+ return true;
591
+ }
592
+ return (originalStderrWrite as any)(chunk, ...args);
593
+ };
594
+
580
595
  console.error = (...args: any[]) => {
581
596
  if (isExpectedLog(...args)) return;
582
597
  originalConsoleError(...args);