shadow-claw 1.26.0 → 1.27.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 (230) hide show
  1. package/.agents/skills/main/skill-creator/SKILL.md +39 -0
  2. package/.agents/skills/main/skill-creator/references/declarative-pipelines.md +44 -0
  3. package/.agents/skills/main/skill-creator/references/declarative-tools.md +35 -0
  4. package/.agents/skills/main/skill-creator/references/frontmatter-spec.md +50 -0
  5. package/.well-known/agent-skills/index.json +14 -0
  6. package/README.md +5 -3
  7. package/bin/build/build.mjs +146 -15
  8. package/bin/build/build.test.mjs +57 -0
  9. package/bin/cli.mjs +26 -0
  10. package/bin/commands/mcp.mjs +277 -55
  11. package/bin/commands/mcp.test.mjs +275 -1
  12. package/bin/commands/skills-index.mjs +328 -0
  13. package/bin/commands/skills-index.test.mjs +161 -0
  14. package/bin/site-config/utils/custom-elements/get-approved-custom-element-scripts.test.mjs +2 -0
  15. package/bin/site-config/utils/custom-elements/sanitize-embedded-custom-element-scripts.test.mjs +10 -1
  16. package/dist/lib/components/index.js +25 -25
  17. package/dist/lib/components/settings/shadow-claw-accounts.js +1 -1
  18. package/dist/lib/components/settings/shadow-claw-channel-config.js +1 -1
  19. package/dist/lib/components/settings/shadow-claw-control-plane.js +1 -1
  20. package/dist/lib/components/settings/shadow-claw-git.js +1 -1
  21. package/dist/lib/components/settings/shadow-claw-integrations.js +1 -1
  22. package/dist/lib/components/settings/shadow-claw-llm.js +1 -1
  23. package/dist/lib/components/settings/shadow-claw-mcp-remote.js +1 -1
  24. package/dist/lib/components/settings/shadow-claw-networking.js +1 -1
  25. package/dist/lib/components/settings/shadow-claw-peerjs.js +1 -1
  26. package/dist/lib/components/settings/shadow-claw-storage.js +1 -1
  27. package/dist/lib/components/settings/shadow-claw-task-server.js +1 -1
  28. package/dist/lib/components/settings/shadow-claw-webvm.js +1 -1
  29. package/dist/lib/components/shadow-claw-a2ui.js +1 -1
  30. package/dist/lib/components/shadow-claw-accounts.js +1 -1
  31. package/dist/lib/components/shadow-claw-channel-config.js +1 -1
  32. package/dist/lib/components/shadow-claw-channels.js +1 -1
  33. package/dist/lib/components/shadow-claw-chat.js +1 -1
  34. package/dist/lib/components/shadow-claw-control-plane.js +1 -1
  35. package/dist/lib/components/shadow-claw-conversations.js +1 -1
  36. package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
  37. package/dist/lib/components/shadow-claw-files.js +1 -1
  38. package/dist/lib/components/shadow-claw-git.js +1 -1
  39. package/dist/lib/components/shadow-claw-integrations.js +1 -1
  40. package/dist/lib/components/shadow-claw-llm.js +1 -1
  41. package/dist/lib/components/shadow-claw-mcp-remote.js +1 -1
  42. package/dist/lib/components/shadow-claw-networking.js +1 -1
  43. package/dist/lib/components/shadow-claw-page-header.js +1 -1
  44. package/dist/lib/components/shadow-claw-pages.js +1 -1
  45. package/dist/lib/components/shadow-claw-peerjs.js +1 -1
  46. package/dist/lib/components/shadow-claw-settings.js +1 -1
  47. package/dist/lib/components/shadow-claw-storage.js +1 -1
  48. package/dist/lib/components/shadow-claw-task-server.js +1 -1
  49. package/dist/lib/components/shadow-claw-tasks.js +1 -1
  50. package/dist/lib/components/shadow-claw-terminal.js +1 -1
  51. package/dist/lib/components/shadow-claw-tools.js +1 -1
  52. package/dist/lib/components/shadow-claw-webvm.js +1 -1
  53. package/dist/lib/components/shadow-claw.js +1 -1
  54. package/dist/lib/{configurePeerJs-CrlGan0m.js → configurePeerJs-oODzltaC.js} +2 -2
  55. package/dist/lib/{configurePeerJs-CrlGan0m.js.map → configurePeerJs-oODzltaC.js.map} +1 -1
  56. package/dist/lib/core/control-plane-client.d.ts +1 -0
  57. package/dist/lib/{downloadGroupFile-DboUjmp2.js → downloadGroupFile-DmcBQU1n.js} +2 -2
  58. package/dist/lib/{downloadGroupFile-DboUjmp2.js.map → downloadGroupFile-DmcBQU1n.js.map} +1 -1
  59. package/dist/lib/{file-viewer-BrK2Wxgh.js → file-viewer-C3YKnq_-.js} +4 -3
  60. package/dist/lib/{file-viewer-BrK2Wxgh.js.map → file-viewer-C3YKnq_-.js.map} +1 -1
  61. package/dist/lib/index.js +26 -26
  62. package/dist/lib/{initControlPlane-CsO4UR6_.js → initControlPlane-C2zURVIS.js} +86 -13
  63. package/dist/lib/initControlPlane-C2zURVIS.js.map +1 -0
  64. package/dist/lib/{markdown-eeSTFnMA.js → markdown-C6EHl9zs.js} +3 -3
  65. package/dist/lib/{markdown-eeSTFnMA.js.map → markdown-C6EHl9zs.js.map} +1 -1
  66. package/dist/lib/{orchestrator-CJvoC2Uu.js → orchestrator-CACLa_Ro.js} +7 -6
  67. package/dist/lib/{orchestrator-CJvoC2Uu.js.map → orchestrator-CACLa_Ro.js.map} +1 -1
  68. package/dist/lib/{orchestrator-DCfomsn9.js → orchestrator-KRjx0eba.js} +6 -534
  69. package/dist/lib/{orchestrator-DCfomsn9.js.map → orchestrator-KRjx0eba.js.map} +1 -1
  70. package/dist/lib/parsePath-DwC_YZjw.js +16 -0
  71. package/dist/lib/parsePath-DwC_YZjw.js.map +1 -0
  72. package/dist/lib/{peerjs-Dp02bXcG.js → peerjs-DFWOBcXf.js} +4 -4
  73. package/dist/lib/{peerjs-Dp02bXcG.js.map → peerjs-DFWOBcXf.js.map} +1 -1
  74. package/dist/lib/{readGroupFileBytes-Bve-e5e_.js → readGroupFileBytes-C8oOe8Iu.js} +3 -16
  75. package/dist/lib/readGroupFileBytes-C8oOe8Iu.js.map +1 -0
  76. package/dist/lib/security/custom-element-security.d.ts +8 -0
  77. package/dist/lib/server/control-plane-types.d.ts +1 -1
  78. package/dist/lib/{shadow-claw-D8xINM3c.js → shadow-claw-6UHdiy4a.js} +5 -5
  79. package/dist/lib/{shadow-claw-D8xINM3c.js.map → shadow-claw-6UHdiy4a.js.map} +1 -1
  80. package/dist/lib/{shadow-claw-a2ui-7rJqG4Ke.js → shadow-claw-a2ui-CpZUTzlh.js} +5 -4
  81. package/dist/lib/{shadow-claw-a2ui-7rJqG4Ke.js.map → shadow-claw-a2ui-CpZUTzlh.js.map} +1 -1
  82. package/dist/lib/{shadow-claw-accounts-CdruwN8A.js → shadow-claw-accounts-03oJ__XC.js} +2 -2
  83. package/dist/lib/{shadow-claw-accounts-CdruwN8A.js.map → shadow-claw-accounts-03oJ__XC.js.map} +1 -1
  84. package/dist/lib/{shadow-claw-channel-config-Bj0-Nk3t.js → shadow-claw-channel-config-DmfwJc6G.js} +3 -3
  85. package/dist/lib/{shadow-claw-channel-config-Bj0-Nk3t.js.map → shadow-claw-channel-config-DmfwJc6G.js.map} +1 -1
  86. package/dist/lib/{shadow-claw-channels-C_jI4KhP.js → shadow-claw-channels-1N6Jl-By.js} +3 -3
  87. package/dist/lib/{shadow-claw-channels-C_jI4KhP.js.map → shadow-claw-channels-1N6Jl-By.js.map} +1 -1
  88. package/dist/lib/{shadow-claw-chat-B16MspoP.js → shadow-claw-chat-BMoqQJRK.js} +10 -10
  89. package/dist/lib/{shadow-claw-chat-B16MspoP.js.map → shadow-claw-chat-BMoqQJRK.js.map} +1 -1
  90. package/dist/lib/{shadow-claw-control-plane-x8Ldid_h.js → shadow-claw-control-plane-UG0o7pCz.js} +6 -6
  91. package/dist/lib/{shadow-claw-control-plane-x8Ldid_h.js.map → shadow-claw-control-plane-UG0o7pCz.js.map} +1 -1
  92. package/dist/lib/{shadow-claw-conversations-Ct1kAYdB.js → shadow-claw-conversations-BwXep3iu.js} +4 -3
  93. package/dist/lib/{shadow-claw-conversations-Ct1kAYdB.js.map → shadow-claw-conversations-BwXep3iu.js.map} +1 -1
  94. package/dist/lib/{shadow-claw-file-viewer-BfDHHgpA.js → shadow-claw-file-viewer-DUxzVxDT.js} +14 -10
  95. package/dist/lib/shadow-claw-file-viewer-DUxzVxDT.js.map +1 -0
  96. package/dist/lib/{shadow-claw-files-EYhOPYIH.js → shadow-claw-files-DIREEnT2.js} +7 -7
  97. package/dist/lib/{shadow-claw-files-EYhOPYIH.js.map → shadow-claw-files-DIREEnT2.js.map} +1 -1
  98. package/dist/lib/{shadow-claw-git-tcSpT-P0.js → shadow-claw-git-Bg1SWgsF.js} +4 -4
  99. package/dist/lib/{shadow-claw-git-tcSpT-P0.js.map → shadow-claw-git-Bg1SWgsF.js.map} +1 -1
  100. package/dist/lib/{shadow-claw-integrations-NbcCx_ZI.js → shadow-claw-integrations-Ds-CLPhj.js} +2 -2
  101. package/dist/lib/{shadow-claw-integrations-NbcCx_ZI.js.map → shadow-claw-integrations-Ds-CLPhj.js.map} +1 -1
  102. package/dist/lib/{shadow-claw-llm-BazJdD5f.js → shadow-claw-llm-CazBrVpM.js} +3 -3
  103. package/dist/lib/{shadow-claw-llm-BazJdD5f.js.map → shadow-claw-llm-CazBrVpM.js.map} +1 -1
  104. package/dist/lib/{shadow-claw-mcp-remote-CYmhpg7a.js → shadow-claw-mcp-remote-CT5Q-54E.js} +2 -2
  105. package/dist/lib/{shadow-claw-mcp-remote-CYmhpg7a.js.map → shadow-claw-mcp-remote-CT5Q-54E.js.map} +1 -1
  106. package/dist/lib/{shadow-claw-networking-DImPxe0H.js → shadow-claw-networking-Df85tCbO.js} +2 -2
  107. package/dist/lib/{shadow-claw-networking-DImPxe0H.js.map → shadow-claw-networking-Df85tCbO.js.map} +1 -1
  108. package/dist/lib/{shadow-claw-page-header-CcHUfFUx.js → shadow-claw-page-header-Cti2fzf-.js} +2 -2
  109. package/dist/lib/{shadow-claw-page-header-CcHUfFUx.js.map → shadow-claw-page-header-Cti2fzf-.js.map} +1 -1
  110. package/dist/lib/{shadow-claw-pages-D54jF1Qq.js → shadow-claw-pages-BgPyIEZO.js} +15 -11
  111. package/dist/lib/shadow-claw-pages-BgPyIEZO.js.map +1 -0
  112. package/dist/lib/{shadow-claw-peerjs-B9-T8Nlx.js → shadow-claw-peerjs-xkBXILed.js} +3 -3
  113. package/dist/lib/{shadow-claw-peerjs-B9-T8Nlx.js.map → shadow-claw-peerjs-xkBXILed.js.map} +1 -1
  114. package/dist/lib/{shadow-claw-settings-Bt1VAe-1.js → shadow-claw-settings-CN89xRVU.js} +4 -4
  115. package/dist/lib/{shadow-claw-settings-Bt1VAe-1.js.map → shadow-claw-settings-CN89xRVU.js.map} +1 -1
  116. package/dist/lib/{shadow-claw-storage-CFOjGf1d.js → shadow-claw-storage-VnhcObCe.js} +3 -3
  117. package/dist/lib/{shadow-claw-storage-CFOjGf1d.js.map → shadow-claw-storage-VnhcObCe.js.map} +1 -1
  118. package/dist/lib/{shadow-claw-task-server-BiKa2GOm.js → shadow-claw-task-server-jgEC2hHB.js} +2 -2
  119. package/dist/lib/{shadow-claw-task-server-BiKa2GOm.js.map → shadow-claw-task-server-jgEC2hHB.js.map} +1 -1
  120. package/dist/lib/{shadow-claw-tasks-DDHd8W4d.js → shadow-claw-tasks-i_evIufy.js} +6 -6
  121. package/dist/lib/{shadow-claw-tasks-DDHd8W4d.js.map → shadow-claw-tasks-i_evIufy.js.map} +1 -1
  122. package/dist/lib/{shadow-claw-terminal-kB6_tWKz.js → shadow-claw-terminal-CwScROlu.js} +2 -2
  123. package/dist/lib/{shadow-claw-terminal-kB6_tWKz.js.map → shadow-claw-terminal-CwScROlu.js.map} +1 -1
  124. package/dist/lib/{shadow-claw-tools-DxXIm0ku.js → shadow-claw-tools-DD3fyXP4.js} +5 -4
  125. package/dist/lib/{shadow-claw-tools-DxXIm0ku.js.map → shadow-claw-tools-DD3fyXP4.js.map} +1 -1
  126. package/dist/lib/{shadow-claw-webvm-Dc8oo7ao.js → shadow-claw-webvm-DbJq4I3-.js} +2 -2
  127. package/dist/lib/{shadow-claw-webvm-Dc8oo7ao.js.map → shadow-claw-webvm-DbJq4I3-.js.map} +1 -1
  128. package/dist/lib/{syncWebMcpRegistration-jyRGbC9w.js → syncWebMcpRegistration-DNLnQ-jz.js} +3 -2
  129. package/dist/lib/{syncWebMcpRegistration-jyRGbC9w.js.map → syncWebMcpRegistration-DNLnQ-jz.js.map} +1 -1
  130. package/dist/lib/tools-DwwB9RBa.js +546 -0
  131. package/dist/lib/tools-DwwB9RBa.js.map +1 -0
  132. package/dist/lib/{trusted-types-CkLDpLiZ.js → trusted-types-Dx0RZlrd.js} +19 -8
  133. package/dist/lib/trusted-types-Dx0RZlrd.js.map +1 -0
  134. package/dist/lib/utils/index.js +1 -1
  135. package/dist/lib/utils/utils.js +1 -1
  136. package/dist/public/.well-known/agent-skills/index.json +14 -0
  137. package/dist/public/AGENTS.md +5 -3
  138. package/dist/public/README.md +5 -3
  139. package/dist/public/agent.worker.js +1 -1
  140. package/dist/public/{configurePeerJs-COcwE0E7.js → configurePeerJs-BqgeBjP7.js} +1 -1
  141. package/dist/public/custom-element-security-FU04Cq05.js +1 -0
  142. package/dist/public/dist-B9m1QM9v.js +58 -0
  143. package/dist/public/docs/README.md +31 -31
  144. package/dist/public/docs/example/article/index.html +1 -1
  145. package/dist/public/docs/publishing/index.html +1 -1
  146. package/dist/public/docs/skill-creator/index.html +1 -1
  147. package/dist/public/docs/subsystems/a2ui.md +11 -1
  148. package/dist/public/docs/subsystems/cli.md +29 -0
  149. package/dist/public/docs/subsystems/control-plane.md +8 -2
  150. package/dist/public/docs/subsystems/custom-element-security.md +19 -9
  151. package/dist/public/docs/subsystems/mcp-server.md +39 -15
  152. package/dist/public/docs/subsystems/skills.md +30 -0
  153. package/dist/public/docs/subsystems/webmcp.md +9 -0
  154. package/dist/public/downloadGroupFile-D8KRJGGL.js +1 -0
  155. package/dist/public/e2e/README.md +7 -4
  156. package/dist/public/{e2e-bridge-CHARpbpW.js → e2e-bridge-Ceqr5kGc.js} +1 -1
  157. package/dist/public/effect-BJCrpFdp.js +1 -0
  158. package/dist/public/file-viewer-C-6Jc_tb.js +1 -0
  159. package/dist/public/index.html +1 -1
  160. package/dist/public/index.js +1 -1
  161. package/dist/public/initControlPlane-BN9cWyO0.js +1 -0
  162. package/dist/public/main/index.html +1 -1
  163. package/dist/public/main/memory/index.html +1 -1
  164. package/dist/public/{markdown-1I3O6Mmd.js → markdown-C3M4OW_A.js} +1 -1
  165. package/dist/public/{orchestrator-Cl95w2vb.js → orchestrator-CMbCTjBf.js} +7 -7
  166. package/dist/public/{orchestrator-D-xlXG92.js → orchestrator-DHjdLtqa.js} +663 -663
  167. package/dist/public/peerjs-DUCKH-m9.js +1 -0
  168. package/dist/public/service-worker.js +1 -1
  169. package/dist/public/{shadow-claw-CaoKmIIB.js → shadow-claw-EAt2ml5Y.js} +3 -3
  170. package/dist/public/{shadow-claw-accounts-CJvYC4li.js → shadow-claw-accounts-xUOLBsQZ.js} +1 -1
  171. package/dist/public/{shadow-claw-channels-3K62sU5f.js → shadow-claw-channels-XsfuWFF8.js} +4 -4
  172. package/dist/public/{shadow-claw-chat-Cu2keU2I.js → shadow-claw-chat-CbSaOZ4q.js} +7 -7
  173. package/dist/public/{shadow-claw-control-plane-D1OwlQI_.js → shadow-claw-control-plane-DHvN3Ukg.js} +2 -2
  174. package/dist/public/{shadow-claw-conversations-DDtg3BEZ.js → shadow-claw-conversations-BWs5VVfs.js} +2 -2
  175. package/dist/public/{shadow-claw-file-viewer-BR__1z9S.js → shadow-claw-file-viewer-BWsoW5QS.js} +5 -4
  176. package/dist/public/{shadow-claw-files-BJSyZ-Bl.js → shadow-claw-files-CG7MqWiV.js} +8 -8
  177. package/dist/public/{shadow-claw-git-MAjhXRLc.js → shadow-claw-git-DBRPZNav.js} +6 -6
  178. package/dist/public/{shadow-claw-integrations-82UoFuDR.js → shadow-claw-integrations-BBVnO0Fc.js} +8 -8
  179. package/dist/public/{shadow-claw-llm-R8j0lwPP.js → shadow-claw-llm-DMvLgKXo.js} +2 -2
  180. package/dist/public/{shadow-claw-mcp-remote-BzZoz4LX.js → shadow-claw-mcp-remote-Bkx0gsmF.js} +2 -2
  181. package/dist/public/{shadow-claw-networking-DLEd9cNm.js → shadow-claw-networking-Dd8Rw31D.js} +2 -2
  182. package/dist/public/{shadow-claw-notifications-1vwR7TbT.js → shadow-claw-notifications-Dbevj-lq.js} +2 -2
  183. package/dist/public/{shadow-claw-page-header-DMKbkv3s.js → shadow-claw-page-header-BccSHItx.js} +1 -1
  184. package/dist/public/{shadow-claw-pages-CjyeIV__.js → shadow-claw-pages-3_lQAv0t.js} +4 -3
  185. package/dist/public/{shadow-claw-settings-BfbT4dBU.js → shadow-claw-settings-t8TIlgrM.js} +5 -5
  186. package/dist/public/{shadow-claw-storage-C-mWQQML.js → shadow-claw-storage-CfEXoSIE.js} +2 -2
  187. package/dist/public/{shadow-claw-task-server-D3lHp0Nc.js → shadow-claw-task-server-DxQSsgRZ.js} +2 -2
  188. package/dist/public/{shadow-claw-tasks-DVVWEm-c.js → shadow-claw-tasks-DNpicQwz.js} +14 -14
  189. package/dist/public/{shadow-claw-terminal-BwuTRp-O.js → shadow-claw-terminal-DpY6zU_r.js} +1 -1
  190. package/dist/public/{shadow-claw-toast-ClCwgzFG.js → shadow-claw-toast-BkrQIP-3.js} +1 -1
  191. package/dist/public/{shadow-claw-tools-zFLk2nbV.js → shadow-claw-tools-WSwWohnk.js} +2 -2
  192. package/dist/public/{shadow-claw-webvm-qR9X7fg7.js → shadow-claw-webvm-CP6oFjg5.js} +2 -2
  193. package/dist/public/syncWebMcpRegistration-DOn9gLnD.js +1 -0
  194. package/dist/public/toast-BXgUfbyh.js +1 -0
  195. package/dist/public/tools-DvckJZG2.js +1 -0
  196. package/dist/server.js +188 -188
  197. package/package.json +3 -1
  198. package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.stories.ts +1380 -93
  199. package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.ts +11 -6
  200. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +8 -1
  201. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +13 -5
  202. package/src/components/shadow-claw-pages/utils/buildHtmlPageSrcdoc.ts +13 -5
  203. package/src/core/control-plane-client.ts +15 -0
  204. package/src/core/utils/initControlPlane.test.ts +187 -0
  205. package/src/core/utils/initControlPlane.ts +194 -20
  206. package/src/security/custom-element-security.test.ts +51 -0
  207. package/src/security/custom-element-security.ts +33 -15
  208. package/src/server/control-plane-types.ts +1 -0
  209. package/src/server/control-plane.test.ts +69 -0
  210. package/src/server/control-plane.ts +45 -3
  211. package/src/server/mcp/tools/built-in-tools.ts +80 -5
  212. package/src/server/mcp/tools/client-tool-relay.ts +183 -26
  213. package/src/server/mcp/tools/mcp-tools.test.ts +140 -0
  214. package/src/server/middleware/static-files.test.ts +142 -0
  215. package/src/server/middleware/static-files.ts +66 -8
  216. package/src/testing/jest-setup.ts +1 -0
  217. package/src/worker/utils/handleMessage.ts +4 -1
  218. package/dist/lib/initControlPlane-CsO4UR6_.js.map +0 -1
  219. package/dist/lib/readGroupFileBytes-Bve-e5e_.js.map +0 -1
  220. package/dist/lib/shadow-claw-file-viewer-BfDHHgpA.js.map +0 -1
  221. package/dist/lib/shadow-claw-pages-D54jF1Qq.js.map +0 -1
  222. package/dist/lib/trusted-types-CkLDpLiZ.js.map +0 -1
  223. package/dist/public/custom-element-security-NCqQ5lmC.js +0 -1
  224. package/dist/public/downloadGroupFile-G0BZQgQg.js +0 -1
  225. package/dist/public/effect-rHYr4nfu.js +0 -1
  226. package/dist/public/file-viewer-8PdCURDD.js +0 -1
  227. package/dist/public/initControlPlane-CcGXax0A.js +0 -1
  228. package/dist/public/peerjs-BZdxWQFv.js +0 -1
  229. package/dist/public/syncWebMcpRegistration-DD42YsYV.js +0 -1
  230. package/dist/public/toast-HAq8XWFj.js +0 -58
@@ -18,6 +18,8 @@ export interface ClientToolRelayOptions {
18
18
  export interface ClientToolRelay {
19
19
  discoverTools: () => Promise<McpTool[]>;
20
20
  attachToServer: (server: McpServer) => void;
21
+ getActiveClientId?: () => string;
22
+ setActiveClientId?: (id: string) => void;
21
23
  }
22
24
 
23
25
  export function createClientToolRelay(
@@ -28,7 +30,10 @@ export function createClientToolRelay(
28
30
 
29
31
  let cachedTools: McpTool[] = [];
30
32
  let cacheTime = 0;
33
+ let activeClientId = targetClientId || "";
31
34
  const registeredHandlers = new Set<string>();
35
+ const toolSupportingClientsMap = new Map<string, any[]>();
36
+ const toolDefMap = new Map<string, any>();
32
37
 
33
38
  async function discoverTools(): Promise<McpTool[]> {
34
39
  const now = Date.now();
@@ -36,36 +41,110 @@ export function createClientToolRelay(
36
41
  return cachedTools;
37
42
  }
38
43
 
39
- const clientId = resolveTargetClientId(controlPlane, targetClientId);
40
- if (!clientId) {
44
+ let clients: any[] = [];
45
+ try {
46
+ clients =
47
+ typeof controlPlane.getConnectedClients === "function"
48
+ ? controlPlane.getConnectedClients()
49
+ : typeof controlPlane.listClients === "function"
50
+ ? await controlPlane.listClients()
51
+ : [];
52
+ } catch (_) {}
53
+
54
+ if (!Array.isArray(clients) || clients.length === 0) {
55
+ toolSupportingClientsMap.clear();
56
+ toolDefMap.clear();
41
57
  return [];
42
58
  }
43
59
 
44
- try {
45
- const res = await controlPlane.sendCommand(clientId, "list-tools", {});
46
- if (
47
- res &&
48
- res.success &&
49
- res.data?.tools &&
50
- Array.isArray(res.data.tools)
51
- ) {
52
- cachedTools = res.data.tools.map((t: any) => ({
53
- name: t.name,
54
- description:
55
- t.description || `Relayed tool '${t.name}' from connected client.`,
56
- inputSchema: t.inputSchema || { type: "object", properties: {} },
57
- annotations: t.annotations,
58
- }));
59
- cacheTime = now;
60
- return cachedTools;
60
+ toolSupportingClientsMap.clear();
61
+ toolDefMap.clear();
62
+
63
+ for (const c of clients) {
64
+ const cid = (c as any).clientId || (c as any).id;
65
+ if (!cid) continue;
66
+
67
+ try {
68
+ const res = await controlPlane.sendCommand(cid, "list-tools", {});
69
+ if (
70
+ res &&
71
+ res.success &&
72
+ res.data?.tools &&
73
+ Array.isArray(res.data.tools)
74
+ ) {
75
+ for (const t of res.data.tools) {
76
+ if (!toolDefMap.has(t.name)) {
77
+ toolDefMap.set(t.name, t);
78
+ toolSupportingClientsMap.set(t.name, []);
79
+ }
80
+ toolSupportingClientsMap.get(t.name)!.push(c);
81
+ }
82
+ }
83
+ } catch (err) {
84
+ console.warn(
85
+ `[ClientToolRelay] Failed to query tools from client ${cid}:`,
86
+ err,
87
+ );
61
88
  }
62
- } catch (err) {
63
- console.warn(
64
- `[ClientToolRelay] Failed to query tools from client ${clientId}:`,
65
- err,
66
- );
67
89
  }
68
90
 
91
+ const targetId = resolveTargetClientId(
92
+ controlPlane,
93
+ activeClientId || targetClientId,
94
+ );
95
+ const toolMap = new Map<string, McpTool>();
96
+
97
+ for (const [toolName, t] of toolDefMap.entries()) {
98
+ const supportingClients = toolSupportingClientsMap.get(toolName) || [];
99
+ const supportingClientIds = supportingClients
100
+ .map((cl: any) => cl.clientId || cl.id)
101
+ .filter(Boolean);
102
+
103
+ const preferredClient =
104
+ supportingClients.find(
105
+ (cl: any) => (cl.clientId || cl.id) === targetId,
106
+ ) || supportingClients[0];
107
+ const toolDefaultId =
108
+ preferredClient?.clientId || preferredClient?.id || "";
109
+ const toolDefaultLabel = preferredClient?.deviceLabel || "Client";
110
+
111
+ const existingSchema =
112
+ t.inputSchema && typeof t.inputSchema === "object"
113
+ ? t.inputSchema
114
+ : { type: "object", properties: {} };
115
+
116
+ const properties = {
117
+ ...(existingSchema.properties || {}),
118
+ clientId: {
119
+ type: "string",
120
+ description: `Target ShadowClaw client ID (optional; defaults to ${toolDefaultId} [${toolDefaultLabel}]). Available on: ${supportingClients.map((cl: any) => `${cl.clientId || cl.id} (${cl.deviceLabel || "Client"})`).join(", ")}`,
121
+ enum: supportingClientIds,
122
+ },
123
+ };
124
+
125
+ const clientNote =
126
+ clients.length > 1
127
+ ? supportingClients.length > 1
128
+ ? ` [Default: ${toolDefaultLabel} (${toolDefaultId.slice(0, 14)}...)]`
129
+ : ` [Client: ${toolDefaultLabel} (${toolDefaultId.slice(0, 14)}...)]`
130
+ : "";
131
+
132
+ toolMap.set(toolName, {
133
+ name: t.name,
134
+ description:
135
+ (t.description || `Relayed tool '${t.name}' from connected client.`) +
136
+ clientNote,
137
+ inputSchema: {
138
+ ...existingSchema,
139
+ type: "object",
140
+ properties,
141
+ },
142
+ annotations: t.annotations,
143
+ });
144
+ }
145
+
146
+ cachedTools = Array.from(toolMap.values());
147
+ cacheTime = now;
69
148
  return cachedTools;
70
149
  }
71
150
 
@@ -94,7 +173,49 @@ export function createClientToolRelay(
94
173
  _meta?: Record<string, unknown>,
95
174
  inputResponses?: Record<string, unknown>,
96
175
  ): Promise<McpToolCallResult> {
97
- const clientId = resolveTargetClientId(controlPlane, targetClientId);
176
+ const connectedClients =
177
+ typeof controlPlane.getConnectedClients === "function"
178
+ ? controlPlane.getConnectedClients()
179
+ : typeof controlPlane.listClients === "function"
180
+ ? await controlPlane.listClients()
181
+ : [];
182
+ if (!Array.isArray(connectedClients) || connectedClients.length === 0) {
183
+ return {
184
+ resultType: "complete",
185
+ isError: true,
186
+ content: [
187
+ {
188
+ type: "text",
189
+ text: "Error: No connected ShadowClaw client available to execute tool.",
190
+ },
191
+ ],
192
+ };
193
+ }
194
+
195
+ if (toolDefMap.size === 0) {
196
+ await discoverTools();
197
+ }
198
+
199
+ const supportingClients = toolSupportingClientsMap.get(toolName) || [];
200
+ const supportingClientIds = supportingClients
201
+ .map((cl: any) => cl.clientId || cl.id)
202
+ .filter(Boolean);
203
+
204
+ let resolvedClientCandidate = args.clientId as string | undefined;
205
+ if (!resolvedClientCandidate) {
206
+ const activeCandidate = activeClientId || targetClientId;
207
+ const activeId = resolveTargetClientId(controlPlane, activeCandidate);
208
+ if (supportingClientIds.includes(activeId)) {
209
+ resolvedClientCandidate = activeId;
210
+ } else if (supportingClientIds.length > 0) {
211
+ resolvedClientCandidate = supportingClientIds[0];
212
+ }
213
+ }
214
+
215
+ const clientId = resolveTargetClientId(
216
+ controlPlane,
217
+ resolvedClientCandidate,
218
+ );
98
219
  if (!clientId) {
99
220
  return {
100
221
  resultType: "complete",
@@ -108,6 +229,23 @@ export function createClientToolRelay(
108
229
  };
109
230
  }
110
231
 
232
+ if (
233
+ supportingClientIds.length > 0 &&
234
+ !supportingClientIds.includes(clientId)
235
+ ) {
236
+ return {
237
+ resultType: "complete",
238
+ isError: true,
239
+ content: [
240
+ {
241
+ type: "text",
242
+ text: `Error: Tool '${toolName}' is not enabled or available on client '${clientId}'. (Available on: ${supportingClientIds.join(", ")})`,
243
+ },
244
+ ],
245
+ _meta: { clientId },
246
+ };
247
+ }
248
+
111
249
  // Interactive tool handling: ask_user via MRTR
112
250
  if (toolName === "ask_user") {
113
251
  if (!inputResponses || !inputResponses["response"]) {
@@ -147,14 +285,19 @@ export function createClientToolRelay(
147
285
  : JSON.stringify(userResponse),
148
286
  },
149
287
  ],
288
+ _meta: {
289
+ clientId,
290
+ },
150
291
  };
151
292
  }
152
293
 
153
294
  // Standard tool execution via Control Plane invoke-tool command
154
295
  try {
296
+ const toolArgs = { ...args };
297
+ delete toolArgs.clientId;
155
298
  const res = await controlPlane.sendCommand(clientId, "invoke-tool", {
156
299
  toolName,
157
- input: args,
300
+ input: toolArgs,
158
301
  });
159
302
 
160
303
  if (!res.success) {
@@ -167,6 +310,9 @@ export function createClientToolRelay(
167
310
  text: `Tool execution error: ${res.error || "Unknown client execution error"}`,
168
311
  },
169
312
  ],
313
+ _meta: {
314
+ clientId,
315
+ },
170
316
  };
171
317
  }
172
318
 
@@ -185,6 +331,9 @@ export function createClientToolRelay(
185
331
  text: textOutput,
186
332
  },
187
333
  ],
334
+ _meta: {
335
+ clientId,
336
+ },
188
337
  };
189
338
  } catch (err: any) {
190
339
  return {
@@ -196,6 +345,9 @@ export function createClientToolRelay(
196
345
  text: `Failed to dispatch tool '${toolName}' to client: ${err.message || String(err)}`,
197
346
  },
198
347
  ],
348
+ _meta: {
349
+ clientId,
350
+ },
199
351
  };
200
352
  }
201
353
  }
@@ -203,5 +355,10 @@ export function createClientToolRelay(
203
355
  return {
204
356
  discoverTools,
205
357
  attachToServer,
358
+ getActiveClientId: () =>
359
+ activeClientId || resolveTargetClientId(controlPlane, targetClientId),
360
+ setActiveClientId: (id: string) => {
361
+ activeClientId = id;
362
+ },
206
363
  };
207
364
  }
@@ -2,6 +2,7 @@ import { describe, it, expect, jest } from "@jest/globals";
2
2
  import {
3
3
  registerBuiltInTools,
4
4
  SHADOWCLAW_BUILTIN_TOOLS,
5
+ resolveTargetClientId,
5
6
  } from "./built-in-tools.js";
6
7
  import { createClientToolRelay } from "./client-tool-relay.js";
7
8
  import { McpServer } from "../mcp-server.js";
@@ -585,4 +586,143 @@ describe("Client Tool Relay", () => {
585
586
  expect(resNoClient.result.content[0].text).toContain("No connected");
586
587
  }
587
588
  });
589
+
590
+ describe("resolveTargetClientId and multi-client routing", () => {
591
+ it("resolves target client by exact ID, index, prefix, and label", () => {
592
+ const mockControlPlane: any = {
593
+ getConnectedClients: jest.fn().mockReturnValue([
594
+ {
595
+ clientId: "client-01m1kxw07jtfvh0c4gmbfty2wz",
596
+ deviceLabel: "Linux Desktop",
597
+ },
598
+ {
599
+ clientId: "client-01m1n03c284hrtybdvx33zv7r4",
600
+ deviceLabel: "Pixel Phone",
601
+ },
602
+ ]),
603
+ };
604
+
605
+ // Default (no requestedId) -> first client
606
+ expect(resolveTargetClientId(mockControlPlane)).toBe(
607
+ "client-01m1kxw07jtfvh0c4gmbfty2wz",
608
+ );
609
+
610
+ // Exact ID
611
+ expect(
612
+ resolveTargetClientId(
613
+ mockControlPlane,
614
+ "client-01m1n03c284hrtybdvx33zv7r4",
615
+ ),
616
+ ).toBe("client-01m1n03c284hrtybdvx33zv7r4");
617
+
618
+ // Index "1"
619
+ expect(resolveTargetClientId(mockControlPlane, "1")).toBe(
620
+ "client-01m1n03c284hrtybdvx33zv7r4",
621
+ );
622
+
623
+ // Prefix match
624
+ expect(resolveTargetClientId(mockControlPlane, "01m1n03")).toBe(
625
+ "client-01m1n03c284hrtybdvx33zv7r4",
626
+ );
627
+
628
+ // Device label match
629
+ expect(resolveTargetClientId(mockControlPlane, "Pixel")).toBe(
630
+ "client-01m1n03c284hrtybdvx33zv7r4",
631
+ );
632
+ });
633
+
634
+ it("executes shadowclaw_set_active_client and routes multi-client tools", async () => {
635
+ const server = new McpServer();
636
+ let activeId = "";
637
+ const mockControlPlane: any = {
638
+ getConnectedClients: jest.fn().mockReturnValue([
639
+ { clientId: "c1", deviceLabel: "Client 1" },
640
+ { clientId: "c2", deviceLabel: "Client 2" },
641
+ ]),
642
+ setActiveClientId: jest.fn((id: string) => {
643
+ activeId = id;
644
+ }),
645
+ getActiveClientId: jest.fn(() => activeId),
646
+ sendCommand: jest.fn().mockImplementation(async (cid, action) => {
647
+ if (action === "list-tools") {
648
+ return {
649
+ success: true,
650
+ data: {
651
+ tools: [
652
+ {
653
+ name: "list_files",
654
+ description: "List files",
655
+ inputSchema: { type: "object", properties: {} },
656
+ },
657
+ ],
658
+ },
659
+ };
660
+ }
661
+ if (action === "invoke-tool") {
662
+ return {
663
+ success: true,
664
+ data: { result: `Output from ${cid}` },
665
+ };
666
+ }
667
+ return { success: true };
668
+ }),
669
+ };
670
+
671
+ registerBuiltInTools(server, mockControlPlane);
672
+ const relay = createClientToolRelay(mockControlPlane);
673
+ relay.attachToServer(server);
674
+
675
+ // 1. Discover tools across multi-client: schema should have clientId enum
676
+ const tools = await server.getTools();
677
+ const listFilesTool = tools.find((t) => t.name === "list_files");
678
+ expect(listFilesTool).toBeDefined();
679
+ const props = listFilesTool?.inputSchema.properties as any;
680
+ expect(props?.clientId).toBeDefined();
681
+ expect(props?.clientId.enum).toEqual(["c1", "c2"]);
682
+
683
+ // 2. Run shadowclaw_set_active_client
684
+ const setRes = await server.handleRequest({
685
+ jsonrpc: "2.0",
686
+ id: 20,
687
+ method: "tools/call",
688
+ params: {
689
+ name: "shadowclaw_set_active_client",
690
+ arguments: { clientId: "c2" },
691
+ },
692
+ });
693
+
694
+ expect(mockControlPlane.setActiveClientId).toHaveBeenCalledWith("c2");
695
+ expect(setRes).not.toBeNull();
696
+
697
+ // 3. Invoking tool with explicit clientId c1 routes to c1
698
+ const callRes1 = await server.handleRequest({
699
+ jsonrpc: "2.0",
700
+ id: 21,
701
+ method: "tools/call",
702
+ params: {
703
+ name: "list_files",
704
+ arguments: { clientId: "c1" },
705
+ },
706
+ });
707
+
708
+ if (callRes1 && "result" in callRes1) {
709
+ expect(callRes1.result.content[0].text).toBe("Output from c1");
710
+ }
711
+
712
+ // 4. Invoking tool without clientId routes to active client c2
713
+ const callRes2 = await server.handleRequest({
714
+ jsonrpc: "2.0",
715
+ id: 22,
716
+ method: "tools/call",
717
+ params: {
718
+ name: "list_files",
719
+ arguments: {},
720
+ },
721
+ });
722
+
723
+ if (callRes2 && "result" in callRes2) {
724
+ expect(callRes2.result.content[0].text).toBe("Output from c2");
725
+ }
726
+ });
727
+ });
588
728
  });
@@ -16,6 +16,11 @@ describe("static-files-middleware", () => {
16
16
  fsMock = {
17
17
  stat: jest.fn(),
18
18
  access: jest.fn(),
19
+ existsSync: jest.fn(() => false),
20
+ statSync: jest.fn(() => ({
21
+ isFile: () => false,
22
+ isDirectory: () => false,
23
+ })),
19
24
  constants: { F_OK: 0 },
20
25
  };
21
26
  rewriteMock = jest.fn(() => (_req: any, _res: any, next: any) => next());
@@ -361,4 +366,141 @@ describe("static-files-middleware", () => {
361
366
  );
362
367
  expect(next).not.toHaveBeenCalled();
363
368
  });
369
+
370
+ it("serves .well-known files with dotfiles allow option", async () => {
371
+ await register();
372
+ const middleware = app.use.mock.calls[1][0];
373
+
374
+ const req = {
375
+ originalUrl: "/.well-known/agent-skills/index.json",
376
+ url: "/.well-known/agent-skills/index.json",
377
+ };
378
+ const res = { setHeader: jest.fn(), sendFile: jest.fn() };
379
+ const next = jest.fn();
380
+
381
+ fsMock.stat.mockImplementation((_path: string, cb: any) =>
382
+ cb(null, { isDirectory: () => false }),
383
+ );
384
+
385
+ middleware(req, res, next);
386
+
387
+ expect(res.sendFile).toHaveBeenCalledWith(
388
+ expect.stringContaining("/root/.well-known/agent-skills/index.json"),
389
+ { dotfiles: "allow" },
390
+ );
391
+ expect(next).not.toHaveBeenCalled();
392
+ });
393
+
394
+ it("serves .agents files with dotfiles allow option", async () => {
395
+ await register();
396
+ const middleware = app.use.mock.calls[1][0];
397
+
398
+ const req = {
399
+ originalUrl: "/.agents/skills/main/toast/SKILL.md",
400
+ url: "/.agents/skills/main/toast/SKILL.md",
401
+ };
402
+ const res = { setHeader: jest.fn(), sendFile: jest.fn() };
403
+ const next = jest.fn();
404
+
405
+ fsMock.stat.mockImplementation((_path: string, cb: any) =>
406
+ cb(null, { isDirectory: () => false }),
407
+ );
408
+
409
+ middleware(req, res, next);
410
+
411
+ expect(res.sendFile).toHaveBeenCalledWith(
412
+ expect.stringContaining("/root/.agents/skills/main/toast/SKILL.md"),
413
+ { dotfiles: "allow" },
414
+ );
415
+ expect(next).not.toHaveBeenCalled();
416
+ });
417
+
418
+ it("serves index.html in allowed dot directories with dotfiles allow option", async () => {
419
+ await register();
420
+ const middleware = app.use.mock.calls[1][0];
421
+
422
+ const req = {
423
+ originalUrl: "/.well-known/agent-skills/",
424
+ url: "/.well-known/agent-skills/",
425
+ };
426
+ const res = { setHeader: jest.fn(), sendFile: jest.fn() };
427
+ const next = jest.fn();
428
+
429
+ fsMock.stat.mockImplementation((_path: string, cb: any) =>
430
+ cb(null, { isDirectory: () => true }),
431
+ );
432
+ fsMock.access.mockImplementation((_path: string, _mode: any, cb: any) =>
433
+ cb(null),
434
+ );
435
+
436
+ middleware(req, res, next);
437
+
438
+ expect(res.sendFile).toHaveBeenCalledWith(
439
+ expect.stringContaining("/root/.well-known/agent-skills/index.html"),
440
+ { dotfiles: "allow" },
441
+ );
442
+ expect(next).not.toHaveBeenCalled();
443
+ });
444
+
445
+ it("serves allowed dotfiles with subpath prefix and dotfiles allow option", async () => {
446
+ await register();
447
+ const middleware = app.use.mock.calls[1][0];
448
+
449
+ const req = {
450
+ originalUrl: "/shadow-claw/.well-known/agent-skills/index.json",
451
+ url: "/shadow-claw/.well-known/agent-skills/index.json",
452
+ };
453
+ const res = { setHeader: jest.fn(), sendFile: jest.fn() };
454
+ const next = jest.fn();
455
+
456
+ // The direct stat under /root/shadow-claw/... will fail (ENOENT), but stripped subpath stat succeeds
457
+ fsMock.stat.mockImplementation((targetPath: string, cb: any) => {
458
+ if (targetPath.includes("/root/shadow-claw/")) {
459
+ cb(new Error("ENOENT"));
460
+ } else {
461
+ cb(null, { isFile: () => true, isDirectory: () => false });
462
+ }
463
+ });
464
+
465
+ fsMock.existsSync.mockImplementation((p: string) =>
466
+ p.includes(".well-known"),
467
+ );
468
+ fsMock.statSync.mockImplementation(() => ({ isFile: () => true }));
469
+
470
+ middleware(req, res, next);
471
+
472
+ expect(res.sendFile).toHaveBeenCalledWith(
473
+ expect.stringContaining("/root/.well-known/agent-skills/index.json"),
474
+ { dotfiles: "allow" },
475
+ );
476
+ expect(next).not.toHaveBeenCalled();
477
+ });
478
+
479
+ it("blocks disallowed dotfiles from being served", async () => {
480
+ await register();
481
+ const middleware = app.use.mock.calls[1][0];
482
+
483
+ const disallowedPaths = [
484
+ "/.git/config",
485
+ "/.env",
486
+ "/.cache/db.sqlite",
487
+ "/.well-known/.env",
488
+ "/.agents/.git",
489
+ "/pages/.secret.txt",
490
+ ];
491
+
492
+ for (const disallowedPath of disallowedPaths) {
493
+ const req = {
494
+ originalUrl: disallowedPath,
495
+ url: disallowedPath,
496
+ };
497
+ const res = { setHeader: jest.fn(), sendFile: jest.fn() };
498
+ const next = jest.fn();
499
+
500
+ middleware(req, res, next);
501
+
502
+ expect(res.sendFile).not.toHaveBeenCalled();
503
+ expect(next).toHaveBeenCalled();
504
+ }
505
+ });
364
506
  });