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
@@ -16,6 +16,7 @@ import { McpServer } from "../mcp/mcp-server.js";
16
16
  import { registerBuiltInTools } from "../mcp/tools/built-in-tools.js";
17
17
  import { createClientToolRelay } from "../mcp/tools/client-tool-relay.js";
18
18
  import { getOrCreateControlToken } from "../client-registry.js";
19
+ import { getPackageVersion } from "../utils/packageVersion.js";
19
20
 
20
21
  export interface McpRouteOptions {
21
22
  mcpServer?: McpServer;
@@ -36,7 +37,6 @@ export function registerMcpRoutes(
36
37
  options.mcpServer ||
37
38
  new McpServer({
38
39
  name: "shadow-claw",
39
- version: "1.25.0",
40
40
  });
41
41
 
42
42
  // Register ShadowClaw built-in and client-relayed tools
@@ -143,7 +143,7 @@ export function registerMcpRoutes(
143
143
  }
144
144
 
145
145
  if (app && typeof app.post === "function") {
146
- app.post("/mcp", async (req: Request, res: Response) => {
146
+ const mcpPostHandler = async (req: Request, res: Response) => {
147
147
  // 1. Origin header security check for DNS rebinding protection
148
148
  const origin = req.headers.origin;
149
149
  if (origin && !isTrustedOrigin(origin, req.headers.host)) {
@@ -165,6 +165,15 @@ export function registerMcpRoutes(
165
165
  : isLocalRequest(req);
166
166
 
167
167
  if (!isAuthenticated) {
168
+ const scheme =
169
+ req.secure || req.headers["x-forwarded-proto"] === "https"
170
+ ? "https"
171
+ : "http";
172
+ const host = req.headers.host || "127.0.0.1";
173
+ res.setHeader(
174
+ "WWW-Authenticate",
175
+ `Bearer resource_metadata="${scheme}://${host}/.well-known/oauth-protected-resource"`,
176
+ );
168
177
  res.status(401).json({
169
178
  jsonrpc: "2.0",
170
179
  id: req.body?.id ?? null,
@@ -222,7 +231,134 @@ export function registerMcpRoutes(
222
231
  }
223
232
 
224
233
  res.status(statusCode).json(response);
234
+ };
235
+
236
+ app.post("/mcp", mcpPostHandler);
237
+ app.post("/.well-known/mcp", mcpPostHandler);
238
+ }
239
+
240
+ if (app && typeof app.get === "function") {
241
+ const getBaseUrl = (req: Request) => {
242
+ const scheme =
243
+ req.secure || req.headers["x-forwarded-proto"] === "https"
244
+ ? "https"
245
+ : "http";
246
+ const host = req.headers.host || "127.0.0.1";
247
+ return `${scheme}://${host}`;
248
+ };
249
+
250
+ // MCP Server Card Discovery (SEP-2127 / well-known discovery)
251
+ const handleServerCard = (req: Request, res: Response) => {
252
+ const origin = getBaseUrl(req);
253
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
254
+ res.json({
255
+ $schema: "https://modelcontextprotocol.io/schemas/server-card/v1.json",
256
+ name: "shadow-claw",
257
+ version: getPackageVersion(),
258
+ description:
259
+ "Browser-native personal AI assistant with local and remote LLM orchestration, Web Workers, OPFS storage, and agentic tool execution.",
260
+ title: "ShadowClaw MCP Server",
261
+ websiteUrl: `${origin}/shadow-claw/`,
262
+ repository: {
263
+ url: "https://github.com/xt-ml/shadow-claw",
264
+ source: "github",
265
+ },
266
+ icons: [
267
+ {
268
+ src: `${origin}/assets/icons/favicon.svg`,
269
+ mimeType: "image/svg+xml",
270
+ sizes: ["any"],
271
+ },
272
+ {
273
+ src: `${origin}/assets/icons/512.png`,
274
+ mimeType: "image/png",
275
+ sizes: ["512x512"],
276
+ },
277
+ ],
278
+ remotes: [
279
+ {
280
+ type: "streamable-http",
281
+ url: `${origin}/mcp`,
282
+ supportedProtocolVersions: [
283
+ "2026-07-28",
284
+ "2025-11-25",
285
+ "2024-11-05",
286
+ ],
287
+ headers: [
288
+ {
289
+ name: "MCP-Protocol-Version",
290
+ default: "2026-07-28",
291
+ isRequired: false,
292
+ },
293
+ {
294
+ name: "x-control-token",
295
+ description: "Control plane token for authorized client access",
296
+ isSecret: true,
297
+ isRequired: false,
298
+ },
299
+ ],
300
+ },
301
+ ],
302
+ });
303
+ };
304
+
305
+ app.get("/.well-known/mcp", handleServerCard);
306
+ app.get("/.well-known/mcp.json", handleServerCard);
307
+ app.get("/mcp/server-card", handleServerCard);
308
+
309
+ // AI Catalog Discovery (SEP-2127)
310
+ app.get("/.well-known/ai-catalog.json", (req: Request, res: Response) => {
311
+ const origin = getBaseUrl(req);
312
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
313
+ res.json({
314
+ $schema: "https://agent-card.github.io/schemas/ai-catalog/v1.json",
315
+ version: "1.0",
316
+ name: "ShadowClaw AI Catalog",
317
+ servers: [
318
+ {
319
+ name: "shadow-claw",
320
+ card: `${origin}/.well-known/mcp.json`,
321
+ },
322
+ ],
323
+ });
225
324
  });
325
+
326
+ // MCP Servers Manifest (Discussion #1147)
327
+ app.get("/.well-known/mcp/servers.json", (req: Request, res: Response) => {
328
+ const origin = getBaseUrl(req);
329
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
330
+ res.json({
331
+ version: "1.0",
332
+ servers: [
333
+ {
334
+ name: "shadow-claw",
335
+ description: "ShadowClaw Browser-Native AI Assistant MCP Server",
336
+ endpoint: `${origin}/mcp`,
337
+ capabilities: ["tools", "extensions"],
338
+ card: `${origin}/.well-known/mcp.json`,
339
+ },
340
+ ],
341
+ });
342
+ });
343
+
344
+ // Protected Resource Metadata (RFC 9728)
345
+ const handleProtectedResource = (req: Request, res: Response) => {
346
+ const origin = getBaseUrl(req);
347
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
348
+ res.json({
349
+ resource: `${origin}/mcp`,
350
+ authorization_servers: [origin],
351
+ bearer_methods_supported: ["header"],
352
+ resource_documentation:
353
+ "https://xt-ml.github.io/shadow-claw/docs/subsystems/mcp-server.md",
354
+ });
355
+ };
356
+
357
+ app.get("/.well-known/oauth-protected-resource", handleProtectedResource);
358
+ app.get(
359
+ "/.well-known/oauth-protected-resource.json",
360
+ handleProtectedResource,
361
+ );
226
362
  }
227
363
 
228
364
  return mcpServer;
@@ -0,0 +1,10 @@
1
+ import { describe, it, expect } from "@jest/globals";
2
+ import { getPackageVersion } from "./packageVersion.js";
3
+
4
+ describe("packageVersion", () => {
5
+ it("resolves the current package.json version as a valid semver string", () => {
6
+ const version = getPackageVersion();
7
+ expect(typeof version).toBe("string");
8
+ expect(version).toMatch(/^\d+\.\d+\.\d+/);
9
+ });
10
+ });
@@ -0,0 +1,32 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ let cachedVersion: string | null = null;
6
+
7
+ export function getPackageVersion(): string {
8
+ if (cachedVersion) {
9
+ return cachedVersion;
10
+ }
11
+
12
+ try {
13
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
14
+ let dir = currentDir;
15
+ while (dir && dir !== path.dirname(dir)) {
16
+ const candidate = path.join(dir, "package.json");
17
+ if (fs.existsSync(candidate)) {
18
+ const pkg = JSON.parse(fs.readFileSync(candidate, "utf8"));
19
+ if (pkg.version) {
20
+ const versionStr = String(pkg.version);
21
+ cachedVersion = versionStr;
22
+ return versionStr;
23
+ }
24
+ }
25
+ dir = path.dirname(dir);
26
+ }
27
+ } catch {}
28
+
29
+ const fallback = "1.27.1";
30
+ cachedVersion = fallback;
31
+ return fallback;
32
+ }
@@ -105,4 +105,77 @@ describe("shouldBypassFetchProxy", () => {
105
105
  shouldBypassFetchProxy(classAUrl, "https://xt-ml.github.io/shadow-claw/"),
106
106
  ).toBe(true);
107
107
  });
108
+
109
+ it("bypasses single-label local hostnames like hostname", () => {
110
+ const pushUrl = new URL("https://hostname:8888/push/vapid-public-key");
111
+ expect(
112
+ shouldBypassFetchProxy(pushUrl, "https://xt-ml.github.io/shadow-claw/"),
113
+ ).toBe(true);
114
+
115
+ const taskUrl = new URL("https://hostname:8888/schedule/tasks");
116
+ expect(
117
+ shouldBypassFetchProxy(taskUrl, "https://xt-ml.github.io/shadow-claw/"),
118
+ ).toBe(true);
119
+ });
120
+
121
+ it("bypasses mDNS .local and local network domain endpoints", () => {
122
+ const localDomainUrl = new URL(
123
+ "https://hostname.local:8888/api/control/events",
124
+ );
125
+ expect(
126
+ shouldBypassFetchProxy(
127
+ localDomainUrl,
128
+ "https://xt-ml.github.io/shadow-claw/",
129
+ ),
130
+ ).toBe(true);
131
+
132
+ const lanDomainUrl = new URL("https://server.lan:8888/schedule/tasks");
133
+ expect(
134
+ shouldBypassFetchProxy(
135
+ lanDomainUrl,
136
+ "https://xt-ml.github.io/shadow-claw/",
137
+ ),
138
+ ).toBe(true);
139
+ });
140
+
141
+ it("bypasses built-in backend routes even on custom domains", () => {
142
+ const pushSubscribeUrl = new URL(
143
+ "https://custom.example.com/push/subscribe",
144
+ );
145
+ expect(
146
+ shouldBypassFetchProxy(
147
+ pushSubscribeUrl,
148
+ "https://xt-ml.github.io/shadow-claw/",
149
+ ),
150
+ ).toBe(true);
151
+
152
+ const scheduleUrl = new URL("https://custom.example.com/schedule/tasks");
153
+ expect(
154
+ shouldBypassFetchProxy(
155
+ scheduleUrl,
156
+ "https://xt-ml.github.io/shadow-claw/",
157
+ ),
158
+ ).toBe(true);
159
+
160
+ const controlUrl = new URL(
161
+ "https://custom.example.com/api/control/messages",
162
+ );
163
+ expect(
164
+ shouldBypassFetchProxy(
165
+ controlUrl,
166
+ "https://xt-ml.github.io/shadow-claw/",
167
+ ),
168
+ ).toBe(true);
169
+ });
170
+
171
+ it("bypasses requests destined for the proxy server itself", () => {
172
+ const targetUrl = new URL("https://proxy.example.com:9000/api/custom");
173
+ expect(
174
+ shouldBypassFetchProxy(
175
+ targetUrl,
176
+ "https://xt-ml.github.io/shadow-claw/",
177
+ "https://proxy.example.com:9000/proxy",
178
+ ),
179
+ ).toBe(true);
180
+ });
108
181
  });
@@ -16,14 +16,49 @@ const BYPASS_PROVIDERS = [
16
16
  "api.telegram.org",
17
17
  ];
18
18
 
19
+ const BACKEND_ROUTE_PREFIXES = [
20
+ "/api/control/",
21
+ "/api/backup/",
22
+ "/api/activity-log/",
23
+ "/api/health",
24
+ "/push/",
25
+ "/schedule/",
26
+ "/mcp",
27
+ "/ws/",
28
+ "/peerjs/",
29
+ ];
30
+
31
+ const LOCAL_DOMAINS = [".local", ".lan", ".home", ".internal"];
32
+
19
33
  export function shouldBypassFetchProxy(
20
34
  requestUrl: URL,
21
35
  locationOrigin: string,
36
+ proxyUrl?: string,
22
37
  ): boolean {
23
38
  if (requestUrl.origin === locationOrigin) {
24
39
  return true;
25
40
  }
26
41
 
42
+ // Never proxy requests directed to the proxy host itself (avoids self-proxy loops)
43
+ if (proxyUrl) {
44
+ try {
45
+ const proxyOrigin = new URL(proxyUrl, locationOrigin).origin;
46
+ if (requestUrl.origin === proxyOrigin) {
47
+ return true;
48
+ }
49
+ } catch (_) {}
50
+ }
51
+
52
+ // Never proxy requests to ShadowClaw built-in backend infrastructure routes
53
+ const pathname = requestUrl.pathname;
54
+ if (
55
+ BACKEND_ROUTE_PREFIXES.some(
56
+ (prefix) => pathname === prefix || pathname.startsWith(prefix),
57
+ )
58
+ ) {
59
+ return true;
60
+ }
61
+
27
62
  const hostname = requestUrl.hostname.toLowerCase();
28
63
 
29
64
  if (
@@ -62,5 +97,14 @@ export function shouldBypassFetchProxy(
62
97
  return true;
63
98
  }
64
99
 
100
+ // Local/private hostnames: single-label (no dots, e.g. "hostname") or local network domain suffixes
101
+ const isLocalHostname =
102
+ !hostname.includes(".") ||
103
+ LOCAL_DOMAINS.some((domain) => hostname.endsWith(domain));
104
+
105
+ if (isLocalHostname) {
106
+ return true;
107
+ }
108
+
65
109
  return false;
66
110
  }
@@ -239,7 +239,7 @@ self.addEventListener("fetch", (event: FetchEvent) => {
239
239
  }
240
240
  }
241
241
 
242
- if (shouldBypassFetchProxy(requestUrl, location.origin)) {
242
+ if (shouldBypassFetchProxy(requestUrl, location.origin, proxyUrl)) {
243
243
  return;
244
244
  }
245
245
 
@@ -291,4 +291,40 @@ describe("IMessageChannel", () => {
291
291
  const mode = await channel.ensureApiMode();
292
292
  expect(mode).toBe("beeper-desktop");
293
293
  });
294
+
295
+ it("ensureConnected aborts hanging poll, resets backoff, and launches fresh poll", async () => {
296
+ const channel = new IMessageChannel();
297
+ channel.configure("https://bridge.example", "secret", []);
298
+
299
+ let pollCount = 0;
300
+ jest.spyOn(channel, "requestJson").mockImplementation((async (
301
+ _path: string,
302
+ _opts: any,
303
+ _timeout: number,
304
+ signal?: AbortSignal,
305
+ ) => {
306
+ pollCount++;
307
+ return new Promise((_resolve, reject) => {
308
+ signal?.addEventListener("abort", () => {
309
+ reject(new DOMException("Aborted", "AbortError"));
310
+ });
311
+ });
312
+ }) as any);
313
+
314
+ channel.start();
315
+ expect(channel.running).toBe(true);
316
+ expect(pollCount).toBe(1);
317
+
318
+ // Simulate high backoff
319
+ channel.reconnectDelayMs = 30000;
320
+ const oldAbort = channel.abortController;
321
+
322
+ channel.ensureConnected();
323
+
324
+ expect(channel.reconnectDelayMs).toBe(1000);
325
+ expect(oldAbort?.signal.aborted).toBe(true);
326
+ expect(pollCount).toBe(2);
327
+
328
+ channel.stop();
329
+ });
294
330
  });
@@ -134,6 +134,17 @@ export class IMessageChannel implements Channel {
134
134
  void this.poll();
135
135
  }
136
136
 
137
+ ensureConnected(): void {
138
+ if (!this.isConfigured() || !this.running) {
139
+ return;
140
+ }
141
+
142
+ this.reconnectDelayMs = 1000;
143
+ this.abortController?.abort();
144
+ this.abortController = new AbortController();
145
+ void this.poll();
146
+ }
147
+
137
148
  stop(): void {
138
149
  this.running = false;
139
150
  this.abortController?.abort();
@@ -38,11 +38,19 @@ class MockDataConnection {
38
38
 
39
39
  class MockPeer {
40
40
  destroyed = false;
41
+ disconnected = false;
42
+ open = true;
41
43
  id: string;
44
+ reconnect = jest.fn(() => {
45
+ this.disconnected = false;
46
+ this.open = true;
47
+ this.emit("open", this.id);
48
+ });
42
49
  private handlers: Map<string, EventHandler[]> = new Map();
43
50
 
44
51
  constructor(id: string, _opts?: unknown) {
45
52
  this.id = id;
53
+ this.open = true;
46
54
  setTimeout(() => this.emit("open", id), 0);
47
55
  }
48
56
 
@@ -55,6 +63,7 @@ class MockPeer {
55
63
 
56
64
  destroy() {
57
65
  this.destroyed = true;
66
+ this.open = false;
58
67
  this.emit("disconnected");
59
68
  }
60
69
 
@@ -829,5 +838,52 @@ describe("PeerJsChannel", () => {
829
838
  expect((ch as any)._isTrustedPeer("cli-12345")).toBe(true);
830
839
  expect((ch as any)._isTrustedPeer("other-12345")).toBe(false);
831
840
  });
841
+
842
+ it("ensureConnected reconnects disconnected peer and resets backoff", async () => {
843
+ const ch = new PeerJsChannel();
844
+ ch.configure("my-test-peer", []);
845
+ ch.start();
846
+ await new Promise((resolve) => setTimeout(resolve, 10));
847
+
848
+ const peer = (ch as any).peer as MockPeer;
849
+ expect(peer).toBeDefined();
850
+
851
+ // Simulate disconnect
852
+ peer.disconnected = true;
853
+ peer.open = false;
854
+ (ch as any)._reconnectAttempts = 3;
855
+ (ch as any)._reconnectTimer = setTimeout(() => {}, 8000);
856
+
857
+ ch.ensureConnected(false);
858
+
859
+ expect((ch as any)._reconnectTimer).toBeNull();
860
+ expect((ch as any)._reconnectAttempts).toBe(0);
861
+ expect(peer.reconnect).toHaveBeenCalled();
862
+
863
+ ch.stop();
864
+ });
865
+
866
+ it("ensureConnected re-initializes a destroyed peer instance", async () => {
867
+ const ch = new PeerJsChannel();
868
+ ch.configure("my-test-peer-2", []);
869
+ ch.start();
870
+ await new Promise((resolve) => setTimeout(resolve, 10));
871
+
872
+ const initialPeer = (ch as any).peer as MockPeer;
873
+ expect(initialPeer).toBeDefined();
874
+
875
+ // Destroy peer instance
876
+ initialPeer.destroy();
877
+ expect(initialPeer.destroyed).toBe(true);
878
+
879
+ ch.ensureConnected(true);
880
+ await new Promise((resolve) => setTimeout(resolve, 10));
881
+
882
+ const newPeer = (ch as any).peer as MockPeer;
883
+ expect(newPeer).not.toBe(initialPeer);
884
+ expect(newPeer.destroyed).toBe(false);
885
+
886
+ ch.stop();
887
+ });
832
888
  });
833
889
  });
@@ -346,6 +346,35 @@ export class PeerJsChannel implements Channel {
346
346
  this._initPeer();
347
347
  }
348
348
 
349
+ ensureConnected(force: boolean = false): void {
350
+ if (!this.running || !this.isConfigured()) {
351
+ return;
352
+ }
353
+
354
+ if (this._reconnectTimer !== null) {
355
+ clearTimeout(this._reconnectTimer);
356
+ this._reconnectTimer = null;
357
+ }
358
+ this._reconnectAttempts = 0;
359
+
360
+ const peer = this.peer as any;
361
+ if (!peer || peer.destroyed || (force && !peer.open)) {
362
+ if (peer && !peer.destroyed) {
363
+ try {
364
+ peer.destroy();
365
+ } catch (_) {}
366
+ }
367
+ this.peer = null;
368
+ this._initPeer();
369
+ } else if (peer.disconnected) {
370
+ try {
371
+ peer.reconnect?.();
372
+ } catch {
373
+ this._initPeer();
374
+ }
375
+ }
376
+ }
377
+
349
378
  stop(): void {
350
379
  this.running = false;
351
380
  if (this._reconnectTimer !== null) {
@@ -508,4 +508,42 @@ describe("TelegramChannel", () => {
508
508
  ),
509
509
  ).toBe(true);
510
510
  });
511
+
512
+ it("ensureConnected aborts hanging poll, resets backoff, and launches fresh poll", async () => {
513
+ const channel = new TelegramChannel();
514
+ channel.configure("test-token", ["123"]);
515
+
516
+ let fetchCount = 0;
517
+ jest.spyOn(globalThis, "fetch").mockImplementation((async (
518
+ _url: any,
519
+ opts: any,
520
+ ) => {
521
+ fetchCount++;
522
+ // Return a promise that hangs until aborted
523
+ return new Promise((_resolve, reject) => {
524
+ opts?.signal?.addEventListener("abort", () => {
525
+ reject(new DOMException("Aborted", "AbortError"));
526
+ });
527
+ });
528
+ }) as any);
529
+
530
+ channel.start();
531
+ expect(channel.running).toBe(true);
532
+ expect(fetchCount).toBe(1);
533
+
534
+ // Simulate high backoff delay
535
+ channel.pollRetryDelayMs = 30_000;
536
+
537
+ const oldSessionId = channel.pollSessionId;
538
+ const oldAbort = channel.abortController;
539
+
540
+ channel.ensureConnected();
541
+
542
+ expect(channel.pollRetryDelayMs).toBe(1_000);
543
+ expect(oldAbort?.signal.aborted).toBe(true);
544
+ expect(channel.pollSessionId).toBe(oldSessionId + 1);
545
+ expect(fetchCount).toBe(2);
546
+
547
+ channel.stop();
548
+ });
511
549
  });
@@ -117,6 +117,19 @@ export class TelegramChannel implements Channel {
117
117
  void this.poll(sessionId, controller);
118
118
  }
119
119
 
120
+ ensureConnected(): void {
121
+ if (!this.token || !this.running) {
122
+ return;
123
+ }
124
+
125
+ this.resetPollBackoff();
126
+ this.abortController?.abort();
127
+ const sessionId = ++this.pollSessionId;
128
+ const controller = new AbortController();
129
+ this.abortController = controller;
130
+ void this.poll(sessionId, controller);
131
+ }
132
+
120
133
  stop(): void {
121
134
  this.running = false;
122
135
  this.resetPollBackoff();
@@ -26,8 +26,10 @@ export type KnownChannelType =
26
26
  | "telegram";
27
27
 
28
28
  export interface Channel {
29
+ ensureConnected?(force?: boolean): void;
29
30
  onMessage(callback: ChannelMessageCallback): void;
30
31
  onTyping?(callback: ChannelTypingCallback): void;
32
+ running?: boolean;
31
33
  send(
32
34
  groupId: string,
33
35
  text: string,