chainlesschain 0.162.123 → 0.162.125

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 (263) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/AIOps-DcqNhZhA.js +1 -0
  4. package/src/assets/web-panel/assets/{ActionButton-BugJtiJc.js → ActionButton-BigtmPoq.js} +1 -1
  5. package/src/assets/web-panel/assets/Analytics-1J_X_HF4.js +3 -0
  6. package/src/assets/web-panel/assets/{AppLayout-Cb0eHetM.js → AppLayout-DlcDG_a_.js} +5 -5
  7. package/src/assets/web-panel/assets/Audit-Do-y8_3w.js +1 -0
  8. package/src/assets/web-panel/assets/Backup-S2Df-50Y.js +1 -0
  9. package/src/assets/web-panel/assets/{BaseInput-CZ0c7QHk.js → BaseInput-D5kgWJfk.js} +1 -1
  10. package/src/assets/web-panel/assets/{Chat-CaKDT3wV.js → Chat-aUcp3kN1.js} +6 -6
  11. package/src/assets/web-panel/assets/ChatBubbleRenderer-Pg7dIsiE.js +1 -0
  12. package/src/assets/web-panel/assets/{Checkbox-C6-Iu3bG.js → Checkbox-DTg1U7Xs.js} +1 -1
  13. package/src/assets/web-panel/assets/Codegen-YO9ZZ4Ky.js +1 -0
  14. package/src/assets/web-panel/assets/{Col-DVBrO7AN.js → Col-BHonE9fU.js} +1 -1
  15. package/src/assets/web-panel/assets/Community-DA2AlEFh.js +1 -0
  16. package/src/assets/web-panel/assets/{Compact-DM4ZgFSE.js → Compact-DULxLRNg.js} +1 -1
  17. package/src/assets/web-panel/assets/Compliance-BZqfuuZL.js +1 -0
  18. package/src/assets/web-panel/assets/{Cowork-CJwiWkUY.js → Cowork-CJe3vyMS.js} +2 -2
  19. package/src/assets/web-panel/assets/{Cron-dsdXHvrO.js → Cron-CeV8AtRu.js} +2 -2
  20. package/src/assets/web-panel/assets/Crosschain-CAg66zS5.js +1 -0
  21. package/src/assets/web-panel/assets/{DID-BB_LBLYT.js → DID-Dtup5JZm.js} +2 -2
  22. package/src/assets/web-panel/assets/Dashboard-DAR_8PNy.js +3 -0
  23. package/src/assets/web-panel/assets/{Dropdown-dTFtVnKk.js → Dropdown-K5bTaCYN.js} +1 -1
  24. package/src/assets/web-panel/assets/{EmailListRenderer-yqdttzXV.js → EmailListRenderer-C890uErx.js} +1 -1
  25. package/src/assets/web-panel/assets/{FamilyGuardDashboard-DfX3YpUU.js → FamilyGuardDashboard-B-Tj_8yM.js} +1 -1
  26. package/src/assets/web-panel/assets/Federation-C6WZ98ni.js +1 -0
  27. package/src/assets/web-panel/assets/{FormItemContext-DNMwIh7X.js → FormItemContext-D-LTfGWd.js} +1 -1
  28. package/src/assets/web-panel/assets/{GenericCardRenderer-C-T8UQf0.js → GenericCardRenderer-C80DK4W7.js} +1 -1
  29. package/src/assets/web-panel/assets/{Git-SURFhkLi.js → Git-Cjvvv3zW.js} +2 -2
  30. package/src/assets/web-panel/assets/Governance-C0BND77H.js +1 -0
  31. package/src/assets/web-panel/assets/Inference-BL9kTtSu.js +1 -0
  32. package/src/assets/web-panel/assets/KnowledgeGraph-CnVTBmJf.js +1 -0
  33. package/src/assets/web-panel/assets/{Logs-DjuWREBJ.js → Logs-CbCbg7K6.js} +2 -2
  34. package/src/assets/web-panel/assets/Marketplace-CG5On-fH.js +1 -0
  35. package/src/assets/web-panel/assets/{McpTools-D0waMomb.js → McpTools-AYYDZZK7.js} +5 -5
  36. package/src/assets/web-panel/assets/{Memory-Ci768GQ3.js → Memory-tMWbMDQq.js} +2 -2
  37. package/src/assets/web-panel/assets/MobileBridge-BAOsf4wB.js +1 -0
  38. package/src/assets/web-panel/assets/{MobileProjects-RpFuAiNz.js → MobileProjects-BT-2komQ.js} +1 -1
  39. package/src/assets/web-panel/assets/{Mtc-DJdw8btT.js → Mtc-BFwfC63n.js} +5 -5
  40. package/src/assets/web-panel/assets/{MtcAudit-Dc596XgH.js → MtcAudit-DYG3CWdH.js} +4 -4
  41. package/src/assets/web-panel/assets/{Multisig--Qs7eGsH.js → Multisig-KJwd0yWT.js} +3 -3
  42. package/src/assets/web-panel/assets/NLProgramming-BmL5dNWm.js +1 -0
  43. package/src/assets/web-panel/assets/{Notes-uMxjj66_.js → Notes-DmkaVaMc.js} +3 -3
  44. package/src/assets/web-panel/assets/{NotificationSettings-C4ikBOsm.js → NotificationSettings-tmVQszDZ.js} +1 -1
  45. package/src/assets/web-panel/assets/{OrderTableRenderer-BpmdunAz.js → OrderTableRenderer-BtrR7anf.js} +1 -1
  46. package/src/assets/web-panel/assets/Organization-pppktQyW.js +4 -0
  47. package/src/assets/web-panel/assets/{Overflow-B-4_IpbB.js → Overflow-D3lBoQDA.js} +1 -1
  48. package/src/assets/web-panel/assets/{P2P-DdOf9BZs.js → P2P-BgE2qGlZ.js} +2 -2
  49. package/src/assets/web-panel/assets/PdhVaultBrowser-D26Bz5gS.js +7 -0
  50. package/src/assets/web-panel/assets/Permissions-BR8no2Xe.js +4 -0
  51. package/src/assets/web-panel/assets/{PersonalDataHub-B3WcvKni.js → PersonalDataHub-DabbyQEF.js} +2 -2
  52. package/src/assets/web-panel/assets/Pipeline-n4sNpEz8.js +1 -0
  53. package/src/assets/web-panel/assets/Privacy-CGNp4n8M.js +1 -0
  54. package/src/assets/web-panel/assets/{ProjectInit-BQjMDkML.js → ProjectInit-CAVA5VsX.js} +2 -2
  55. package/src/assets/web-panel/assets/ProjectSettings-GCgkfM7A.js +2 -0
  56. package/src/assets/web-panel/assets/Projects-BYK17p52.js +1 -0
  57. package/src/assets/web-panel/assets/{Providers-Bl0dw6cI.js → Providers-DpUT5W-d.js} +1 -1
  58. package/src/assets/web-panel/assets/{QuickAsk-BDrk7l3t.js → QuickAsk-ZJxTb7vi.js} +1 -1
  59. package/src/assets/web-panel/assets/Recommend-CIzFRDk1.js +1 -0
  60. package/src/assets/web-panel/assets/Reputation-lQ_WDNZn.js +1 -0
  61. package/src/assets/web-panel/assets/Row-1MEtrgtF.js +1 -0
  62. package/src/assets/web-panel/assets/{RssFeed-9-Fxh3_w.js → RssFeed-SSgcPPl4.js} +2 -2
  63. package/src/assets/web-panel/assets/Search-CcTMOGNY.js +1 -0
  64. package/src/assets/web-panel/assets/{Security-D6CySV7r.js → Security-BA1KMaDx.js} +3 -3
  65. package/src/assets/web-panel/assets/{Services-Co8Mk8qE.js → Services-_aj7czZn.js} +2 -2
  66. package/src/assets/web-panel/assets/{Skeleton-BdON8M-1.js → Skeleton-BWgg_0Zr.js} +1 -1
  67. package/src/assets/web-panel/assets/{Skills-CEJE1Go1.js → Skills-C2ZIziYM.js} +1 -1
  68. package/src/assets/web-panel/assets/Sla-CYxp4axY.js +1 -0
  69. package/src/assets/web-panel/assets/SpeechSettings-C7Csp1jV.js +1 -0
  70. package/src/assets/web-panel/assets/{SyncSettings-52NXsfyj.js → SyncSettings-svGeswiw.js} +2 -2
  71. package/src/assets/web-panel/assets/{Tasks-He7CweW6.js → Tasks-ClISj6oK.js} +1 -1
  72. package/src/assets/web-panel/assets/Templates-CmO-Fp7r.js +1 -0
  73. package/src/assets/web-panel/assets/Tenant-a3Ac3lxz.js +1 -0
  74. package/src/assets/web-panel/assets/Terminal-DyJIRh81.js +3 -0
  75. package/src/assets/web-panel/assets/{TimelineRenderer-BdfqBQmM.js → TimelineRenderer-acXS5N5h.js} +1 -1
  76. package/src/assets/web-panel/assets/Tokens-DJW0KqV4.js +1 -0
  77. package/src/assets/web-panel/assets/{Trigger-Dt_N_rvo.js → Trigger-0HeTi4r6.js} +1 -1
  78. package/src/assets/web-panel/assets/Trust-aFym34eo.js +1 -0
  79. package/src/assets/web-panel/assets/{UkeySign-CW8YdFxg.js → UkeySign-CYhszHJv.js} +1 -1
  80. package/src/assets/web-panel/assets/{VideoEditing-CU6o7qvp.js → VideoEditing-BbJxPF9X.js} +1 -1
  81. package/src/assets/web-panel/assets/Wallet-DDthEwiu.js +4 -0
  82. package/src/assets/web-panel/assets/WebAuthn-DQ6McYPg.js +5 -0
  83. package/src/assets/web-panel/assets/{WorkflowEditor-BcCA2U8H.js → WorkflowEditor-CnF8zRsi.js} +1 -1
  84. package/src/assets/web-panel/assets/{chat-Cxl5DGsc.js → chat-Dzkx2gTP.js} +1 -1
  85. package/src/assets/web-panel/assets/{colors-FS_7Xggs.js → colors-35T51Oo5.js} +1 -1
  86. package/src/assets/web-panel/assets/{compact-item-BzXjd3wJ.js → compact-item-TtzNrlu1.js} +1 -1
  87. package/src/assets/web-panel/assets/{createContext-DhF9bi_u.js → createContext-Y1LkWrYb.js} +1 -1
  88. package/src/assets/web-panel/assets/devWarning-D_lwLZ6O.js +1 -0
  89. package/src/assets/web-panel/assets/{hasIn-Y1a0aV9N.js → hasIn-BOYw1BgS.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-JUukFpxi.js → index-8jffdb6b.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-BBtULjKY.js → index-B7aKAZzS.js} +1 -1
  92. package/src/assets/web-panel/assets/index-BGPEaeB1.js +1 -0
  93. package/src/assets/web-panel/assets/index-BKRlWHUp.js +1 -0
  94. package/src/assets/web-panel/assets/{index-Dmjxpu0L.js → index-BSpFe6j5.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-1iacQgo5.js → index-BWgNn9As.js} +28 -26
  96. package/src/assets/web-panel/assets/{index-Na733kBa.js → index-Bs69SI96.js} +2 -2
  97. package/src/assets/web-panel/assets/{index-B48PZ-xM.js → index-BtlJWaSP.js} +2 -2
  98. package/src/assets/web-panel/assets/index-Bw1iOGhM.js +1 -0
  99. package/src/assets/web-panel/assets/{index-DVTex5M8.js → index-BxWUEFKy.js} +1 -1
  100. package/src/assets/web-panel/assets/index-C7Wt5feN.js +1 -0
  101. package/src/assets/web-panel/assets/index-CEwuCM44.js +1 -0
  102. package/src/assets/web-panel/assets/index-CLqC2sRT.js +3 -0
  103. package/src/assets/web-panel/assets/{index-DxGVbuqr.js → index-CPGNc2tF.js} +2 -2
  104. package/src/assets/web-panel/assets/{index-DfaSKEVD.js → index-CVrUs6rf.js} +1 -1
  105. package/src/assets/web-panel/assets/index-CZ16GlOs.js +1 -0
  106. package/src/assets/web-panel/assets/{index-CELVZPNt.js → index-CdyFg4FI.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-CETvk0NN.js → index-CqhwG1ox.js} +4 -4
  108. package/src/assets/web-panel/assets/index-Cs82BHAj.js +1 -0
  109. package/src/assets/web-panel/assets/{index-D3IVAG9l.js → index-D2od7Bgx.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-Sl-sLj3T.js → index-D47robkX.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-Ci7PSoQn.js → index-D4BgCBa3.js} +2 -2
  112. package/src/assets/web-panel/assets/index-D6tG4B25.js +55 -0
  113. package/src/assets/web-panel/assets/index-D7dCBw_M.js +1 -0
  114. package/src/assets/web-panel/assets/index-DAizH273.js +21 -0
  115. package/src/assets/web-panel/assets/{index-GCd5hs58.js → index-DFhlelzN.js} +2 -2
  116. package/src/assets/web-panel/assets/index-DhMSOd_2.js +1 -0
  117. package/src/assets/web-panel/assets/index-Dhfw-BXs.js +12 -0
  118. package/src/assets/web-panel/assets/index-DjeUZxE5.js +1 -0
  119. package/src/assets/web-panel/assets/{index-BnvaUocg.js → index-DmkG479D.js} +2 -2
  120. package/src/assets/web-panel/assets/index-Dof7lWA_.js +13 -0
  121. package/src/assets/web-panel/assets/{index-BNmWkPqm.js → index-Dx0hIfC-.js} +1 -1
  122. package/src/assets/web-panel/assets/index-DzzgU4IU.js +1 -0
  123. package/src/assets/web-panel/assets/index-G4ClSmJc.js +1 -0
  124. package/src/assets/web-panel/assets/{index-Cn9lbqaS.js → index-KgXrlfNF.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DUcVF8e6.js → index-NM9Oke2k.js} +2 -2
  126. package/src/assets/web-panel/assets/{index-DLyIxnjc.js → index-XIuZV9by.js} +3 -3
  127. package/src/assets/web-panel/assets/{index-BTltEtoG.js → index-hyqEKkBT.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-uLoKviUT.js → index-zF77jnI2.js} +2 -2
  129. package/src/assets/web-panel/assets/{initDefaultProps-D6QmlnQ-.js → initDefaultProps-DD5y5msp.js} +1 -1
  130. package/src/assets/web-panel/assets/motion-BuoutMia.js +11 -0
  131. package/src/assets/web-panel/assets/{move-Bpph6B_N.js → move-DkpjqOXg.js} +1 -1
  132. package/src/assets/web-panel/assets/mtc-parser-BmGJD-v-.js +1 -0
  133. package/src/assets/web-panel/assets/{omit-Dx0YMbWn.js → omit-DftUE0Sz.js} +1 -1
  134. package/src/assets/web-panel/assets/{pickAttrs-D-a8Fm6C.js → pickAttrs-DDRb9FIu.js} +1 -1
  135. package/src/assets/web-panel/assets/{placementArrow-DXFSVgxv.js → placementArrow-DPgtxOOD.js} +1 -1
  136. package/src/assets/web-panel/assets/{responsiveObserve-CxhQ1EaK.js → responsiveObserve-BQFRzFeZ.js} +1 -1
  137. package/src/assets/web-panel/assets/{slide-IlBI0XU8.js → slide-DQ4lmUSz.js} +1 -1
  138. package/src/assets/web-panel/assets/{statusUtils-COoXVzje.js → statusUtils-C1TR4ZiQ.js} +1 -1
  139. package/src/assets/web-panel/assets/{styleChecker-DCuq89M_.js → styleChecker-B-Suj978.js} +1 -1
  140. package/src/assets/web-panel/assets/{useFlexGapSupport-tfNE1ALO.js → useFlexGapSupport-Q7bDZ3EI.js} +1 -1
  141. package/src/assets/web-panel/assets/{useFs-DD4bzBkU.js → useFs-DHjRLyQH.js} +1 -1
  142. package/src/assets/web-panel/assets/{usePersonalDataHub-DVkH1hbT.js → usePersonalDataHub-lQOvCvCr.js} +1 -1
  143. package/src/assets/web-panel/assets/{vnode-D1ZmXyxM.js → vnode-Ie0g4-p3.js} +1 -1
  144. package/src/assets/web-panel/assets/{zoom-DblBUHfi.js → zoom-CHP0YEoH.js} +1 -1
  145. package/src/assets/web-panel/index.html +1 -1
  146. package/src/commands/agent.js +10 -0
  147. package/src/commands/ask.js +24 -1
  148. package/src/commands/ide.js +60 -5
  149. package/src/commands/incentive.js +22 -22
  150. package/src/commands/init.js +5 -4
  151. package/src/commands/instinct.js +18 -5
  152. package/src/commands/mcp.js +30 -3
  153. package/src/commands/memory.js +18 -5
  154. package/src/commands/mtc.js +5 -1
  155. package/src/gateways/http/envelope-http-server.js +17 -2
  156. package/src/gateways/terminal/PtyManager.js +15 -2
  157. package/src/gateways/ws/message-dispatcher.js +4 -1
  158. package/src/gateways/ws/ws-server.js +20 -0
  159. package/src/gateways/ws/ws-session-gateway.js +72 -4
  160. package/src/harness/background-task-manager.js +5 -1
  161. package/src/harness/jsonl-session-store.js +51 -0
  162. package/src/harness/mcp-client.js +119 -21
  163. package/src/harness/prompt-compressor.js +24 -5
  164. package/src/harness/worktree-isolator.js +25 -10
  165. package/src/lib/agent-core.js +4 -0
  166. package/src/lib/autonomous-agent.js +36 -6
  167. package/src/lib/chat-intent-service.js +18 -4
  168. package/src/lib/checkpoint-store.js +54 -4
  169. package/src/lib/claude-code-bridge.js +48 -3
  170. package/src/lib/cost-budget.js +13 -2
  171. package/src/lib/credential-guard.js +45 -0
  172. package/src/lib/git-integration.js +68 -3
  173. package/src/lib/goal-store.js +11 -0
  174. package/src/lib/hook-manager.js +4 -0
  175. package/src/lib/hook-runner.cjs +11 -1
  176. package/src/lib/interactive-planner.js +4 -3
  177. package/src/lib/jetbrains-bridge.js +147 -0
  178. package/src/lib/json-schema-output.js +47 -0
  179. package/src/lib/jsonl-session-store.js +1 -0
  180. package/src/lib/learning/reflection-engine.js +4 -3
  181. package/src/lib/learning/skill-improver.js +4 -3
  182. package/src/lib/learning/skill-synthesizer.js +4 -3
  183. package/src/lib/llm-pricing.js +11 -4
  184. package/src/lib/llm-providers.js +11 -1
  185. package/src/lib/mcp-oauth.js +220 -20
  186. package/src/lib/mcp-serve.js +85 -6
  187. package/src/lib/orchestrator.js +42 -4
  188. package/src/lib/process-manager.js +31 -3
  189. package/src/lib/repl-bang-memorize.js +9 -1
  190. package/src/lib/repl-completer.js +29 -0
  191. package/src/lib/runnable-provider.js +7 -1
  192. package/src/lib/session-insights.js +13 -6
  193. package/src/lib/session-usage.js +21 -3
  194. package/src/lib/slot-filler.js +4 -3
  195. package/src/lib/status-line.cjs +4 -1
  196. package/src/lib/sub-agent-context.js +15 -0
  197. package/src/lib/web-fetch.js +54 -3
  198. package/src/lib/workflow-engine.js +35 -11
  199. package/src/repl/agent-repl.js +29 -0
  200. package/src/runtime/agent-core.js +252 -28
  201. package/src/runtime/headless-runner.js +13 -0
  202. package/src/runtime/headless-stream.js +133 -17
  203. package/src/runtime/mcp-config.js +72 -3
  204. package/src/runtime/pipe-safety.js +51 -0
  205. package/src/runtime/policies/agent-policy.js +3 -0
  206. package/src/assets/web-panel/assets/AIOps-C5CSd8pY.js +0 -1
  207. package/src/assets/web-panel/assets/Analytics-CzxpcV9E.js +0 -3
  208. package/src/assets/web-panel/assets/Audit-qjq04wh8.js +0 -1
  209. package/src/assets/web-panel/assets/Backup-C2Y_XNBA.js +0 -1
  210. package/src/assets/web-panel/assets/ChatBubbleRenderer-TeKnMwPj.js +0 -1
  211. package/src/assets/web-panel/assets/Codegen-TGLFk6P1.js +0 -1
  212. package/src/assets/web-panel/assets/Community-tg6-Rik1.js +0 -1
  213. package/src/assets/web-panel/assets/Compliance-DFeWryxt.js +0 -1
  214. package/src/assets/web-panel/assets/Crosschain-p7_5Gq-7.js +0 -1
  215. package/src/assets/web-panel/assets/Dashboard-Bh_HkNSD.js +0 -3
  216. package/src/assets/web-panel/assets/Federation-BJApfPV4.js +0 -1
  217. package/src/assets/web-panel/assets/Governance-B4u_KsfE.js +0 -1
  218. package/src/assets/web-panel/assets/Inference-2kr_rKQd.js +0 -1
  219. package/src/assets/web-panel/assets/KnowledgeGraph-h-nL7J6x.js +0 -1
  220. package/src/assets/web-panel/assets/Marketplace-W9Iz8b2m.js +0 -1
  221. package/src/assets/web-panel/assets/MobileBridge-4jAehfcz.js +0 -3
  222. package/src/assets/web-panel/assets/NLProgramming-D3y71FDX.js +0 -1
  223. package/src/assets/web-panel/assets/Organization-BBEmFNGM.js +0 -4
  224. package/src/assets/web-panel/assets/PdhVaultBrowser-DDU2Zc9Q.js +0 -7
  225. package/src/assets/web-panel/assets/Permissions-B1ebgevF.js +0 -4
  226. package/src/assets/web-panel/assets/Pipeline-DASuPDc3.js +0 -1
  227. package/src/assets/web-panel/assets/Privacy-DnixwmhJ.js +0 -1
  228. package/src/assets/web-panel/assets/ProjectSettings-dHyZ3SxW.js +0 -2
  229. package/src/assets/web-panel/assets/Projects-BGWEJIVT.js +0 -1
  230. package/src/assets/web-panel/assets/Recommend-BPKof_CA.js +0 -1
  231. package/src/assets/web-panel/assets/Reputation-6MNJLklK.js +0 -1
  232. package/src/assets/web-panel/assets/Row-DmGJwpfd.js +0 -1
  233. package/src/assets/web-panel/assets/Search-CCHjey_D.js +0 -1
  234. package/src/assets/web-panel/assets/Sla-BSxFvFX9.js +0 -1
  235. package/src/assets/web-panel/assets/SpeechSettings-C3rShYlV.js +0 -1
  236. package/src/assets/web-panel/assets/Templates-UtjBlJIO.js +0 -1
  237. package/src/assets/web-panel/assets/Tenant-Dd5XNbYF.js +0 -1
  238. package/src/assets/web-panel/assets/Terminal-pFyh42cu.js +0 -3
  239. package/src/assets/web-panel/assets/Tokens-CsgNsrdQ.js +0 -1
  240. package/src/assets/web-panel/assets/Trust-B1lJzsPn.js +0 -1
  241. package/src/assets/web-panel/assets/Wallet-DPxjEhbN.js +0 -4
  242. package/src/assets/web-panel/assets/WebAuthn-CuJ29YAF.js +0 -5
  243. package/src/assets/web-panel/assets/community-parser-Cuyslaku.js +0 -3
  244. package/src/assets/web-panel/assets/devWarning-B1j78JeH.js +0 -1
  245. package/src/assets/web-panel/assets/index--fcP34eT.js +0 -1
  246. package/src/assets/web-panel/assets/index-1khPb4ZS.js +0 -1
  247. package/src/assets/web-panel/assets/index-B_Z0PHC0.js +0 -12
  248. package/src/assets/web-panel/assets/index-BaN6wKWx.js +0 -1
  249. package/src/assets/web-panel/assets/index-BepInmSi.js +0 -1
  250. package/src/assets/web-panel/assets/index-BllceSdr.js +0 -1
  251. package/src/assets/web-panel/assets/index-Bt_0ygjA.js +0 -1
  252. package/src/assets/web-panel/assets/index-BzQkdgQW.js +0 -1
  253. package/src/assets/web-panel/assets/index-C2S8McM1.js +0 -55
  254. package/src/assets/web-panel/assets/index-CRSkKj9M.js +0 -1
  255. package/src/assets/web-panel/assets/index-Cgo1J_Kf.js +0 -1
  256. package/src/assets/web-panel/assets/index-CuTKOay7.js +0 -1
  257. package/src/assets/web-panel/assets/index-CxYHV8nb.js +0 -13
  258. package/src/assets/web-panel/assets/index-CzlCP4jP.js +0 -3
  259. package/src/assets/web-panel/assets/index-DZJMsVI1.js +0 -1
  260. package/src/assets/web-panel/assets/index-DgHWZVjv.js +0 -21
  261. package/src/assets/web-panel/assets/index-N7JKRLCC.js +0 -1
  262. package/src/assets/web-panel/assets/motion-BHrTGY1_.js +0 -11
  263. package/src/assets/web-panel/assets/mtc-parser-CEQffxds.js +0 -1
@@ -96,9 +96,10 @@ const round = (n, dp = 6) => {
96
96
  *
97
97
  * Per provider, a user entry whose `match` equals a built-in pattern REPLACES
98
98
  * it; brand-new patterns are prepended so they win ties; unknown providers are
99
- * added. Malformed entries (missing match / non-numeric rate) are skipped so a
100
- * bad config line can't crash cost reporting. Returns a new table; never
101
- * mutates PRICE_TABLE or the input.
99
+ * added. Malformed entries (missing match / non-numeric / negative / Infinity
100
+ * rate) are skipped so a bad config line can't crash cost reporting or produce a
101
+ * negative cost that undercounts spend. Returns a new table; never mutates
102
+ * PRICE_TABLE or the input.
102
103
  *
103
104
  * @param {object} [overrides]
104
105
  * @param {object} [base=PRICE_TABLE]
@@ -123,8 +124,14 @@ export function mergePricing(overrides, base = PRICE_TABLE) {
123
124
  e &&
124
125
  typeof e.match === "string" &&
125
126
  e.match.trim() &&
127
+ // Rates must be finite AND non-negative — a negative price would
128
+ // produce a negative cost that undercounts spend (and could keep a
129
+ // CostBudget under its cap). `>= 0` also rejects NaN (NaN >= 0 is
130
+ // false); isFinite additionally rejects Infinity.
126
131
  Number.isFinite(Number(e.in)) &&
127
- Number.isFinite(Number(e.out)),
132
+ Number(e.in) >= 0 &&
133
+ Number.isFinite(Number(e.out)) &&
134
+ Number(e.out) >= 0,
128
135
  )
129
136
  .map((e) => ({
130
137
  match: e.match.toLowerCase(),
@@ -181,12 +181,22 @@ export class LLMProviderRegistry {
181
181
  .prepare("SELECT * FROM llm_providers WHERE custom = 1")
182
182
  .all();
183
183
  for (const row of rows) {
184
+ // Tolerate a corrupt `models` TEXT column: `|| "[]"` only covers null,
185
+ // not malformed JSON, and an unguarded throw here would abort the whole
186
+ // custom-provider load (and the registry constructor) over one bad row.
187
+ let models = [];
188
+ try {
189
+ const parsed = JSON.parse(row.models || "[]");
190
+ if (Array.isArray(parsed)) models = parsed;
191
+ } catch {
192
+ /* corrupt models column — fall back to no models for this provider */
193
+ }
184
194
  this.providers.set(row.name, {
185
195
  name: row.name,
186
196
  displayName: row.display_name,
187
197
  baseUrl: row.base_url,
188
198
  apiKeyEnv: row.api_key_env,
189
- models: JSON.parse(row.models || "[]"),
199
+ models,
190
200
  custom: true,
191
201
  free: false,
192
202
  });
@@ -62,6 +62,43 @@ export function randomState(bytes = 16) {
62
62
  return base64url(_deps.randomBytes(bytes));
63
63
  }
64
64
 
65
+ /**
66
+ * Is `hostname` a loopback address? Plain http is tolerated only for these (a
67
+ * self-hosted MCP server on the dev box). Handles the bracketed IPv6 form that
68
+ * `new URL().hostname` yields (e.g. "[::1]") and the whole 127.0.0.0/8 range.
69
+ */
70
+ export function isLoopbackHost(hostname) {
71
+ const h = String(hostname || "")
72
+ .replace(/^\[|\]$/g, "")
73
+ .toLowerCase();
74
+ if (h === "localhost") return true;
75
+ if (h === "::1") return true;
76
+ return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h);
77
+ }
78
+
79
+ /**
80
+ * Require an OAuth endpoint to be HTTPS (or plain http on loopback for local
81
+ * dev). Authorization codes, PKCE verifiers, client secrets, and bearer/refresh
82
+ * tokens flow through these endpoints, so a discovered (or stored) endpoint that
83
+ * downgrades to cleartext http on a remote host must be refused rather than
84
+ * leaking secrets over the wire (RFC 6749 §3.1.2.1 / RFC 8252). Returns the
85
+ * endpoint unchanged when allowed; throws otherwise.
86
+ */
87
+ export function assertSecureEndpoint(endpoint, label = "endpoint") {
88
+ let u;
89
+ try {
90
+ u = new URL(endpoint);
91
+ } catch {
92
+ throw new Error(`OAuth ${label} is not a valid URL: ${endpoint}`);
93
+ }
94
+ if (u.protocol === "https:") return endpoint;
95
+ if (u.protocol === "http:" && isLoopbackHost(u.hostname)) return endpoint;
96
+ throw new Error(
97
+ `refusing to use insecure OAuth ${label} (${endpoint}); ` +
98
+ `HTTPS is required for non-loopback endpoints`,
99
+ );
100
+ }
101
+
65
102
  /** Minimal HTML-escape for values reflected into the callback page. */
66
103
  function escapeHtml(s) {
67
104
  return String(s).replace(
@@ -157,6 +194,12 @@ export async function discoverAuthMetadata(
157
194
  { resourceMetadataUrl } = {},
158
195
  ) {
159
196
  const origin = new URL(serverUrl).origin;
197
+ // Discovery itself carries no secrets, but its responses dictate the
198
+ // authorization/token endpoints — so a cleartext-remote discovery is a
199
+ // downgrade vector (a MITM could inject endpoints). Refuse a non-loopback
200
+ // http MCP URL up front (loopback http stays allowed for local dev), matching
201
+ // the endpoint enforcement below.
202
+ assertSecureEndpoint(origin, "MCP server URL");
160
203
  // 1. protected-resource metadata (RFC 9728) → authorization_servers[]
161
204
  let authServer = origin;
162
205
  try {
@@ -172,6 +215,12 @@ export async function discoverAuthMetadata(
172
215
  } catch {
173
216
  // no protected-resource doc → assume the origin is its own auth server
174
217
  }
218
+ // The authorization-server location can come from the REMOTE protected-
219
+ // resource doc, so a malicious/MITM'd doc could point discovery at a
220
+ // cleartext host. Refuse a non-loopback http authServer before fetching its
221
+ // metadata (the discovered endpoints are HTTPS-checked too, but this fails
222
+ // closed earlier and blocks the downgrade).
223
+ assertSecureEndpoint(authServer, "authorization server");
175
224
  // 2. authorization-server metadata (RFC 8414)
176
225
  const candidates = [
177
226
  `${authServer}/.well-known/oauth-authorization-server`,
@@ -209,6 +258,7 @@ export async function registerClient(
209
258
  "server has no registration_endpoint and no --client-id was given",
210
259
  );
211
260
  }
261
+ assertSecureEndpoint(metadata.registration_endpoint, "registration_endpoint");
212
262
  const reg = await fetchJson(metadata.registration_endpoint, {
213
263
  method: "POST",
214
264
  headers: { "content-type": "application/json" },
@@ -230,6 +280,10 @@ export function buildAuthorizeUrl(
230
280
  metadata,
231
281
  { clientId, redirectUri, scope, codeChallenge, state, resource },
232
282
  ) {
283
+ assertSecureEndpoint(
284
+ metadata.authorization_endpoint,
285
+ "authorization_endpoint",
286
+ );
233
287
  const u = new URL(metadata.authorization_endpoint);
234
288
  u.searchParams.set("response_type", "code");
235
289
  u.searchParams.set("client_id", clientId);
@@ -261,6 +315,7 @@ export async function exchangeCodeForToken(
261
315
  });
262
316
  if (clientSecret) body.set("client_secret", clientSecret);
263
317
  if (resource) body.set("resource", resource);
318
+ assertSecureEndpoint(metadata.token_endpoint, "token_endpoint");
264
319
  const tok = await fetchJson(metadata.token_endpoint, {
265
320
  method: "POST",
266
321
  headers: { "content-type": "application/x-www-form-urlencoded" },
@@ -287,6 +342,7 @@ export async function refreshAccessToken(
287
342
  });
288
343
  if (clientSecret) body.set("client_secret", clientSecret);
289
344
  if (resource) body.set("resource", resource);
345
+ assertSecureEndpoint(metadata.token_endpoint, "token_endpoint");
290
346
  const tok = await fetchJson(metadata.token_endpoint, {
291
347
  method: "POST",
292
348
  headers: { "content-type": "application/x-www-form-urlencoded" },
@@ -372,26 +428,126 @@ function _writeStoreSecure(file, store) {
372
428
  }
373
429
  }
374
430
 
431
+ /** Bounded synchronous sleep (the store lock is held only for a few file ops). */
432
+ function _sleepSyncMs(ms) {
433
+ try {
434
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
435
+ } catch {
436
+ const end = Date.now() + ms;
437
+ while (Date.now() < end) {
438
+ /* spin — Atomics.wait unavailable (should not happen on modern Node) */
439
+ }
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Run `fn` while holding a cross-process advisory lock on the token store.
445
+ *
446
+ * saveStoredToken / deleteStoredToken are read-modify-write (load → mutate →
447
+ * write). Two concurrent `cc mcp login` PROCESSES for different servers would
448
+ * each load the old store and the second writer would clobber the first's new
449
+ * entry (lost update) — the atomic temp+rename only prevents a torn file, not a
450
+ * lost update. An O_EXCL lockfile makes the whole RMW mutually exclusive across
451
+ * processes, so each writer re-reads the latest store inside the lock.
452
+ *
453
+ * Robustness: a stale lock (holder crashed) older than STALE_MS is stolen; if
454
+ * the lock can't be acquired within MAX_WAIT_MS we proceed anyway (degrading to
455
+ * the pre-lock last-writer-wins rather than failing the login). When the
456
+ * injected fs lacks the lock primitives (test fs / exotic fs) we run `fn`
457
+ * directly — single-process correctness is unchanged, only cross-process mutual
458
+ * exclusion is lost. Lock timing uses the wall clock (not the `now` test seam).
459
+ */
460
+ function withStoreLock(file, fn) {
461
+ const fs = _deps.fs;
462
+ if (
463
+ typeof fs.openSync !== "function" ||
464
+ typeof fs.closeSync !== "function" ||
465
+ typeof fs.unlinkSync !== "function"
466
+ ) {
467
+ return fn(); // no lock primitives → run unlocked (back-compat)
468
+ }
469
+ const STALE_MS = 10_000;
470
+ const MAX_WAIT_MS = 5_000;
471
+ const lockPath = `${file}.lock`;
472
+ try {
473
+ fs.mkdirSync(pathDefault.dirname(file), { recursive: true, mode: 0o700 });
474
+ } catch {
475
+ /* dir may already exist — openSync below reports any real problem */
476
+ }
477
+ const start = Date.now();
478
+ let fd = null;
479
+ for (;;) {
480
+ try {
481
+ fd = fs.openSync(lockPath, "wx"); // O_CREAT|O_EXCL — fails if held
482
+ break;
483
+ } catch (err) {
484
+ if (!err || err.code !== "EEXIST") {
485
+ // Unexpected (e.g. ENOENT/EACCES): don't fail the login over the lock —
486
+ // proceed unlocked, same as a missing-primitives fs.
487
+ return fn();
488
+ }
489
+ let stale = false;
490
+ try {
491
+ const st = fs.statSync(lockPath);
492
+ const mtime = st.mtimeMs != null ? st.mtimeMs : 0;
493
+ stale = Date.now() - mtime > STALE_MS;
494
+ } catch {
495
+ /* lock vanished between open and stat → retry immediately */
496
+ }
497
+ if (stale) {
498
+ try {
499
+ fs.unlinkSync(lockPath); // steal a lock from a crashed holder
500
+ } catch {
501
+ /* someone else stole it first — retry */
502
+ }
503
+ continue;
504
+ }
505
+ if (Date.now() - start > MAX_WAIT_MS) break; // give up waiting; proceed
506
+ _sleepSyncMs(50);
507
+ }
508
+ }
509
+ try {
510
+ return fn();
511
+ } finally {
512
+ if (fd !== null) {
513
+ try {
514
+ fs.closeSync(fd);
515
+ } catch {
516
+ /* ignore */
517
+ }
518
+ try {
519
+ fs.unlinkSync(lockPath);
520
+ } catch {
521
+ /* ignore — a stale-steal by another process may have removed it */
522
+ }
523
+ }
524
+ }
525
+ }
526
+
375
527
  export function saveStoredToken(serverUrl, record) {
376
528
  const file = tokenStorePath();
377
- const store = loadTokenStore();
378
- store[serverKey(serverUrl)] = { server: serverKey(serverUrl), ...record };
379
- _writeStoreSecure(file, store);
380
- return store[serverKey(serverUrl)];
529
+ return withStoreLock(file, () => {
530
+ const store = loadTokenStore(); // re-read inside the lock (latest state)
531
+ store[serverKey(serverUrl)] = { server: serverKey(serverUrl), ...record };
532
+ _writeStoreSecure(file, store);
533
+ return store[serverKey(serverUrl)];
534
+ });
381
535
  }
382
536
 
383
537
  export function deleteStoredToken(serverUrl) {
384
538
  const file = tokenStorePath();
385
- const store = loadTokenStore();
386
- const key = serverKey(serverUrl);
387
- if (!(key in store)) return false;
388
- delete store[key];
389
- try {
390
- _writeStoreSecure(file, store);
391
- } catch {
392
- /* best-effort */
393
- }
394
- return true;
539
+ return withStoreLock(file, () => {
540
+ const store = loadTokenStore(); // re-read inside the lock (latest state)
541
+ const key = serverKey(serverUrl);
542
+ if (!(key in store)) return false;
543
+ delete store[key];
544
+ try {
545
+ _writeStoreSecure(file, store);
546
+ } catch {
547
+ /* best-effort */
548
+ }
549
+ return true;
550
+ });
395
551
  }
396
552
 
397
553
  /** True when a record's access token is missing or within `skewMs` of expiry. */
@@ -440,12 +596,25 @@ function defaultOpenBrowser(url) {
440
596
  }
441
597
  }
442
598
 
443
- /** Wait for the OAuth redirect on a localhost callback server; resolve {code,state}. */
599
+ /**
600
+ * Wait for the OAuth redirect on a localhost callback server; resolve
601
+ * {code,state}.
602
+ *
603
+ * When `expectedState` is given, a SUCCESS callback (`code`) is only accepted if
604
+ * its `state` matches: the callback port is fixed and guessable, so a duplicate
605
+ * browser request, a probe, or a drive-by request from a malicious local page
606
+ * must not abort the real login or inject a forged code — those are answered
607
+ * politely but the server keeps waiting for the genuine redirect (until the
608
+ * backstop timeout). A provider `error` is always terminal (so denying consent
609
+ * fails fast even if the provider omits state on error). With no `expectedState`
610
+ * the legacy contract is preserved (first `/callback` hit is terminal).
611
+ */
444
612
  export function waitForCallback({
445
613
  port,
446
614
  host = "127.0.0.1",
447
615
  path = "/callback",
448
616
  timeout = 300_000,
617
+ expectedState = null,
449
618
  }) {
450
619
  return new Promise((resolve, reject) => {
451
620
  const server = _deps.createServer((req, res) => {
@@ -465,13 +634,41 @@ export function waitForCallback({
465
634
  const code = u.searchParams.get("code");
466
635
  const state = u.searchParams.get("state");
467
636
  const error = u.searchParams.get("error");
468
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
469
- res.end(renderCallbackPage(error));
637
+
638
+ // Provider error (e.g. user denied) — always terminal so we fail fast.
639
+ if (error) {
640
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
641
+ res.end(renderCallbackPage(error));
642
+ server.close();
643
+ clearTimeout(timer);
644
+ reject(new Error(`authorization error: ${error}`));
645
+ return;
646
+ }
647
+
648
+ const stateOk = expectedState == null || state === expectedState;
649
+ if (code && stateOk) {
650
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
651
+ res.end(renderCallbackPage(null));
652
+ server.close();
653
+ clearTimeout(timer);
654
+ resolve({ code, state });
655
+ return;
656
+ }
657
+
658
+ // A mismatched/forged/duplicate request to our fixed port must not abort
659
+ // the real login — answer it but keep listening for the genuine redirect.
660
+ if (expectedState != null) {
661
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
662
+ res.end(renderCallbackPage("state mismatch"));
663
+ return;
664
+ }
665
+
666
+ // Legacy contract (no expectedState): a /callback hit with no code ends it.
667
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
668
+ res.end(renderCallbackPage("missing code"));
470
669
  server.close();
471
670
  clearTimeout(timer);
472
- if (error) reject(new Error(`authorization error: ${error}`));
473
- else if (!code) reject(new Error("no authorization code in callback"));
474
- else resolve({ code, state });
671
+ reject(new Error("no authorization code in callback"));
475
672
  });
476
673
  const timer = setTimeout(() => {
477
674
  server.close();
@@ -540,6 +737,9 @@ export async function authorizeInteractive(serverUrl, opts = {}) {
540
737
  host,
541
738
  path: redirectPath,
542
739
  timeout,
740
+ // Ignore callbacks that don't carry the state we issued (forged / drive-by /
741
+ // duplicate hits to the fixed localhost port) instead of aborting the login.
742
+ expectedState: state,
543
743
  });
544
744
  const opened = _deps.openBrowser(authorizeUrl);
545
745
  writeOut(
@@ -17,7 +17,7 @@
17
17
  import http from "http";
18
18
  import fsDefault from "fs";
19
19
  import pathDefault from "path";
20
- import { randomBytes } from "crypto";
20
+ import { randomBytes, timingSafeEqual } from "crypto";
21
21
 
22
22
  export const MAX_READ_BYTES = 200 * 1024;
23
23
  export const MAX_LIST_ENTRIES = 500;
@@ -29,14 +29,65 @@ export const _deps = { fs: fsDefault, path: pathDefault };
29
29
 
30
30
  /** Resolve `rel` inside `root`; throws on escape (.. traversal, abs paths out). */
31
31
  export function confine(root, rel, deps = _deps) {
32
- const abs = deps.path.resolve(root, rel || ".");
33
- const normRoot = deps.path.resolve(root);
34
- if (abs !== normRoot && !abs.startsWith(normRoot + deps.path.sep)) {
32
+ const { path } = deps;
33
+ const fs = deps.fs;
34
+ const normRoot = path.resolve(root);
35
+ const abs = path.resolve(normRoot, rel || ".");
36
+ // Fast string guard: blocks `..` traversal and absolute-path escapes.
37
+ if (abs !== normRoot && !abs.startsWith(normRoot + path.sep)) {
35
38
  throw new Error(`path escapes serve root: ${rel}`);
36
39
  }
40
+ // Symlink guard: path.resolve does NOT follow symlinks, so a symlink INSIDE
41
+ // root pointing OUTSIDE would pass the string check yet the fs op (read/write/
42
+ // list) would escape the serve boundary — e.g. reading ~/.ssh through a
43
+ // workspace symlink, defeating a `--read-only` exposure. Resolve the real path
44
+ // of the deepest EXISTING ancestor (a not-yet-created file can't be a symlink)
45
+ // and re-check containment against the real root.
46
+ if (fs && typeof fs.realpathSync === "function") {
47
+ let realRoot;
48
+ try {
49
+ realRoot = fs.realpathSync(normRoot);
50
+ } catch {
51
+ return abs; // root itself unresolvable → the string guard is all we have
52
+ }
53
+ let probe = abs;
54
+ for (;;) {
55
+ let real;
56
+ try {
57
+ real = fs.realpathSync(probe);
58
+ } catch {
59
+ const parent = path.dirname(probe);
60
+ if (parent === probe) break; // reached fs root, nothing existed
61
+ probe = parent;
62
+ continue;
63
+ }
64
+ if (real !== realRoot && !real.startsWith(realRoot + path.sep)) {
65
+ throw new Error(`path escapes serve root (symlink): ${rel}`);
66
+ }
67
+ break;
68
+ }
69
+ }
37
70
  return abs;
38
71
  }
39
72
 
73
+ /**
74
+ * Constant-time check of an `Authorization: Bearer <token>` header against the
75
+ * expected token. A plain `!==` leaks timing that could let a caller recover the
76
+ * token byte-by-byte; hash-length differs are rejected up front (the token is a
77
+ * fixed-length random value, so its length is not secret), then `timingSafeEqual`
78
+ * compares the bytes. Mirrors the ws-server's token check.
79
+ */
80
+ export function bearerMatches(authHeader, token) {
81
+ const prefix = "Bearer ";
82
+ if (typeof authHeader !== "string" || !authHeader.startsWith(prefix)) {
83
+ return false;
84
+ }
85
+ const a = Buffer.from(authHeader.slice(prefix.length), "utf-8");
86
+ const b = Buffer.from(String(token), "utf-8");
87
+ if (a.length !== b.length) return false;
88
+ return timingSafeEqual(a, b);
89
+ }
90
+
40
91
  function ok(text) {
41
92
  return { content: [{ type: "text", text: String(text) }] };
42
93
  }
@@ -57,6 +108,35 @@ export function buildTools({ root, readOnly = false, deps = _deps }) {
57
108
  },
58
109
  handler: ({ path: rel }) => {
59
110
  const abs = confine(root, rel, deps);
111
+ // Bounded read: pull at most MAX_READ_BYTES+1 bytes into memory. The
112
+ // old `readFileSync(abs)` loaded the WHOLE file before truncating, so
113
+ // MAX_READ_BYTES only capped the RESPONSE — a multi-GB file under the
114
+ // serve root would OOM the process. The +1 byte detects "longer than
115
+ // the cap" without reading the rest. Fall back to readFileSync only
116
+ // when the fd API isn't available on an injected fs.
117
+ if (typeof fs.openSync === "function") {
118
+ const fd = fs.openSync(abs, "r");
119
+ try {
120
+ const buf = Buffer.alloc(MAX_READ_BYTES + 1);
121
+ const n = fs.readSync(fd, buf, 0, MAX_READ_BYTES + 1, 0);
122
+ const truncated = n > MAX_READ_BYTES;
123
+ const text = buf
124
+ .slice(0, truncated ? MAX_READ_BYTES : n)
125
+ .toString("utf-8");
126
+ if (!truncated) return ok(text);
127
+ let total = null;
128
+ try {
129
+ total = fs.statSync(abs).size;
130
+ } catch {
131
+ /* size is best-effort for the message */
132
+ }
133
+ return ok(
134
+ `${text}\n… [truncated${total != null ? ` ${total} bytes` : ""}]`,
135
+ );
136
+ } finally {
137
+ fs.closeSync(fd);
138
+ }
139
+ }
60
140
  const buf = fs.readFileSync(abs);
61
141
  const truncated = buf.length > MAX_READ_BYTES;
62
142
  const text = (truncated ? buf.slice(0, MAX_READ_BYTES) : buf).toString(
@@ -194,8 +274,7 @@ export function startMcpServe(opts = {}) {
194
274
  return send(405, rpcError(null, -32600, "POST only"));
195
275
  }
196
276
  if (token) {
197
- const auth = req.headers.authorization || "";
198
- if (auth !== `Bearer ${token}`) {
277
+ if (!bearerMatches(req.headers.authorization, token)) {
199
278
  return send(401, rpcError(null, -32001, "unauthorized"));
200
279
  }
201
280
  }
@@ -27,6 +27,7 @@ import crypto from "crypto";
27
27
  import { AgentRouter } from "./agent-router.js";
28
28
  import { NotificationManager } from "./notifiers/index.js";
29
29
  import { createChatFn } from "./cowork-adapter.js";
30
+ import { firstBalancedJson } from "./json-schema-output.js";
30
31
 
31
32
  /* ---------- _deps injection (Vitest CJS mock pattern) ---------- */
32
33
  export const _deps = { execSync };
@@ -81,6 +82,16 @@ export class Orchestrator extends EventEmitter {
81
82
  this.ciCommand = options.ciCommand || "npm test";
82
83
  this.verbose = options.verbose || false;
83
84
 
85
+ // Cap retained task history. Every addTask stores a record in _tasks and
86
+ // nothing ever removes it, so a long-running orchestrator (cron-watch /
87
+ // daemon) accumulates tasks without bound. Prune the oldest TERMINAL tasks
88
+ // beyond this cap (in-flight tasks are never dropped). Generous default;
89
+ // 0/invalid → 500.
90
+ this.maxTasks =
91
+ Number.isFinite(options.maxTasks) && options.maxTasks > 0
92
+ ? options.maxTasks
93
+ : 500;
94
+
84
95
  /** @type {Map<string, object>} taskId → task */
85
96
  this._tasks = new Map();
86
97
 
@@ -150,6 +161,7 @@ export class Orchestrator extends EventEmitter {
150
161
  };
151
162
 
152
163
  this._tasks.set(task.id, task);
164
+ this._pruneTasks();
153
165
  this.emit("task:added", task);
154
166
 
155
167
  if (this.notifier.isConfigured && task.notify) {
@@ -182,7 +194,7 @@ export class Orchestrator extends EventEmitter {
182
194
  return {
183
195
  tasks,
184
196
  pool: this._router.summary(),
185
- cliCommand: this.cliCommand,
197
+ ciCommand: this.ciCommand,
186
198
  cronActive: this._cronTimer !== null,
187
199
  };
188
200
  }
@@ -277,7 +289,7 @@ export class Orchestrator extends EventEmitter {
277
289
  async _dispatch(task) {
278
290
  this.emit("agents:dispatched", { task, count: task.subtasks.length });
279
291
  this._log(
280
- `Dispatching ${task.subtasks.length} subtask(s) to ${this.cliCommand} agents`,
292
+ `Dispatching ${task.subtasks.length} subtask(s) to ${this._router.summary().length} agent backend(s)`,
281
293
  );
282
294
 
283
295
  const results = await this._router.dispatch(task.subtasks, {
@@ -371,6 +383,11 @@ export class Orchestrator extends EventEmitter {
371
383
  cwd,
372
384
  encoding: "utf-8",
373
385
  timeout: 180_000,
386
+ // execSync defaults maxBuffer to 1 MB; a verbose-but-PASSING test run
387
+ // that prints more than that throws ENOBUFS, which the catch below
388
+ // would misreport as a CI failure (triggering needless retries +
389
+ // re-dispatch). Give CI output generous headroom.
390
+ maxBuffer: 64 * 1024 * 1024,
374
391
  stdio: "pipe",
375
392
  });
376
393
  return {
@@ -386,6 +403,26 @@ export class Orchestrator extends EventEmitter {
386
403
  }
387
404
  }
388
405
 
406
+ /**
407
+ * Bound retained task history to maxTasks. Evicts the oldest TERMINAL tasks
408
+ * first (Map preserves insertion order = FIFO); an in-flight task is never
409
+ * dropped, so a burst of concurrent runs can briefly exceed the cap rather
410
+ * than losing live work. The dropped records are completed/failed history
411
+ * the caller has already seen via task:complete / task:failed events.
412
+ */
413
+ _pruneTasks() {
414
+ if (this._tasks.size <= this.maxTasks) return;
415
+ for (const [id, t] of this._tasks) {
416
+ if (this._tasks.size <= this.maxTasks) break;
417
+ if (
418
+ t.status === TASK_STATUS.COMPLETED ||
419
+ t.status === TASK_STATUS.FAILED
420
+ ) {
421
+ this._tasks.delete(id);
422
+ }
423
+ }
424
+ }
425
+
389
426
  async _cronTick() {
390
427
  this.emit("cron:tick", { at: new Date().toISOString() });
391
428
  // Override this method or listen to the event to add input source polling
@@ -403,8 +440,9 @@ export class Orchestrator extends EventEmitter {
403
440
 
404
441
  /** Extract the first JSON array from an LLM response string. */
405
442
  function _extractJson(text) {
406
- const match = text.match(/\[[\s\S]*\]/);
407
- return match ? match[0] : "[]";
443
+ // Balanced extraction stops at the first complete array, so trailing prose
444
+ // (or a second array) no longer over-captures into an unparseable string.
445
+ return firstBalancedJson(text, "[") || "[]";
408
446
  }
409
447
 
410
448
  /** Parse error lines from CI output (test failures, lint errors). */
@@ -58,6 +58,17 @@ export function startApp(options = {}) {
58
58
  return child.pid;
59
59
  }
60
60
 
61
+ /**
62
+ * Parse a PID-file's contents into a valid positive integer, or null.
63
+ * A corrupt / empty / partially-written file must not yield NaN — that NaN
64
+ * would otherwise flow into `taskkill /PID NaN` or `process.kill(NaN, …)`,
65
+ * which silently fails to act on the real process.
66
+ */
67
+ export function parsePid(content) {
68
+ const pid = parseInt(String(content).trim(), 10);
69
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
70
+ }
71
+
61
72
  export function stopApp() {
62
73
  const pidFile = getPidFilePath();
63
74
 
@@ -66,7 +77,16 @@ export function stopApp() {
66
77
  return false;
67
78
  }
68
79
 
69
- const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
80
+ const pid = parsePid(readFileSync(pidFile, "utf-8"));
81
+ if (pid === null) {
82
+ logger.warn("PID file is empty or corrupt; removing it.");
83
+ try {
84
+ unlinkSync(pidFile);
85
+ } catch {
86
+ /* best-effort */
87
+ }
88
+ return false;
89
+ }
70
90
 
71
91
  try {
72
92
  if (isWindows()) {
@@ -90,7 +110,15 @@ export function isAppRunning() {
90
110
  return false;
91
111
  }
92
112
 
93
- const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
113
+ const pid = parsePid(readFileSync(pidFile, "utf-8"));
114
+ if (pid === null) {
115
+ try {
116
+ unlinkSync(pidFile);
117
+ } catch {
118
+ /* best-effort */
119
+ }
120
+ return false;
121
+ }
94
122
 
95
123
  try {
96
124
  process.kill(pid, 0);
@@ -104,7 +132,7 @@ export function isAppRunning() {
104
132
  export function getAppPid() {
105
133
  const pidFile = getPidFilePath();
106
134
  if (!existsSync(pidFile)) return null;
107
- return parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
135
+ return parsePid(readFileSync(pidFile, "utf-8"));
108
136
  }
109
137
 
110
138
  function findExecutable(binDir, extension) {