shadow-claw 1.31.0 → 1.33.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 (231) hide show
  1. package/.well-known/mcp/server-card.json +1 -1
  2. package/.well-known/mcp.json +1 -1
  3. package/dist/electron/main.cjs +1 -1
  4. package/dist/lib/components/index.js +26 -26
  5. package/dist/lib/components/settings/shadow-claw-accounts.js +1 -1
  6. package/dist/lib/components/settings/shadow-claw-channel-config.js +1 -1
  7. package/dist/lib/components/settings/shadow-claw-control-plane.js +1 -1
  8. package/dist/lib/components/settings/shadow-claw-git.js +1 -1
  9. package/dist/lib/components/settings/shadow-claw-integrations.js +1 -1
  10. package/dist/lib/components/settings/shadow-claw-llm.js +1 -1
  11. package/dist/lib/components/settings/shadow-claw-mcp-remote.js +1 -1
  12. package/dist/lib/components/settings/shadow-claw-networking.js +1 -1
  13. package/dist/lib/components/settings/shadow-claw-notifications.js +1 -1
  14. package/dist/lib/components/settings/shadow-claw-peerjs.js +1 -1
  15. package/dist/lib/components/settings/shadow-claw-storage.js +1 -1
  16. package/dist/lib/components/settings/shadow-claw-task-server.js +1 -1
  17. package/dist/lib/components/settings/shadow-claw-webvm.js +1 -1
  18. package/dist/lib/components/shadow-claw-a2ui.js +1 -1
  19. package/dist/lib/components/shadow-claw-accounts.js +1 -1
  20. package/dist/lib/components/shadow-claw-channel-config.js +1 -1
  21. package/dist/lib/components/shadow-claw-channels.js +1 -1
  22. package/dist/lib/components/shadow-claw-chat.js +1 -1
  23. package/dist/lib/components/shadow-claw-control-plane.js +1 -1
  24. package/dist/lib/components/shadow-claw-conversations.js +1 -1
  25. package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
  26. package/dist/lib/components/shadow-claw-files.js +1 -1
  27. package/dist/lib/components/shadow-claw-git.js +1 -1
  28. package/dist/lib/components/shadow-claw-integrations.js +1 -1
  29. package/dist/lib/components/shadow-claw-llm.js +1 -1
  30. package/dist/lib/components/shadow-claw-mcp-remote.js +1 -1
  31. package/dist/lib/components/shadow-claw-networking.js +1 -1
  32. package/dist/lib/components/shadow-claw-notifications.js +1 -1
  33. package/dist/lib/components/shadow-claw-page-header.js +1 -1
  34. package/dist/lib/components/shadow-claw-pages.js +1 -1
  35. package/dist/lib/components/shadow-claw-peerjs.js +1 -1
  36. package/dist/lib/components/shadow-claw-settings.js +1 -1
  37. package/dist/lib/components/shadow-claw-storage.js +1 -1
  38. package/dist/lib/components/shadow-claw-task-server.js +1 -1
  39. package/dist/lib/components/shadow-claw-tasks.js +1 -1
  40. package/dist/lib/components/shadow-claw-terminal.js +1 -1
  41. package/dist/lib/components/shadow-claw-tools.js +1 -1
  42. package/dist/lib/components/shadow-claw-webvm.js +1 -1
  43. package/dist/lib/components/shadow-claw.js +1 -1
  44. package/dist/lib/{config-DjZ9QWc4.js → config-BcFbkaQv.js} +21 -4
  45. package/dist/lib/config-BcFbkaQv.js.map +1 -0
  46. package/dist/lib/{configurePeerJs-u7CYSquX.js → configurePeerJs-CBMPHbwa.js} +3 -3
  47. package/dist/lib/{configurePeerJs-u7CYSquX.js.map → configurePeerJs-CBMPHbwa.js.map} +1 -1
  48. package/dist/lib/{connections-BURqMIUv.js → connections-PZrUDf49.js} +2 -2
  49. package/dist/lib/{connections-BURqMIUv.js.map → connections-PZrUDf49.js.map} +1 -1
  50. package/dist/lib/{downloadGroupFile-B8J4up2a.js → downloadGroupFile-DSRUa9Au.js} +2 -2
  51. package/dist/lib/{downloadGroupFile-B8J4up2a.js.map → downloadGroupFile-DSRUa9Au.js.map} +1 -1
  52. package/dist/lib/{file-viewer-DoHM75Jz.js → file-viewer-11VVOpJv.js} +5 -5
  53. package/dist/lib/{file-viewer-DoHM75Jz.js.map → file-viewer-11VVOpJv.js.map} +1 -1
  54. package/dist/lib/{getGroupDir-QrN2aTo8.js → getGroupDir-BxKMYPPm.js} +3 -3
  55. package/dist/lib/{getGroupDir-QrN2aTo8.js.map → getGroupDir-BxKMYPPm.js.map} +1 -1
  56. package/dist/lib/{git-DAP_EV6R.js → git-BppibnJW.js} +27 -10
  57. package/dist/lib/{git-DAP_EV6R.js.map → git-BppibnJW.js.map} +1 -1
  58. package/dist/lib/{iframe-sanitizer-Bujgq14T.js → iframe-sanitizer-Dp8EYffI.js} +2 -2
  59. package/dist/lib/{iframe-sanitizer-Bujgq14T.js.map → iframe-sanitizer-Dp8EYffI.js.map} +1 -1
  60. package/dist/lib/index.js +27 -27
  61. package/dist/lib/{initChatSplitResize-BjiWoqC_.js → initChatSplitResize-CE_kWaLf.js} +2 -2
  62. package/dist/lib/{initChatSplitResize-BjiWoqC_.js.map → initChatSplitResize-CE_kWaLf.js.map} +1 -1
  63. package/dist/lib/{initControlPlane-DCK_SHeR.js → initControlPlane-BRIIOzoH.js} +18 -17
  64. package/dist/lib/{initControlPlane-DCK_SHeR.js.map → initControlPlane-BRIIOzoH.js.map} +1 -1
  65. package/dist/lib/{markdown-BFUTPQoO.js → markdown-Cq4sVwUT.js} +4 -4
  66. package/dist/lib/{markdown-BFUTPQoO.js.map → markdown-Cq4sVwUT.js.map} +1 -1
  67. package/dist/lib/{mcp-reconnect-BxJ77WSb.js → mcp-reconnect-CoHDMjmY.js} +2 -2
  68. package/dist/lib/{mcp-reconnect-BxJ77WSb.js.map → mcp-reconnect-CoHDMjmY.js.map} +1 -1
  69. package/dist/lib/{memoryStorage-D3FBwO9T.js → memoryStorage-DcFWrn_y.js} +2 -2
  70. package/dist/lib/{memoryStorage-D3FBwO9T.js.map → memoryStorage-DcFWrn_y.js.map} +1 -1
  71. package/dist/lib/{orchestrator-DNamnY-k.js → orchestrator-C3TgDZvq.js} +100 -19
  72. package/dist/lib/{orchestrator-DNamnY-k.js.map → orchestrator-C3TgDZvq.js.map} +1 -1
  73. package/dist/lib/{orchestrator-XygttQw2.js → orchestrator-Ce9_lmL2.js} +12 -12
  74. package/dist/lib/{orchestrator-XygttQw2.js.map → orchestrator-Ce9_lmL2.js.map} +1 -1
  75. package/dist/lib/{peerjs-BgkPSHKa.js → peerjs-PTYQ7yEG.js} +6 -6
  76. package/dist/lib/{peerjs-BgkPSHKa.js.map → peerjs-PTYQ7yEG.js.map} +1 -1
  77. package/dist/lib/{push-client-C09NQEk5.js → push-client-DDV5Rcg6.js} +3 -3
  78. package/dist/lib/{push-client-C09NQEk5.js.map → push-client-DDV5Rcg6.js.map} +1 -1
  79. package/dist/lib/{readGroupFileBytes-CSA7u-nJ.js → readGroupFileBytes-7C8G-yps.js} +2 -2
  80. package/dist/lib/{readGroupFileBytes-CSA7u-nJ.js.map → readGroupFileBytes-7C8G-yps.js.map} +1 -1
  81. package/dist/lib/{shadow-claw-vzcggiax.js → shadow-claw-WrWoPqFx.js} +10 -10
  82. package/dist/lib/{shadow-claw-vzcggiax.js.map → shadow-claw-WrWoPqFx.js.map} +1 -1
  83. package/dist/lib/{shadow-claw-a2ui-2I5nffIn.js → shadow-claw-a2ui-CcmJNXQP.js} +2 -2
  84. package/dist/lib/{shadow-claw-a2ui-2I5nffIn.js.map → shadow-claw-a2ui-CcmJNXQP.js.map} +1 -1
  85. package/dist/lib/{shadow-claw-accounts-DMb0y4ca.js → shadow-claw-accounts-CcB2429E.js} +3 -3
  86. package/dist/lib/{shadow-claw-accounts-DMb0y4ca.js.map → shadow-claw-accounts-CcB2429E.js.map} +1 -1
  87. package/dist/lib/{shadow-claw-channel-config-Cl3JvxIJ.js → shadow-claw-channel-config-C4v0h8oa.js} +3 -3
  88. package/dist/lib/{shadow-claw-channel-config-Cl3JvxIJ.js.map → shadow-claw-channel-config-C4v0h8oa.js.map} +1 -1
  89. package/dist/lib/{shadow-claw-channels-kv21lr_O.js → shadow-claw-channels-CyV88k9T.js} +3 -3
  90. package/dist/lib/{shadow-claw-channels-kv21lr_O.js.map → shadow-claw-channels-CyV88k9T.js.map} +1 -1
  91. package/dist/lib/{shadow-claw-chat-Cj1fHQIl.js → shadow-claw-chat-DSYtuZod.js} +11 -11
  92. package/dist/lib/{shadow-claw-chat-Cj1fHQIl.js.map → shadow-claw-chat-DSYtuZod.js.map} +1 -1
  93. package/dist/lib/{shadow-claw-control-plane-DySeKB36.js → shadow-claw-control-plane-DBSGuDT1.js} +7 -7
  94. package/dist/lib/{shadow-claw-control-plane-DySeKB36.js.map → shadow-claw-control-plane-DBSGuDT1.js.map} +1 -1
  95. package/dist/lib/{shadow-claw-conversations-DghueOOD.js → shadow-claw-conversations-Cp8pK4PM.js} +5 -5
  96. package/dist/lib/{shadow-claw-conversations-DghueOOD.js.map → shadow-claw-conversations-Cp8pK4PM.js.map} +1 -1
  97. package/dist/lib/{shadow-claw-file-viewer-C_ITv8T6.js → shadow-claw-file-viewer-B8G8yo48.js} +8 -8
  98. package/dist/lib/{shadow-claw-file-viewer-C_ITv8T6.js.map → shadow-claw-file-viewer-B8G8yo48.js.map} +1 -1
  99. package/dist/lib/{shadow-claw-files-CWBaYt9p.js → shadow-claw-files-D1COKzuC.js} +9 -9
  100. package/dist/lib/{shadow-claw-files-CWBaYt9p.js.map → shadow-claw-files-D1COKzuC.js.map} +1 -1
  101. package/dist/lib/{shadow-claw-git-XfPBpfXd.js → shadow-claw-git-60tWU4Ee.js} +6 -6
  102. package/dist/lib/{shadow-claw-git-XfPBpfXd.js.map → shadow-claw-git-60tWU4Ee.js.map} +1 -1
  103. package/dist/lib/{shadow-claw-integrations-yT7j07TM.js → shadow-claw-integrations-Bw1cG1ze.js} +4 -4
  104. package/dist/lib/{shadow-claw-integrations-yT7j07TM.js.map → shadow-claw-integrations-Bw1cG1ze.js.map} +1 -1
  105. package/dist/lib/{shadow-claw-llm-Gzx4ufL_.js → shadow-claw-llm-By5CvsFJ.js} +6 -6
  106. package/dist/lib/{shadow-claw-llm-Gzx4ufL_.js.map → shadow-claw-llm-By5CvsFJ.js.map} +1 -1
  107. package/dist/lib/{shadow-claw-mcp-remote-DGDWjByQ.js → shadow-claw-mcp-remote-BaNODd1v.js} +4 -4
  108. package/dist/lib/{shadow-claw-mcp-remote-DGDWjByQ.js.map → shadow-claw-mcp-remote-BaNODd1v.js.map} +1 -1
  109. package/dist/lib/{shadow-claw-networking-CYEbc5TR.js → shadow-claw-networking-DtZ5bsTz.js} +2 -2
  110. package/dist/lib/{shadow-claw-networking-CYEbc5TR.js.map → shadow-claw-networking-DtZ5bsTz.js.map} +1 -1
  111. package/dist/lib/{shadow-claw-notifications-DHvkTrgv.js → shadow-claw-notifications-C3VEba_A.js} +3 -3
  112. package/dist/lib/{shadow-claw-notifications-DHvkTrgv.js.map → shadow-claw-notifications-C3VEba_A.js.map} +1 -1
  113. package/dist/lib/{shadow-claw-page-header-DrAcWCZ9.js → shadow-claw-page-header-BLowJopA.js} +2 -2
  114. package/dist/lib/{shadow-claw-page-header-DrAcWCZ9.js.map → shadow-claw-page-header-BLowJopA.js.map} +1 -1
  115. package/dist/lib/{shadow-claw-pages-Bj3w3V6Y.js → shadow-claw-pages-CpeUV-hw.js} +9 -9
  116. package/dist/lib/{shadow-claw-pages-Bj3w3V6Y.js.map → shadow-claw-pages-CpeUV-hw.js.map} +1 -1
  117. package/dist/lib/{shadow-claw-peerjs-B3sx2xLA.js → shadow-claw-peerjs-DYAzJbTy.js} +3 -3
  118. package/dist/lib/{shadow-claw-peerjs-B3sx2xLA.js.map → shadow-claw-peerjs-DYAzJbTy.js.map} +1 -1
  119. package/dist/lib/{shadow-claw-settings-DK-0kp3A.js → shadow-claw-settings-ByxwW3WK.js} +9 -9
  120. package/dist/lib/{shadow-claw-settings-DK-0kp3A.js.map → shadow-claw-settings-ByxwW3WK.js.map} +1 -1
  121. package/dist/lib/{shadow-claw-storage-p9nVMQPF.js → shadow-claw-storage-CsSPYVDJ.js} +5 -5
  122. package/dist/lib/{shadow-claw-storage-p9nVMQPF.js.map → shadow-claw-storage-CsSPYVDJ.js.map} +1 -1
  123. package/dist/lib/{shadow-claw-task-server-BNKCWG2M.js → shadow-claw-task-server-De2WCEw4.js} +2 -2
  124. package/dist/lib/{shadow-claw-task-server-BNKCWG2M.js.map → shadow-claw-task-server-De2WCEw4.js.map} +1 -1
  125. package/dist/lib/{shadow-claw-tasks-Dbq9rUku.js → shadow-claw-tasks-zHWT4GaE.js} +7 -7
  126. package/dist/lib/{shadow-claw-tasks-Dbq9rUku.js.map → shadow-claw-tasks-zHWT4GaE.js.map} +1 -1
  127. package/dist/lib/{shadow-claw-terminal-Wk2qRWJ9.js → shadow-claw-terminal-Bpawfv4N.js} +2 -2
  128. package/dist/lib/{shadow-claw-terminal-Wk2qRWJ9.js.map → shadow-claw-terminal-Bpawfv4N.js.map} +1 -1
  129. package/dist/lib/{shadow-claw-tools-C9TEf2k7.js → shadow-claw-tools-BbKdWcps.js} +7 -7
  130. package/dist/lib/{shadow-claw-tools-C9TEf2k7.js.map → shadow-claw-tools-BbKdWcps.js.map} +1 -1
  131. package/dist/lib/{shadow-claw-webvm-VoKerWW8.js → shadow-claw-webvm-DLqK0AxW.js} +3 -3
  132. package/dist/lib/{shadow-claw-webvm-VoKerWW8.js.map → shadow-claw-webvm-DLqK0AxW.js.map} +1 -1
  133. package/dist/lib/subsystems/git/git.d.ts +8 -1
  134. package/dist/lib/subsystems/git/types.d.ts +7 -1
  135. package/dist/lib/subsystems/mcp/webmcp.d.ts +37 -0
  136. package/dist/lib/subsystems/providers/model-registry.d.ts +4 -1
  137. package/dist/lib/subsystems/providers/providers.d.ts +10 -0
  138. package/dist/lib/{syncWebMcpRegistration-DEs4JjI_.js → syncWebMcpRegistration-mlbbvcdm.js} +5 -5
  139. package/dist/lib/{syncWebMcpRegistration-DEs4JjI_.js.map → syncWebMcpRegistration-mlbbvcdm.js.map} +1 -1
  140. package/dist/lib/{tools-f1WPNDSS.js → tools-Bb2ujM2E.js} +4 -4
  141. package/dist/lib/{tools-f1WPNDSS.js.map → tools-Bb2ujM2E.js.map} +1 -1
  142. package/dist/lib/utils/index.js +1 -1
  143. package/dist/lib/utils/utils.js +1 -1
  144. package/dist/lib/{utils-B99XP70b.js → utils-mpsYPfS7.js} +3 -3
  145. package/dist/lib/{utils-B99XP70b.js.map → utils-mpsYPfS7.js.map} +1 -1
  146. package/dist/lib/webmcp-BVR2lBVo.js +1544 -0
  147. package/dist/lib/webmcp-BVR2lBVo.js.map +1 -0
  148. package/dist/public/.well-known/mcp/server-card.json +1 -1
  149. package/dist/public/.well-known/mcp.json +1 -1
  150. package/dist/public/agent.worker.js +44 -44
  151. package/dist/public/config-BqecTktB.js +1 -0
  152. package/dist/public/{configurePeerJs-B68egTBd.js → configurePeerJs-BPKfWuEt.js} +1 -1
  153. package/dist/public/{connections-6575KDGE.js → connections-C_3tYsmX.js} +1 -1
  154. package/dist/public/{custom-element-security-Da_6HQ6X.js → custom-element-security-DXwU3Lae.js} +1 -1
  155. package/dist/public/docs/example/article/index.html +2 -2
  156. package/dist/public/docs/publishing/index.html +2 -2
  157. package/dist/public/docs/skill-creator/index.html +2 -2
  158. package/dist/public/docs/subsystems/tools.md +4 -3
  159. package/dist/public/docs/subsystems/webmcp.md +58 -13
  160. package/dist/public/{downloadGroupFile-Dj-AMNsd.js → downloadGroupFile-DTyf5DIC.js} +1 -1
  161. package/dist/public/{e2e-bridge-DQ4Mb_eC.js → e2e-bridge-DXwWCQKw.js} +1 -1
  162. package/dist/public/{file-viewer-BqNHyqzy.js → file-viewer-ConEyDDG.js} +1 -1
  163. package/dist/public/{getGroupDir-BwprrdrQ.js → getGroupDir-ZfZsiq6d.js} +1 -1
  164. package/dist/public/git-BtBaLC1-.js +100 -0
  165. package/dist/public/git-DgFF7xTW.js +100 -0
  166. package/dist/public/{iframe-sanitizer-viROQkRn.js → iframe-sanitizer-Corkvo_d.js} +1 -1
  167. package/dist/public/index.html +2 -2
  168. package/dist/public/index.js +1 -1
  169. package/dist/public/{initChatSplitResize-BA18wLQQ.js → initChatSplitResize-CUc-TEmT.js} +1 -1
  170. package/dist/public/{initControlPlane-DG_LcLDy.js → initControlPlane-B_uYS8a4.js} +1 -1
  171. package/dist/public/main/index.html +2 -2
  172. package/dist/public/main/memory/index.html +2 -2
  173. package/dist/public/{markdown-eFKxxpVP.js → markdown-Cfa6GuJs.js} +1 -1
  174. package/dist/public/{mcp-reconnect-CBHuaYdF.js → mcp-reconnect-CUOkdPEz.js} +1 -1
  175. package/dist/public/{memoryStorage-C1llaYus.js → memoryStorage-Bh8fjmip.js} +1 -1
  176. package/dist/public/{orchestrator-BXwxW4N2.js → orchestrator-BcSzrg6E.js} +2 -2
  177. package/dist/public/{orchestrator-DAJkyJmV.js → orchestrator-Bi5OdVZD.js} +11 -11
  178. package/dist/public/{peerjs-BaDbzfgl.js → peerjs-CzCXG-NW.js} +1 -1
  179. package/dist/public/{push-client-B1CCaldJ.js → push-client-Cr3Ruahp.js} +1 -1
  180. package/dist/public/service-worker.js +1 -1
  181. package/dist/public/{shadow-claw-CpBmveOh.js → shadow-claw-CzUYuuOa.js} +2 -2
  182. package/dist/public/{shadow-claw-accounts-D1y-zbMs.js → shadow-claw-accounts-BgIHB_aP.js} +1 -1
  183. package/dist/public/{shadow-claw-channels-7Oew09u0.js → shadow-claw-channels-Ce_trJ5D.js} +1 -1
  184. package/dist/public/{shadow-claw-chat-CskwjDzZ.js → shadow-claw-chat-CdECLLwL.js} +1 -1
  185. package/dist/public/{shadow-claw-control-plane-BqjEyhx7.js → shadow-claw-control-plane-DM62UmOz.js} +2 -2
  186. package/dist/public/{shadow-claw-conversations-ClGgBPVY.js → shadow-claw-conversations-AM5g3C00.js} +1 -1
  187. package/dist/public/{shadow-claw-file-viewer-rDepUwFJ.js → shadow-claw-file-viewer-BP7gWS5g.js} +1 -1
  188. package/dist/public/{shadow-claw-files-C0r05PYf.js → shadow-claw-files-RPPGszcK.js} +1 -1
  189. package/dist/public/{shadow-claw-git-CXrECHKy.js → shadow-claw-git-BsHEvLrb.js} +2 -2
  190. package/dist/public/{shadow-claw-integrations-CRGL78Fy.js → shadow-claw-integrations-B0vbcOqg.js} +1 -1
  191. package/dist/public/{shadow-claw-llm-jeGVNciG.js → shadow-claw-llm-FNNamrc_.js} +2 -2
  192. package/dist/public/{shadow-claw-mcp-remote-DyGnK4S_.js → shadow-claw-mcp-remote-7mDgniQu.js} +1 -1
  193. package/dist/public/{shadow-claw-networking--q6MgEAT.js → shadow-claw-networking-CGsetaLp.js} +1 -1
  194. package/dist/public/{shadow-claw-notifications-CrFUF0fl.js → shadow-claw-notifications-pnwRcR-4.js} +1 -1
  195. package/dist/public/{shadow-claw-page-header-DA-FG08G.js → shadow-claw-page-header-BUy15Poz.js} +1 -1
  196. package/dist/public/{shadow-claw-pages-_n1wVXAV.js → shadow-claw-pages-ILAjGWNk.js} +1 -1
  197. package/dist/public/{shadow-claw-settings-5yIfa3ie.js → shadow-claw-settings-Bx5IgVqN.js} +3 -3
  198. package/dist/public/{shadow-claw-storage-PLX9EFGn.js → shadow-claw-storage-PAtNykaF.js} +1 -1
  199. package/dist/public/{shadow-claw-task-server-BMps7aSr.js → shadow-claw-task-server-Cbbi_2OO.js} +1 -1
  200. package/dist/public/{shadow-claw-tasks-CHO1xvsu.js → shadow-claw-tasks-B-jKYWZJ.js} +1 -1
  201. package/dist/public/{shadow-claw-terminal-BJnSXRsb.js → shadow-claw-terminal-BRejND-Z.js} +1 -1
  202. package/dist/public/{shadow-claw-tools-BHfxRckC.js → shadow-claw-tools-DNQe3oa-.js} +1 -1
  203. package/dist/public/{shadow-claw-webvm-CyI23mQT.js → shadow-claw-webvm-BFuwRifU.js} +1 -1
  204. package/dist/public/{syncWebMcpRegistration-R0tha3kP.js → syncWebMcpRegistration-FxKAPwaz.js} +1 -1
  205. package/dist/public/theme-init.js +1 -1
  206. package/dist/public/{tools-Cp1gequ8.js → tools-BwQKGsro.js} +1 -1
  207. package/dist/public/transformers-js.worker.js +1 -1
  208. package/dist/public/webmcp-C28PAlfy.js +1 -0
  209. package/dist/server.js +1 -1
  210. package/package.json +3 -3
  211. package/src/core/utils/initControlPlane.test.ts +122 -1
  212. package/src/core/utils/initControlPlane.ts +12 -8
  213. package/src/storage/staticMainSite.ts +16 -2
  214. package/src/stores/orchestrator.test.ts +33 -0
  215. package/src/stores/orchestrator.ts +36 -0
  216. package/src/subsystems/git/git.test.ts +99 -0
  217. package/src/subsystems/git/git.ts +92 -12
  218. package/src/subsystems/git/types.ts +7 -0
  219. package/src/subsystems/mcp/webmcp.test.ts +253 -1
  220. package/src/subsystems/mcp/webmcp.ts +147 -4
  221. package/src/subsystems/providers/model-registry.test.ts +139 -0
  222. package/src/subsystems/providers/model-registry.ts +54 -2
  223. package/src/subsystems/providers/providers.test.ts +329 -0
  224. package/src/subsystems/providers/providers.ts +135 -11
  225. package/dist/lib/config-DjZ9QWc4.js.map +0 -1
  226. package/dist/lib/webmcp-BC8_WafM.js +0 -1710
  227. package/dist/lib/webmcp-BC8_WafM.js.map +0 -1
  228. package/dist/public/config-Bnwx4EfS.js +0 -1
  229. package/dist/public/git-BDaM5VT5.js +0 -100
  230. package/dist/public/git-D4n46G9B.js +0 -100
  231. package/dist/public/webmcp-DBONjQWd.js +0 -2
@@ -33,6 +33,13 @@ export interface GitToolDeps {
33
33
  getProxyUrl: (
34
34
  pref: "local" | "public" | "custom",
35
35
  customUrl?: string,
36
+ loc?: {
37
+ protocol?: string;
38
+ host?: string;
39
+ hostname?: string;
40
+ origin?: string;
41
+ href?: string;
42
+ },
36
43
  ) => string;
37
44
  getRemoteUrl: (input: {
38
45
  groupRoot?: FileSystemDirectoryHandle;
@@ -330,10 +330,33 @@ describe("webmcp integration", () => {
330
330
 
331
331
  expect(listTasksRegistration).toBeDefined();
332
332
  expect(listTasksRegistration.annotations).toEqual({
333
- readOnlyHint: false,
333
+ readOnlyHint: true,
334
+ consequentialHint: false,
334
335
  untrustedContentHint: true,
335
336
  });
336
337
 
338
+ // Verify consequential tools receive consequentialHint: true
339
+ const consequentialToolNames = [
340
+ "bash",
341
+ "delete_file",
342
+ "delete_task",
343
+ "git_delete_repo",
344
+ "git_delete_branch",
345
+ "git_reset",
346
+ "git_push",
347
+ "email_send_message",
348
+ "clear_chat",
349
+ ];
350
+
351
+ for (const toolName of consequentialToolNames) {
352
+ const reg = mockRegisterTool.mock.calls
353
+ .map((args: any[]) => args[0])
354
+ .find((r: any) => r.name === toolName);
355
+ expect(reg).toBeDefined();
356
+ expect(reg.annotations.consequentialHint).toBe(true);
357
+ expect(reg.annotations.readOnlyHint).toBe(false);
358
+ }
359
+
337
360
  // Test that execute sends a postMessage
338
361
  const executePromise = listTasksRegistration.execute({ foo: "bar" });
339
362
 
@@ -486,4 +509,233 @@ describe("webmcp integration", () => {
486
509
  await registerWebMcpTools(null, jest.fn(), "group-webmcp");
487
510
  expect(mockRegisterTool.mock.calls.length).toBe(firstCount * 2);
488
511
  });
512
+
513
+ describe("executeWebMcpTool", () => {
514
+ it("executes tool passing JavaScript object directly on Chrome 155+ (PR #246/#251)", async () => {
515
+ const { executeWebMcpTool } = await import("./webmcp.js");
516
+
517
+ const mockTool = { name: "my_tool" };
518
+ const mockModelContext = {
519
+ executeTool: jest.fn(async (tool: any, input: any) => {
520
+ expect(tool).toBe(mockTool);
521
+ expect(typeof input).toBe("object");
522
+ expect(input).toEqual({ answer: 42 });
523
+ return "result-ok";
524
+ }),
525
+ };
526
+
527
+ const res = await executeWebMcpTool(mockModelContext, mockTool, {
528
+ answer: 42,
529
+ });
530
+ expect(res).toBe("result-ok");
531
+ expect(mockModelContext.executeTool).toHaveBeenCalledTimes(1);
532
+ expect(mockModelContext.executeTool).toHaveBeenCalledWith(
533
+ mockTool,
534
+ { answer: 42 },
535
+ undefined,
536
+ );
537
+ });
538
+
539
+ it("falls back to stringified JSON when executeTool throws 'Failed to parse input' (Chrome < 155 / polyfill)", async () => {
540
+ const { executeWebMcpTool } = await import("./webmcp.js");
541
+
542
+ const mockTool = { name: "my_tool" };
543
+ const calls: any[] = [];
544
+ const mockModelContext = {
545
+ executeTool: jest.fn(async (tool: any, input: any) => {
546
+ calls.push({ tool, input });
547
+ if (typeof input !== "string") {
548
+ throw new Error(
549
+ "Failed to parse input arguments: expected DOMString",
550
+ );
551
+ }
552
+ return "legacy-result";
553
+ }),
554
+ };
555
+
556
+ const res = await executeWebMcpTool(mockModelContext, mockTool, {
557
+ answer: 42,
558
+ });
559
+
560
+ expect(res).toBe("legacy-result");
561
+ expect(calls).toHaveLength(2);
562
+ expect(calls[0].input).toEqual({ answer: 42 });
563
+ expect(calls[1].input).toBe(JSON.stringify({ answer: 42 }));
564
+ });
565
+
566
+ it("falls back to stringified JSON when executeTool throws TypeError (WebIDL parameter rejection)", async () => {
567
+ const { executeWebMcpTool } = await import("./webmcp.js");
568
+
569
+ const mockTool = { name: "my_tool" };
570
+ const calls: any[] = [];
571
+ const mockModelContext = {
572
+ executeTool: jest.fn(async (_tool: any, input: any) => {
573
+ calls.push(input);
574
+ if (typeof input !== "string") {
575
+ throw new TypeError(
576
+ "Failed to execute 'executeTool' on 'ModelContext': parameter 2 is not of type 'DOMString'",
577
+ );
578
+ }
579
+ return "typeerror-fallback-result";
580
+ }),
581
+ };
582
+
583
+ const res = await executeWebMcpTool(mockModelContext, mockTool, {
584
+ param: "value",
585
+ });
586
+
587
+ expect(res).toBe("typeerror-fallback-result");
588
+ expect(calls).toHaveLength(2);
589
+ expect(calls[0]).toEqual({ param: "value" });
590
+ expect(calls[1]).toBe(JSON.stringify({ param: "value" }));
591
+ });
592
+
593
+ it("normalizes string input and executes with parsed object first then string fallback", async () => {
594
+ const { executeWebMcpTool } = await import("./webmcp.js");
595
+
596
+ const mockTool = { name: "my_tool" };
597
+ const calls: any[] = [];
598
+ const mockModelContext = {
599
+ executeTool: jest.fn(async (_tool: any, input: any) => {
600
+ calls.push(input);
601
+ if (typeof input !== "string") {
602
+ throw new Error("Failed to parse input");
603
+ }
604
+ return "string-input-result";
605
+ }),
606
+ };
607
+
608
+ const res = await executeWebMcpTool(
609
+ mockModelContext,
610
+ mockTool,
611
+ '{"foo":"bar"}',
612
+ );
613
+
614
+ expect(res).toBe("string-input-result");
615
+ expect(calls).toHaveLength(2);
616
+ expect(calls[0]).toEqual({ foo: "bar" });
617
+ expect(calls[1]).toBe('{"foo":"bar"}');
618
+ });
619
+
620
+ it("handles undefined/null input by passing empty object then empty JSON string on fallback", async () => {
621
+ const { executeWebMcpTool } = await import("./webmcp.js");
622
+
623
+ const mockTool = { name: "my_tool" };
624
+ const calls: any[] = [];
625
+ const mockModelContext = {
626
+ executeTool: jest.fn(async (_tool: any, input: any) => {
627
+ calls.push(input);
628
+ if (typeof input !== "string") {
629
+ throw new Error("Failed to parse input");
630
+ }
631
+ return "empty-result";
632
+ }),
633
+ };
634
+
635
+ const res = await executeWebMcpTool(mockModelContext, mockTool);
636
+
637
+ expect(res).toBe("empty-result");
638
+ expect(calls).toHaveLength(2);
639
+ expect(calls[0]).toEqual({});
640
+ expect(calls[1]).toBe("{}");
641
+ });
642
+
643
+ it("propagates other errors without attempting string fallback", async () => {
644
+ const { executeWebMcpTool } = await import("./webmcp.js");
645
+
646
+ const mockTool = { name: "my_tool" };
647
+ const mockModelContext = {
648
+ executeTool: jest.fn(async () => {
649
+ throw new Error("Tool internal execution failed");
650
+ }),
651
+ };
652
+
653
+ await expect(
654
+ executeWebMcpTool(mockModelContext, mockTool, { foo: "bar" }),
655
+ ).rejects.toThrow("Tool internal execution failed");
656
+
657
+ expect(mockModelContext.executeTool).toHaveBeenCalledTimes(1);
658
+ });
659
+
660
+ it("passes execution options or signal context to executeTool", async () => {
661
+ const { executeWebMcpTool } = await import("./webmcp.js");
662
+
663
+ const mockTool = { name: "my_tool" };
664
+ const controller = new AbortController();
665
+ const mockModelContext = {
666
+ executeTool: jest.fn(async (_tool: any, _input: any, options: any) => {
667
+ expect(options).toEqual({ signal: controller.signal });
668
+ return "with-signal";
669
+ }),
670
+ };
671
+
672
+ const res = await executeWebMcpTool(
673
+ mockModelContext,
674
+ mockTool,
675
+ { foo: "bar" },
676
+ { signal: controller.signal },
677
+ );
678
+
679
+ expect(res).toBe("with-signal");
680
+ });
681
+ });
682
+
683
+ describe("resolveWebMcpToolAnnotations", () => {
684
+ it("marks destructive and high-stakes tools with consequentialHint: true", async () => {
685
+ const { resolveWebMcpToolAnnotations, CONSEQUENTIAL_TOOL_NAMES } =
686
+ await import("./webmcp.js");
687
+
688
+ for (const toolName of CONSEQUENTIAL_TOOL_NAMES) {
689
+ const annotations = resolveWebMcpToolAnnotations(toolName);
690
+ expect(annotations.consequentialHint).toBe(true);
691
+ expect(annotations.readOnlyHint).toBe(false);
692
+ expect(annotations.untrustedContentHint).toBe(true);
693
+ }
694
+ });
695
+
696
+ it("marks read-only query tools with readOnlyHint: true and consequentialHint: false", async () => {
697
+ const { resolveWebMcpToolAnnotations, READ_ONLY_TOOL_NAMES } =
698
+ await import("./webmcp.js");
699
+
700
+ for (const toolName of READ_ONLY_TOOL_NAMES) {
701
+ const annotations = resolveWebMcpToolAnnotations(toolName);
702
+ expect(annotations.readOnlyHint).toBe(true);
703
+ expect(annotations.consequentialHint).toBe(false);
704
+ expect(annotations.untrustedContentHint).toBe(true);
705
+ }
706
+ });
707
+
708
+ it("marks regular state-mutating tools with readOnlyHint: false and consequentialHint: false", async () => {
709
+ const { resolveWebMcpToolAnnotations } = await import("./webmcp.js");
710
+
711
+ const mutatingTools = [
712
+ "write_file",
713
+ "patch_file",
714
+ "create_task",
715
+ "update_task",
716
+ "show_toast",
717
+ "send_notification",
718
+ ];
719
+
720
+ for (const toolName of mutatingTools) {
721
+ const annotations = resolveWebMcpToolAnnotations(toolName);
722
+ expect(annotations.readOnlyHint).toBe(false);
723
+ expect(annotations.consequentialHint).toBe(false);
724
+ expect(annotations.untrustedContentHint).toBe(true);
725
+ }
726
+ });
727
+
728
+ it("allows custom annotations to override defaults", async () => {
729
+ const { resolveWebMcpToolAnnotations } = await import("./webmcp.js");
730
+
731
+ const custom = resolveWebMcpToolAnnotations("read_file", {
732
+ consequentialHint: true,
733
+ customMeta: "important",
734
+ });
735
+
736
+ expect(custom.consequentialHint).toBe(true);
737
+ expect(custom.readOnlyHint).toBe(true);
738
+ expect(custom.customMeta).toBe("important");
739
+ });
740
+ });
489
741
  });
@@ -235,6 +235,72 @@ export async function getWebMcpTools(): Promise<
235
235
  }
236
236
  }
237
237
 
238
+ /**
239
+ * Gracefully execute a WebMCP tool on a ModelContext instance with backwards compatibility.
240
+ *
241
+ * Starting in Chrome 155.0.8052.0 (PR #246, PR #251), `document.modelContext.executeTool()`
242
+ * accepts an optional JavaScript object directly instead of a JSON DOMString.
243
+ * In Chrome < 155 and earlier polyfills, `executeTool()` required a stringified JSON arguments string.
244
+ *
245
+ * This function practices graceful degradation:
246
+ * 1. Prepares both an `inputObject` (for Chrome 155+) and an `inputStr` (for Chrome < 155 & polyfills).
247
+ * 2. Attempts to execute `executeTool(tool, inputObject, options)` first.
248
+ * 3. Catches errors starting with "Failed to parse input" (or TypeErrors from WebIDL DOMString checks)
249
+ * and retries transparently with `JSON.stringify(inputObject)`.
250
+ * 4. Re-throws any unrelated execution errors.
251
+ */
252
+ export async function executeWebMcpTool(
253
+ modelContext: any,
254
+ tool: any,
255
+ input?: unknown,
256
+ options?: WebMcpExecuteContext | AbortSignal,
257
+ ): Promise<unknown> {
258
+ if (!modelContext || typeof modelContext.executeTool !== "function") {
259
+ throw new Error("ModelContext executeTool is not available");
260
+ }
261
+
262
+ let inputObject: Record<string, unknown>;
263
+ let inputStr: string;
264
+
265
+ if (typeof input === "string") {
266
+ inputStr = input;
267
+ try {
268
+ const parsed = JSON.parse(input);
269
+ inputObject =
270
+ parsed && typeof parsed === "object" && !Array.isArray(parsed)
271
+ ? (parsed as Record<string, unknown>)
272
+ : {};
273
+ } catch {
274
+ inputObject = {};
275
+ }
276
+ } else if (input && typeof input === "object" && !Array.isArray(input)) {
277
+ inputObject = input as Record<string, unknown>;
278
+ try {
279
+ inputStr = JSON.stringify(input);
280
+ } catch {
281
+ inputStr = "{}";
282
+ }
283
+ } else {
284
+ inputObject = {};
285
+ inputStr = "{}";
286
+ }
287
+
288
+ try {
289
+ return await modelContext.executeTool(tool, inputObject, options);
290
+ } catch (err: any) {
291
+ const isParseError =
292
+ typeof err?.message === "string" &&
293
+ err.message.startsWith("Failed to parse input");
294
+ const isTypeError = err instanceof TypeError;
295
+
296
+ if (isParseError || isTypeError) {
297
+ return await modelContext.executeTool(tool, inputStr, options);
298
+ }
299
+
300
+ throw err;
301
+ }
302
+ }
303
+
238
304
  /**
239
305
  * Feature detection for the WebMCP imperative API.
240
306
  *
@@ -246,6 +312,83 @@ export function isWebMcpSupported(): boolean {
246
312
  return !!getModelContextApi();
247
313
  }
248
314
 
315
+ /**
316
+ * Consequential tools that perform high-stakes, irreversible, or external real-world actions
317
+ * (such as deleting data, executing arbitrary system commands, or transmitting live communications).
318
+ * Signals consuming agents to require explicit user confirmation prior to execution (Chrome 154.0.8017.0+).
319
+ */
320
+ export const CONSEQUENTIAL_TOOL_NAMES: ReadonlySet<string> = new Set([
321
+ "bash",
322
+ "clear_chat",
323
+ "delete_file",
324
+ "delete_task",
325
+ "email_send_message",
326
+ "git_delete_branch",
327
+ "git_delete_repo",
328
+ "git_push",
329
+ "git_reset",
330
+ ]);
331
+
332
+ /**
333
+ * Read-only tools that query information without mutating state.
334
+ */
335
+ export const READ_ONLY_TOOL_NAMES: ReadonlySet<string> = new Set([
336
+ "ask_user",
337
+ "detect_language",
338
+ "diff_files",
339
+ "email_read_messages",
340
+ "fetch_url",
341
+ "get_current_time",
342
+ "git_branches",
343
+ "git_diff",
344
+ "git_list_repos",
345
+ "git_log",
346
+ "git_read_file_at_ref",
347
+ "git_show",
348
+ "git_status",
349
+ "list_components",
350
+ "list_files",
351
+ "list_room_members",
352
+ "list_tasks",
353
+ "list_tool_profiles",
354
+ "proofread_text",
355
+ "read_file",
356
+ "remote_mcp_list_tools",
357
+ "rewrite_text",
358
+ "search_files",
359
+ "summarize_text",
360
+ "translate_text",
361
+ "web_search",
362
+ "write_text",
363
+ ]);
364
+
365
+ export interface WebMcpToolAnnotations {
366
+ readOnlyHint?: boolean;
367
+ consequentialHint?: boolean;
368
+ untrustedContentHint?: boolean;
369
+ [key: string]: unknown;
370
+ }
371
+
372
+ /**
373
+ * Resolve annotations for a WebMCP tool registration.
374
+ * Allows custom annotations (from declarative or customized tool definitions)
375
+ * to override defaults.
376
+ */
377
+ export function resolveWebMcpToolAnnotations(
378
+ toolName: string,
379
+ customAnnotations?: Record<string, unknown>,
380
+ ): WebMcpToolAnnotations {
381
+ const isConsequential = CONSEQUENTIAL_TOOL_NAMES.has(toolName);
382
+ const isReadOnly = !isConsequential && READ_ONLY_TOOL_NAMES.has(toolName);
383
+
384
+ return {
385
+ readOnlyHint: isReadOnly,
386
+ consequentialHint: isConsequential,
387
+ untrustedContentHint: true,
388
+ ...(customAnnotations || {}),
389
+ };
390
+ }
391
+
249
392
  /**
250
393
  * Register ShadowClaw tools with the WebMCP ModelContext API.
251
394
  *
@@ -292,10 +435,10 @@ export async function registerWebMcpTools(
292
435
  name: def.name,
293
436
  description: def.description,
294
437
  inputSchema: parseWebMcpInputSchema(def.input_schema),
295
- annotations: {
296
- readOnlyHint: false,
297
- untrustedContentHint: true,
298
- },
438
+ annotations: resolveWebMcpToolAnnotations(
439
+ def.name,
440
+ (def as any).annotations,
441
+ ),
299
442
  execute: (
300
443
  input: Record<string, unknown>,
301
444
  context?: WebMcpExecuteContext | AbortSignal,
@@ -19,11 +19,150 @@ describe("ModelRegistry", () => {
19
19
  });
20
20
  });
21
21
 
22
+ it("should register and get model info with supportsPromptCaching", () => {
23
+ modelRegistry.registerModelInfo("test-cached-model", {
24
+ contextWindow: 128000,
25
+ maxOutput: 4096,
26
+ supportsPromptCaching: true,
27
+ });
28
+
29
+ const info = modelRegistry.getModelInfo("test-cached-model");
30
+ expect(info).toEqual({
31
+ contextWindow: 128000,
32
+ maxOutput: 4096,
33
+ supportsPromptCaching: true,
34
+ });
35
+ });
36
+
37
+ it("should extract supportsPromptCaching from supported_parameters in fetchModelInfo", async () => {
38
+ const mockData: any = {
39
+ data: [
40
+ {
41
+ id: "custom/cached-model",
42
+ context_length: 128000,
43
+ supported_parameters: ["tools", "cache_control"],
44
+ },
45
+ {
46
+ id: "custom/non-cached-model",
47
+ context_length: 32000,
48
+ supported_parameters: ["tools"],
49
+ },
50
+ {
51
+ id: "custom/explicit-cached-model",
52
+ context_length: 64000,
53
+ supports_prompt_caching: true,
54
+ },
55
+ ],
56
+ };
57
+
58
+ const originalFetch = (globalThis as any).fetch;
59
+ (globalThis as any).fetch = async () => ({
60
+ ok: true,
61
+ json: async () => mockData,
62
+ });
63
+
64
+ try {
65
+ await modelRegistry.fetchModelInfo({
66
+ id: "openrouter",
67
+ modelsUrl: "https://openrouter.ai/api/v1/models",
68
+ headers: {},
69
+ } as any);
70
+
71
+ expect(
72
+ modelRegistry.getModelInfo("custom/cached-model")
73
+ ?.supportsPromptCaching,
74
+ ).toBe(true);
75
+ expect(
76
+ modelRegistry.getModelInfo("custom/non-cached-model")
77
+ ?.supportsPromptCaching,
78
+ ).toBeUndefined();
79
+ expect(
80
+ modelRegistry.getModelInfo("custom/explicit-cached-model")
81
+ ?.supportsPromptCaching,
82
+ ).toBe(true);
83
+ } finally {
84
+ (globalThis as any).fetch = originalFetch;
85
+ }
86
+ });
87
+
22
88
  it("should return null for unknown model", () => {
23
89
  const info = modelRegistry.getModelInfo("unknown");
24
90
  expect(info).toBeNull();
25
91
  });
26
92
 
93
+ it("should resolve models with prefix aliases, without prefixes, and with tag suffixes", () => {
94
+ modelRegistry.registerModelInfo("anthropic/claude-3.7-sonnet", {
95
+ contextWindow: 200000,
96
+ maxOutput: 8192,
97
+ supportsPromptCaching: true,
98
+ });
99
+ modelRegistry.registerModelInfo("gpt-4o", {
100
+ contextWindow: 128000,
101
+ maxOutput: 4096,
102
+ });
103
+
104
+ // Lookup bare name when registered with provider prefix
105
+ expect(
106
+ modelRegistry.getModelInfo("claude-3.7-sonnet")?.supportsPromptCaching,
107
+ ).toBe(true);
108
+ // Lookup with tag suffix
109
+ expect(
110
+ modelRegistry.getModelInfo("anthropic/claude-3.7-sonnet:thinking")
111
+ ?.supportsPromptCaching,
112
+ ).toBe(true);
113
+ // Lookup with provider prefix when registered as bare name
114
+ expect(modelRegistry.getModelInfo("openai/gpt-4o")?.contextWindow).toBe(
115
+ 128000,
116
+ );
117
+ });
118
+
119
+ it("should update alias metadata when a model is re-registered with updated capabilities", () => {
120
+ modelRegistry.registerModelInfo("provider/dynamic-model", {
121
+ contextWindow: 100000,
122
+ maxOutput: 4096,
123
+ supportsPromptCaching: false,
124
+ });
125
+
126
+ expect(
127
+ modelRegistry.getModelInfo("dynamic-model")?.supportsPromptCaching,
128
+ ).toBe(false);
129
+
130
+ // Re-register with updated capabilities (e.g. on provider model refresh)
131
+ modelRegistry.registerModelInfo("provider/dynamic-model", {
132
+ contextWindow: 128000,
133
+ maxOutput: 8192,
134
+ supportsPromptCaching: true,
135
+ });
136
+
137
+ expect(
138
+ modelRegistry.getModelInfo("dynamic-model")?.supportsPromptCaching,
139
+ ).toBe(true);
140
+ expect(modelRegistry.getModelInfo("dynamic-model")?.contextWindow).toBe(
141
+ 128000,
142
+ );
143
+ });
144
+
145
+ it("should not retain stale alias lookups when models map is cleared directly", () => {
146
+ modelRegistry.registerModelInfo("provider/temp-model", {
147
+ contextWindow: 100000,
148
+ maxOutput: 4096,
149
+ });
150
+
151
+ expect(modelRegistry.getModelInfo("temp-model")).not.toBeNull();
152
+
153
+ // Directly clear models Map as done in external test setups
154
+ modelRegistry.models.clear();
155
+
156
+ // Re-register a different model
157
+ modelRegistry.registerModelInfo("provider/other-model", {
158
+ contextWindow: 50000,
159
+ maxOutput: 2048,
160
+ });
161
+
162
+ expect(modelRegistry.getModelInfo("temp-model")).toBeNull();
163
+ expect(modelRegistry.getModelInfo("other-model")).not.toBeNull();
164
+ });
165
+
27
166
  it("should fetch model info from OpenRouter format", async () => {
28
167
  const mockData: any = {
29
168
  data: [
@@ -18,6 +18,7 @@ export interface ModelMetadata {
18
18
  supportsVideoInput?: boolean;
19
19
  supportsDocumentInput?: boolean;
20
20
  routesByRequestFeatures?: boolean;
21
+ supportsPromptCaching?: boolean;
21
22
  }
22
23
 
23
24
  /**
@@ -34,10 +35,48 @@ class ModelRegistry {
34
35
  models: Map<string, ModelMetadata> = new Map();
35
36
 
36
37
  /**
37
- * Get metadata for a model ID. Lookup is case-insensitive.
38
+ * Get metadata for a model ID. Lookup is case-insensitive and resilient
39
+ * to provider prefixes (e.g. "anthropic/claude-3.7-sonnet" vs "claude-3.7-sonnet")
40
+ * and variant tags (e.g. ":thinking").
38
41
  */
39
42
  getModelInfo(modelId: string): ModelMetadata | null {
40
- return this.models.get(modelId.toLowerCase()) ?? null;
43
+ if (!modelId || typeof modelId !== "string") {
44
+ return null;
45
+ }
46
+
47
+ const key = modelId.toLowerCase().trim();
48
+ const direct = this.models.get(key);
49
+ if (direct) {
50
+ return direct;
51
+ }
52
+
53
+ // Try without provider prefix (e.g. "anthropic/claude-3.7-sonnet" -> "claude-3.7-sonnet")
54
+ if (key.includes("/")) {
55
+ const bare = key.split("/").pop();
56
+ if (bare && this.models.has(bare)) {
57
+ return this.models.get(bare)!;
58
+ }
59
+ } else {
60
+ // Or if key doesn't have provider prefix, find a registered model ending in "/<key>".
61
+ // Note: Assumes bare model names are reasonably unique across registered providers.
62
+ // If duplicates exist, the first registered match is returned.
63
+ for (const [regKey, meta] of this.models.entries()) {
64
+ if (regKey.endsWith(`/${key}`)) {
65
+ return meta;
66
+ }
67
+ }
68
+ }
69
+
70
+ // Try stripping tag/variant suffix (e.g. "anthropic/claude-3.7-sonnet:thinking" -> "anthropic/claude-3.7-sonnet")
71
+ if (key.includes(":")) {
72
+ const withoutTag = key.split(":")[0];
73
+ const match = this.getModelInfo(withoutTag);
74
+ if (match) {
75
+ return match;
76
+ }
77
+ }
78
+
79
+ return null;
41
80
  }
42
81
 
43
82
  /**
@@ -158,6 +197,18 @@ class ModelRegistry {
158
197
  "structured",
159
198
  );
160
199
  const reasoning = this.extractReasoning(model);
200
+ const supportsPromptCaching =
201
+ typeof model.supports_prompt_caching === "boolean"
202
+ ? model.supports_prompt_caching
203
+ : typeof model.supportsPromptCaching === "boolean"
204
+ ? model.supportsPromptCaching
205
+ : this.supportedParametersInclude(
206
+ model,
207
+ "cache_control",
208
+ "prompt_caching",
209
+ )
210
+ ? true
211
+ : undefined;
161
212
 
162
213
  // HuggingFace Router structure nests info inside `providers` array
163
214
  if (
@@ -179,6 +230,7 @@ class ModelRegistry {
179
230
  ...(supportsVideoInput !== undefined && { supportsVideoInput }),
180
231
  ...(reasoning && { reasoning }),
181
232
  ...(routesByRequestFeatures && { routesByRequestFeatures }),
233
+ ...(supportsPromptCaching !== undefined && { supportsPromptCaching }),
182
234
  });
183
235
  }
184
236