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
@@ -7,6 +7,7 @@ import {
7
7
  DEFAULT_IFRAME_SANDBOX_POLICY,
8
8
  getAllowedCustomElementHostPatterns,
9
9
  getAllowedCustomElements,
10
+ getApprovedCustomElementScriptDescriptors,
10
11
  getApprovedCustomElementScripts,
11
12
  getCustomElementPurifyConfig,
12
13
  getIframeCsp,
@@ -398,6 +399,12 @@ describe("custom-element-security", () => {
398
399
  expect(getApprovedCustomElementScripts()).toEqual([
399
400
  "https://kherrick.github.io/block-garden/block-garden-bundle-min.mjs",
400
401
  ]);
402
+ expect(getApprovedCustomElementScriptDescriptors()).toEqual([
403
+ {
404
+ src: "https://kherrick.github.io/block-garden/block-garden-bundle-min.mjs",
405
+ hasInit: false,
406
+ },
407
+ ]);
401
408
 
402
409
  const csp = getIframeCsp("test-nonce");
403
410
  expect(csp).toContain("script-src 'nonce-test-nonce'");
@@ -409,5 +416,49 @@ describe("custom-element-security", () => {
409
416
  setIframeSandboxPolicy(DEFAULT_IFRAME_SANDBOX_POLICY);
410
417
  }
411
418
  });
419
+
420
+ it("parses object script descriptors with src and hasInit", () => {
421
+ const siteConfigScript = document.createElement("script");
422
+ siteConfigScript.id = "shadow-claw-site-config";
423
+ siteConfigScript.type = "application/json";
424
+ siteConfigScript.textContent = JSON.stringify({
425
+ customElements: {
426
+ allowedDomains: ["kherrick.github.io"],
427
+ scripts: [
428
+ { src: ".agents/scripts/main/pwgen-adapter.js", hasInit: true },
429
+ { src: "https://kherrick.github.io/x-pwgen.js", hasInit: false },
430
+ "local-element.js",
431
+ ],
432
+ },
433
+ });
434
+ document.head.appendChild(siteConfigScript);
435
+
436
+ try {
437
+ installCustomElementsRegistryGuard();
438
+
439
+ expect(getApprovedCustomElementScripts()).toEqual([
440
+ ".agents/scripts/main/pwgen-adapter.js",
441
+ "https://kherrick.github.io/x-pwgen.js",
442
+ "local-element.js",
443
+ ]);
444
+ expect(getApprovedCustomElementScriptDescriptors()).toEqual([
445
+ {
446
+ src: ".agents/scripts/main/pwgen-adapter.js",
447
+ hasInit: true,
448
+ },
449
+ {
450
+ src: "https://kherrick.github.io/x-pwgen.js",
451
+ hasInit: false,
452
+ },
453
+ {
454
+ src: "local-element.js",
455
+ hasInit: false,
456
+ },
457
+ ]);
458
+ } finally {
459
+ uninstallCustomElementsRegistryGuard();
460
+ siteConfigScript.remove();
461
+ }
462
+ });
412
463
  });
413
464
  });
@@ -150,10 +150,10 @@ export function isSafeCustomElementSource(urlStr: string): boolean {
150
150
 
151
151
  try {
152
152
  const base =
153
- typeof window !== "undefined" &&
154
- window.location?.origin &&
155
- window.location.origin !== "null"
156
- ? window.location.origin
153
+ typeof globalThis !== "undefined" &&
154
+ globalThis.location?.origin &&
155
+ globalThis.location.origin !== "null"
156
+ ? globalThis.location.origin
157
157
  : "http://localhost";
158
158
  const url = new URL(trimmed, base);
159
159
 
@@ -163,10 +163,10 @@ export function isSafeCustomElementSource(urlStr: string): boolean {
163
163
 
164
164
  // Same-origin is trusted
165
165
  if (
166
- typeof window !== "undefined" &&
167
- window.location?.origin &&
168
- window.location.origin !== "null" &&
169
- url.origin === window.location.origin
166
+ typeof globalThis !== "undefined" &&
167
+ globalThis.location?.origin &&
168
+ globalThis.location.origin !== "null" &&
169
+ url.origin === globalThis.location.origin
170
170
  ) {
171
171
  return true;
172
172
  }
@@ -240,8 +240,8 @@ export function getIframeCsp(nonce: string): string {
240
240
  try {
241
241
  const parsed = new URL(
242
242
  scriptUrl,
243
- typeof window !== "undefined" && window.location?.origin
244
- ? window.location.origin
243
+ typeof globalThis !== "undefined" && globalThis.location?.origin
244
+ ? globalThis.location.origin
245
245
  : "http://localhost",
246
246
  );
247
247
  allowedScriptHosts.add(parsed.origin);
@@ -372,10 +372,15 @@ export function initCustomElementSecurityFromEmbeddedConfig(): void {
372
372
  }
373
373
  }
374
374
 
375
+ export interface CustomElementScriptDescriptor {
376
+ src: string;
377
+ hasInit: boolean;
378
+ }
379
+
375
380
  /**
376
- * Retrieve approved custom element script URLs from the embedded site config.
381
+ * Retrieve approved custom element script descriptors from the embedded site config.
377
382
  */
378
- export function getApprovedCustomElementScripts(): string[] {
383
+ export function getApprovedCustomElementScriptDescriptors(): CustomElementScriptDescriptor[] {
379
384
  if (typeof document === "undefined") {
380
385
  return [];
381
386
  }
@@ -395,10 +400,16 @@ export function getApprovedCustomElementScripts(): string[] {
395
400
 
396
401
  if (Array.isArray(scripts)) {
397
402
  return scripts
398
- .map((s: any) => (typeof s === "string" ? s : s?.src))
403
+ .map((s: any) => {
404
+ const src = typeof s === "string" ? s : s?.src;
405
+ const hasInit =
406
+ typeof s === "object" && s !== null ? Boolean(s.hasInit) : false;
407
+ return { src, hasInit };
408
+ })
399
409
  .filter(
400
- (s): s is string =>
401
- typeof s === "string" && isSafeCustomElementSource(s),
410
+ (entry): entry is CustomElementScriptDescriptor =>
411
+ typeof entry.src === "string" &&
412
+ isSafeCustomElementSource(entry.src),
402
413
  );
403
414
  }
404
415
  } catch {}
@@ -406,6 +417,13 @@ export function getApprovedCustomElementScripts(): string[] {
406
417
  return [];
407
418
  }
408
419
 
420
+ /**
421
+ * Retrieve approved custom element script URLs from the embedded site config.
422
+ */
423
+ export function getApprovedCustomElementScripts(): string[] {
424
+ return getApprovedCustomElementScriptDescriptors().map((d) => d.src);
425
+ }
426
+
409
427
  /**
410
428
  * Load allowed custom elements and host patterns from IndexedDB.
411
429
  */
@@ -117,6 +117,7 @@ export interface ClientInfo {
117
117
 
118
118
  export type ClientToServerType =
119
119
  | "client:register"
120
+ | "client:unregister"
120
121
  | "client:heartbeat"
121
122
  | "command:result"
122
123
  | "backup:ready";
@@ -224,6 +224,75 @@ describe("control-plane", () => {
224
224
 
225
225
  ws.close();
226
226
  });
227
+
228
+ it("removes client when WebSocket closes or sends client:unregister", async () => {
229
+ const ws = new WebSocket(
230
+ `ws://127.0.0.1:${port}/ws/control?token=${token}`,
231
+ );
232
+ await new Promise<void>((resolve) => ws.on("open", () => resolve()));
233
+
234
+ ws.send(
235
+ JSON.stringify({
236
+ id: "reg-unreg",
237
+ type: "client:register",
238
+ payload: {
239
+ clientId: "test-client-unreg",
240
+ deviceLabel: "Test Device",
241
+ capabilities: ["opfs"],
242
+ version: "1.0.0",
243
+ },
244
+ }),
245
+ );
246
+
247
+ // Wait for registration
248
+ await new Promise((resolve) => ws.once("message", resolve));
249
+
250
+ // 1. Should be listed as connected
251
+ let clients = controlPlane.getConnectedClients();
252
+ expect(clients.some((c) => c.clientId === "test-client-unreg")).toBe(
253
+ true,
254
+ );
255
+
256
+ const resConnected = await makeHttpRequest({
257
+ port,
258
+ path: "/api/control/clients",
259
+ headers: { "x-control-token": token },
260
+ });
261
+ expect(
262
+ resConnected.data.clients.some(
263
+ (c: any) => c.clientId === "test-client-unreg",
264
+ ),
265
+ ).toBe(true);
266
+
267
+ // 2. Send client:unregister
268
+ ws.send(
269
+ JSON.stringify({
270
+ id: "unreg-msg",
271
+ type: "client:unregister",
272
+ payload: { clientId: "test-client-unreg" },
273
+ }),
274
+ );
275
+ await new Promise((resolve) => setTimeout(resolve, 30));
276
+
277
+ // 3. Should no longer be listed in getConnectedClients or /api/control/clients
278
+ clients = controlPlane.getConnectedClients();
279
+ expect(clients.some((c) => c.clientId === "test-client-unreg")).toBe(
280
+ false,
281
+ );
282
+
283
+ const resAfterUnreg = await makeHttpRequest({
284
+ port,
285
+ path: "/api/control/clients",
286
+ headers: { "x-control-token": token },
287
+ });
288
+ expect(
289
+ resAfterUnreg.data.clients.some(
290
+ (c: any) => c.clientId === "test-client-unreg",
291
+ ),
292
+ ).toBe(false);
293
+
294
+ ws.close();
295
+ });
227
296
  });
228
297
 
229
298
  describe("Command execution", () => {
@@ -15,6 +15,7 @@ import { ulid } from "../utils/ulid.js";
15
15
  import {
16
16
  registerClient,
17
17
  updateClientHeartbeat,
18
+ unregisterClient,
18
19
  getAllClients,
19
20
  pruneStaleClients,
20
21
  getOrCreateControlToken,
@@ -60,6 +61,8 @@ export interface ControlPlane {
60
61
  ) => Promise<CommandResultPayload[]>;
61
62
  isClientConnected: (clientId: string) => boolean;
62
63
  getConnectedClients: () => ClientInfo[];
64
+ getActiveClientId?: () => string;
65
+ setActiveClientId?: (id: string) => void;
63
66
  getToken: () => string;
64
67
  close: () => void;
65
68
  }
@@ -296,6 +299,22 @@ export function createControlPlane(options: ControlPlaneOptions): ControlPlane {
296
299
  updateClientHeartbeat(hb.clientId);
297
300
  log(`Heartbeat from client: ${hb.clientId}`);
298
301
  }
302
+ } else if (type === "client:unregister") {
303
+ const unreg = payload as { clientId?: string };
304
+ const unregId =
305
+ (unreg?.clientId &&
306
+ typeof unreg.clientId === "string" &&
307
+ unreg.clientId.trim()) ||
308
+ (ws ? socketToClientId.get(ws) : undefined);
309
+ if (unregId) {
310
+ unregisterClient(unregId);
311
+ if (ws) {
312
+ activeSockets.delete(unregId);
313
+ socketToClientId.delete(ws);
314
+ }
315
+ sseClientsByClientId.delete(unregId);
316
+ log(`Client explicitly unregistered: ${unregId}`);
317
+ }
299
318
  } else if (type === "command:result") {
300
319
  const res = payload as CommandResultPayload;
301
320
  if (res?.commandId && pendingCommands.has(res.commandId)) {
@@ -327,6 +346,9 @@ export function createControlPlane(options: ControlPlaneOptions): ControlPlane {
327
346
  activeSockets.delete(clientId);
328
347
  socketToClientId.delete(ws);
329
348
  log(`Client disconnected: ${clientId}`);
349
+ if (!isClientConnected(clientId)) {
350
+ unregisterClient(clientId);
351
+ }
330
352
  }
331
353
  });
332
354
 
@@ -381,12 +403,13 @@ export function createControlPlane(options: ControlPlaneOptions): ControlPlane {
381
403
  });
382
404
  });
383
405
 
384
- // List all registered clients
406
+ // List connected clients (or all registered clients if ?all=true)
385
407
  app.get(
386
408
  "/api/control/clients",
387
409
  adminAuthMiddleware,
388
- (_req: Request, res: Response) => {
389
- const clients = getAllClients();
410
+ (req: Request, res: Response) => {
411
+ const showAll = req.query.all === "true" || req.query.all === "1";
412
+ const clients = showAll ? getAllClients() : getConnectedClients();
390
413
  res.json({ clients });
391
414
  },
392
415
  );
@@ -468,6 +491,9 @@ export function createControlPlane(options: ControlPlaneOptions): ControlPlane {
468
491
  if (clientId && sseClientsByClientId.get(clientId) === res) {
469
492
  sseClientsByClientId.delete(clientId);
470
493
  log(`SSE client disconnected: ${clientId}`);
494
+ if (!isClientConnected(clientId)) {
495
+ unregisterClient(clientId);
496
+ }
471
497
  }
472
498
  });
473
499
  },
@@ -574,11 +600,25 @@ export function createControlPlane(options: ControlPlaneOptions): ControlPlane {
574
600
  return false;
575
601
  }
576
602
 
603
+ let activeClientId: string = "";
604
+
577
605
  function getConnectedClients(): ClientInfo[] {
578
606
  const all = getAllClients();
579
607
  return all.filter((c) => isClientConnected(c.clientId));
580
608
  }
581
609
 
610
+ function getActiveClientId(): string {
611
+ if (activeClientId && isClientConnected(activeClientId)) {
612
+ return activeClientId;
613
+ }
614
+ const connected = getConnectedClients();
615
+ return connected.length > 0 ? connected[0].clientId : "";
616
+ }
617
+
618
+ function setActiveClientId(id: string): void {
619
+ activeClientId = id;
620
+ }
621
+
582
622
  function close(): void {
583
623
  clearInterval(prunerInterval);
584
624
  httpServer.removeListener("upgrade", upgradeHandler);
@@ -615,6 +655,8 @@ export function createControlPlane(options: ControlPlaneOptions): ControlPlane {
615
655
  broadcastCommand,
616
656
  isClientConnected,
617
657
  getConnectedClients,
658
+ getActiveClientId,
659
+ setActiveClientId,
618
660
  getToken: () => token,
619
661
  close,
620
662
  };
@@ -105,6 +105,22 @@ export const SHADOWCLAW_BUILTIN_TOOLS: McpTool[] = [
105
105
  },
106
106
  },
107
107
  },
108
+ {
109
+ name: "shadowclaw_set_active_client",
110
+ description:
111
+ "Set the active default connected client used when no clientId is explicitly provided in tool calls.",
112
+ inputSchema: {
113
+ type: "object",
114
+ properties: {
115
+ clientId: {
116
+ type: "string",
117
+ description:
118
+ "Target client ID (or index '0', '1', prefix, or device label) to make active.",
119
+ },
120
+ },
121
+ required: ["clientId"],
122
+ },
123
+ },
108
124
  {
109
125
  name: "shadowclaw_server_status",
110
126
  description:
@@ -120,10 +136,6 @@ export function resolveTargetClientId(
120
136
  controlPlane: any,
121
137
  requestedId?: unknown,
122
138
  ): string {
123
- if (typeof requestedId === "string" && requestedId.trim()) {
124
- return requestedId.trim();
125
- }
126
-
127
139
  try {
128
140
  const clients =
129
141
  typeof controlPlane.getConnectedClients === "function"
@@ -133,11 +145,43 @@ export function resolveTargetClientId(
133
145
  : [];
134
146
 
135
147
  if (Array.isArray(clients) && clients.length > 0) {
148
+ if (typeof requestedId === "string" && requestedId.trim()) {
149
+ const trimmed = requestedId.trim();
150
+ const exact = clients.find(
151
+ (c: any) => (c.clientId || c.id) === trimmed,
152
+ );
153
+ if (exact) return exact.clientId || exact.id;
154
+
155
+ const idx = parseInt(trimmed, 10);
156
+ if (!isNaN(idx) && idx >= 0 && idx < clients.length) {
157
+ return clients[idx].clientId || clients[idx].id;
158
+ }
159
+
160
+ const prefix = clients.find(
161
+ (c: any) =>
162
+ (c.clientId || c.id || "").startsWith(trimmed) ||
163
+ (c.clientId || "").replace(/^client-/, "").startsWith(trimmed),
164
+ );
165
+ if (prefix) return prefix.clientId || prefix.id;
166
+
167
+ const byLabel = clients.find((c: any) =>
168
+ c.deviceLabel?.toLowerCase().includes(trimmed.toLowerCase()),
169
+ );
170
+ if (byLabel) return byLabel.clientId || byLabel.id;
171
+
172
+ return trimmed;
173
+ }
174
+
175
+ if (typeof controlPlane.getActiveClientId === "function") {
176
+ const active = controlPlane.getActiveClientId();
177
+ if (active) return active;
178
+ }
179
+
136
180
  return clients[0].clientId || clients[0].id || "";
137
181
  }
138
182
  } catch (_) {}
139
183
 
140
- return "";
184
+ return typeof requestedId === "string" ? requestedId.trim() : "";
141
185
  }
142
186
 
143
187
  export function registerBuiltInTools(
@@ -356,6 +400,37 @@ export function registerBuiltInTools(
356
400
  };
357
401
  }
358
402
 
403
+ case "shadowclaw_set_active_client": {
404
+ const targetId = resolveTargetClientId(controlPlane, args.clientId);
405
+ if (!targetId) {
406
+ return {
407
+ isError: true,
408
+ content: [
409
+ {
410
+ type: "text",
411
+ text: `Error: Client '${args.clientId}' not found among connected clients.`,
412
+ },
413
+ ],
414
+ };
415
+ }
416
+
417
+ if (typeof controlPlane.setActiveClientId === "function") {
418
+ controlPlane.setActiveClientId(targetId);
419
+ }
420
+
421
+ return {
422
+ content: [
423
+ {
424
+ type: "text",
425
+ text: `Active default client set to: ${targetId}`,
426
+ },
427
+ ],
428
+ _meta: {
429
+ activeClientId: targetId,
430
+ },
431
+ };
432
+ }
433
+
359
434
  case "shadowclaw_server_status": {
360
435
  const clients =
361
436
  typeof controlPlane.getConnectedClients === "function"