@runfusion/fusion 0.43.1 → 0.45.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 (295) hide show
  1. package/README.md +2 -1
  2. package/dist/automation-store-EKXXEE2S.js +10 -0
  3. package/dist/automation-store-PLNG3X72.js +10 -0
  4. package/dist/automation-store-RDDYZFUJ.js +10 -0
  5. package/dist/bin.js +14420 -3360
  6. package/dist/chunk-2JI3F6MG.js +948 -0
  7. package/dist/chunk-4L6CLWJC.js +8345 -0
  8. package/dist/chunk-4VNS5WPM.js +42 -0
  9. package/dist/chunk-5RGECGQL.js +1139 -0
  10. package/dist/chunk-6JDLN7IL.js +409 -0
  11. package/dist/chunk-AICJATVW.js +948 -0
  12. package/dist/chunk-AWKNRUOR.js +1148 -0
  13. package/dist/chunk-BHM3UYWS.js +4651 -0
  14. package/dist/chunk-BMUXDSBF.js +4647 -0
  15. package/dist/chunk-GETECT6V.js +353 -0
  16. package/dist/chunk-GUV4QQT3.js +1143 -0
  17. package/dist/chunk-JKQO7YYZ.js +8369 -0
  18. package/dist/chunk-LYZVU6UV.js +409 -0
  19. package/dist/chunk-MAN4WYHO.js +353 -0
  20. package/dist/chunk-MCUG2Z7M.js +4651 -0
  21. package/dist/chunk-PSDYB45U.js +948 -0
  22. package/dist/chunk-RR2HM46R.js +8345 -0
  23. package/dist/chunk-RXMQ5OMY.js +9226 -0
  24. package/dist/chunk-S2EVJUY3.js +353 -0
  25. package/dist/chunk-UXR5XZFZ.js +409 -0
  26. package/dist/client/.vite/manifest.json +1138 -340
  27. package/dist/client/assets/AgentDetailView-CgbB51Iw.css +1 -0
  28. package/dist/client/assets/AgentDetailView-DJTi6hKC.js +18 -0
  29. package/dist/client/assets/{AgentPermissionPolicyEditor-BiY1a4KR.js → AgentPermissionPolicyEditor-C5DR9dUS.js} +2 -2
  30. package/dist/client/assets/AgentsView-BNd90PSx.js +29 -0
  31. package/dist/client/assets/AgentsView-CzMa5Olf.css +1 -0
  32. package/dist/client/assets/ChatView-B7JaZCL_.js +19 -0
  33. package/dist/client/assets/ChatView-BtjkE0V7.css +1 -0
  34. package/dist/client/assets/CommandCenter-CHO21xEW.js +74 -0
  35. package/dist/client/assets/CommandCenter-Xs5Ftv8s.css +1 -0
  36. package/dist/client/assets/DevServerView-B_p91Mj_.css +1 -0
  37. package/dist/client/assets/DevServerView-H9OkT1ld.js +1 -0
  38. package/dist/client/assets/{DirectoryPicker-DTKtd8L2.css → DirectoryPicker-0rspvcMz.css} +1 -1
  39. package/dist/client/assets/DirectoryPicker-DegxT8ia.js +1 -0
  40. package/dist/client/assets/DocumentsView-kVVQYISc.js +1 -0
  41. package/dist/client/assets/EvalsView-C9-iTSat.js +1 -0
  42. package/dist/client/assets/EvalsView-CTbgPUne.css +1 -0
  43. package/dist/client/assets/ExperimentalAgentOnboardingModal-3ZEFv8sr.css +1 -0
  44. package/dist/client/assets/ExperimentalAgentOnboardingModal-DjP_xNkc.js +1 -0
  45. package/dist/client/assets/GoalsView-CUh8JBnJ.css +1 -0
  46. package/dist/client/assets/GoalsView-VoIHJJ5l.js +2 -0
  47. package/dist/client/assets/InsightsView-BIZaUcpd.js +11 -0
  48. package/dist/client/assets/InsightsView-C7yRTMPn.css +1 -0
  49. package/dist/client/assets/MemoryView-7v7rv6ns.js +2 -0
  50. package/dist/client/assets/MemoryView-CBDUr2Jy.css +1 -0
  51. package/dist/client/assets/PiExtensionsManager-CB89rV4U.js +11 -0
  52. package/dist/client/assets/PluginManager-BNSuW_R4.js +1 -0
  53. package/dist/client/assets/PluginManager-DSiHizRD.css +1 -0
  54. package/dist/client/assets/PullRequestView-CI5jdKBL.js +6 -0
  55. package/dist/client/assets/PullRequestView-CpdE3Yo_.css +1 -0
  56. package/dist/client/assets/ResearchView-BT5dR_m8.js +1 -0
  57. package/dist/client/assets/ResearchView-DIVNQF4_.css +1 -0
  58. package/dist/client/assets/SecretsView-CvFioX7R.css +1 -0
  59. package/dist/client/assets/SecretsView-WPYI58Uo.js +1 -0
  60. package/dist/client/assets/SessionTerminal-C6gyOWLt.js +2 -0
  61. package/dist/client/assets/SessionTerminal-D4R3ScvQ.css +1 -0
  62. package/dist/client/assets/SettingsModal-BESN59Ao.css +1 -0
  63. package/dist/client/assets/SettingsModal-BZokaNyx.css +1 -0
  64. package/dist/client/assets/SettingsModal-Bq_3AbaB.js +1 -0
  65. package/dist/client/assets/SettingsModal-JHACoQJe.js +21 -0
  66. package/dist/client/assets/SetupWizardModal-CrgbnR_d.js +2 -0
  67. package/dist/client/assets/ShadcnColorPicker-1icHuwhC.js +11 -0
  68. package/dist/client/assets/ShadcnColorPicker-pqVWCC5-.css +1 -0
  69. package/dist/client/assets/SkillsView-CJ8snrma.css +1 -0
  70. package/dist/client/assets/SkillsView-byuSe3e0.js +1 -0
  71. package/dist/client/assets/TodoView-CIsL_uvQ.css +1 -0
  72. package/dist/client/assets/TodoView-CQ9GjYBg.js +6 -0
  73. package/dist/client/assets/WorkflowNodeEditor-DW7sJZUX.js +8 -0
  74. package/dist/client/assets/WorkflowNodeEditor-f_XPV5Jd.css +1 -0
  75. package/dist/client/assets/{addon-fit-B4jkcKk4.js → addon-fit-C1WQXRvu.js} +1 -1
  76. package/dist/client/assets/{addon-unicode11-Cdne2Sj4.js → addon-unicode11-BVmNDUBM.js} +1 -1
  77. package/dist/client/assets/{addon-web-links-CRNLzj7m.js → addon-web-links-CkVew5xs.js} +1 -1
  78. package/dist/client/assets/{addon-webgl-CeCSULF1.js → addon-webgl-uLc0RKcC.js} +1 -1
  79. package/dist/client/assets/agentSkills-CeytXLHF.js +1 -0
  80. package/dist/client/assets/agentSkills-bQJ4X1gH.css +1 -0
  81. package/dist/client/assets/agentsOrgChartLayout-BMvyuaLA.js +1 -0
  82. package/dist/client/assets/app-3wbZ08YC.js +13 -0
  83. package/dist/client/assets/app-7-5DWLtg.js +13 -0
  84. package/dist/client/assets/app-CSvE5Noc.js +13 -0
  85. package/dist/client/assets/app-CZ2LTFce.js +13 -0
  86. package/dist/client/assets/app-Cv9xEnll.js +13 -0
  87. package/dist/client/assets/app-x_yPcO5Y.js +13 -0
  88. package/dist/client/assets/arc-cxkkNsnK.js +1 -0
  89. package/dist/client/assets/architectureDiagram-3BPJPVTR-BRI0QwDf.js +36 -0
  90. package/dist/client/assets/band-CquvqAHh.js +1 -0
  91. package/dist/client/assets/blockDiagram-GPEHLZMM-BXkN8Hv_.js +132 -0
  92. package/dist/client/assets/c4Diagram-AAUBKEIU-C_Kcokep.js +10 -0
  93. package/dist/client/assets/channel-Clzpzu32.js +1 -0
  94. package/dist/client/assets/chunk-2J33WTMH-BGeuCz9C.js +1 -0
  95. package/dist/client/assets/chunk-4BX2VUAB-DcVdctr0.js +1 -0
  96. package/dist/client/assets/chunk-55IACEB6-Dg6gDigQ.js +1 -0
  97. package/dist/client/assets/chunk-727SXJPM-B9B-vlEb.js +206 -0
  98. package/dist/client/assets/chunk-AQP2D5EJ-LrWQ-2yi.js +231 -0
  99. package/dist/client/assets/chunk-FMBD7UC4-Bnd6hAED.js +15 -0
  100. package/dist/client/assets/chunk-ND2GUHAM-nAw93UB5.js +1 -0
  101. package/dist/client/assets/chunk-QZHKN3VN-CgprKcZo.js +1 -0
  102. package/dist/client/assets/classDiagram-4FO5ZUOK-CzYHP-af.js +1 -0
  103. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CzYHP-af.js +1 -0
  104. package/dist/client/assets/common-BDp2y6CF.js +1 -0
  105. package/dist/client/assets/common-BnEo5fLN.js +1 -0
  106. package/dist/client/assets/common-Bzr_Y8h4.js +1 -0
  107. package/dist/client/assets/common-CvwyFzfv.js +1 -0
  108. package/dist/client/assets/common-CztCXRkz.js +1 -0
  109. package/dist/client/assets/common-DPe2mbb7.js +1 -0
  110. package/dist/client/assets/cose-bilkent-S5V4N54A-Bhn0g_iK.js +1 -0
  111. package/dist/client/assets/cytoscape.esm-OyMbaexL.js +331 -0
  112. package/dist/client/assets/dagre-BM42HDAG-CP4kfwqd.js +4 -0
  113. package/dist/client/assets/{dashboard-view-C3SsjXo1.js → dashboard-view-CM6Xin_H.js} +1680 -1620
  114. package/dist/client/assets/dashboard-view-DV1nEhh9.js +1 -0
  115. package/dist/client/assets/dashboard-view-D_zsA-kz.css +1 -0
  116. package/dist/client/assets/defaultLocale-DX6XiGOO.js +1 -0
  117. package/dist/client/assets/diagram-2AECGRRQ-DPhzVEUf.js +43 -0
  118. package/dist/client/assets/diagram-5GNKFQAL-8QOC3K_L.js +10 -0
  119. package/dist/client/assets/diagram-KO2AKTUF-C1twgEch.js +3 -0
  120. package/dist/client/assets/diagram-LMA3HP47-3bEegSuf.js +24 -0
  121. package/dist/client/assets/diagram-OG6HWLK6-BJ_x0rm6.js +24 -0
  122. package/dist/client/assets/erDiagram-TEJ5UH35-DNiiS5q6.js +85 -0
  123. package/dist/client/assets/flowDiagram-I6XJVG4X-DoouPklt.js +162 -0
  124. package/dist/client/assets/{folder-open-Bw1mxVfa.js → folder-open-CuUs2W7f.js} +1 -1
  125. package/dist/client/assets/ganttDiagram-6RSMTGT7-BrR6VnP5.js +292 -0
  126. package/dist/client/assets/gitGraphDiagram-PVQCEYII-BV8_J6Ee.js +106 -0
  127. package/dist/client/assets/graph--OzhPTMs.js +1 -0
  128. package/dist/client/assets/index-BZBQ_KsL.css +1 -0
  129. package/dist/client/assets/index-CZcWjLAb.js +1459 -0
  130. package/dist/client/assets/infoDiagram-5YYISTIA-DGwIIFkR.js +2 -0
  131. package/dist/client/assets/init-Dmth1JHB.js +1 -0
  132. package/dist/client/assets/ishikawaDiagram-YF4QCWOH-D5dEH4RE.js +70 -0
  133. package/dist/client/assets/journeyDiagram-JHISSGLW-BuuSWPw5.js +139 -0
  134. package/dist/client/assets/kanban-definition-UN3LZRKU-DA34uUla.js +89 -0
  135. package/dist/client/assets/katex-HP8lGamR.js +257 -0
  136. package/dist/client/assets/layout-SsrduOYp.js +1 -0
  137. package/dist/client/assets/linear-dXivciGL.js +1 -0
  138. package/dist/client/assets/mermaid.core-Ch4HatqA.js +309 -0
  139. package/dist/client/assets/mindmap-definition-RKZ34NQL-DUCG-qD3.js +96 -0
  140. package/dist/client/assets/ordinal-DILIJJjt.js +1 -0
  141. package/dist/client/assets/pieDiagram-4H26LBE5-DWvFGiLa.js +30 -0
  142. package/dist/client/assets/quadrantDiagram-W4KKPZXB-OPTVW3WT.js +7 -0
  143. package/dist/client/assets/requirementDiagram-4Y6WPE33-D7feZvlU.js +84 -0
  144. package/dist/client/assets/sankeyDiagram-5OEKKPKP-AtnSsEX1.js +40 -0
  145. package/dist/client/assets/sequenceDiagram-3UESZ5HK-BZjHUeI5.js +162 -0
  146. package/dist/client/assets/shield-alert-CzaF9ROV.js +6 -0
  147. package/dist/client/assets/stateDiagram-AJRCARHV-C8j23Pvn.js +1 -0
  148. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CiZR_S5-.js +1 -0
  149. package/dist/client/assets/step-yuy0qJer.js +1 -0
  150. package/dist/client/assets/time-DoDTxsna.js +1 -0
  151. package/dist/client/assets/timeline-definition-PNZ67QCA-DvfNz25n.js +120 -0
  152. package/dist/client/assets/{upload-BCmd-1I0.js → upload-DTsYywhN.js} +1 -1
  153. package/dist/client/assets/{users-DO_t3V1p.js → users-yAd09tKP.js} +1 -1
  154. package/dist/client/assets/{vendor-i18n-CV0mwX_-.js → vendor-i18n-GF3RlKbq.js} +2 -2
  155. package/dist/client/assets/vendor-react-SZ_qB27O.js +49 -0
  156. package/dist/client/assets/vendor-reactflow-Bg-Qjzh8.js +15 -0
  157. package/dist/client/assets/{vendor-xterm-tcedd2Jt.js → vendor-xterm-BNq00GSj.js} +1 -1
  158. package/dist/client/assets/vennDiagram-CIIHVFJN-CG2aMt3v.js +34 -0
  159. package/dist/client/assets/wardley-L42UT6IY-BnhBteen.js +173 -0
  160. package/dist/client/assets/wardleyDiagram-YWT4CUSO-DwqTzM3T.js +78 -0
  161. package/dist/client/assets/xychartDiagram-2RQKCTM6-5Cv2ZKMV.js +7 -0
  162. package/dist/client/assets/zoom-out-NWbVkreR.js +41 -0
  163. package/dist/client/icons/icon-192.png +0 -0
  164. package/dist/client/icons/icon-512.png +0 -0
  165. package/dist/client/index.html +30 -10
  166. package/dist/client/sw.js +1 -1
  167. package/dist/client/theme-data.css +3348 -469
  168. package/dist/client/version.json +1 -1
  169. package/dist/droid-cli/index.ts +56 -40
  170. package/dist/droid-cli/package.json +1 -1
  171. package/dist/droid-cli/src/__tests__/execsync-guard.test.ts +17 -0
  172. package/dist/droid-cli/src/__tests__/index.test.ts +47 -21
  173. package/dist/droid-cli/src/__tests__/process-manager.test.ts +51 -78
  174. package/dist/extension.js +9651 -1556
  175. package/dist/extract-zip-WL55Z3AU.js +2497 -0
  176. package/dist/index.min-CHQ3ABYJ.js +2926 -0
  177. package/dist/memory-dreams-7USISKVE.js +32 -0
  178. package/dist/memory-dreams-PBPISUDD.js +32 -0
  179. package/dist/memory-dreams-XNO2B2QF.js +32 -0
  180. package/dist/migration-EH2YOCYQ.js +15 -0
  181. package/dist/migration-PLGTTKL5.js +15 -0
  182. package/dist/migration-WZEZK22R.js +15 -0
  183. package/dist/pi-claude-cli/index.ts +47 -8
  184. package/dist/pi-claude-cli/package.json +4 -1
  185. package/dist/pi-claude-cli/src/__tests__/acp-dispatch.test.ts +80 -0
  186. package/dist/pi-claude-cli/src/__tests__/acp-driver.test.ts +423 -0
  187. package/dist/pi-claude-cli/src/__tests__/execsync-guard.test.ts +14 -0
  188. package/dist/pi-claude-cli/src/__tests__/process-manager.test.ts +26 -45
  189. package/dist/pi-claude-cli/src/__tests__/provider.test.ts +0 -1
  190. package/dist/pi-claude-cli/src/acp-driver.ts +585 -0
  191. package/dist/pi-claude-cli/src/event-bridge.ts +7 -0
  192. package/dist/pi-claude-cli/src/mcp-config.ts +29 -0
  193. package/dist/pi-claude-cli/src/process-manager.ts +17 -39
  194. package/dist/plugin-sdk/index.d.ts +5305 -5148
  195. package/dist/plugins/fusion-plugin-acp-runtime/bundled.js +263 -19
  196. package/dist/plugins/fusion-plugin-acp-runtime/package.json +1 -1
  197. package/dist/plugins/fusion-plugin-cli-printing-press/package.json +1 -1
  198. package/dist/plugins/fusion-plugin-compound-engineering/bundled.js +37 -7
  199. package/dist/plugins/fusion-plugin-compound-engineering/manifest.json +4 -4
  200. package/dist/plugins/fusion-plugin-compound-engineering/package.json +1 -1
  201. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  202. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  203. package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +69 -45
  204. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  205. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  206. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  207. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  208. package/dist/plugins/fusion-plugin-reports/package.json +1 -1
  209. package/dist/plugins/fusion-plugin-roadmap/bundled.js +5 -11
  210. package/dist/plugins/fusion-plugin-roadmap/manifest.json +1 -11
  211. package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
  212. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
  213. package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/index.test.ts +5 -1
  214. package/dist/routine-store-GIJ6MXWY.js +10 -0
  215. package/dist/routine-store-HTVC2PUQ.js +10 -0
  216. package/dist/routine-store-KGHZBPDF.js +10 -0
  217. package/package.json +3 -3
  218. package/skill/fusion/SKILL.md +1 -1
  219. package/skill/fusion/references/cli-commands.md +2 -1
  220. package/skill/fusion/references/engine-tools.md +6 -4
  221. package/skill/fusion/references/extension-tools.md +10 -0
  222. package/skill/fusion/references/fusion-capabilities.md +3 -1
  223. package/dist/client/assets/AgentDetailView-BGN03dyz.css +0 -1
  224. package/dist/client/assets/AgentDetailView-DKMz3r04.js +0 -18
  225. package/dist/client/assets/AgentsView-Co-k4vjh.js +0 -29
  226. package/dist/client/assets/AgentsView-DfznJDek.css +0 -1
  227. package/dist/client/assets/ChatView-BkAG99bj.js +0 -6
  228. package/dist/client/assets/ChatView-DwJAd5G1.css +0 -1
  229. package/dist/client/assets/DevServerView-C6DC0URX.js +0 -1
  230. package/dist/client/assets/DevServerView-VTQY28P7.css +0 -1
  231. package/dist/client/assets/DirectoryPicker-CkcGjyv0.js +0 -1
  232. package/dist/client/assets/DocumentsView-OCK1Vsw_.js +0 -1
  233. package/dist/client/assets/DocumentsView-WfVoTZt8.css +0 -1
  234. package/dist/client/assets/EvalsView-CUNJ1TLc.css +0 -1
  235. package/dist/client/assets/EvalsView-CUfyj5GW.js +0 -1
  236. package/dist/client/assets/ExperimentalAgentOnboardingModal-BNpveCEk.css +0 -1
  237. package/dist/client/assets/ExperimentalAgentOnboardingModal-Dau9RTU_.js +0 -499
  238. package/dist/client/assets/GoalsView-CNLTn0rX.js +0 -2
  239. package/dist/client/assets/GoalsView-DI66UsjT.css +0 -1
  240. package/dist/client/assets/InsightsView-BQPxHxKI.js +0 -11
  241. package/dist/client/assets/InsightsView-C9-mqVvf.css +0 -1
  242. package/dist/client/assets/MemoryView-D3xQIFjj.js +0 -2
  243. package/dist/client/assets/MemoryView-zaXewZzi.css +0 -1
  244. package/dist/client/assets/NodesView-7VInxyf_.css +0 -1
  245. package/dist/client/assets/NodesView-CSMvGwDU.js +0 -14
  246. package/dist/client/assets/PiExtensionsManager-BtqXF3fl.js +0 -11
  247. package/dist/client/assets/PluginManager-CSSDb3VO.js +0 -1
  248. package/dist/client/assets/PluginManager-UHVLvWWl.css +0 -1
  249. package/dist/client/assets/PullRequestView-Bjyfffdu.js +0 -6
  250. package/dist/client/assets/PullRequestView-WQIS0zUC.css +0 -1
  251. package/dist/client/assets/ReliabilityView-QKMUDebT.js +0 -1
  252. package/dist/client/assets/ReliabilityView-eoKBhBc4.css +0 -1
  253. package/dist/client/assets/ResearchView-CRU_NPho.js +0 -1
  254. package/dist/client/assets/ResearchView-DTh8FLUm.css +0 -1
  255. package/dist/client/assets/SecretsView-Bs8ySyC5.js +0 -1
  256. package/dist/client/assets/SecretsView-Djo1rahi.css +0 -1
  257. package/dist/client/assets/SessionTerminal-BiigPP2l.css +0 -1
  258. package/dist/client/assets/SessionTerminal-Dk1NgmN2.js +0 -2
  259. package/dist/client/assets/SettingsModal-DpAxDq6M.css +0 -1
  260. package/dist/client/assets/SettingsModal-JCaNvdAb.js +0 -31
  261. package/dist/client/assets/SettingsModal-Nw5cTKAK.js +0 -1
  262. package/dist/client/assets/SettingsModal-nF_GaeeK.css +0 -1
  263. package/dist/client/assets/SetupWizardModal-CE79myEy.js +0 -1
  264. package/dist/client/assets/SetupWizardModal-CIqFbkrD.css +0 -1
  265. package/dist/client/assets/SkillsView-3bhGyWs1.css +0 -1
  266. package/dist/client/assets/SkillsView-Dc8YskAX.js +0 -1
  267. package/dist/client/assets/StashRecoveryView-B_8WIQEo.css +0 -1
  268. package/dist/client/assets/StashRecoveryView-CqKOFiXT.js +0 -1
  269. package/dist/client/assets/TodoView-7xIh-Fmi.css +0 -1
  270. package/dist/client/assets/TodoView-BqIRwcZw.js +0 -6
  271. package/dist/client/assets/WorkflowNodeEditor-2jDuk2FR.js +0 -8
  272. package/dist/client/assets/WorkflowNodeEditor-BokhW3de.css +0 -1
  273. package/dist/client/assets/app-B2ZxMfJT.js +0 -13
  274. package/dist/client/assets/app-BffGU_HQ.js +0 -13
  275. package/dist/client/assets/app-C8AHhs5N.js +0 -13
  276. package/dist/client/assets/app-CUu1sm3x.js +0 -13
  277. package/dist/client/assets/app-Ri9Te2Dv.js +0 -13
  278. package/dist/client/assets/app-sEkTyT-w.js +0 -13
  279. package/dist/client/assets/common-BRyX4VxB.js +0 -1
  280. package/dist/client/assets/common-CN9QPI2d.js +0 -1
  281. package/dist/client/assets/common-CNbjPELi.js +0 -1
  282. package/dist/client/assets/common-DFDqu_n0.js +0 -1
  283. package/dist/client/assets/common-DgWWKG8Z.js +0 -1
  284. package/dist/client/assets/common-X6PzBnVA.js +0 -1
  285. package/dist/client/assets/createLucideIcon-BYvOGRiO.js +0 -21
  286. package/dist/client/assets/dashboard-view-BhbN8LPh.js +0 -1
  287. package/dist/client/assets/dashboard-view-DdGlfuu-.css +0 -1
  288. package/dist/client/assets/dashboard-view-DlKsVySD.js +0 -3
  289. package/dist/client/assets/dashboard-view-Du53JbmQ.css +0 -1
  290. package/dist/client/assets/index-CpTBp1RG.js +0 -793
  291. package/dist/client/assets/index-DqhCiU35.css +0 -1
  292. package/dist/client/assets/vendor-react-D37HfzIf.js +0 -49
  293. package/dist/client/assets/vendor-reactflow-CRZcPCXF.js +0 -15
  294. package/dist/client/assets/x-CqNX_9E2.js +0 -61
  295. package/dist/client/assets/zoom-out-CSTHbJgD.js +0 -21
@@ -0,0 +1,423 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { EventEmitter } from "node:events";
3
+ import { PassThrough } from "node:stream";
4
+
5
+ // Synthetic ACP session/update sequence the mocked prompt() will replay.
6
+ let scriptedUpdates: Array<Record<string, unknown>> = [];
7
+ let scriptedUsage: Record<string, number> | undefined;
8
+ // When set, prompt() never resolves — simulates a turn waiting on the bridge so
9
+ // only an out-of-band event (child death / abort) can end it.
10
+ let scriptedHang = false;
11
+
12
+ // Driver validates the bridge path with existsSync — make the fake path "exist".
13
+ // writeFileSync/unlinkSync back the R17 auth-failure signal (spied).
14
+ const fsSpies = vi.hoisted(() => ({ writeFileSync: vi.fn(), unlinkSync: vi.fn() }));
15
+ vi.mock("node:fs", () => ({ existsSync: () => true, writeFileSync: fsSpies.writeFileSync, unlinkSync: fsSpies.unlinkSync }));
16
+
17
+ vi.mock("node:child_process", () => ({
18
+ spawn: vi.fn(() => {
19
+ const proc = new EventEmitter() as EventEmitter & Record<string, unknown>;
20
+ proc.stdin = new PassThrough();
21
+ proc.stdout = new PassThrough();
22
+ proc.stderr = new PassThrough();
23
+ proc.kill = vi.fn();
24
+ proc.pid = 4242;
25
+ return proc;
26
+ }),
27
+ }));
28
+
29
+ // Mock the ACP SDK: ClientSideConnection.prompt() replays scriptedUpdates onto
30
+ // the client handler, then resolves — so we exercise the real translation logic.
31
+ vi.mock("@agentclientprotocol/sdk", () => ({
32
+ PROTOCOL_VERSION: 1,
33
+ ndJsonStream: vi.fn(() => ({})),
34
+ ClientSideConnection: vi.fn(function (this: Record<string, unknown>, factory: () => { sessionUpdate: (p: unknown) => Promise<void> }) {
35
+ const handler = factory();
36
+ this.initialize = vi.fn(async () => ({ protocolVersion: 1 }));
37
+ this.newSession = vi.fn(async () => ({ sessionId: "s1" }));
38
+ this.prompt = vi.fn(async () => {
39
+ if (scriptedHang) return new Promise(() => {}); // never resolves
40
+ for (const u of scriptedUpdates) await handler.sessionUpdate({ update: u });
41
+ return { stopReason: "end_turn", usage: scriptedUsage };
42
+ });
43
+ }),
44
+ }));
45
+
46
+ const { MockStream } = vi.hoisted(() => {
47
+ const MockStream: unknown = vi.fn(function (this: Record<string, unknown>) {
48
+ const events: Array<Record<string, unknown>> = [];
49
+ this.push = vi.fn((e: Record<string, unknown>) => events.push(e));
50
+ this.end = vi.fn();
51
+ this._events = events;
52
+ });
53
+ return { MockStream };
54
+ });
55
+
56
+ vi.mock("@earendil-works/pi-ai", () => ({
57
+ AssistantMessageEventStream: MockStream,
58
+ calculateCost: vi.fn(),
59
+ }));
60
+
61
+ import { spawn } from "node:child_process";
62
+ import { ClientSideConnection } from "@agentclientprotocol/sdk";
63
+ import { streamViaAcp, buildBridgeEnv } from "../acp-driver.js";
64
+
65
+ const MODEL = { id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5" } as never;
66
+ const CTX = { messages: [{ role: "user", content: "hi" }] } as never;
67
+ const OPTS = { bridgePath: "/fake/claude-code-cli-acp", cwd: "/tmp", mcpServers: [], bridgeEnv: { HOME: "/h", PATH: "/b" } };
68
+
69
+ function eventsOf(stream: { _events: Array<Record<string, unknown>> }) {
70
+ return stream._events;
71
+ }
72
+ const flush = () => new Promise((r) => setTimeout(r, 30));
73
+
74
+ describe("streamViaAcp — ACP→pi translation (U11)", () => {
75
+ beforeEach(() => { scriptedUpdates = []; scriptedUsage = undefined; scriptedHang = false; });
76
+
77
+ it("feeds ACP token usage (incl. cache tokens) into the done message (item 2)", async () => {
78
+ scriptedUsage = { inputTokens: 11, outputTokens: 22, cachedReadTokens: 5, cachedWriteTokens: 3 };
79
+ scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "hi" } }];
80
+ const stream = streamViaAcp(MODEL, CTX, OPTS) as unknown as { _events: Array<Record<string, unknown>> };
81
+ await flush();
82
+ const done = stream._events.find((e) => e.type === "done") as { message?: { usage?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number; totalTokens?: number } } };
83
+ expect(done?.message?.usage?.input).toBe(11);
84
+ expect(done?.message?.usage?.output).toBe(22);
85
+ expect(done?.message?.usage?.cacheRead).toBe(5);
86
+ expect(done?.message?.usage?.cacheWrite).toBe(3);
87
+ expect(done?.message?.usage?.totalTokens).toBe(41);
88
+ });
89
+
90
+ it("ignores a malformed/untrusted usage payload (string/NaN/negative)", async () => {
91
+ scriptedUsage = { inputTokens: "99" as unknown as number, outputTokens: NaN, cachedReadTokens: -5 };
92
+ scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "hi" } }];
93
+ const stream = streamViaAcp(MODEL, CTX, OPTS) as unknown as { _events: Array<Record<string, unknown>> };
94
+ await flush();
95
+ const done = stream._events.find((e) => e.type === "done") as { message?: { usage?: { input?: number; output?: number } } };
96
+ // Coerced to undefined → bridge leaves usage at 0; never a string/NaN.
97
+ expect(done?.message?.usage?.input).toBe(0);
98
+ expect(Number.isNaN(done?.message?.usage?.output)).toBe(false);
99
+ });
100
+
101
+ it("does not emit usage on a tool-use (break-early) turn", async () => {
102
+ scriptedUsage = { inputTokens: 11, outputTokens: 22 };
103
+ scriptedUpdates = [
104
+ { sessionUpdate: "tool_call", toolCallId: "t1", _meta: { claudeCode: { toolName: "mcp__custom-tools__fn_task_list" } }, rawInput: {} },
105
+ ];
106
+ const stream = streamViaAcp(MODEL, CTX, OPTS) as unknown as { _events: Array<Record<string, unknown>> };
107
+ await flush();
108
+ const done = stream._events.find((e) => e.type === "done") as { message?: { usage?: { input?: number } } };
109
+ expect(done?.message?.usage?.input ?? 0).toBe(0); // tool-use turn reports zero usage
110
+ });
111
+
112
+ it("translates agent_message_chunk text into pi text events + done(stop)", async () => {
113
+ scriptedUpdates = [
114
+ { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "Hello " } },
115
+ { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "world" } },
116
+ ];
117
+ const stream = streamViaAcp(MODEL, CTX, OPTS) as unknown as { _events: Array<Record<string, unknown>> };
118
+ await flush();
119
+ const types = eventsOf(stream).map((e) => e.type);
120
+ expect(types).toContain("start");
121
+ expect(types).toContain("text_start");
122
+ expect(types.filter((t) => t === "text_delta").length).toBe(2);
123
+ const done = eventsOf(stream).find((e) => e.type === "done");
124
+ expect(done).toBeDefined();
125
+ expect(done!.reason).toBe("stop");
126
+ });
127
+
128
+ it("breaks early on a tool_call: emits toolcall_start + done(toolUse), no execution", async () => {
129
+ scriptedUpdates = [
130
+ { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "let me check" } },
131
+ { sessionUpdate: "tool_call", toolCallId: "t1", _meta: { claudeCode: { toolName: "mcp__custom-tools__fn_task_list" } }, rawInput: {} },
132
+ // anything after the tool call must be ignored (break-early)
133
+ { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "SHOULD NOT APPEAR" } },
134
+ ];
135
+ const stream = streamViaAcp(MODEL, CTX, OPTS) as unknown as { _events: Array<Record<string, unknown>> };
136
+ await flush();
137
+ const types = eventsOf(stream).map((e) => e.type);
138
+ expect(types).toContain("toolcall_start");
139
+ const done = eventsOf(stream).find((e) => e.type === "done");
140
+ expect(done!.reason).toBe("toolUse");
141
+ // break-early: the post-tool text delta must not have been translated
142
+ const deltas = eventsOf(stream).filter((e) => e.type === "text_delta").map((e) => e.delta);
143
+ expect(deltas.join("")).not.toContain("SHOULD NOT APPEAR");
144
+ });
145
+
146
+ it("does NOT break early on an internal ToolSearch; breaks on the real fn_* tool (U9 sequence)", async () => {
147
+ // Claude emits ToolSearch (not pi-known) to load the deferred MCP tool FIRST,
148
+ // then the real mcp__custom-tools__fn_task_list. The old code aborted on
149
+ // ToolSearch; the gated code must wait for the real tool (P0 fix).
150
+ scriptedUpdates = [
151
+ { sessionUpdate: "tool_call", toolCallId: "ts1", _meta: { claudeCode: { toolName: "ToolSearch" } }, rawInput: { query: "x" } },
152
+ { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "found it, calling" } },
153
+ { sessionUpdate: "tool_call", toolCallId: "real", _meta: { claudeCode: { toolName: "mcp__custom-tools__fn_task_list" } }, rawInput: {} },
154
+ ];
155
+ const stream = streamViaAcp(MODEL, CTX, OPTS) as unknown as { _events: Array<Record<string, unknown>> };
156
+ await flush();
157
+ // The text AFTER ToolSearch must have been processed (we didn't abort on ToolSearch)
158
+ const deltas = eventsOf(stream).filter((e) => e.type === "text_delta").map((e) => e.delta);
159
+ expect(deltas.join("")).toContain("found it");
160
+ // And we broke on the real tool
161
+ expect(eventsOf(stream).some((e) => e.type === "toolcall_start")).toBe(true);
162
+ const done = eventsOf(stream).find((e) => e.type === "done");
163
+ expect(done!.reason).toBe("toolUse");
164
+ });
165
+
166
+ it("R17 auth-signal: sets on a 'Not logged in' turn, clears on a real response, ignores long answers", async () => {
167
+ const run = async (text: string) => {
168
+ scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text } }];
169
+ streamViaAcp(MODEL, CTX, OPTS);
170
+ await flush();
171
+ };
172
+ const wroteAuthFailed = () =>
173
+ fsSpies.writeFileSync.mock.calls.some((c) => String(c[1]).includes('"authFailed":true'));
174
+
175
+ // Baseline: a real response leaves the signal cleared (lastAuthFailed=false).
176
+ await run("Here is a normal answer.");
177
+ fsSpies.writeFileSync.mockClear();
178
+ fsSpies.unlinkSync.mockClear();
179
+
180
+ // 1. A turn that is ONLY the bridge's "Not logged in" message → signal written.
181
+ await run("Not logged in · Please run /login");
182
+ expect(wroteAuthFailed()).toBe(true);
183
+
184
+ // 2. A real response → signal cleared (unlink).
185
+ fsSpies.unlinkSync.mockClear();
186
+ await run("Sure — here's the result you asked for.");
187
+ expect(fsSpies.unlinkSync).toHaveBeenCalled();
188
+
189
+ // 3. A LONG legit answer that merely mentions the phrase → NOT flagged.
190
+ fsSpies.writeFileSync.mockClear();
191
+ await run(`If you are not logged in, the CLI prompts you to authenticate. ${"detail ".repeat(20)}`);
192
+ expect(wroteAuthFailed()).toBe(false);
193
+ });
194
+
195
+ it("ends with done even when the turn produces no content", async () => {
196
+ scriptedUpdates = [];
197
+ const stream = streamViaAcp(MODEL, CTX, OPTS) as unknown as { _events: Array<Record<string, unknown>> };
198
+ await flush();
199
+ expect(eventsOf(stream).some((e) => e.type === "done")).toBe(true);
200
+ });
201
+ });
202
+
203
+ describe("connection reuse (item 1) — gated by FUSION_CLAUDE_ACP_REUSE", () => {
204
+ const savedReuse = process.env.FUSION_CLAUDE_ACP_REUSE;
205
+ beforeEach(() => {
206
+ scriptedUpdates = [];
207
+ scriptedUsage = undefined;
208
+ scriptedHang = false;
209
+ vi.mocked(spawn).mockClear();
210
+ vi.mocked(ClientSideConnection).mockClear();
211
+ });
212
+ afterEach(() => {
213
+ if (savedReuse === undefined) delete process.env.FUSION_CLAUDE_ACP_REUSE;
214
+ else process.env.FUSION_CLAUDE_ACP_REUSE = savedReuse;
215
+ });
216
+
217
+ it("reuses one warm bridge connection across turns; turn 2 skips spawn + session/new", async () => {
218
+ process.env.FUSION_CLAUDE_ACP_REUSE = "1";
219
+ const reuseOpts = { ...OPTS, sessionId: "conv-reuse-1" };
220
+
221
+ // Turn 1 (cold): needs >1 message so reuseKey activates and the connection caches.
222
+ const ctx1 = { messages: [{ role: "user", content: "hi" }, { role: "assistant", content: "hello" }] } as never;
223
+ scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "turn one" } }];
224
+ streamViaAcp(MODEL, ctx1, reuseOpts);
225
+ await flush();
226
+ expect(vi.mocked(spawn)).toHaveBeenCalledTimes(1);
227
+ expect(vi.mocked(ClientSideConnection)).toHaveBeenCalledTimes(1);
228
+
229
+ // Turn 2 (warm): same sessionId → no new spawn, no new connection.
230
+ const ctx2 = { messages: [...(ctx1 as unknown as { messages: unknown[] }).messages, { role: "user", content: "again" }] } as never;
231
+ scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "turn two" } }];
232
+ const s2 = streamViaAcp(MODEL, ctx2, reuseOpts) as unknown as { _events: Array<Record<string, unknown>> };
233
+ await flush();
234
+ expect(vi.mocked(spawn)).toHaveBeenCalledTimes(1); // no second spawn
235
+ expect(vi.mocked(ClientSideConnection)).toHaveBeenCalledTimes(1); // no second connection
236
+
237
+ // The single warm connection's prompt() ran once per turn.
238
+ const conn = vi.mocked(ClientSideConnection).mock.instances[0] as unknown as { prompt: ReturnType<typeof vi.fn>; newSession: ReturnType<typeof vi.fn> };
239
+ expect(conn.prompt).toHaveBeenCalledTimes(2);
240
+ expect(conn.newSession).toHaveBeenCalledTimes(1); // session/new only on the cold turn
241
+ const done = s2._events.find((e) => e.type === "done");
242
+ expect(done!.reason).toBe("stop");
243
+
244
+ // Cleanup: evict the warm connection + clear its (unref'd) idle timer.
245
+ (vi.mocked(spawn).mock.results[0].value as EventEmitter).emit("close", 0);
246
+ });
247
+
248
+ it("fails a reuse turn FAST when the warm child dies mid-prompt (P0: no 30min hang)", async () => {
249
+ process.env.FUSION_CLAUDE_ACP_REUSE = "1";
250
+ const reuseOpts = { ...OPTS, sessionId: "conv-death" };
251
+
252
+ // Turn 1 (cold) caches the warm connection.
253
+ const ctx1 = { messages: [{ role: "user", content: "hi" }, { role: "assistant", content: "hello" }] } as never;
254
+ scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "one" } }];
255
+ streamViaAcp(MODEL, ctx1, reuseOpts);
256
+ await flush();
257
+ const child = vi.mocked(spawn).mock.results[0].value as EventEmitter;
258
+
259
+ // Turn 2 (warm) hangs on prompt() — only the child-death path can end it.
260
+ scriptedHang = true;
261
+ const ctx2 = { messages: [...(ctx1 as unknown as { messages: unknown[] }).messages, { role: "user", content: "again" }] } as never;
262
+ const s2 = streamViaAcp(MODEL, ctx2, reuseOpts) as unknown as { _events: Array<Record<string, unknown>> };
263
+ await flush();
264
+ expect(s2._events.some((e) => e.type === "done")).toBe(false); // still waiting
265
+
266
+ // The warm child dies. The cold turn's close handler routes failure to the
267
+ // CURRENT (reuse) turn via router.fail, so it ends immediately.
268
+ child.emit("close", 1);
269
+ await flush();
270
+ const done = s2._events.find((e) => e.type === "done") as { reason?: string; message?: { content?: Array<{ text?: string }> } };
271
+ expect(done).toBeDefined();
272
+ expect(done!.reason).toBe("stop");
273
+ expect(JSON.stringify(done!.message?.content)).toContain("Error");
274
+
275
+ // Cache was evicted: a subsequent turn cold-spawns a fresh bridge.
276
+ scriptedHang = false;
277
+ scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "fresh" } }];
278
+ const ctx3 = { messages: [...(ctx2 as unknown as { messages: unknown[] }).messages, { role: "assistant", content: "" }, { role: "user", content: "q3" }] } as never;
279
+ streamViaAcp(MODEL, ctx3, reuseOpts);
280
+ await flush();
281
+ expect(vi.mocked(spawn)).toHaveBeenCalledTimes(2); // turn 1 + the post-death cold restart
282
+ });
283
+
284
+ it("does NOT keep the connection warm after a tool-use (break-early) turn — prompt() still pending", async () => {
285
+ process.env.FUSION_CLAUDE_ACP_REUSE = "1";
286
+ const reuseOpts = { ...OPTS, sessionId: "conv-tooluse" };
287
+
288
+ // Turn 1 (cold) breaks early on a pi-known tool — prompt() never resolves
289
+ // (the break happens mid-stream), so the connection must be torn down, not
290
+ // released warm, or turn 2 would launch a concurrent prompt on it.
291
+ const ctx1 = { messages: [{ role: "user", content: "hi" }, { role: "assistant", content: "hello" }] } as never;
292
+ scriptedUpdates = [
293
+ { sessionUpdate: "tool_call", toolCallId: "t1", _meta: { claudeCode: { toolName: "mcp__custom-tools__fn_task_list" } }, rawInput: {} },
294
+ ];
295
+ const s1 = streamViaAcp(MODEL, ctx1, reuseOpts) as unknown as { _events: Array<Record<string, unknown>> };
296
+ await flush();
297
+ expect(s1._events.find((e) => e.type === "done")!.reason).toBe("toolUse");
298
+ expect(vi.mocked(spawn)).toHaveBeenCalledTimes(1);
299
+
300
+ // Turn 2 must cold-spawn a fresh bridge (no warm reuse after a tool turn).
301
+ scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "after tool" } }];
302
+ const ctx2 = { messages: [...(ctx1 as unknown as { messages: unknown[] }).messages, { role: "user", content: "again" }] } as never;
303
+ streamViaAcp(MODEL, ctx2, reuseOpts);
304
+ await flush();
305
+ expect(vi.mocked(spawn)).toHaveBeenCalledTimes(2); // fresh spawn → no concurrent prompt on a warm conn
306
+ });
307
+
308
+ it("cold-starts (no warm reuse) when the resume delta is empty (P1: no empty-prompt hang)", async () => {
309
+ process.env.FUSION_CLAUDE_ACP_REUSE = "1";
310
+ const reuseOpts = { ...OPTS, sessionId: "conv-empty" };
311
+
312
+ // Turn 1 (cold) caches.
313
+ const ctx1 = { messages: [{ role: "user", content: "hi" }, { role: "assistant", content: "hello" }] } as never;
314
+ scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "one" } }];
315
+ streamViaAcp(MODEL, ctx1, reuseOpts);
316
+ await flush();
317
+ expect(vi.mocked(spawn)).toHaveBeenCalledTimes(1);
318
+
319
+ // Turn 2 whose context ends in an assistant message → buildResumePrompt is
320
+ // empty → must NOT take the warm path (would hang); cold-starts instead.
321
+ const ctx2 = { messages: [...(ctx1 as unknown as { messages: unknown[] }).messages, { role: "user", content: "x" }, { role: "assistant", content: "y" }] } as never;
322
+ scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "two" } }];
323
+ const s2 = streamViaAcp(MODEL, ctx2, reuseOpts) as unknown as { _events: Array<Record<string, unknown>> };
324
+ await flush();
325
+ expect(vi.mocked(spawn)).toHaveBeenCalledTimes(2); // empty resume → fresh spawn
326
+ const done = s2._events.find((e) => e.type === "done");
327
+ expect(done!.reason).toBe("stop"); // produced a normal turn, did not hang
328
+ });
329
+
330
+ it("does NOT reuse when the flag is off (default): each turn spawns a fresh bridge", async () => {
331
+ delete process.env.FUSION_CLAUDE_ACP_REUSE;
332
+ const reuseOpts = { ...OPTS, sessionId: "conv-off" };
333
+ const ctx = { messages: [{ role: "user", content: "hi" }, { role: "assistant", content: "x" }] } as never;
334
+ scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "a" } }];
335
+ streamViaAcp(MODEL, ctx, reuseOpts);
336
+ await flush();
337
+ streamViaAcp(MODEL, ctx, reuseOpts);
338
+ await flush();
339
+ expect(vi.mocked(spawn)).toHaveBeenCalledTimes(2);
340
+ expect(vi.mocked(ClientSideConnection)).toHaveBeenCalledTimes(2);
341
+ });
342
+ });
343
+
344
+ describe("buildBridgeEnv — R17 auth opt-in (item 3)", () => {
345
+ const saved = {
346
+ flag: process.env.FUSION_CLAUDE_ACP_FORWARD_AUTH,
347
+ oauth: process.env.CLAUDE_CODE_OAUTH_TOKEN,
348
+ authTok: process.env.ANTHROPIC_AUTH_TOKEN,
349
+ key: process.env.ANTHROPIC_API_KEY,
350
+ };
351
+ // Start each case from a clean slate so an ambient auth var in the runner's
352
+ // env can't shadow the token a test means to exercise (precedence is global).
353
+ beforeEach(() => {
354
+ delete process.env.FUSION_CLAUDE_ACP_FORWARD_AUTH;
355
+ delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
356
+ delete process.env.ANTHROPIC_AUTH_TOKEN;
357
+ delete process.env.ANTHROPIC_API_KEY;
358
+ });
359
+ afterEach(() => {
360
+ for (const [k, v] of [
361
+ ["FUSION_CLAUDE_ACP_FORWARD_AUTH", saved.flag],
362
+ ["CLAUDE_CODE_OAUTH_TOKEN", saved.oauth],
363
+ ["ANTHROPIC_AUTH_TOKEN", saved.authTok],
364
+ ["ANTHROPIC_API_KEY", saved.key],
365
+ ] as const) {
366
+ if (v === undefined) delete process.env[k]; else process.env[k] = v;
367
+ }
368
+ });
369
+
370
+ it("does NOT forward auth vars by default (secure default)", () => {
371
+ delete process.env.FUSION_CLAUDE_ACP_FORWARD_AUTH;
372
+ process.env.ANTHROPIC_API_KEY = "sk-secret";
373
+ const env = buildBridgeEnv({ HOME: "/h", PATH: "/b" });
374
+ expect(env.ANTHROPIC_API_KEY).toBeUndefined();
375
+ expect(env.HOME).toBe("/h");
376
+ });
377
+
378
+ it("forwards a single auth token when opted in", () => {
379
+ process.env.FUSION_CLAUDE_ACP_FORWARD_AUTH = "1";
380
+ delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
381
+ process.env.ANTHROPIC_API_KEY = "sk-secret";
382
+ const env = buildBridgeEnv({ HOME: "/h", PATH: "/b" });
383
+ expect(env.ANTHROPIC_API_KEY).toBe("sk-secret");
384
+ });
385
+
386
+ it("prefers CLAUDE_CODE_OAUTH_TOKEN and forwards only one token", () => {
387
+ process.env.FUSION_CLAUDE_ACP_FORWARD_AUTH = "1";
388
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-tok";
389
+ process.env.ANTHROPIC_API_KEY = "sk-secret";
390
+ const env = buildBridgeEnv({ HOME: "/h", PATH: "/b" });
391
+ expect(env.CLAUDE_CODE_OAUTH_TOKEN).toBe("oauth-tok");
392
+ expect(env.ANTHROPIC_API_KEY).toBeUndefined();
393
+ });
394
+
395
+ it("forwards ANTHROPIC_AUTH_TOKEN (middle precedence) when no OAuth token", () => {
396
+ process.env.FUSION_CLAUDE_ACP_FORWARD_AUTH = "1";
397
+ delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
398
+ process.env.ANTHROPIC_AUTH_TOKEN = "auth-tok";
399
+ process.env.ANTHROPIC_API_KEY = "sk-secret";
400
+ const env = buildBridgeEnv({ HOME: "/h", PATH: "/b" });
401
+ expect(env.ANTHROPIC_AUTH_TOKEN).toBe("auth-tok");
402
+ expect(env.ANTHROPIC_API_KEY).toBeUndefined();
403
+ });
404
+
405
+ it("treats a whitespace-only higher-preference token as absent (no shadowing, no blank forward)", () => {
406
+ process.env.FUSION_CLAUDE_ACP_FORWARD_AUTH = "1";
407
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = " "; // blank → must be skipped
408
+ process.env.ANTHROPIC_API_KEY = "sk-real";
409
+ const env = buildBridgeEnv({ HOME: "/h", PATH: "/b" });
410
+ expect(env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined();
411
+ expect(env.ANTHROPIC_API_KEY).toBe("sk-real"); // real lower-preference token wins
412
+ });
413
+
414
+ it("reads the auth token from process.env, never a caller-supplied value (no token substitution)", () => {
415
+ process.env.FUSION_CLAUDE_ACP_FORWARD_AUTH = "1";
416
+ delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
417
+ delete process.env.ANTHROPIC_AUTH_TOKEN;
418
+ process.env.ANTHROPIC_API_KEY = "real-from-env";
419
+ // A caller trying to inject a different token via the supplied env must be ignored.
420
+ const env = buildBridgeEnv({ HOME: "/h", PATH: "/b", ANTHROPIC_API_KEY: "attacker" } as NodeJS.ProcessEnv);
421
+ expect(env.ANTHROPIC_API_KEY).toBe("real-from-env");
422
+ });
423
+ });
@@ -0,0 +1,14 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ describe("process-manager execSync guard", () => {
6
+ it("keeps Claude CLI probing non-blocking", () => {
7
+ const sourcePath = path.resolve(import.meta.dirname, "../process-manager.ts");
8
+
9
+ expect(existsSync(sourcePath)).toBe(true);
10
+ const src = readFileSync(sourcePath, "utf-8");
11
+
12
+ expect(/\bexecSync\b/.test(src)).toBe(false);
13
+ });
14
+ });
@@ -16,7 +16,6 @@ vi.mock("node:child_process", () => ({
16
16
  proc.pid = 12345;
17
17
  return proc;
18
18
  }),
19
- execSync: vi.fn(),
20
19
  }));
21
20
 
22
21
  const mocks = vi.hoisted(() => ({
@@ -38,15 +37,13 @@ vi.mock("node:os", () => ({
38
37
  tmpdir: mocks.tmpdir,
39
38
  }));
40
39
 
41
- import { spawn, execSync } from "node:child_process";
40
+ import { spawn } from "node:child_process";
42
41
  import {
43
42
  spawnClaude,
44
43
  buildClaudeSpawnArgs,
45
44
  writeUserMessage,
46
45
  cleanupProcess,
47
46
  captureStderr,
48
- validateCliPresence,
49
- validateCliAuth,
50
47
  validateCliPresenceAsync,
51
48
  validateCliAuthAsync,
52
49
  forceKillProcess,
@@ -368,47 +365,6 @@ describe("captureStderr", () => {
368
365
  });
369
366
  });
370
367
 
371
- describe("validateCliPresence", () => {
372
- it("does not throw when claude --version succeeds", () => {
373
- (execSync as any).mockReturnValue(Buffer.from("1.0.0"));
374
- expect(() => validateCliPresence()).not.toThrow();
375
- });
376
-
377
- it("throws with install instructions when claude --version fails", () => {
378
- (execSync as any).mockImplementation(() => {
379
- throw new Error("command not found");
380
- });
381
-
382
- expect(() => validateCliPresence()).toThrow();
383
- try {
384
- validateCliPresence();
385
- } catch (e: any) {
386
- expect(e.message).toContain("Claude Code CLI not found");
387
- expect(e.message).toContain("npm install");
388
- }
389
- });
390
- });
391
-
392
- describe("validateCliAuth", () => {
393
- it("returns true when claude auth status succeeds", () => {
394
- (execSync as any).mockReturnValue(Buffer.from("Logged in"));
395
- expect(validateCliAuth()).toBe(true);
396
- });
397
-
398
- it("returns false and warns when claude auth status fails", () => {
399
- (execSync as any).mockImplementation(() => {
400
- throw new Error("not authenticated");
401
- });
402
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
403
-
404
- expect(validateCliAuth()).toBe(false);
405
- expect(warnSpy).toHaveBeenCalledWith(
406
- expect.stringContaining("not authenticated"),
407
- );
408
- warnSpy.mockRestore();
409
- });
410
- });
411
-
412
368
  describe("validateCliPresenceAsync", () => {
413
369
  beforeEach(() => {
414
370
  vi.clearAllMocks();
@@ -458,6 +414,16 @@ describe("validateCliPresenceAsync", () => {
458
414
  const result = await validateCliPresenceAsync();
459
415
  expect(result.ok).toBe(false);
460
416
  });
417
+
418
+ it("resolves ok=false instead of rejecting when claude spawn throws synchronously", async () => {
419
+ (spawn as any).mockImplementationOnce(() => {
420
+ throw new Error("Real AI CLI launch blocked during tests: claude --version");
421
+ });
422
+
423
+ await expect(validateCliPresenceAsync()).resolves.toMatchObject({
424
+ ok: false,
425
+ });
426
+ });
461
427
  });
462
428
 
463
429
  describe("validateCliAuthAsync", () => {
@@ -495,6 +461,21 @@ describe("validateCliAuthAsync", () => {
495
461
  );
496
462
  warnSpy.mockRestore();
497
463
  });
464
+
465
+ it("resolves false instead of rejecting when claude auth spawn throws synchronously", async () => {
466
+ (spawn as any).mockImplementationOnce(() => {
467
+ throw new Error(
468
+ "Real AI CLI launch blocked during tests: claude auth status",
469
+ );
470
+ });
471
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
472
+
473
+ await expect(validateCliAuthAsync()).resolves.toBe(false);
474
+ expect(warnSpy).toHaveBeenCalledWith(
475
+ expect.stringContaining("not authenticated"),
476
+ );
477
+ warnSpy.mockRestore();
478
+ });
498
479
  });
499
480
 
500
481
  describe("CLI flags", () => {
@@ -20,7 +20,6 @@ vi.mock("node:child_process", () => ({
20
20
  (proc as any).pid = 99999;
21
21
  return proc;
22
22
  }),
23
- execSync: vi.fn(() => Buffer.from("1.0.0")),
24
23
  }));
25
24
 
26
25
  // Mock @earendil-works/pi-ai