specrails-desktop 2.26.2 → 2.28.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 (319) hide show
  1. package/client/dist/assets/{ActivityFeedPage-bDe4VitF.js → ActivityFeedPage-DcMmDAP3.js} +1 -1
  2. package/client/dist/assets/AgentBrowserCapture-CZ3oc1UL.js +1 -0
  3. package/client/dist/assets/AgentIntegrationsModal-DkqooCFn.js +1 -0
  4. package/client/dist/assets/{AgentModeCodePane-Ci39DcOo.js → AgentModeCodePane-CM77ljte.js} +2 -2
  5. package/client/dist/assets/AgentModeJobsPane-DbZzogbi.js +1 -0
  6. package/client/dist/assets/AgentsPage-BZIml7eL.js +86 -0
  7. package/client/dist/assets/{AnalyticsPage-Da21wWWw.js → AnalyticsPage-B4qflNxi.js} +1 -1
  8. package/client/dist/assets/{BarChart-BiLTq3uC.js → BarChart-Ckstq9fl.js} +1 -1
  9. package/client/dist/assets/CodePage-DFZXLbI1.js +2 -0
  10. package/client/dist/assets/{DesktopAnalyticsPage-COkzgMmd.js → DesktopAnalyticsPage-DbgMa8OO.js} +1 -1
  11. package/client/dist/assets/DocsDialog-BQc9Kwev.js +11 -0
  12. package/client/dist/assets/DocsPage-OsV44ECy.js +11 -0
  13. package/client/dist/assets/{ExportDropdown-DDhJH-Jr.js → ExportDropdown-a43XHO3Y.js} +1 -1
  14. package/client/dist/assets/IntegrationsPage-C3E8hZhB.js +1 -0
  15. package/client/dist/assets/InteractiveJobComposer-zj8SrOAl.js +19 -0
  16. package/client/dist/assets/JobDetailModal-Cm6ZeVoh.js +1 -0
  17. package/client/dist/assets/JobDetailPage-DN67LsdX.js +1 -0
  18. package/client/dist/assets/JobsPage-DF9wKGbw.js +1 -0
  19. package/client/dist/assets/{LoopBuilderPage-DsLLrtHa.js → LoopBuilderPage-RJ6GGjli.js} +2 -2
  20. package/client/dist/assets/{LoopPreviewModal-C-fDEgnu.js → LoopPreviewModal-B2wmifF6.js} +1 -1
  21. package/client/dist/assets/LoopsPage-DdlvH-dJ.js +1 -0
  22. package/client/dist/assets/PluginsPage-9DI0a6jI.js +2 -0
  23. package/client/dist/assets/ProjectSettingsDialog-dgy5hqPd.js +1 -0
  24. package/client/dist/assets/{TemplatePreviewModal-DcHFbLA8.js → TemplatePreviewModal-CERf1IYB.js} +1 -1
  25. package/client/dist/assets/agent-BRJCESkc2.js +1 -0
  26. package/client/dist/assets/agent-BuYz6fJd.js +1 -0
  27. package/client/dist/assets/agent-CxnRzL6Z.js +1 -0
  28. package/client/dist/assets/agent-D4JAuEGN2.js +1 -0
  29. package/client/dist/assets/agent-DNQlI8EN.js +1 -0
  30. package/client/dist/assets/agent-DyzUH89R.js +1 -0
  31. package/client/dist/assets/agent-MaTxBlg5.js +1 -0
  32. package/client/dist/assets/agent-ostmMYNz2.js +1 -0
  33. package/client/dist/assets/{brain-BBNUUrUI.js → brain-BPbhM1Jx.js} +1 -1
  34. package/client/dist/assets/dashboard-5C2FOZQX.js +1 -0
  35. package/client/dist/assets/{dashboard-C6w0rICe.js → dashboard-B6iQ-IL_.js} +1 -1
  36. package/client/dist/assets/dashboard-BFfnCtq52.js +1 -0
  37. package/client/dist/assets/dashboard-BIC8KtRf2.js +1 -0
  38. package/client/dist/assets/dashboard-CEgg9NAr.js +1 -0
  39. package/client/dist/assets/dashboard-CiOyDh9L.js +1 -0
  40. package/client/dist/assets/dashboard-D6x5OnNj2.js +1 -0
  41. package/client/dist/assets/dashboard-DBuLfWo9.js +1 -0
  42. package/client/dist/assets/{dist-js-BxjTIvdT.js → dist-js-Byjcju1W.js} +1 -1
  43. package/client/dist/assets/{dist-js-DdP_Gl3p.js → dist-js-Dh-9lbLD.js} +1 -1
  44. package/client/dist/assets/index-89SuhD4Y.js +158 -0
  45. package/client/dist/assets/index-DOGzWJhO.css +2 -0
  46. package/client/dist/assets/integrations-By80EBkf.js +1 -0
  47. package/client/dist/assets/integrations-CAjq5tdW2.js +1 -0
  48. package/client/dist/assets/integrations-CG2dp3Kd.js +1 -0
  49. package/client/dist/assets/integrations-CP_aiV_8.js +1 -0
  50. package/client/dist/assets/integrations-CUNe3t9l2.js +1 -0
  51. package/client/dist/assets/integrations-CWjh5VRk.js +1 -0
  52. package/client/dist/assets/integrations-JOHSCvB3.js +1 -0
  53. package/client/dist/assets/integrations-RJfzXkPx2.js +1 -0
  54. package/client/dist/assets/{lib-BviHZ_gK.js → lib-yKyTNJ1c.js} +1 -1
  55. package/client/dist/assets/nav-B2_ToNqS.js +1 -0
  56. package/client/dist/assets/nav-BAMkVEId.js +1 -0
  57. package/client/dist/assets/nav-BTg8s-cD.js +1 -0
  58. package/client/dist/assets/nav-BY_4EWRv.js +1 -0
  59. package/client/dist/assets/nav-CVXtv0wX2.js +1 -0
  60. package/client/dist/assets/nav-D0Erv9ZJ2.js +1 -0
  61. package/client/dist/assets/nav-DUuw9VIU2.js +1 -0
  62. package/client/dist/assets/nav-S4HM94vW.js +1 -0
  63. package/client/dist/assets/settings-BYCMtxOz.js +1 -0
  64. package/client/dist/assets/{settings-BHdPchJP.js → settings-B_v3zgJf.js} +1 -1
  65. package/client/dist/assets/{settings-BeCczBFS2.js → settings-BemR_e3F.js} +1 -1
  66. package/client/dist/assets/{settings-D54_cgl12.js → settings-Bp24JHlv2.js} +1 -1
  67. package/client/dist/assets/{settings-C_nY14Yz.js → settings-BvIwQ3A1.js} +1 -1
  68. package/client/dist/assets/{settings-BlLi719f.js → settings-Bx2ObkTR2.js} +1 -1
  69. package/client/dist/assets/{settings-CaZ26a0S.js → settings-C_T0-j9d.js} +1 -1
  70. package/client/dist/assets/{settings-cn0tbve62.js → settings-DhaFKpcw2.js} +1 -1
  71. package/client/dist/assets/{upload-CRuuzzvp.js → upload-Qh1NE3xi.js} +1 -1
  72. package/client/dist/assets/{value-CIr5hDMt.js → value-BMPeyInx.js} +1 -1
  73. package/client/dist/index.html +29 -29
  74. package/package.json +1 -1
  75. package/server/dist/agent-chat-router.js +2 -0
  76. package/server/dist/agent-operator-prompt.js +17 -1
  77. package/server/dist/db.js +51 -2
  78. package/server/dist/dev-ports.js +35 -0
  79. package/server/dist/global-plugins-router.js +62 -0
  80. package/server/dist/headroom-manager.js +971 -0
  81. package/server/dist/headroom-routing.js +53 -0
  82. package/server/dist/index.js +9 -6
  83. package/server/dist/mcp/guide.js +3 -0
  84. package/server/dist/mcp/tools/catalog.js +2 -0
  85. package/server/dist/mcp/tools/env.js +72 -0
  86. package/server/dist/mcp/tools/jobs.js +110 -2
  87. package/server/dist/path-resolver.js +84 -18
  88. package/server/dist/project-env.js +30 -0
  89. package/server/dist/project-registry.js +4 -2
  90. package/server/dist/project-router-jobs.js +126 -0
  91. package/server/dist/project-router-settings.js +10 -1
  92. package/server/dist/queue-manager.js +9 -1
  93. package/server/dist/rail-isolated-launch.js +5 -4
  94. package/server/dist/rail-pr-decision.js +27 -17
  95. package/server/dist/rail-pr-store.js +36 -0
  96. package/server/dist/rails-router.js +7 -1
  97. package/server/dist/setup-manager.js +32 -29
  98. package/server/dist/setup-prerequisites.js +3 -1
  99. package/server/dist/transient-children.js +266 -9
  100. package/server/dist/util/cli-prompt.js +3 -0
  101. package/server/dist/workspace-resolution.js +3 -3
  102. package/server/dist/worktree-env-discovery.js +172 -0
  103. package/client/dist/assets/AgentBrowserCapture-DQyeEm2V.js +0 -1
  104. package/client/dist/assets/AgentModeJobsPane-C-g1WNVG.js +0 -1
  105. package/client/dist/assets/AgentsPage-BWgglK7V.js +0 -86
  106. package/client/dist/assets/CodePage-BUcAp7SC.js +0 -2
  107. package/client/dist/assets/DocsDialog-PZHMN18B.js +0 -11
  108. package/client/dist/assets/DocsPage-D3fChfkj.js +0 -11
  109. package/client/dist/assets/IntegrationsPage-F87uxxVH.js +0 -3
  110. package/client/dist/assets/InteractiveJobComposer-DqDCiCa0.js +0 -19
  111. package/client/dist/assets/JobDetailModal-BWJR60fi.js +0 -1
  112. package/client/dist/assets/JobDetailPage-CjWE_jyd.js +0 -1
  113. package/client/dist/assets/JobsPage-LmnXkyCa.js +0 -1
  114. package/client/dist/assets/LoopsPage-DDNO9h9T.js +0 -1
  115. package/client/dist/assets/ProjectSettingsDialog-CX854C8e.js +0 -1
  116. package/client/dist/assets/agent-BI6vr-To.js +0 -1
  117. package/client/dist/assets/agent-BUGXI3Ok2.js +0 -1
  118. package/client/dist/assets/agent-Bph_-xt7.js +0 -1
  119. package/client/dist/assets/agent-Bvy2Csyd.js +0 -1
  120. package/client/dist/assets/agent-Bwkyg2oX2.js +0 -1
  121. package/client/dist/assets/agent-CLCyDaZh.js +0 -1
  122. package/client/dist/assets/agent-Cqs14QMr2.js +0 -1
  123. package/client/dist/assets/agent-DJ0EBUuP.js +0 -1
  124. package/client/dist/assets/dashboard-BjjS1OrL.js +0 -1
  125. package/client/dist/assets/dashboard-Bk6oSk1H2.js +0 -1
  126. package/client/dist/assets/dashboard-BrH-GdOg2.js +0 -1
  127. package/client/dist/assets/dashboard-BurhUI8_.js +0 -1
  128. package/client/dist/assets/dashboard-CAUnfDQL2.js +0 -1
  129. package/client/dist/assets/dashboard-C_ljpeYO.js +0 -1
  130. package/client/dist/assets/dashboard-D6OqDdj6.js +0 -1
  131. package/client/dist/assets/index-C695aPdV.css +0 -2
  132. package/client/dist/assets/index-Dw1N2-ci.js +0 -156
  133. package/client/dist/assets/integrations-4KZnYECp.js +0 -1
  134. package/client/dist/assets/integrations-7W266oqF2.js +0 -1
  135. package/client/dist/assets/integrations-BNuauK3o.js +0 -1
  136. package/client/dist/assets/integrations-CHy8tnpV.js +0 -1
  137. package/client/dist/assets/integrations-Cl_wD5AI.js +0 -1
  138. package/client/dist/assets/integrations-CuWnMR5L.js +0 -1
  139. package/client/dist/assets/integrations-Cy8nZ_dv2.js +0 -1
  140. package/client/dist/assets/integrations-zMqY4VU12.js +0 -1
  141. package/client/dist/assets/nav-40HzpIFb.js +0 -1
  142. package/client/dist/assets/nav-8yPhBn00.js +0 -1
  143. package/client/dist/assets/nav-B6PkGfTk2.js +0 -1
  144. package/client/dist/assets/nav-BHqMLrS72.js +0 -1
  145. package/client/dist/assets/nav-CGy4yx3C.js +0 -1
  146. package/client/dist/assets/nav-CXEDcRdv2.js +0 -1
  147. package/client/dist/assets/nav-CzKzuwH8.js +0 -1
  148. package/client/dist/assets/nav-EI_OhEAo.js +0 -1
  149. package/client/dist/assets/settings-D9bDDe2O.js +0 -1
  150. /package/client/dist/assets/{activity-BZNfOdiS.js → activity-BZZlEG3a.js} +0 -0
  151. /package/client/dist/assets/{activity--iN4Ad2R2.js → activity-BbWiDfn-2.js} +0 -0
  152. /package/client/dist/assets/{activity-CIOLxGvh.js → activity-D3Ic-HTS.js} +0 -0
  153. /package/client/dist/assets/{activity-DtxrAUMo2.js → activity-DFbvNqhE2.js} +0 -0
  154. /package/client/dist/assets/{activity-D8vQJrpW.js → activity-Dbl71S-E.js} +0 -0
  155. /package/client/dist/assets/{activity-BuVo5sPB.js → activity-DtzRy92x.js} +0 -0
  156. /package/client/dist/assets/{activity-6hM_ZSg9.js → activity-DvECTYGt.js} +0 -0
  157. /package/client/dist/assets/{activity-D2r2h3SE2.js → activity-FleOlK0l2.js} +0 -0
  158. /package/client/dist/assets/{addspec-D-JLDsUV.js → addspec-BKrH2Ucz.js} +0 -0
  159. /package/client/dist/assets/{addspec-DfhDbg5c.js → addspec-B_LQMHW3.js} +0 -0
  160. /package/client/dist/assets/{addspec-bsNDdqRm2.js → addspec-Bw_YQwo62.js} +0 -0
  161. /package/client/dist/assets/{addspec-BMtLZLW62.js → addspec-CFxGvmJR2.js} +0 -0
  162. /package/client/dist/assets/{addspec-mgPOt5q02.js → addspec-CR8MB5Fr2.js} +0 -0
  163. /package/client/dist/assets/{addspec-CP0HRuxY.js → addspec-DaA-CbTt.js} +0 -0
  164. /package/client/dist/assets/{addspec-DxW6oPsk.js → addspec-DrXqP8ip.js} +0 -0
  165. /package/client/dist/assets/{addspec-CRuvWn8d.js → addspec-NBbcTbLc.js} +0 -0
  166. /package/client/dist/assets/{agents-BpoUaoW92.js → agents-B9pFSJQI2.js} +0 -0
  167. /package/client/dist/assets/{agents-78bTZGZh.js → agents-BHgn--3j.js} +0 -0
  168. /package/client/dist/assets/{agents-BUdEi3-N.js → agents-B_qGaB0F.js} +0 -0
  169. /package/client/dist/assets/{agents-Cev2oFfX.js → agents-CGDn_3kB.js} +0 -0
  170. /package/client/dist/assets/{agents-bvyRLczL.js → agents-CP8_ezYG.js} +0 -0
  171. /package/client/dist/assets/{agents-BLhWCmDS2.js → agents-CV-_xBvX2.js} +0 -0
  172. /package/client/dist/assets/{agents-CWo-KyAB.js → agents-HFCqQH0Z.js} +0 -0
  173. /package/client/dist/assets/{agents-DDtCJ5M02.js → agents-VsyFhGOS2.js} +0 -0
  174. /package/client/dist/assets/{agentstudio-BMQxc_bz.js → agentstudio-BEp7Sb46.js} +0 -0
  175. /package/client/dist/assets/{agentstudio-Dtq3dGYk.js → agentstudio-BOjG6tgJ.js} +0 -0
  176. /package/client/dist/assets/{agentstudio-C_Jq8Tl4.js → agentstudio-Br3WD7Xs.js} +0 -0
  177. /package/client/dist/assets/{agentstudio-sI27TpBu2.js → agentstudio-CuTt3ZGe2.js} +0 -0
  178. /package/client/dist/assets/{agentstudio-CWgXG5W52.js → agentstudio-D2A5peWV2.js} +0 -0
  179. /package/client/dist/assets/{agentstudio-CZm2rOzt.js → agentstudio-Dxe_94PK.js} +0 -0
  180. /package/client/dist/assets/{agentstudio-COZriimI2.js → agentstudio-RfZavULt2.js} +0 -0
  181. /package/client/dist/assets/{agentstudio-jKMTdT70.js → agentstudio-RsxnZtUA.js} +0 -0
  182. /package/client/dist/assets/{aiedit-igmw-ZoZ2.js → aiedit-26p5A_HY2.js} +0 -0
  183. /package/client/dist/assets/{aiedit-6CHdb_iv2.js → aiedit-CV520D4W2.js} +0 -0
  184. /package/client/dist/assets/{aiedit-pc5JDUq1.js → aiedit-CcugGPbS.js} +0 -0
  185. /package/client/dist/assets/{aiedit-eMN4eQPF.js → aiedit-D0Syl-mC.js} +0 -0
  186. /package/client/dist/assets/{aiedit-hknZlly82.js → aiedit-DnBzjlVz2.js} +0 -0
  187. /package/client/dist/assets/{aiedit-B03pD_Na.js → aiedit-DrIjSojo.js} +0 -0
  188. /package/client/dist/assets/{aiedit-DYtnZU9H.js → aiedit-DrfHQnlz.js} +0 -0
  189. /package/client/dist/assets/{aiedit-BtRqRxML.js → aiedit-a-Z-g5iC.js} +0 -0
  190. /package/client/dist/assets/{analytics-C5dpiS0o.js → analytics-4-eskbtX.js} +0 -0
  191. /package/client/dist/assets/{analytics-CR1TJqUi2.js → analytics-4HvXb0Vn2.js} +0 -0
  192. /package/client/dist/assets/{analytics-rmHuJv0V.js → analytics-Bd9rHGjV.js} +0 -0
  193. /package/client/dist/assets/{analytics-BWMUZ2Pq2.js → analytics-Bpe41fnX2.js} +0 -0
  194. /package/client/dist/assets/{analytics-Bj-SmKHa.js → analytics-CpSTNfui.js} +0 -0
  195. /package/client/dist/assets/{analytics-Ce-sPORu.js → analytics-CxDYcBl9.js} +0 -0
  196. /package/client/dist/assets/{analytics-CCprAqk3.js → analytics-DEe5BMz8.js} +0 -0
  197. /package/client/dist/assets/{analytics-DfNdXR0y2.js → analytics-DcOITHla2.js} +0 -0
  198. /package/client/dist/assets/{attachments-BjyMmrwE.js → attachments-CA2f3qL3.js} +0 -0
  199. /package/client/dist/assets/{attachments-DCZOL1RU.js → attachments-CHDbhKeH.js} +0 -0
  200. /package/client/dist/assets/{attachments-rNn1ekRy2.js → attachments-CKAY5aYG2.js} +0 -0
  201. /package/client/dist/assets/{attachments-BgBNSqQd.js → attachments-CgOQ9K-6.js} +0 -0
  202. /package/client/dist/assets/{attachments-DPCmkFZu2.js → attachments-D2jwJXQ42.js} +0 -0
  203. /package/client/dist/assets/{attachments-C9GTTJsx2.js → attachments-DlEQu56g2.js} +0 -0
  204. /package/client/dist/assets/{attachments-Ci6_42Kh.js → attachments-DvxQbBTC.js} +0 -0
  205. /package/client/dist/assets/{attachments-DL-bPy9E.js → attachments-jVdKzAXn.js} +0 -0
  206. /package/client/dist/assets/{auth-Cvsb7-Ek.js → auth-DpZq5p1e.js} +0 -0
  207. /package/client/dist/assets/{browser-Cvg-QHvd2.js → browser-BQFDxrRq.js} +0 -0
  208. /package/client/dist/assets/{browser-rWKj-RR_2.js → browser-BbJ_q_OT2.js} +0 -0
  209. /package/client/dist/assets/{browser-Cc0iDqUi.js → browser-C4VJ5CW12.js} +0 -0
  210. /package/client/dist/assets/{browser-BPkhILSJ.js → browser-Cl-FwPXs.js} +0 -0
  211. /package/client/dist/assets/{browser-CdS6O1Qb.js → browser-DM8G151q.js} +0 -0
  212. /package/client/dist/assets/{browser-FRv8P02E.js → browser-Do2jf8_N.js} +0 -0
  213. /package/client/dist/assets/{browser-BqLqJVVT.js → browser-JsiohNmW.js} +0 -0
  214. /package/client/dist/assets/{browser-BijVYG0Y2.js → browser-kA31eYhu2.js} +0 -0
  215. /package/client/dist/assets/{chat-VMflhzxn.js → chat-0dg5hOo_.js} +0 -0
  216. /package/client/dist/assets/{chat-DdwZjT8K2.js → chat-BMWD_Rx12.js} +0 -0
  217. /package/client/dist/assets/{chat-Bom6bqK3.js → chat-BalviUAD.js} +0 -0
  218. /package/client/dist/assets/{chat-DsbwJhus.js → chat-CJddjN3D2.js} +0 -0
  219. /package/client/dist/assets/{chat-Dnwg-NpQ.js → chat-D5xEtsf3.js} +0 -0
  220. /package/client/dist/assets/{chat-vylxqR0P2.js → chat-Ds8GvCnv.js} +0 -0
  221. /package/client/dist/assets/{chat-BgL3zv6x2.js → chat-SaG-ng1x2.js} +0 -0
  222. /package/client/dist/assets/{chat-Bh3NdgJs.js → chat-n43nZGvn.js} +0 -0
  223. /package/client/dist/assets/{code-thSEptMx.js → code-B62fClk-.js} +0 -0
  224. /package/client/dist/assets/{code-DzC1rMk12.js → code-Bit_z2Xd2.js} +0 -0
  225. /package/client/dist/assets/{code-BRcDlze52.js → code-BotxM43N2.js} +0 -0
  226. /package/client/dist/assets/{code-UPVvmAKM2.js → code-C9U6M5-e2.js} +0 -0
  227. /package/client/dist/assets/{code-BdMy3Ffq.js → code-CIPmDEd0.js} +0 -0
  228. /package/client/dist/assets/{code-DgeHz9CB.js → code-DUe1Bouz.js} +0 -0
  229. /package/client/dist/assets/{code-CwnBPPyA.js → code-F_KjccGf.js} +0 -0
  230. /package/client/dist/assets/{code-CeQ0FZpC.js → code-HDK4P6Rx.js} +0 -0
  231. /package/client/dist/assets/{commands-DkU0qliq.js → commands-BFQZc9AG.js} +0 -0
  232. /package/client/dist/assets/{commands-u-4eEHCN.js → commands-BMkC5-ki.js} +0 -0
  233. /package/client/dist/assets/{commands-CSDs9q_2.js → commands-BllkJauO.js} +0 -0
  234. /package/client/dist/assets/{commands-xHDDGfcP2.js → commands-CWX9WmDX2.js} +0 -0
  235. /package/client/dist/assets/{commands-C4Y3C-PF2.js → commands-CzFSoIiq2.js} +0 -0
  236. /package/client/dist/assets/{commands-CckaBWdV.js → commands-DCJ-R9E4.js} +0 -0
  237. /package/client/dist/assets/{commands-JWMfGDmr.js → commands-KBbhsnw8.js} +0 -0
  238. /package/client/dist/assets/{commands-RHOchN4n2.js → commands-cKTaLwwV2.js} +0 -0
  239. /package/client/dist/assets/{common-DmqBPwZG.js → common-BfQD173j.js} +0 -0
  240. /package/client/dist/assets/{common-DhWTkocg.js → common-CF8iGXrG.js} +0 -0
  241. /package/client/dist/assets/{common-Bwafujnc2.js → common-Cfmwcrl52.js} +0 -0
  242. /package/client/dist/assets/{common-C-vU-j-I.js → common-D2cimsul.js} +0 -0
  243. /package/client/dist/assets/{common-C5HKSbbz.js → common-DH7fjyL1.js} +0 -0
  244. /package/client/dist/assets/{common-CYaT2yHp.js → common-DXrmj6WD.js} +0 -0
  245. /package/client/dist/assets/{common-BM3LFLCE2.js → common-DZbXlmq72.js} +0 -0
  246. /package/client/dist/assets/{common-DiPTT7V_2.js → common-DtXmzX2o2.js} +0 -0
  247. /package/client/dist/assets/{explore-UvW1jEMa.js → explore-BEpIs343.js} +0 -0
  248. /package/client/dist/assets/{explore-CNBzrEPS.js → explore-BO_OinBD.js} +0 -0
  249. /package/client/dist/assets/{explore-C0k8HrkN.js → explore-Bhy7WWXv.js} +0 -0
  250. /package/client/dist/assets/{explore-B7fblXBb2.js → explore-CAc8Qzu32.js} +0 -0
  251. /package/client/dist/assets/{explore-D_mWBYCS2.js → explore-DCZL9gRN2.js} +0 -0
  252. /package/client/dist/assets/{explore-Dahc1lOx.js → explore-DW_YTkQh.js} +0 -0
  253. /package/client/dist/assets/{explore-BHtEpoNN2.js → explore-ELbrSZyQ2.js} +0 -0
  254. /package/client/dist/assets/{explore-BKVV5JSu.js → explore-_XyU5e7R.js} +0 -0
  255. /package/client/dist/assets/{format-command-CuWuRvka.js → format-command-Db1viey1.js} +0 -0
  256. /package/client/dist/assets/{jira-uNH1gETL2.js → jira-B5wszJ1-2.js} +0 -0
  257. /package/client/dist/assets/{jira-BHAKtc4B.js → jira-C5UjxHE4.js} +0 -0
  258. /package/client/dist/assets/{jira-D5efgwp_.js → jira-CQ7mrZde.js} +0 -0
  259. /package/client/dist/assets/{jira-bDs7Im2C.js → jira-DIRkZk9U.js} +0 -0
  260. /package/client/dist/assets/{jira-CPpgA-5z2.js → jira-DTYu3hJi2.js} +0 -0
  261. /package/client/dist/assets/{jira-B3613-gj.js → jira-DeIjwCK3.js} +0 -0
  262. /package/client/dist/assets/{jira-DS7ownDy.js → jira-DtZpokjJ.js} +0 -0
  263. /package/client/dist/assets/{jira-Be369RIR2.js → jira-ZdKb-50F2.js} +0 -0
  264. /package/client/dist/assets/{jobs-CE8qeIYj.js → jobs-BFIdeDTw.js} +0 -0
  265. /package/client/dist/assets/{jobs-BgQzB0Sx2.js → jobs-BJ-Gcao22.js} +0 -0
  266. /package/client/dist/assets/{jobs-1GV2-4zd2.js → jobs-DAGiPVIS2.js} +0 -0
  267. /package/client/dist/assets/{jobs-26F9gGcE.js → jobs-DAU8C_tZ.js} +0 -0
  268. /package/client/dist/assets/{jobs-BqWui9_f.js → jobs-Da0GaaSc.js} +0 -0
  269. /package/client/dist/assets/{jobs-D5sZzgLs.js → jobs-DrYmsKy5.js} +0 -0
  270. /package/client/dist/assets/{jobs-Wh7dn28q2.js → jobs-P3unxtxo2.js} +0 -0
  271. /package/client/dist/assets/{jobs-DzeuASRf.js → jobs-p360zdf1.js} +0 -0
  272. /package/client/dist/assets/{loops-CvOWsi5r.js → loops-Bi4ywYGn.js} +0 -0
  273. /package/client/dist/assets/{loops-BvqQ-bHR2.js → loops-BjK8lmoZ2.js} +0 -0
  274. /package/client/dist/assets/{loops-Brnx4Fji.js → loops-CO6q75W8.js} +0 -0
  275. /package/client/dist/assets/{loops-K5_HXfJa.js → loops-D3wdUkZV.js} +0 -0
  276. /package/client/dist/assets/{loops-D6D3XHjj.js → loops-DDs4KPu_.js} +0 -0
  277. /package/client/dist/assets/{loops-LIgO_-eM.js → loops-DKzYFcq6.js} +0 -0
  278. /package/client/dist/assets/{loops-DJNhp17p2.js → loops-Du-bNVpc2.js} +0 -0
  279. /package/client/dist/assets/{loops-vhJdbcz92.js → loops-DvGbSfxw2.js} +0 -0
  280. /package/client/dist/assets/{mcp-XJNRfxVp2.js → mcp-BG-OCJca2.js} +0 -0
  281. /package/client/dist/assets/{mcp-DtbxuH3V2.js → mcp-CbJ04oFS2.js} +0 -0
  282. /package/client/dist/assets/{mcp-sRmWE8WJ.js → mcp-CrWdq5db.js} +0 -0
  283. /package/client/dist/assets/{mcp-H-gnQUdN.js → mcp-Dd844F5_.js} +0 -0
  284. /package/client/dist/assets/{mcp-Dcl-XxkR.js → mcp-DfmzoT5t.js} +0 -0
  285. /package/client/dist/assets/{mcp-CjVKXAhD2.js → mcp-DyULurYS2.js} +0 -0
  286. /package/client/dist/assets/{mcp-CIH0pH1K.js → mcp-HwBbQDk0.js} +0 -0
  287. /package/client/dist/assets/{mcp-N1nzp8oL.js → mcp-zSJ2TsJj.js} +0 -0
  288. /package/client/dist/assets/{setup-BuQNKztt2.js → setup-1vN48B-m2.js} +0 -0
  289. /package/client/dist/assets/{setup-BwMovM9z2.js → setup-BmJ0Kx16.js} +0 -0
  290. /package/client/dist/assets/{setup-RiUHzq7E.js → setup-D2CmgAro.js} +0 -0
  291. /package/client/dist/assets/{setup-BXpw07r-.js → setup-DQWsu2TR.js} +0 -0
  292. /package/client/dist/assets/{setup-D84y3wx-.js → setup-JeBwoXoM.js} +0 -0
  293. /package/client/dist/assets/{setup-DU0ULX8q2.js → setup-T_ApeQDV2.js} +0 -0
  294. /package/client/dist/assets/{setup-BiduSX9d.js → setup-mXnzww4f.js} +0 -0
  295. /package/client/dist/assets/{setup-7J8TjG4O.js → setup-q0wRHUp1.js} +0 -0
  296. /package/client/dist/assets/{specs-CWtKcVDC.js → specs-BGRKhM8t.js} +0 -0
  297. /package/client/dist/assets/{specs-CBDvEYKv.js → specs-C6J2zrLM.js} +0 -0
  298. /package/client/dist/assets/{specs-D2clrt2c2.js → specs-CT8exGOy2.js} +0 -0
  299. /package/client/dist/assets/{specs-C9Sd7BhH2.js → specs-Ce5y2iFc2.js} +0 -0
  300. /package/client/dist/assets/{specs-CIPr9reA2.js → specs-DZDoZsDq.js} +0 -0
  301. /package/client/dist/assets/{specs-PozUNhqb.js → specs-DbtIzV8a.js} +0 -0
  302. /package/client/dist/assets/{specs-DTplt2XM.js → specs-GkiL-snY.js} +0 -0
  303. /package/client/dist/assets/{specs-CmEZY57v.js → specs-PGIOzMtc.js} +0 -0
  304. /package/client/dist/assets/{terminal-xGbCZJHg.js → terminal-BNSg2lR7.js} +0 -0
  305. /package/client/dist/assets/{terminal-De2Brd23.js → terminal-BTpE7iki.js} +0 -0
  306. /package/client/dist/assets/{terminal-DFPNRLKp.js → terminal-CFt56A5f.js} +0 -0
  307. /package/client/dist/assets/{terminal-BRm03iFA2.js → terminal-Cvabyzm42.js} +0 -0
  308. /package/client/dist/assets/{terminal-DxQWCqSD2.js → terminal-D0z12_sJ2.js} +0 -0
  309. /package/client/dist/assets/{terminal-Z7KsDM_n.js → terminal-DDk_bTw0.js} +0 -0
  310. /package/client/dist/assets/{terminal-cqfd3EDV.js → terminal-DEyt0prF.js} +0 -0
  311. /package/client/dist/assets/{terminal-WSAvHXoU.js → terminal-a6Uq8B0p.js} +0 -0
  312. /package/client/dist/assets/{tickets-CkfbISuK.js → tickets-BHv3ewvq.js} +0 -0
  313. /package/client/dist/assets/{tickets-C3b8cHhd2.js → tickets-BIxU86j12.js} +0 -0
  314. /package/client/dist/assets/{tickets-SuX_HkK4.js → tickets-C2BhwzwR.js} +0 -0
  315. /package/client/dist/assets/{tickets-DTHo9c3C.js → tickets-D1IeX2EW.js} +0 -0
  316. /package/client/dist/assets/{tickets-BoXom7l8.js → tickets-DZut8Ryt.js} +0 -0
  317. /package/client/dist/assets/{tickets-CWYFvx1W2.js → tickets-LfK6LzKl2.js} +0 -0
  318. /package/client/dist/assets/{tickets-CFXnJsYF.js → tickets-dQfsc7Ph.js} +0 -0
  319. /package/client/dist/assets/{tickets-B7zT4IRf.js → tickets-sVun0I6v.js} +0 -0
@@ -0,0 +1,971 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HeadroomManager = void 0;
7
+ const child_process_1 = require("child_process");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const desktop_db_1 = require("./desktop-db");
12
+ const headroom_routing_1 = require("./headroom-routing");
13
+ const STATE_KEY = 'plugins.headroom.state';
14
+ const DEFAULT_PORT = 8787;
15
+ function nowIso() {
16
+ return new Date().toISOString();
17
+ }
18
+ function toolRoot() {
19
+ return path_1.default.join(os_1.default.homedir(), '.specrails', 'tools');
20
+ }
21
+ function uvToolDir() {
22
+ return path_1.default.join(toolRoot(), 'uv', 'tools');
23
+ }
24
+ function uvCacheDir() {
25
+ return path_1.default.join(toolRoot(), 'uv', 'cache');
26
+ }
27
+ function uvBinDir() {
28
+ return path_1.default.join(toolRoot(), 'bin');
29
+ }
30
+ function headroomExeName() {
31
+ return process.platform === 'win32' ? 'headroom.exe' : 'headroom';
32
+ }
33
+ function fileExists(p) {
34
+ if (!p)
35
+ return false;
36
+ try {
37
+ return fs_1.default.existsSync(p);
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ function bundledUvCandidates() {
44
+ const base = process.env.SPECRAILS_BUNDLED_RUNTIMES_PATH;
45
+ if (!base)
46
+ return [];
47
+ if (process.platform === 'win32') {
48
+ return [
49
+ path_1.default.join(base, 'uv', 'uv.exe'),
50
+ path_1.default.join(base, 'uv', 'bin', 'uv.exe'),
51
+ ];
52
+ }
53
+ return [
54
+ path_1.default.join(base, 'uv', 'bin', 'uv'),
55
+ path_1.default.join(base, 'uv', 'uv'),
56
+ ];
57
+ }
58
+ function which(command) {
59
+ const probe = (0, child_process_1.spawnSync)(process.platform === 'win32' ? 'where' : 'which', [command], {
60
+ encoding: 'utf8',
61
+ timeout: 5000,
62
+ env: process.env,
63
+ });
64
+ if (probe.status !== 0)
65
+ return null;
66
+ return `${probe.stdout ?? ''}`.trim().split(/\r?\n/)[0] || null;
67
+ }
68
+ function parseHeadroomVersion(raw) {
69
+ const match = raw.match(/(\d+\.\d+\.\d+)/);
70
+ return match?.[1] ?? null;
71
+ }
72
+ function issue(code, detail, command) {
73
+ const copy = {
74
+ uv_missing_or_corrupt: {
75
+ title: 'Bundled uv is not available',
76
+ guidance: 'Specrails could not start its bundled uv runtime. Reinstall Specrails or use Repair once the runtime is available.',
77
+ },
78
+ network_unavailable: {
79
+ title: 'Headroom could not be downloaded',
80
+ guidance: 'Check your network connection and retry. Specrails will keep the install isolated in its own tool directory.',
81
+ },
82
+ package_resolution_failed: {
83
+ title: 'Headroom dependencies could not be resolved',
84
+ guidance: 'Retry the install. If it keeps failing, open diagnostics and share the uv output.',
85
+ },
86
+ install_permission_failed: {
87
+ title: 'Specrails cannot write its tool directory',
88
+ guidance: 'Check permissions under ~/.specrails/tools, then retry the install.',
89
+ },
90
+ headroom_not_found_after_install: {
91
+ title: 'Install finished but Headroom was not found',
92
+ guidance: 'Run Repair install. Specrails will reinstall Headroom and verify the executable.',
93
+ },
94
+ learning_setup_failed: {
95
+ title: 'Headroom learning could not be enabled',
96
+ guidance: 'Headroom is installed, but output-savings calibration did not finish. Retry learning from diagnostics or keep using proxy routing while metrics warm up.',
97
+ },
98
+ metrics_unavailable: {
99
+ title: 'Headroom metrics are not available yet',
100
+ guidance: 'Start routing traffic through Headroom. Specrails will refresh token-savings metrics as soon as the proxy reports samples.',
101
+ },
102
+ proxy_port_busy: {
103
+ title: 'Headroom proxy port is already in use',
104
+ guidance: 'Choose another port or stop the process using the current port, then retry activation.',
105
+ },
106
+ proxy_unhealthy: {
107
+ title: 'Headroom proxy did not become healthy',
108
+ guidance: 'Retry activation. If it repeats, open diagnostics to inspect the proxy output.',
109
+ },
110
+ provider_route_failed: {
111
+ title: 'Provider route verification failed',
112
+ guidance: 'Specrails could not verify the provider route. The provider remains inactive; retry after checking diagnostics.',
113
+ },
114
+ provider_config_conflict: {
115
+ title: 'Existing provider configuration conflicts with Headroom',
116
+ guidance: 'Review diagnostics before enabling system-wide activation. Specrails-managed routing remains reversible.',
117
+ },
118
+ activation_partially_applied: {
119
+ title: 'Activation was only partially applied',
120
+ guidance: 'Specrails attempted rollback. Review diagnostics and retry the affected provider.',
121
+ },
122
+ not_installed: {
123
+ title: 'Headroom is not installed',
124
+ guidance: 'Install Headroom first, then activate Codex or Claude.',
125
+ },
126
+ uninstall_failed: {
127
+ title: 'Headroom could not be uninstalled',
128
+ guidance: 'Specrails disabled Headroom routing but could not remove the managed tool. Open diagnostics, then retry uninstall.',
129
+ },
130
+ unknown: {
131
+ title: 'Headroom operation failed',
132
+ guidance: 'Retry the operation. If it repeats, open diagnostics and share the details.',
133
+ },
134
+ };
135
+ return { code, title: copy[code].title, guidance: copy[code].guidance, detail, command };
136
+ }
137
+ function classifyInstallFailure(output, command) {
138
+ const lower = output.toLowerCase();
139
+ if (lower.includes('permission') || lower.includes('eacces') || lower.includes('access is denied')) {
140
+ return issue('install_permission_failed', output, command);
141
+ }
142
+ if (lower.includes('network') ||
143
+ lower.includes('timed out') ||
144
+ lower.includes('temporary failure') ||
145
+ lower.includes('could not resolve') ||
146
+ lower.includes('connection refused') ||
147
+ lower.includes('connection reset')) {
148
+ return issue('network_unavailable', output, command);
149
+ }
150
+ if (lower.includes('no solution') || lower.includes('failed to resolve') || lower.includes('resolution')) {
151
+ return issue('package_resolution_failed', output, command);
152
+ }
153
+ return issue('unknown', output, command);
154
+ }
155
+ function classifyProxyFailure(output, command) {
156
+ const lower = output.toLowerCase();
157
+ if (lower.includes('address already in use') ||
158
+ lower.includes('eaddrinuse') ||
159
+ lower.includes('only one usage of each socket address')) {
160
+ return issue('proxy_port_busy', output, command);
161
+ }
162
+ return issue('proxy_unhealthy', output, command);
163
+ }
164
+ function providerLabel(provider) {
165
+ return provider === 'codex' ? 'Codex' : 'Claude';
166
+ }
167
+ function providerRecord(value) {
168
+ return {
169
+ codex: !!value?.codex,
170
+ claude: !!value?.claude,
171
+ };
172
+ }
173
+ function emptyProviderMetric(provider) {
174
+ return {
175
+ provider,
176
+ label: providerLabel(provider),
177
+ active: false,
178
+ available: false,
179
+ detectedRoute: false,
180
+ requests: 0,
181
+ inputTokens: 0,
182
+ inputTokensSaved: 0,
183
+ outputTokens: 0,
184
+ outputTokensSaved: 0,
185
+ outputSavingsPercent: 0,
186
+ outputSavingsMethod: 'none',
187
+ outputSavingsAllocated: false,
188
+ };
189
+ }
190
+ function emptyMetricsState(issueValue = null) {
191
+ return {
192
+ updatedAt: null,
193
+ proxyStatsAvailable: false,
194
+ durableSavingsAvailable: false,
195
+ outputSavingsAvailable: false,
196
+ outputSavingsMethod: null,
197
+ outputConfidence: null,
198
+ providers: {
199
+ codex: emptyProviderMetric('codex'),
200
+ claude: emptyProviderMetric('claude'),
201
+ },
202
+ lastIssue: issueValue,
203
+ };
204
+ }
205
+ function normalizeProviderKey(value) {
206
+ const raw = String(value ?? '').trim().toLowerCase().replace(/_/g, '-');
207
+ if (!raw)
208
+ return null;
209
+ if (raw === 'codex' || raw === 'codex-cli' || raw === 'openai')
210
+ return 'codex';
211
+ if (raw === 'claude' || raw === 'claude-code' || raw === 'claude-cli' || raw === 'anthropic')
212
+ return 'claude';
213
+ return null;
214
+ }
215
+ function num(value) {
216
+ const parsed = Number(value);
217
+ return Number.isFinite(parsed) ? parsed : 0;
218
+ }
219
+ function maxNumericField(row, fields) {
220
+ return Math.max(0, ...fields.map((field) => Math.trunc(num(row[field]))));
221
+ }
222
+ const INPUT_TOKEN_FIELDS = [
223
+ 'input_tokens',
224
+ 'input_tokens_total',
225
+ 'input_tokens_processed',
226
+ 'total_input_tokens',
227
+ 'prompt_tokens',
228
+ 'prompt_tokens_total',
229
+ 'prompt_tokens_processed',
230
+ 'tokens_in',
231
+ 'tokens_input',
232
+ 'original_tokens',
233
+ 'tokens_original',
234
+ 'tokens_before',
235
+ ];
236
+ const OUTPUT_TOKEN_FIELDS = [
237
+ 'output_tokens',
238
+ 'output_tokens_total',
239
+ 'output_tokens_processed',
240
+ 'total_output_tokens',
241
+ 'completion_tokens',
242
+ 'completion_tokens_total',
243
+ 'tokens_out',
244
+ 'tokens_output',
245
+ ];
246
+ class HeadroomManager {
247
+ db;
248
+ broadcast;
249
+ availableProvidersSupplier;
250
+ proxy = null;
251
+ proxyTail = '';
252
+ routeDetectionCache = null;
253
+ metricsCache = emptyMetricsState();
254
+ metricsRefresh = null;
255
+ constructor(db, broadcast, availableProvidersSupplier = () => ['claude', 'codex']) {
256
+ this.db = db;
257
+ this.broadcast = broadcast;
258
+ this.availableProvidersSupplier = availableProvidersSupplier;
259
+ this.syncRouting();
260
+ }
261
+ getState() {
262
+ const persisted = this.readPersisted();
263
+ const uvPath = this.resolveUvPath();
264
+ const install = this.resolveHeadroomInstall(persisted.executablePath ?? null, !!persisted.ignoreSystemInstall);
265
+ const exe = install?.path ?? null;
266
+ const version = exe ? this.readHeadroomVersion(exe) : null;
267
+ const installed = !!exe && !!version;
268
+ const availableProviders = this.availableProviderRecord();
269
+ const detectedRoutes = installed && exe ? this.detectProviderRoutes(exe) : providerRecord(persisted.detectedRoutes);
270
+ const activeProviders = {
271
+ codex: persisted.activeProviders?.codex ?? (availableProviders.codex && detectedRoutes.codex),
272
+ claude: persisted.activeProviders?.claude ?? (availableProviders.claude && detectedRoutes.claude),
273
+ };
274
+ if (installed) {
275
+ const shouldPersistInstall = persisted.installed !== true ||
276
+ persisted.executablePath !== exe ||
277
+ persisted.version !== version ||
278
+ persisted.installSource !== install?.source ||
279
+ persisted.ignoreSystemInstall === true;
280
+ const shouldPersistRoutes = persisted.detectedRoutes?.codex !== detectedRoutes.codex ||
281
+ persisted.detectedRoutes?.claude !== detectedRoutes.claude ||
282
+ persisted.activeProviders?.codex !== activeProviders.codex ||
283
+ persisted.activeProviders?.claude !== activeProviders.claude;
284
+ if (shouldPersistInstall || shouldPersistRoutes) {
285
+ this.updatePersisted({
286
+ installed: true,
287
+ version,
288
+ executablePath: exe,
289
+ installSource: install?.source ?? null,
290
+ ignoreSystemInstall: false,
291
+ detectedRoutes,
292
+ activeProviders,
293
+ });
294
+ }
295
+ }
296
+ this.refreshMetricsInBackground();
297
+ const learning = {
298
+ enabled: persisted.learningEnabled ?? installed,
299
+ ...this.readLearningBaseline(),
300
+ updatedAt: persisted.learningUpdatedAt ?? null,
301
+ lastIssue: persisted.learningIssue ?? null,
302
+ };
303
+ const metrics = this.decorateMetrics(this.metricsCache, activeProviders, availableProviders, detectedRoutes);
304
+ return {
305
+ installed,
306
+ installSource: installed ? install?.source ?? null : null,
307
+ version: version ?? persisted.version ?? null,
308
+ executablePath: exe,
309
+ uvPath,
310
+ port: this.validPort(persisted.port),
311
+ phase: installed ? (activeProviders.codex || activeProviders.claude ? 'active' : 'installed') : 'idle',
312
+ activeProviders,
313
+ availableProviders,
314
+ detectedRoutes,
315
+ proxyRunning: (activeProviders.codex || activeProviders.claude) && this.isProxyAvailable(),
316
+ proxyPid: this.proxy?.pid ?? null,
317
+ learning,
318
+ metrics,
319
+ lastIssue: persisted.lastIssue ?? null,
320
+ updatedAt: persisted.updatedAt ?? null,
321
+ };
322
+ }
323
+ async getFreshState() {
324
+ await this.refreshMetrics();
325
+ return this.getState();
326
+ }
327
+ async install() {
328
+ const uv = this.resolveUvPath();
329
+ if (!uv) {
330
+ const failure = issue('uv_missing_or_corrupt');
331
+ this.updatePersisted({ lastIssue: failure });
332
+ return { ok: false, state: this.getState(), issue: failure };
333
+ }
334
+ fs_1.default.mkdirSync(uvToolDir(), { recursive: true });
335
+ fs_1.default.mkdirSync(uvCacheDir(), { recursive: true });
336
+ fs_1.default.mkdirSync(uvBinDir(), { recursive: true });
337
+ const args = ['tool', 'install', 'headroom-ai[all]', '--force'];
338
+ const command = `${uv} ${args.join(' ')}`;
339
+ const logs = [`Running ${command}`];
340
+ this.emit('installing', logs[0]);
341
+ const result = await this.runCommand(uv, args, {
342
+ UV_TOOL_DIR: uvToolDir(),
343
+ UV_TOOL_BIN_DIR: uvBinDir(),
344
+ UV_CACHE_DIR: uvCacheDir(),
345
+ UV_NO_PROGRESS: '1',
346
+ }, logs);
347
+ if (result.code !== 0) {
348
+ const failure = classifyInstallFailure(logs.join('\n'), command);
349
+ this.updatePersisted({ lastIssue: failure });
350
+ this.emit('failed', failure.title);
351
+ return { ok: false, state: this.getState(), issue: failure, logs };
352
+ }
353
+ const exe = this.resolveHeadroomInstall(null, false)?.path ?? null;
354
+ const version = exe ? this.readHeadroomVersion(exe) : null;
355
+ if (!exe || !version) {
356
+ const failure = issue('headroom_not_found_after_install', logs.join('\n'), command);
357
+ this.updatePersisted({ lastIssue: failure });
358
+ this.emit('failed', failure.title);
359
+ return { ok: false, state: this.getState(), issue: failure, logs };
360
+ }
361
+ this.updatePersisted({
362
+ installed: true,
363
+ version,
364
+ executablePath: exe,
365
+ installSource: 'managed',
366
+ ignoreSystemInstall: false,
367
+ lastIssue: null,
368
+ });
369
+ await this.enableOutputLearning(exe, logs);
370
+ this.emit('installed', `Headroom ${version} installed`);
371
+ await this.refreshMetrics();
372
+ return { ok: true, state: this.getState(), logs };
373
+ }
374
+ async activate(provider) {
375
+ const state = this.getState();
376
+ if (!state.installed || !state.executablePath) {
377
+ const failure = issue('not_installed');
378
+ this.updatePersisted({ lastIssue: failure });
379
+ return { ok: false, state: this.getState(), issue: failure };
380
+ }
381
+ const proxy = await this.ensureProxy();
382
+ if (!proxy.ok)
383
+ return proxy;
384
+ const current = this.readPersisted();
385
+ const activeProviders = { ...current.activeProviders, [provider]: true };
386
+ this.updatePersisted({ activeProviders, lastIssue: null });
387
+ this.syncRouting();
388
+ if (!this.verifyProviderRoute(provider)) {
389
+ const rolledBack = { ...activeProviders, [provider]: false };
390
+ this.updatePersisted({ activeProviders: rolledBack, lastIssue: issue('provider_route_failed') });
391
+ this.syncRouting();
392
+ return { ok: false, state: this.getState(), issue: issue('provider_route_failed') };
393
+ }
394
+ this.emit('active', `${provider} routed through Headroom`);
395
+ return { ok: true, state: this.getState() };
396
+ }
397
+ async deactivate(provider) {
398
+ const current = this.readPersisted();
399
+ const activeProviders = { ...current.activeProviders, [provider]: false };
400
+ this.updatePersisted({ activeProviders, lastIssue: null });
401
+ this.syncRouting();
402
+ if (!activeProviders.codex && !activeProviders.claude)
403
+ this.stopProxy();
404
+ this.emit('installed', `${provider} Headroom route disabled`);
405
+ return { ok: true, state: this.getState() };
406
+ }
407
+ async uninstall() {
408
+ const before = this.getState();
409
+ this.updatePersisted({
410
+ activeProviders: { codex: false, claude: false },
411
+ lastIssue: null,
412
+ });
413
+ this.stopProxy();
414
+ this.syncRouting();
415
+ if (!before.installed && !before.executablePath) {
416
+ this.updatePersisted({
417
+ installed: false,
418
+ version: null,
419
+ executablePath: null,
420
+ installSource: null,
421
+ activeProviders: { codex: false, claude: false },
422
+ detectedRoutes: { codex: false, claude: false },
423
+ lastIssue: null,
424
+ });
425
+ return { ok: true, state: this.getState() };
426
+ }
427
+ if (before.installSource === 'system') {
428
+ this.updatePersisted({
429
+ installed: false,
430
+ version: null,
431
+ executablePath: null,
432
+ installSource: null,
433
+ ignoreSystemInstall: true,
434
+ activeProviders: { codex: false, claude: false },
435
+ detectedRoutes: { codex: false, claude: false },
436
+ lastIssue: null,
437
+ });
438
+ this.emit('idle', 'External Headroom detached from Specrails');
439
+ return { ok: true, state: this.getState(), logs: ['External Headroom install left untouched; Specrails routing disabled.'] };
440
+ }
441
+ const uv = this.resolveUvPath();
442
+ if (!uv) {
443
+ const failure = issue('uv_missing_or_corrupt');
444
+ this.updatePersisted({ lastIssue: failure });
445
+ return { ok: false, state: this.getState(), issue: failure };
446
+ }
447
+ const args = ['tool', 'uninstall', 'headroom-ai'];
448
+ const command = `${uv} ${args.join(' ')}`;
449
+ const logs = [`Running ${command}`];
450
+ this.emit('installing', logs[0]);
451
+ const result = await this.runCommand(uv, args, {
452
+ UV_TOOL_DIR: uvToolDir(),
453
+ UV_TOOL_BIN_DIR: uvBinDir(),
454
+ UV_CACHE_DIR: uvCacheDir(),
455
+ UV_NO_PROGRESS: '1',
456
+ }, logs);
457
+ const stillInstalled = fileExists(path_1.default.join(uvBinDir(), headroomExeName()));
458
+ if (result.code !== 0 || stillInstalled) {
459
+ const failure = issue('uninstall_failed', logs.join('\n'), command);
460
+ this.updatePersisted({ lastIssue: failure });
461
+ this.emit('failed', failure.title);
462
+ return { ok: false, state: this.getState(), issue: failure, logs };
463
+ }
464
+ this.updatePersisted({
465
+ installed: false,
466
+ version: null,
467
+ executablePath: null,
468
+ installSource: null,
469
+ activeProviders: { codex: false, claude: false },
470
+ detectedRoutes: { codex: false, claude: false },
471
+ lastIssue: null,
472
+ });
473
+ this.emit('idle', 'Headroom uninstalled');
474
+ return { ok: true, state: this.getState(), logs };
475
+ }
476
+ async setPort(port) {
477
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
478
+ const failure = issue('proxy_port_busy', 'Port must be an integer between 1 and 65535.');
479
+ return { ok: false, state: this.getState(), issue: failure };
480
+ }
481
+ const active = this.getState().activeProviders;
482
+ this.stopProxy();
483
+ this.updatePersisted({ port, lastIssue: null });
484
+ this.syncRouting();
485
+ if (active.codex || active.claude) {
486
+ return this.ensureProxy();
487
+ }
488
+ return { ok: true, state: this.getState() };
489
+ }
490
+ diagnostics() {
491
+ const state = this.getState();
492
+ return {
493
+ state,
494
+ toolDir: uvToolDir(),
495
+ binDir: uvBinDir(),
496
+ cacheDir: uvCacheDir(),
497
+ proxyTail: this.proxyTail.slice(-4000),
498
+ installSource: state.installSource,
499
+ detectedRoutes: state.detectedRoutes,
500
+ availableProviders: state.availableProviders,
501
+ learning: state.learning,
502
+ metrics: state.metrics,
503
+ routing: {
504
+ codex: state.activeProviders.codex ? `OPENAI_BASE_URL=http://127.0.0.1:${state.port}/v1` : null,
505
+ claude: state.activeProviders.claude ? `ANTHROPIC_BASE_URL=http://127.0.0.1:${state.port}` : null,
506
+ },
507
+ };
508
+ }
509
+ async startActiveProxyOnBoot() {
510
+ const active = this.getState().activeProviders;
511
+ this.syncRouting();
512
+ if (active.codex || active.claude) {
513
+ const result = await this.ensureProxy();
514
+ if (!result.ok) {
515
+ console.warn('[headroom] boot proxy start failed:', result.issue);
516
+ }
517
+ }
518
+ }
519
+ async ensureProxy() {
520
+ const state = this.getState();
521
+ if (!state.installed || !state.executablePath) {
522
+ const failure = issue('not_installed');
523
+ return { ok: false, state, issue: failure };
524
+ }
525
+ if (this.isProxyRunning())
526
+ return { ok: true, state };
527
+ if (await this.adoptHealthyProxy(state.port)) {
528
+ return { ok: true, state: this.getState() };
529
+ }
530
+ const args = ['proxy', '--host', '127.0.0.1', '--port', String(state.port)];
531
+ const command = `${state.executablePath} ${args.join(' ')}`;
532
+ this.emit('starting-proxy', `Starting proxy on ${state.port}`);
533
+ this.proxyTail = '';
534
+ const child = (0, child_process_1.spawn)(state.executablePath, args, {
535
+ env: {
536
+ ...process.env,
537
+ HEADROOM_PORT: String(state.port),
538
+ HEADROOM_OUTPUT_SHAPER: '1',
539
+ HEADROOM_LEARN: state.learning.enabled ? '1' : '0',
540
+ HEADROOM_SAVINGS_PROFILE: process.env.HEADROOM_SAVINGS_PROFILE ?? 'agent-90',
541
+ HEADROOM_MODE: process.env.HEADROOM_MODE ?? 'token',
542
+ },
543
+ stdio: ['ignore', 'pipe', 'pipe'],
544
+ });
545
+ this.proxy = child;
546
+ child.stdout?.on('data', (chunk) => { this.proxyTail += chunk.toString(); this.trimProxyTail(); });
547
+ child.stderr?.on('data', (chunk) => { this.proxyTail += chunk.toString(); this.trimProxyTail(); });
548
+ child.on('close', () => {
549
+ if (this.proxy === child)
550
+ this.proxy = null;
551
+ });
552
+ const healthy = await this.waitForProxyHealthy(state.port, 6000);
553
+ if (!healthy) {
554
+ const failure = classifyProxyFailure(this.proxyTail, command);
555
+ this.stopProxy();
556
+ this.updatePersisted({ lastIssue: failure });
557
+ this.emit('failed', failure.title);
558
+ return { ok: false, state: this.getState(), issue: failure };
559
+ }
560
+ this.updatePersisted({ lastIssue: null });
561
+ await this.refreshMetrics();
562
+ return { ok: true, state: this.getState() };
563
+ }
564
+ resolveUvPath() {
565
+ for (const p of bundledUvCandidates()) {
566
+ if (fileExists(p))
567
+ return p;
568
+ }
569
+ return which('uv');
570
+ }
571
+ resolveHeadroomInstall(persisted, ignoreSystemInstall) {
572
+ const managed = path_1.default.join(uvBinDir(), headroomExeName());
573
+ if (fileExists(managed))
574
+ return { path: managed, source: 'managed' };
575
+ if (fileExists(persisted)) {
576
+ return { path: persisted, source: persisted === managed ? 'managed' : 'system' };
577
+ }
578
+ if (ignoreSystemInstall)
579
+ return null;
580
+ const system = which('headroom');
581
+ return system ? { path: system, source: 'system' } : null;
582
+ }
583
+ availableProviderRecord() {
584
+ const available = new Set(this.availableProvidersSupplier());
585
+ return {
586
+ codex: available.has('codex'),
587
+ claude: available.has('claude'),
588
+ };
589
+ }
590
+ detectProviderRoutes(exe) {
591
+ const now = Date.now();
592
+ if (this.routeDetectionCache?.executablePath === exe && this.routeDetectionCache.expiresAt > now) {
593
+ return this.routeDetectionCache.routes;
594
+ }
595
+ const routes = { codex: false, claude: false };
596
+ try {
597
+ const state = this.readPersisted();
598
+ const probe = (0, child_process_1.spawnSync)(exe, ['doctor', '--json', '--port', String(this.validPort(state.port))], {
599
+ encoding: 'utf8',
600
+ timeout: 5000,
601
+ env: process.env,
602
+ });
603
+ const raw = `${probe.stdout ?? ''}`.trim();
604
+ const parsed = raw ? JSON.parse(raw) : {};
605
+ for (const check of parsed.checks ?? []) {
606
+ const provider = normalizeProviderKey(check.name);
607
+ if (!provider)
608
+ continue;
609
+ const routed = check.status === 'pass' || String(check.summary ?? '').toLowerCase().includes('routed');
610
+ if (routed)
611
+ routes[provider] = true;
612
+ }
613
+ this.routeDetectionCache = { executablePath: exe, expiresAt: now + 10_000, routes, raw: parsed };
614
+ }
615
+ catch {
616
+ this.routeDetectionCache = { executablePath: exe, expiresAt: now + 10_000, routes, raw: null };
617
+ }
618
+ return routes;
619
+ }
620
+ readHeadroomVersion(exe) {
621
+ const probe = (0, child_process_1.spawnSync)(exe, ['--version'], { encoding: 'utf8', timeout: 5000 });
622
+ if (probe.status !== 0)
623
+ return null;
624
+ return parseHeadroomVersion(`${probe.stdout ?? ''}${probe.stderr ?? ''}`);
625
+ }
626
+ readLearningBaseline() {
627
+ const candidates = [
628
+ process.env.HEADROOM_WORKSPACE_DIR
629
+ ? path_1.default.join(process.env.HEADROOM_WORKSPACE_DIR, 'output_savings.json')
630
+ : null,
631
+ path_1.default.join(os_1.default.homedir(), '.headroom', 'output_savings.json'),
632
+ ].filter((value) => !!value);
633
+ for (const candidate of candidates) {
634
+ try {
635
+ if (!fs_1.default.existsSync(candidate))
636
+ continue;
637
+ const raw = JSON.parse(fs_1.default.readFileSync(candidate, 'utf8'));
638
+ const samples = Math.max(0, Math.trunc(num(raw.baseline?.glob?.n)));
639
+ return { baselineReady: samples > 0, baselineSamples: samples };
640
+ }
641
+ catch {
642
+ // Keep looking; a corrupt baseline should not break plugin state.
643
+ }
644
+ }
645
+ return { baselineReady: false, baselineSamples: 0 };
646
+ }
647
+ async enableOutputLearning(exe, logs) {
648
+ const args = ['learn', '--verbosity', '--apply'];
649
+ const command = `${exe} ${args.join(' ')}`;
650
+ logs.push(`Running ${command}`);
651
+ this.emit('installing', 'Calibrating output-savings baseline');
652
+ const result = await this.runCommand(exe, args, {
653
+ HEADROOM_OUTPUT_SHAPER: '1',
654
+ HEADROOM_SAVINGS_PROFILE: process.env.HEADROOM_SAVINGS_PROFILE ?? 'agent-90',
655
+ }, logs);
656
+ if (result.code === 0) {
657
+ this.updatePersisted({
658
+ learningEnabled: true,
659
+ learningUpdatedAt: nowIso(),
660
+ learningIssue: null,
661
+ });
662
+ return;
663
+ }
664
+ const failure = issue('learning_setup_failed', logs.slice(-30).join('\n'), command);
665
+ this.updatePersisted({
666
+ learningEnabled: true,
667
+ learningUpdatedAt: nowIso(),
668
+ learningIssue: failure,
669
+ });
670
+ }
671
+ refreshMetricsInBackground() {
672
+ const state = this.getMetricsPrereqState();
673
+ if (!state.installed || !state.executablePath)
674
+ return;
675
+ if (this.metricsRefresh)
676
+ return;
677
+ const last = this.metricsCache.updatedAt ? Date.parse(this.metricsCache.updatedAt) : 0;
678
+ if (Date.now() - last < 5000)
679
+ return;
680
+ this.metricsRefresh = this.refreshMetrics().finally(() => {
681
+ this.metricsRefresh = null;
682
+ });
683
+ }
684
+ getMetricsPrereqState() {
685
+ const persisted = this.readPersisted();
686
+ const install = this.resolveHeadroomInstall(persisted.executablePath ?? null, !!persisted.ignoreSystemInstall);
687
+ const availableProviders = this.availableProviderRecord();
688
+ const detectedRoutes = install?.path ? this.detectProviderRoutes(install.path) : providerRecord(persisted.detectedRoutes);
689
+ return {
690
+ installed: !!install?.path,
691
+ executablePath: install?.path ?? null,
692
+ port: this.validPort(persisted.port),
693
+ availableProviders,
694
+ detectedRoutes,
695
+ activeProviders: {
696
+ codex: persisted.activeProviders?.codex ?? (availableProviders.codex && detectedRoutes.codex),
697
+ claude: persisted.activeProviders?.claude ?? (availableProviders.claude && detectedRoutes.claude),
698
+ },
699
+ };
700
+ }
701
+ async refreshMetrics() {
702
+ const state = this.getMetricsPrereqState();
703
+ if (!state.installed || !state.executablePath) {
704
+ this.metricsCache = emptyMetricsState();
705
+ return;
706
+ }
707
+ const metrics = emptyMetricsState();
708
+ const byProvider = {
709
+ codex: { ...emptyProviderMetric('codex') },
710
+ claude: { ...emptyProviderMetric('claude') },
711
+ };
712
+ try {
713
+ const savings = await this.readDurableSavings(state.executablePath);
714
+ if (savings) {
715
+ metrics.durableSavingsAvailable = true;
716
+ for (const row of savings.by_client ?? []) {
717
+ const provider = normalizeProviderKey(row.client);
718
+ if (!provider)
719
+ continue;
720
+ byProvider[provider].requests = Math.max(byProvider[provider].requests, Math.trunc(num(row.calls)));
721
+ byProvider[provider].inputTokensSaved = Math.max(byProvider[provider].inputTokensSaved, Math.trunc(num(row.tokens_saved)));
722
+ byProvider[provider].inputTokens = Math.max(byProvider[provider].inputTokens, maxNumericField(row, INPUT_TOKEN_FIELDS));
723
+ }
724
+ }
725
+ }
726
+ catch (err) {
727
+ metrics.lastIssue = issue('metrics_unavailable', err instanceof Error ? err.message : String(err));
728
+ }
729
+ const stats = state.activeProviders.codex || state.activeProviders.claude
730
+ ? await this.readProxyStats(state.port).catch((err) => {
731
+ metrics.lastIssue = issue('metrics_unavailable', err instanceof Error ? err.message : String(err));
732
+ return null;
733
+ })
734
+ : null;
735
+ if (stats) {
736
+ metrics.proxyStatsAvailable = true;
737
+ const agents = Array.isArray(stats.agent_usage?.agents) ? stats.agent_usage.agents : [];
738
+ for (const agent of agents) {
739
+ const provider = normalizeProviderKey(agent.agent ?? agent.label ?? agent.source);
740
+ if (!provider)
741
+ continue;
742
+ byProvider[provider].requests = Math.max(byProvider[provider].requests, Math.trunc(num(agent.requests)));
743
+ byProvider[provider].inputTokensSaved = Math.max(byProvider[provider].inputTokensSaved, Math.trunc(num(agent.tokens_saved)));
744
+ byProvider[provider].inputTokens = Math.max(byProvider[provider].inputTokens, maxNumericField(agent, INPUT_TOKEN_FIELDS));
745
+ byProvider[provider].outputTokens = Math.max(byProvider[provider].outputTokens, maxNumericField(agent, OUTPUT_TOKEN_FIELDS));
746
+ }
747
+ const outputReduction = stats.tokens?.output_reduction;
748
+ if (outputReduction?.available) {
749
+ const method = outputReduction.method === 'measured' ? 'measured' : 'estimated';
750
+ const totalOutputSaved = Math.round(num(outputReduction.tokens_saved));
751
+ const totalWeight = ['codex', 'claude'].reduce((sum, provider) => {
752
+ const row = byProvider[provider];
753
+ return sum + (row.outputTokens || row.requests || (state.activeProviders[provider] ? 1 : 0));
754
+ }, 0);
755
+ metrics.outputSavingsAvailable = true;
756
+ metrics.outputSavingsMethod = method;
757
+ metrics.outputConfidence = {
758
+ lowPercent: num(outputReduction.ci_low_percent),
759
+ highPercent: num(outputReduction.ci_high_percent),
760
+ };
761
+ for (const provider of ['codex', 'claude']) {
762
+ const row = byProvider[provider];
763
+ const weight = row.outputTokens || row.requests || (state.activeProviders[provider] ? 1 : 0);
764
+ const outputSaved = totalWeight > 0 ? Math.round(totalOutputSaved * (weight / totalWeight)) : 0;
765
+ row.outputTokensSaved = outputSaved;
766
+ row.outputSavingsPercent = num(outputReduction.reduction_percent);
767
+ row.outputSavingsMethod = method;
768
+ row.outputSavingsAllocated = totalWeight > weight;
769
+ }
770
+ }
771
+ }
772
+ metrics.updatedAt = nowIso();
773
+ metrics.providers = this.decorateMetricProviders(byProvider, state.activeProviders, state.availableProviders, state.detectedRoutes);
774
+ this.metricsCache = metrics;
775
+ }
776
+ async readDurableSavings(exe) {
777
+ const result = await this.runCommandCapture(exe, ['savings', '--json'], {}, 5000);
778
+ if (result.code !== 0 || !result.output.trim())
779
+ return null;
780
+ return JSON.parse(result.output);
781
+ }
782
+ async readProxyStats(port) {
783
+ const controller = new AbortController();
784
+ const timer = setTimeout(() => controller.abort(), 2500);
785
+ try {
786
+ const res = await fetch(`http://127.0.0.1:${port}/stats?cached=1`, { signal: controller.signal });
787
+ if (!res.ok)
788
+ return null;
789
+ return await res.json();
790
+ }
791
+ finally {
792
+ clearTimeout(timer);
793
+ }
794
+ }
795
+ decorateMetrics(metrics, activeProviders, availableProviders, detectedRoutes) {
796
+ return {
797
+ ...metrics,
798
+ providers: this.decorateMetricProviders(metrics.providers, activeProviders, availableProviders, detectedRoutes),
799
+ };
800
+ }
801
+ decorateMetricProviders(providers, activeProviders, availableProviders, detectedRoutes) {
802
+ return {
803
+ codex: {
804
+ ...emptyProviderMetric('codex'),
805
+ ...providers.codex,
806
+ active: activeProviders.codex,
807
+ available: availableProviders.codex,
808
+ detectedRoute: detectedRoutes.codex,
809
+ },
810
+ claude: {
811
+ ...emptyProviderMetric('claude'),
812
+ ...providers.claude,
813
+ active: activeProviders.claude,
814
+ available: availableProviders.claude,
815
+ detectedRoute: detectedRoutes.claude,
816
+ },
817
+ };
818
+ }
819
+ async runCommand(command, args, extraEnv, logs) {
820
+ return new Promise((resolve) => {
821
+ const child = (0, child_process_1.spawn)(command, args, {
822
+ env: { ...process.env, ...extraEnv },
823
+ stdio: ['ignore', 'pipe', 'pipe'],
824
+ });
825
+ child.stdout?.on('data', (chunk) => {
826
+ for (const line of chunk.toString().split(/\r?\n/)) {
827
+ if (!line)
828
+ continue;
829
+ logs.push(line);
830
+ this.emit('installing', line);
831
+ }
832
+ });
833
+ child.stderr?.on('data', (chunk) => {
834
+ for (const line of chunk.toString().split(/\r?\n/)) {
835
+ if (!line)
836
+ continue;
837
+ logs.push(line);
838
+ this.emit('installing', line);
839
+ }
840
+ });
841
+ child.on('error', (err) => {
842
+ logs.push(err.message);
843
+ resolve({ code: null });
844
+ });
845
+ child.on('close', (code) => resolve({ code }));
846
+ });
847
+ }
848
+ async runCommandCapture(command, args, extraEnv, timeoutMs) {
849
+ return new Promise((resolve) => {
850
+ let output = '';
851
+ let settled = false;
852
+ const child = (0, child_process_1.spawn)(command, args, {
853
+ env: { ...process.env, ...extraEnv },
854
+ stdio: ['ignore', 'pipe', 'pipe'],
855
+ });
856
+ const settle = (code) => {
857
+ if (settled)
858
+ return;
859
+ settled = true;
860
+ clearTimeout(timer);
861
+ resolve({ code, output });
862
+ };
863
+ const timer = setTimeout(() => {
864
+ try {
865
+ child.kill('SIGTERM');
866
+ }
867
+ catch { /* ignore */ }
868
+ settle(null);
869
+ }, timeoutMs);
870
+ child.stdout?.on('data', (chunk) => { output += chunk.toString(); });
871
+ child.stderr?.on('data', (chunk) => { output += chunk.toString(); });
872
+ child.on('error', (err) => {
873
+ output += err.message;
874
+ settle(null);
875
+ });
876
+ child.on('close', (code) => settle(code));
877
+ });
878
+ }
879
+ async waitForProxyHealthy(port, timeoutMs) {
880
+ const deadline = Date.now() + timeoutMs;
881
+ while (Date.now() < deadline) {
882
+ try {
883
+ const res = await fetch(`http://127.0.0.1:${port}/livez`);
884
+ if (res.ok)
885
+ return true;
886
+ }
887
+ catch {
888
+ // retry until timeout
889
+ }
890
+ await new Promise((r) => setTimeout(r, 250));
891
+ }
892
+ return false;
893
+ }
894
+ verifyProviderRoute(provider) {
895
+ const state = this.getState();
896
+ return provider === 'codex'
897
+ ? !!state.activeProviders.codex && state.port > 0
898
+ : !!state.activeProviders.claude && state.port > 0;
899
+ }
900
+ isProxyRunning() {
901
+ return !!this.proxy && !this.proxy.killed && this.proxy.exitCode == null;
902
+ }
903
+ async adoptHealthyProxy(port) {
904
+ if (!await this.waitForProxyHealthy(port, 750))
905
+ return false;
906
+ await this.refreshMetrics();
907
+ this.updatePersisted({ lastIssue: null });
908
+ return true;
909
+ }
910
+ isProxyAvailable() {
911
+ return this.isProxyRunning() || this.metricsCache.proxyStatsAvailable;
912
+ }
913
+ stopProxy() {
914
+ const child = this.proxy;
915
+ this.proxy = null;
916
+ if (child && !child.killed) {
917
+ try {
918
+ child.kill('SIGTERM');
919
+ }
920
+ catch { /* ignore */ }
921
+ }
922
+ }
923
+ trimProxyTail() {
924
+ if (this.proxyTail.length > 32_000)
925
+ this.proxyTail = this.proxyTail.slice(-32_000);
926
+ }
927
+ validPort(value) {
928
+ return Number.isInteger(value) && Number(value) >= 1 && Number(value) <= 65535 ? Number(value) : DEFAULT_PORT;
929
+ }
930
+ readPersisted() {
931
+ const raw = (0, desktop_db_1.getDesktopSetting)(this.db, STATE_KEY);
932
+ if (!raw)
933
+ return { port: DEFAULT_PORT, activeProviders: {} };
934
+ try {
935
+ const parsed = JSON.parse(raw);
936
+ return { port: DEFAULT_PORT, activeProviders: {}, ...parsed };
937
+ }
938
+ catch {
939
+ return { port: DEFAULT_PORT, activeProviders: {} };
940
+ }
941
+ }
942
+ updatePersisted(patch) {
943
+ const next = {
944
+ ...this.readPersisted(),
945
+ ...patch,
946
+ updatedAt: nowIso(),
947
+ };
948
+ (0, desktop_db_1.setDesktopSetting)(this.db, STATE_KEY, JSON.stringify(next));
949
+ this.syncRouting();
950
+ }
951
+ syncRouting() {
952
+ const persisted = this.readPersisted();
953
+ (0, headroom_routing_1.setHeadroomRoutingState)({
954
+ port: this.validPort(persisted.port),
955
+ activeProviders: {
956
+ codex: !!persisted.activeProviders?.codex,
957
+ claude: !!persisted.activeProviders?.claude,
958
+ },
959
+ });
960
+ }
961
+ emit(phase, line) {
962
+ this.broadcast({
963
+ type: 'plugin.headroom_progress',
964
+ name: 'headroom-ai',
965
+ status: phase,
966
+ line,
967
+ timestamp: nowIso(),
968
+ });
969
+ }
970
+ }
971
+ exports.HeadroomManager = HeadroomManager;