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
@@ -71,6 +71,22 @@ export const CLI_BUILTIN_TOOLS = [
71
71
  },
72
72
  },
73
73
  },
74
+ {
75
+ name: "shadowclaw_set_active_client",
76
+ description:
77
+ "Set the active default connected client used when no clientId is explicitly provided in tool calls.",
78
+ inputSchema: {
79
+ type: "object",
80
+ properties: {
81
+ clientId: {
82
+ type: "string",
83
+ description:
84
+ "Target client ID (or index '0', '1', prefix, or device label) to make active.",
85
+ },
86
+ },
87
+ required: ["clientId"],
88
+ },
89
+ },
74
90
  {
75
91
  name: "shadowclaw_manage_backup",
76
92
  description:
@@ -95,6 +111,7 @@ export function createCliMcpEngine(options = {}) {
95
111
  const client = options.client || new CliControlClient(options);
96
112
  const relayClientTools = options.relayClientTools !== false;
97
113
  const targetClientId = options.targetClientId || options.clientTarget;
114
+ let activeClientId = targetClientId || "";
98
115
 
99
116
  const serverInfo = {
100
117
  name: "shadow-claw",
@@ -107,47 +124,138 @@ export function createCliMcpEngine(options = {}) {
107
124
  };
108
125
 
109
126
  async function resolveTargetId(requestedId) {
110
- if (requestedId && typeof requestedId === "string" && requestedId.trim()) {
111
- return requestedId.trim();
112
- }
113
- if (targetClientId) {
114
- return targetClientId;
115
- }
127
+ const candidate = requestedId || activeClientId || targetClientId;
116
128
  try {
117
129
  const clients = await client.listClients();
118
130
  if (Array.isArray(clients) && clients.length > 0) {
131
+ if (candidate && typeof candidate === "string" && candidate.trim()) {
132
+ const trimmed = candidate.trim();
133
+ const exact = clients.find((c) => (c.clientId || c.id) === trimmed);
134
+ if (exact) return exact.clientId || exact.id;
135
+
136
+ const idx = parseInt(trimmed, 10);
137
+ if (!isNaN(idx) && idx >= 0 && idx < clients.length) {
138
+ return clients[idx].clientId || clients[idx].id;
139
+ }
140
+
141
+ const prefix = clients.find(
142
+ (c) =>
143
+ (c.clientId || c.id || "").startsWith(trimmed) ||
144
+ (c.clientId || "").replace(/^client-/, "").startsWith(trimmed),
145
+ );
146
+ if (prefix) return prefix.clientId || prefix.id;
147
+
148
+ const byLabel = clients.find((c) =>
149
+ c.deviceLabel?.toLowerCase().includes(trimmed.toLowerCase()),
150
+ );
151
+ if (byLabel) return byLabel.clientId || byLabel.id;
152
+
153
+ return trimmed;
154
+ }
155
+
119
156
  return clients[0].clientId || clients[0].id || "";
120
157
  }
121
158
  } catch (_) {}
122
- return "";
159
+ return typeof candidate === "string" ? candidate.trim() : "";
123
160
  }
124
161
 
162
+ const toolSupportingClientsMap = new Map();
163
+ const toolDefMap = new Map();
164
+
125
165
  async function getRelayedTools() {
126
166
  if (!relayClientTools) {
127
167
  return [];
128
168
  }
129
- const clientId = await resolveTargetId();
130
- if (!clientId) {
131
- return [];
132
- }
169
+ let clients = [];
133
170
  try {
134
- const res = await client.sendCommand(clientId, "list-tools", {});
135
- if (
136
- res &&
137
- res.success &&
138
- res.data?.tools &&
139
- Array.isArray(res.data.tools)
140
- ) {
141
- return res.data.tools.map((t) => ({
142
- name: t.name,
143
- description:
144
- t.description || `Relayed tool '${t.name}' from connected client.`,
145
- inputSchema: t.inputSchema || { type: "object", properties: {} },
146
- annotations: t.annotations,
147
- }));
148
- }
171
+ clients = await client.listClients();
149
172
  } catch (_) {}
150
- return [];
173
+
174
+ if (!Array.isArray(clients) || clients.length === 0) {
175
+ toolSupportingClientsMap.clear();
176
+ toolDefMap.clear();
177
+ return [];
178
+ }
179
+
180
+ toolSupportingClientsMap.clear();
181
+ toolDefMap.clear();
182
+
183
+ for (const c of clients) {
184
+ const cid = c.clientId || c.id;
185
+ if (!cid) continue;
186
+
187
+ try {
188
+ const res = await client.sendCommand(cid, "list-tools", {});
189
+ if (
190
+ res &&
191
+ res.success &&
192
+ res.data?.tools &&
193
+ Array.isArray(res.data.tools)
194
+ ) {
195
+ for (const t of res.data.tools) {
196
+ if (!toolDefMap.has(t.name)) {
197
+ toolDefMap.set(t.name, t);
198
+ toolSupportingClientsMap.set(t.name, []);
199
+ }
200
+ toolSupportingClientsMap.get(t.name).push(c);
201
+ }
202
+ }
203
+ } catch (_) {}
204
+ }
205
+
206
+ const activeTargetId = await resolveTargetId();
207
+ const relayedTools = [];
208
+
209
+ for (const [toolName, t] of toolDefMap.entries()) {
210
+ const supportingClients = toolSupportingClientsMap.get(toolName) || [];
211
+ const supportingClientIds = supportingClients
212
+ .map((cl) => cl.clientId || cl.id)
213
+ .filter(Boolean);
214
+
215
+ const preferredClient =
216
+ supportingClients.find(
217
+ (cl) => (cl.clientId || cl.id) === activeTargetId,
218
+ ) || supportingClients[0];
219
+ const toolDefaultId =
220
+ preferredClient?.clientId || preferredClient?.id || "";
221
+ const toolDefaultLabel = preferredClient?.deviceLabel || "Client";
222
+
223
+ const existingSchema =
224
+ t.inputSchema && typeof t.inputSchema === "object"
225
+ ? t.inputSchema
226
+ : { type: "object", properties: {} };
227
+
228
+ const properties = {
229
+ ...(existingSchema.properties || {}),
230
+ clientId: {
231
+ type: "string",
232
+ description: `Target ShadowClaw client ID (optional; defaults to ${toolDefaultId} [${toolDefaultLabel}]). Available on: ${supportingClients.map((cl) => `${cl.clientId || cl.id} (${cl.deviceLabel || "Client"})`).join(", ")}`,
233
+ enum: supportingClientIds,
234
+ },
235
+ };
236
+
237
+ const clientNote =
238
+ clients.length > 1
239
+ ? supportingClients.length > 1
240
+ ? ` [Default: ${toolDefaultLabel} (${toolDefaultId.slice(0, 14)}...)]`
241
+ : ` [Client: ${toolDefaultLabel} (${toolDefaultId.slice(0, 14)}...)]`
242
+ : "";
243
+
244
+ relayedTools.push({
245
+ name: t.name,
246
+ description:
247
+ (t.description || `Relayed tool '${t.name}' from connected client.`) +
248
+ clientNote,
249
+ inputSchema: {
250
+ ...existingSchema,
251
+ type: "object",
252
+ properties,
253
+ },
254
+ annotations: t.annotations,
255
+ });
256
+ }
257
+
258
+ return relayedTools;
151
259
  }
152
260
 
153
261
  async function handleMessage(request, headers = {}) {
@@ -260,6 +368,48 @@ export function createCliMcpEngine(options = {}) {
260
368
  };
261
369
  }
262
370
 
371
+ if (toolName === "shadowclaw_set_active_client") {
372
+ const targetId = await resolveTargetId(args.clientId);
373
+ if (!targetId) {
374
+ return {
375
+ jsonrpc: "2.0",
376
+ id: reqId,
377
+ result: {
378
+ resultType: "complete",
379
+ isError: true,
380
+ content: [
381
+ {
382
+ type: "text",
383
+ text: `Error: Client '${args.clientId}' not found among connected clients.`,
384
+ },
385
+ ],
386
+ _meta: { "io.modelcontextprotocol/serverInfo": serverInfo },
387
+ },
388
+ };
389
+ }
390
+ activeClientId = targetId;
391
+ console.error(
392
+ `[ShadowClaw MCP] Active default client set to: ${activeClientId}`,
393
+ );
394
+ return {
395
+ jsonrpc: "2.0",
396
+ id: reqId,
397
+ result: {
398
+ resultType: "complete",
399
+ content: [
400
+ {
401
+ type: "text",
402
+ text: `Active default client set to: ${activeClientId}`,
403
+ },
404
+ ],
405
+ _meta: {
406
+ activeClientId,
407
+ "io.modelcontextprotocol/serverInfo": serverInfo,
408
+ },
409
+ },
410
+ };
411
+ }
412
+
263
413
  if (toolName === "shadowclaw_send_message") {
264
414
  const targetId = await resolveTargetId(args.clientId);
265
415
  if (!targetId) {
@@ -391,26 +541,12 @@ export function createCliMcpEngine(options = {}) {
391
541
  };
392
542
  }
393
543
 
394
- // Relayed interactive ask_user tool (MRTR)
395
- if (toolName === "ask_user") {
396
- if (!inputResponses || !inputResponses["response"]) {
397
- return {
398
- jsonrpc: "2.0",
399
- id: reqId,
400
- result: {
401
- resultType: "input_required",
402
- inputRequests: [
403
- {
404
- id: "response",
405
- type: "prompt",
406
- message:
407
- args.question || args.prompt || "User input requested",
408
- },
409
- ],
410
- _meta: { "io.modelcontextprotocol/serverInfo": serverInfo },
411
- },
412
- };
413
- }
544
+ // Relayed interactive ask_user tool with MRTR response fulfillment if provided
545
+ if (
546
+ toolName === "ask_user" &&
547
+ inputResponses &&
548
+ inputResponses["response"]
549
+ ) {
414
550
  return {
415
551
  jsonrpc: "2.0",
416
552
  id: reqId,
@@ -425,7 +561,29 @@ export function createCliMcpEngine(options = {}) {
425
561
  }
426
562
 
427
563
  // Relayed client tools (invoke-tool)
428
- const targetId = await resolveTargetId(args.clientId);
564
+ if (toolDefMap.size === 0) {
565
+ await getRelayedTools();
566
+ }
567
+
568
+ const supportingClients = toolSupportingClientsMap.get(toolName) || [];
569
+ const supportingClientIds = supportingClients
570
+ .map((cl) => cl.clientId || cl.id)
571
+ .filter(Boolean);
572
+
573
+ let targetId = "";
574
+ if (args.clientId) {
575
+ targetId = await resolveTargetId(args.clientId);
576
+ } else {
577
+ const activeSessionId = await resolveTargetId();
578
+ if (supportingClientIds.includes(activeSessionId)) {
579
+ targetId = activeSessionId;
580
+ } else if (supportingClientIds.length > 0) {
581
+ targetId = supportingClientIds[0];
582
+ } else {
583
+ targetId = activeSessionId;
584
+ }
585
+ }
586
+
429
587
  if (!targetId) {
430
588
  return {
431
589
  jsonrpc: "2.0",
@@ -441,11 +599,51 @@ export function createCliMcpEngine(options = {}) {
441
599
  };
442
600
  }
443
601
 
602
+ if (
603
+ supportingClientIds.length > 0 &&
604
+ !supportingClientIds.includes(targetId)
605
+ ) {
606
+ return {
607
+ jsonrpc: "2.0",
608
+ id: reqId,
609
+ result: {
610
+ resultType: "complete",
611
+ isError: true,
612
+ content: [
613
+ {
614
+ type: "text",
615
+ text: `Error: Tool '${toolName}' is not enabled or available on client '${targetId}'. (Available on: ${supportingClientIds.join(", ")})`,
616
+ },
617
+ ],
618
+ _meta: {
619
+ clientId: targetId,
620
+ "io.modelcontextprotocol/serverInfo": serverInfo,
621
+ },
622
+ },
623
+ };
624
+ }
625
+
444
626
  try {
445
- const res = await client.sendCommand(targetId, "invoke-tool", {
446
- toolName,
447
- input: args,
448
- });
627
+ console.error(
628
+ `[ShadowClaw MCP] Executing '${toolName}' on client: ${targetId}`,
629
+ );
630
+ const toolArgs = { ...args };
631
+ delete toolArgs.clientId;
632
+ const isInteractive = toolName === "ask_user";
633
+ const res = isInteractive
634
+ ? await client.sendCommand(
635
+ targetId,
636
+ "invoke-tool",
637
+ {
638
+ toolName,
639
+ input: toolArgs,
640
+ },
641
+ 300000,
642
+ )
643
+ : await client.sendCommand(targetId, "invoke-tool", {
644
+ toolName,
645
+ input: toolArgs,
646
+ });
449
647
 
450
648
  const rawResult =
451
649
  res.data?.result !== undefined ? res.data.result : res.data;
@@ -454,14 +652,31 @@ export function createCliMcpEngine(options = {}) {
454
652
  ? rawResult
455
653
  : JSON.stringify(rawResult, null, 2);
456
654
 
655
+ const errorMessage =
656
+ res.error ||
657
+ (res.data && res.data.error) ||
658
+ "Unknown tool execution error";
659
+
457
660
  return {
458
661
  jsonrpc: "2.0",
459
662
  id: reqId,
460
663
  result: {
461
664
  resultType: "complete",
462
665
  isError: !res.success,
463
- content: [{ type: "text", text: textOutput || "" }],
464
- _meta: { "io.modelcontextprotocol/serverInfo": serverInfo },
666
+ content: [
667
+ {
668
+ type: "text",
669
+ text: res.success
670
+ ? textOutput || ""
671
+ : textOutput && textOutput !== "null" && textOutput !== "{}"
672
+ ? `${textOutput}\n${errorMessage}`
673
+ : `Tool execution error: ${errorMessage}`,
674
+ },
675
+ ],
676
+ _meta: {
677
+ clientId: targetId,
678
+ "io.modelcontextprotocol/serverInfo": serverInfo,
679
+ },
465
680
  },
466
681
  };
467
682
  } catch (err) {
@@ -472,7 +687,10 @@ export function createCliMcpEngine(options = {}) {
472
687
  resultType: "complete",
473
688
  isError: true,
474
689
  content: [{ type: "text", text: `Tool error: ${err.message}` }],
475
- _meta: { "io.modelcontextprotocol/serverInfo": serverInfo },
690
+ _meta: {
691
+ clientId: targetId,
692
+ "io.modelcontextprotocol/serverInfo": serverInfo,
693
+ },
476
694
  },
477
695
  };
478
696
  }
@@ -511,6 +729,10 @@ export function createCliMcpEngine(options = {}) {
511
729
 
512
730
  return {
513
731
  handleMessage,
732
+ getActiveClientId: () => activeClientId,
733
+ setActiveClientId: (id) => {
734
+ activeClientId = id;
735
+ },
514
736
  };
515
737
  }
516
738
 
@@ -1,7 +1,24 @@
1
- import { describe, it, expect, jest } from "@jest/globals";
1
+ import {
2
+ describe,
3
+ it,
4
+ expect,
5
+ jest,
6
+ beforeEach,
7
+ afterEach,
8
+ } from "@jest/globals";
2
9
  import { createCliMcpEngine } from "./mcp.mjs";
3
10
 
4
11
  describe("CLI MCP Engine (createCliMcpEngine)", () => {
12
+ let errorSpy;
13
+
14
+ beforeEach(() => {
15
+ errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
16
+ });
17
+
18
+ afterEach(() => {
19
+ errorSpy?.mockRestore();
20
+ });
21
+
5
22
  it("processes server/discover request", async () => {
6
23
  const mockClient = {
7
24
  listClients: jest
@@ -97,4 +114,261 @@ describe("CLI MCP Engine (createCliMcpEngine)", () => {
97
114
  groupId: undefined,
98
115
  });
99
116
  });
117
+
118
+ it("processes tools/call for relayed client tool (e.g. list_files)", async () => {
119
+ const mockClient = {
120
+ listClients: jest
121
+ .fn()
122
+ .mockResolvedValue([{ clientId: "c1", deviceLabel: "Browser Test" }]),
123
+ sendCommand: jest
124
+ .fn()
125
+ .mockImplementation(async (clientId, action, payload) => {
126
+ if (action === "invoke-tool" && payload.toolName === "list_files") {
127
+ return {
128
+ success: true,
129
+ data: {
130
+ result: ".agents/ MEMORY.md index.html -/",
131
+ },
132
+ };
133
+ }
134
+ return { success: false, error: "Not handled" };
135
+ }),
136
+ };
137
+
138
+ const engine = createCliMcpEngine({ client: mockClient });
139
+ const res = await engine.handleMessage({
140
+ jsonrpc: "2.0",
141
+ id: 4,
142
+ method: "tools/call",
143
+ params: {
144
+ name: "list_files",
145
+ arguments: {},
146
+ },
147
+ });
148
+
149
+ expect(res).toBeDefined();
150
+ expect(res.id).toBe(4);
151
+ expect(res.result.resultType).toBe("complete");
152
+ expect(res.result.isError).toBe(false);
153
+ expect(res.result.content[0].text).toBe(".agents/ MEMORY.md index.html -/");
154
+ expect(mockClient.sendCommand).toHaveBeenCalledWith("c1", "invoke-tool", {
155
+ toolName: "list_files",
156
+ input: {},
157
+ });
158
+ });
159
+
160
+ it("handles tools/call relayed client tool failure with informative error content", async () => {
161
+ const mockClient = {
162
+ listClients: jest
163
+ .fn()
164
+ .mockResolvedValue([{ clientId: "c1", deviceLabel: "Browser Test" }]),
165
+ sendCommand: jest.fn().mockImplementation(async () => {
166
+ return {
167
+ success: false,
168
+ error: "Permission denied reading directory",
169
+ };
170
+ }),
171
+ };
172
+
173
+ const engine = createCliMcpEngine({ client: mockClient });
174
+ const res = await engine.handleMessage({
175
+ jsonrpc: "2.0",
176
+ id: 5,
177
+ method: "tools/call",
178
+ params: {
179
+ name: "list_files",
180
+ arguments: { path: "restricted" },
181
+ },
182
+ });
183
+
184
+ expect(res).toBeDefined();
185
+ expect(res.id).toBe(5);
186
+ expect(res.result.resultType).toBe("complete");
187
+ expect(res.result.isError).toBe(true);
188
+ expect(res.result.content[0].text).toContain(
189
+ "Permission denied reading directory",
190
+ );
191
+ });
192
+
193
+ it("handles multi-client discovery and explicit clientId targeting", async () => {
194
+ const mockClient = {
195
+ listClients: jest.fn().mockResolvedValue([
196
+ { clientId: "client-alpha", deviceLabel: "Linux Desktop" },
197
+ { clientId: "client-beta", deviceLabel: "Android Phone" },
198
+ ]),
199
+ sendCommand: jest
200
+ .fn()
201
+ .mockImplementation(async (clientId, action, payload) => {
202
+ if (action === "list-tools") {
203
+ const tools = [
204
+ {
205
+ name: "list_files",
206
+ description: "List workspace files.",
207
+ inputSchema: {
208
+ type: "object",
209
+ properties: { path: { type: "string" } },
210
+ },
211
+ },
212
+ ];
213
+ if (clientId === "client-alpha") {
214
+ tools.push({
215
+ name: "ask_user",
216
+ description: "Ask user for confirmation.",
217
+ inputSchema: {
218
+ type: "object",
219
+ properties: { question: { type: "string" } },
220
+ },
221
+ });
222
+ }
223
+ return {
224
+ success: true,
225
+ data: { tools },
226
+ };
227
+ }
228
+ if (action === "invoke-tool") {
229
+ return {
230
+ success: true,
231
+ data: {
232
+ result: `Files from ${clientId}`,
233
+ },
234
+ };
235
+ }
236
+ return { success: false };
237
+ }),
238
+ };
239
+
240
+ const engine = createCliMcpEngine({ client: mockClient });
241
+
242
+ // 1. tools/list should augment schema with clientId enum and default client info
243
+ const listRes = await engine.handleMessage({
244
+ jsonrpc: "2.0",
245
+ id: 10,
246
+ method: "tools/list",
247
+ });
248
+
249
+ const listFilesTool = listRes.result.tools.find(
250
+ (t) => t.name === "list_files",
251
+ );
252
+ expect(listFilesTool).toBeDefined();
253
+ expect(listFilesTool.description).toContain("[Default: Linux Desktop");
254
+ expect(listFilesTool.inputSchema.properties.clientId).toBeDefined();
255
+ expect(listFilesTool.inputSchema.properties.clientId.enum).toEqual([
256
+ "client-alpha",
257
+ "client-beta",
258
+ ]);
259
+
260
+ // ask_user should ONLY be available on client-alpha
261
+ const askUserTool = listRes.result.tools.find((t) => t.name === "ask_user");
262
+ expect(askUserTool).toBeDefined();
263
+ expect(askUserTool.description).toContain("[Client: Linux Desktop");
264
+ expect(askUserTool.inputSchema.properties.clientId.enum).toEqual([
265
+ "client-alpha",
266
+ ]);
267
+
268
+ // 2. tools/call targeting client-beta explicitly
269
+ const callResBeta = await engine.handleMessage({
270
+ jsonrpc: "2.0",
271
+ id: 11,
272
+ method: "tools/call",
273
+ params: {
274
+ name: "list_files",
275
+ arguments: { clientId: "client-beta" },
276
+ },
277
+ });
278
+
279
+ expect(mockClient.sendCommand).toHaveBeenCalledWith(
280
+ "client-beta",
281
+ "invoke-tool",
282
+ {
283
+ toolName: "list_files",
284
+ input: {},
285
+ },
286
+ );
287
+ expect(callResBeta.result.content[0].text).toBe("Files from client-beta");
288
+
289
+ // 3. Switch active default client to client-beta
290
+ const setActiveRes = await engine.handleMessage({
291
+ jsonrpc: "2.0",
292
+ id: 12,
293
+ method: "tools/call",
294
+ params: {
295
+ name: "shadowclaw_set_active_client",
296
+ arguments: { clientId: "client-beta" },
297
+ },
298
+ });
299
+
300
+ expect(setActiveRes.result.content[0].text).toContain(
301
+ "Active default client set to: client-beta",
302
+ );
303
+
304
+ // 4. tools/call without clientId now defaults to client-beta for list_files
305
+ const callResDefault = await engine.handleMessage({
306
+ jsonrpc: "2.0",
307
+ id: 13,
308
+ method: "tools/call",
309
+ params: {
310
+ name: "list_files",
311
+ arguments: {},
312
+ },
313
+ });
314
+
315
+ expect(callResDefault.result.content[0].text).toBe(
316
+ "Files from client-beta",
317
+ );
318
+
319
+ // 5. Attempting to call ask_user on client-beta should be REJECTED because client-beta does not support it
320
+ const unsupportedRes = await engine.handleMessage({
321
+ jsonrpc: "2.0",
322
+ id: 14,
323
+ method: "tools/call",
324
+ params: {
325
+ name: "ask_user",
326
+ arguments: { clientId: "client-beta", question: "Prompt client beta?" },
327
+ },
328
+ });
329
+
330
+ expect(unsupportedRes.result.isError).toBe(true);
331
+ expect(unsupportedRes.result.content[0].text).toContain(
332
+ "Tool 'ask_user' is not enabled or available on client 'client-beta'",
333
+ );
334
+ expect(unsupportedRes.result.content[0].text).toContain(
335
+ "Available on: client-alpha",
336
+ );
337
+
338
+ // 6. Calling ask_user without clientId should automatically default to client-alpha
339
+ // (the only supporting client), even though active default is client-beta!
340
+ const askUserRes = await engine.handleMessage({
341
+ jsonrpc: "2.0",
342
+ id: 15,
343
+ method: "tools/call",
344
+ params: {
345
+ name: "ask_user",
346
+ arguments: { question: "Are you ready?" },
347
+ },
348
+ });
349
+
350
+ expect(mockClient.sendCommand).toHaveBeenCalledWith(
351
+ "client-alpha",
352
+ "invoke-tool",
353
+ {
354
+ toolName: "ask_user",
355
+ input: { question: "Are you ready?" },
356
+ },
357
+ 300000,
358
+ );
359
+ expect(askUserRes.result.content[0].text).toBe("Files from client-alpha");
360
+
361
+ // 7. tools/call for ask_user with inputResponses returns fulfilled response
362
+ const fulfilledRes = await engine.handleMessage({
363
+ jsonrpc: "2.0",
364
+ id: 16,
365
+ method: "tools/call",
366
+ params: {
367
+ name: "ask_user",
368
+ arguments: { question: "Are you ready?" },
369
+ inputResponses: { response: "Yes, ready to go!" },
370
+ },
371
+ });
372
+ expect(fulfilledRes.result.content[0].text).toBe("Yes, ready to go!");
373
+ });
100
374
  });