chainlesschain 0.162.78 → 0.162.79

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 (196) hide show
  1. package/README.md +37 -1
  2. package/bin/chainlesschain.js +20 -1
  3. package/package.json +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-BhKMd38k.js → AIOps-D89fD_7T.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-BzhKY5C_.js → ActionButton-d75flwX8.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-CATIz2Jc.js → Analytics-gvvBEb4T.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-eCx64YWg.js → AppLayout-DzHOVS64.js} +5 -5
  8. package/src/assets/web-panel/assets/{Audit-CGOHfCHj.js → Audit-15K7MhRC.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-Dyr6R0Ra.js → Backup-BgVHsglI.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-CVhBu7NZ.js → BaseInput-BRnvd4sF.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-DOCCKp2k.js → Chat-B2vtXu7s.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-DcXCCnjN.js → ChatBubbleRenderer-d1EFM3dh.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-3yjnENud.js → Checkbox-B5uV5Jhr.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-D06sn8kB.js → Codegen-cVDMBVGV.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-Bjn5vFES.js → Col-DtL6q7Hw.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-BYHpQHmf.js → Community-DSyPZkqQ.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-MKRmnUDQ.js → Compact-CzfV_q4O.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-CVtPe8dh.js → Compliance-B_xhhLE1.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-BRu5M3dv.js → Cowork-Y49Qf0JX.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-D_7eU5Ut.js → Cron-B7JG-oLj.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-BFyVp_e9.js → Crosschain-CukEkQtf.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-DRoG7638.js → DID-Bd4UVKdn.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-NfM_v-Oe.js → Dashboard-DRCQl7H1.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-CGN1Ksu0.js → Dropdown-De47CuRY.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-DhsY_z_a.js → EmailListRenderer-C-wAkceB.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-vW_VsKUG.js → FamilyGuardDashboard-DhLF_E0k.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-D2ob_c7h.js → Federation-DAipUS1m.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-Cpem15Pr.js → FormItemContext-BCFyAd2j.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-P6XfmXwT.js → GenericCardRenderer-wMdxrEV9.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-CfSeec1R.js → Git-CcwP7HUN.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-4ipIpqNl.js → Governance-VF760JcB.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-CiQY_P9Y.js → Inference-DnR-GIn2.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-B3Qem78R.js → KnowledgeGraph-j-ewVKWO.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-De2zWVy6.js → Logs-DgB7wSor.js} +2 -2
  35. package/src/assets/web-panel/assets/{Marketplace-CTNu4c1A.js → Marketplace-fiKjzVWE.js} +1 -1
  36. package/src/assets/web-panel/assets/{McpTools-Xx25EA2f.js → McpTools-BjXSMQrd.js} +5 -5
  37. package/src/assets/web-panel/assets/{Memory-YzzCCrch.js → Memory-C0L5lnSS.js} +2 -2
  38. package/src/assets/web-panel/assets/{MobileBridge-CJZY98f0.js → MobileBridge-C0Rgg1Su.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileProjects-Dw7yl4KN.js → MobileProjects-BPA50hQb.js} +1 -1
  40. package/src/assets/web-panel/assets/{Mtc-D7TzGJhH.js → Mtc-BUsHuAjB.js} +4 -4
  41. package/src/assets/web-panel/assets/{MtcAudit-THyGhf0h.js → MtcAudit-dlH8Q_7U.js} +6 -6
  42. package/src/assets/web-panel/assets/{Multisig-RVxuGPUR.js → Multisig-L3_9beuW.js} +3 -3
  43. package/src/assets/web-panel/assets/{NLProgramming-DK7TCzKF.js → NLProgramming-BG4sXy27.js} +1 -1
  44. package/src/assets/web-panel/assets/Notes-felgIvGS.js +7 -0
  45. package/src/assets/web-panel/assets/{NotificationSettings-BMivJy85.js → NotificationSettings-DHDT96AK.js} +1 -1
  46. package/src/assets/web-panel/assets/OrderTableRenderer-BYrkEfvR.js +1 -0
  47. package/src/assets/web-panel/assets/{Organization-zVRtW1n_.js → Organization-DdtOLkHG.js} +4 -4
  48. package/src/assets/web-panel/assets/{Overflow-C0YLldFH.js → Overflow-DLw5Pni7.js} +1 -1
  49. package/src/assets/web-panel/assets/{P2P-Xxgzghqp.js → P2P-B-mZ5EXz.js} +2 -2
  50. package/src/assets/web-panel/assets/{PdhVaultBrowser-DH_LO13b.js → PdhVaultBrowser-DeOmNCwK.js} +5 -5
  51. package/src/assets/web-panel/assets/{Permissions-CvAd1VBw.js → Permissions-BeQMX71K.js} +4 -4
  52. package/src/assets/web-panel/assets/{PersonalDataHub-CWQGgCAK.js → PersonalDataHub-D5PoqtQI.js} +3 -3
  53. package/src/assets/web-panel/assets/{Pipeline-BNAoh-Lb.js → Pipeline-B0f5sqKF.js} +1 -1
  54. package/src/assets/web-panel/assets/{Privacy-CNO5pFq-.js → Privacy-Dj_gcxio.js} +1 -1
  55. package/src/assets/web-panel/assets/{ProjectInit-WaVVDsm3.js → ProjectInit-DmfPgied.js} +2 -2
  56. package/src/assets/web-panel/assets/{ProjectSettings-D1WfkuJ3.js → ProjectSettings-BmBfocrv.js} +2 -2
  57. package/src/assets/web-panel/assets/Projects-BUif48cc.js +1 -0
  58. package/src/assets/web-panel/assets/{Providers-IOOJ4_wy.js → Providers-B0ztEAOV.js} +1 -1
  59. package/src/assets/web-panel/assets/{QuickAsk-ChHZqVZy.js → QuickAsk-BtMG4jH5.js} +1 -1
  60. package/src/assets/web-panel/assets/{Recommend-CSiW6Qv9.js → Recommend-DQKN1En8.js} +1 -1
  61. package/src/assets/web-panel/assets/{Reputation-BQe0rkfF.js → Reputation-D34CvUxg.js} +1 -1
  62. package/src/assets/web-panel/assets/{Row-Dem0Wxxb.js → Row-nnuDNx31.js} +1 -1
  63. package/src/assets/web-panel/assets/{RssFeed-pBY5G41C.js → RssFeed-DHx9x8_B.js} +2 -2
  64. package/src/assets/web-panel/assets/{Search-CtRepO6B.js → Search-CH-JYtn_.js} +1 -1
  65. package/src/assets/web-panel/assets/{Security-nrSlKpWq.js → Security-nNKBmzm4.js} +4 -4
  66. package/src/assets/web-panel/assets/{Services-DeaDBASi.js → Services-xYk_lDxy.js} +2 -2
  67. package/src/assets/web-panel/assets/{Skeleton-Cz9R-Wjb.js → Skeleton-DTYyRduA.js} +1 -1
  68. package/src/assets/web-panel/assets/{Skills-B3U-XLH3.js → Skills-DS19-9sF.js} +1 -1
  69. package/src/assets/web-panel/assets/{Sla-Bu46dIA_.js → Sla-Cr_ir42Y.js} +1 -1
  70. package/src/assets/web-panel/assets/{SpeechSettings-C9Z0V0pk.js → SpeechSettings-Ch5Iq7Wy.js} +1 -1
  71. package/src/assets/web-panel/assets/{SyncSettings-Ctj9KHHr.js → SyncSettings-CTblfa_E.js} +2 -2
  72. package/src/assets/web-panel/assets/{Tasks-D4upQgR_.js → Tasks-z7lz3hjG.js} +1 -1
  73. package/src/assets/web-panel/assets/{Templates-JHsPGU_c.js → Templates-DGoMFd4r.js} +1 -1
  74. package/src/assets/web-panel/assets/{Tenant-uoaQL3fB.js → Tenant-DQUPvHxx.js} +1 -1
  75. package/src/assets/web-panel/assets/{Terminal-CWRWr8bq.js → Terminal-Wt2wrbE6.js} +2 -2
  76. package/src/assets/web-panel/assets/{TimelineRenderer-BTicmSAV.js → TimelineRenderer-DGTc2UqL.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tokens-Bp3BUe2K.js → Tokens-DPVnuARF.js} +1 -1
  78. package/src/assets/web-panel/assets/{Trigger-CgoISw5d.js → Trigger-DV5MPXC1.js} +1 -1
  79. package/src/assets/web-panel/assets/{Trust-CC29awNT.js → Trust-BBkMKqwS.js} +1 -1
  80. package/src/assets/web-panel/assets/{UkeySign-CB1SB6Nc.js → UkeySign-B_2E0qev.js} +1 -1
  81. package/src/assets/web-panel/assets/{VideoEditing-D7vptDUg.js → VideoEditing-CA-qKeVb.js} +1 -1
  82. package/src/assets/web-panel/assets/{Wallet-BWfjzF7p.js → Wallet-Ds4WURh-.js} +4 -4
  83. package/src/assets/web-panel/assets/{WebAuthn-Dzz5OnPc.js → WebAuthn-BApiHjzz.js} +5 -5
  84. package/src/assets/web-panel/assets/{WorkflowEditor-CiDeVmsG.js → WorkflowEditor-DFzmAnzV.js} +1 -1
  85. package/src/assets/web-panel/assets/{chat-DQbciNb5.js → chat-DZ6sHPit.js} +1 -1
  86. package/src/assets/web-panel/assets/{colors-DcLbPJzb.js → colors-DgbwhHtE.js} +1 -1
  87. package/src/assets/web-panel/assets/{compact-item-CvYrR3rc.js → compact-item-C4QVYSzd.js} +1 -1
  88. package/src/assets/web-panel/assets/{createContext-BR4P7Rgm.js → createContext-yXIs4TwU.js} +1 -1
  89. package/src/assets/web-panel/assets/devWarning-C-HfIeF7.js +1 -0
  90. package/src/assets/web-panel/assets/{hasIn-IQ88RNRJ.js → hasIn-CPtuUtBl.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-B5W1vQHV.js → index-4g6pGxnX.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-DHIp5msb.js → index-B91hax9S.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-BUTN1VlO.js → index-B9DUiQ24.js} +3 -3
  94. package/src/assets/web-panel/assets/index-BBgWatYO.js +1 -0
  95. package/src/assets/web-panel/assets/{index-DgaCUxpi.js → index-BCb7MYMS.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BmPuR0aA.js → index-BIAxMDe9.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-Dpmnk2qv.js → index-BJiGrT6V.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-D8OJdOc_.js → index-BOKVSmKh.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-Mn8_ryOe.js → index-BWJGry74.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BiMlLIZ-.js → index-BaVYCMJa.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CPOupQSX.js → index-Bd0JznCS.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Bo7HAK6G.js → index-BpBCKK6W.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-Db5LFFCN.js → index-BsXF9cn5.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BQXs-5db.js → index-BxclR5gc.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BVb6RI7f.js → index-C0LyE6R6.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-CiOZ_Whh.js → index-CBAgy5pf.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DdQBxvpt.js → index-CJ550XXg.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-Cm74AosZ.js → index-CTlz3MP6.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-Bw0Dm_P6.js → index-CaC4gaQZ.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-BxY0ozve.js → index-CnP2ftM5.js} +1 -1
  111. package/src/assets/web-panel/assets/index-CvCTol_u.js +1 -0
  112. package/src/assets/web-panel/assets/{index-eKd1n8pw.js → index-D2KRfjEI.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-b6FjzfoJ.js → index-DA0ePxNn.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-CE2mqX8w.js → index-DC5iP1VB.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Bl1TSbTE.js → index-DCGmeXfl.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-BH2RT15D.js → index-DHnWI0jj.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-UbB2IcFR.js → index-DN4qpkKQ.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CgP5aQmA.js → index-DTLaRwKx.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BHxJnExB.js → index-DdFjcgqH.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-JqOP7puJ.js → index-DopuoTzG.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-Dox9vEhP.js → index-DpDAGvyU.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-DGwa8mnJ.js → index-EGVlUtH-.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-bRT7u-51.js → index-IFh9qCi0.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-Bu8931Yi.js → index-UEeNpaDD.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-CCO8yc1h.js → index-ZCyyy3Zt.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CzERBV9P.js → index-aKZjRwjv.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CeCWyiFl.js → index-jBlY6-bF.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BhkZZXtI.js → index-kbcTc6Pf.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BvQpTO67.js → index-x9bXmSNB.js} +1 -1
  130. package/src/assets/web-panel/assets/{initDefaultProps-C0arzCLE.js → initDefaultProps-BDsxioXk.js} +1 -1
  131. package/src/assets/web-panel/assets/{motion-C1K6JxwD.js → motion-erZ2fiIQ.js} +1 -1
  132. package/src/assets/web-panel/assets/{move-DREsRLHj.js → move-BFiiw4WK.js} +1 -1
  133. package/src/assets/web-panel/assets/{omit-BtPS3EDq.js → omit-BRMVd4pM.js} +1 -1
  134. package/src/assets/web-panel/assets/{pickAttrs-BPz6tHoT.js → pickAttrs-CGjoFXsq.js} +1 -1
  135. package/src/assets/web-panel/assets/{placementArrow-B0CR_CSI.js → placementArrow-DU_Bnf7w.js} +1 -1
  136. package/src/assets/web-panel/assets/{responsiveObserve-Ch2ojiNn.js → responsiveObserve-DPdkYupg.js} +1 -1
  137. package/src/assets/web-panel/assets/{slide-9qU9vOhj.js → slide-JfHiG96y.js} +1 -1
  138. package/src/assets/web-panel/assets/{statusUtils-Cr4fICjV.js → statusUtils-Dy-1guyd.js} +1 -1
  139. package/src/assets/web-panel/assets/{styleChecker-Cor2-FwV.js → styleChecker-Chaljnux.js} +1 -1
  140. package/src/assets/web-panel/assets/{useFlexGapSupport-BINo_rNH.js → useFlexGapSupport-DOHIHBgY.js} +1 -1
  141. package/src/assets/web-panel/assets/{useFs-Dm1tDNYC.js → useFs-BxSoQhIY.js} +1 -1
  142. package/src/assets/web-panel/assets/{usePersonalDataHub-__JgBEkX.js → usePersonalDataHub-MOR76PYB.js} +1 -1
  143. package/src/assets/web-panel/assets/{vnode-1hQKpRgP.js → vnode-BBGIyeYD.js} +1 -1
  144. package/src/assets/web-panel/assets/{zoom-C1EY9X2J.js → zoom-C1oFENec.js} +1 -1
  145. package/src/assets/web-panel/index.html +1 -1
  146. package/src/commands/audit.js +4 -3
  147. package/src/commands/automation.js +6 -14
  148. package/src/commands/bi.js +10 -9
  149. package/src/commands/codegen.js +5 -13
  150. package/src/commands/dao.js +8 -6
  151. package/src/commands/dbevo.js +13 -14
  152. package/src/commands/economy.js +3 -2
  153. package/src/commands/evolution.js +3 -2
  154. package/src/commands/federation.js +4 -3
  155. package/src/commands/governance.js +9 -4
  156. package/src/commands/hardening.js +5 -4
  157. package/src/commands/incentive.js +6 -5
  158. package/src/commands/kg.js +17 -10
  159. package/src/commands/lowcode.js +23 -11
  160. package/src/commands/marketplace.js +4 -3
  161. package/src/commands/mcp.js +17 -5
  162. package/src/commands/ops.js +9 -4
  163. package/src/commands/recommend.js +7 -5
  164. package/src/commands/scim.js +3 -2
  165. package/src/commands/session.js +9 -6
  166. package/src/commands/social.js +4 -3
  167. package/src/commands/sync.js +3 -2
  168. package/src/commands/tenant.js +11 -6
  169. package/src/commands/zkp.js +8 -9
  170. package/src/gateways/ws/ws-agent-handler.js +12 -3
  171. package/src/gateways/ws/ws-server.js +6 -0
  172. package/src/harness/background-task-manager.js +44 -18
  173. package/src/harness/mcp-client.js +125 -46
  174. package/src/lib/chat-core.js +209 -107
  175. package/src/lib/claude-code-bridge.js +13 -1
  176. package/src/lib/dao-governance.js +3 -3
  177. package/src/lib/downloader.js +82 -25
  178. package/src/lib/headless-config-command.js +62 -0
  179. package/src/lib/json-schema-output.js +55 -11
  180. package/src/lib/mcp-oauth.js +110 -21
  181. package/src/lib/mcp-serve.js +70 -11
  182. package/src/lib/multisig-runtime.js +22 -3
  183. package/src/lib/parse-json-option.js +35 -0
  184. package/src/lib/parse-number-option.js +27 -0
  185. package/src/lib/runnable-provider.js +97 -0
  186. package/src/repl/agent-repl.js +76 -17
  187. package/src/repl/config-summary.js +66 -0
  188. package/src/runtime/agent-core.js +189 -36
  189. package/src/runtime/headless-runner.js +49 -1
  190. package/src/runtime/headless-stream.js +34 -0
  191. package/src/assets/web-panel/assets/Notes-BaOU0psj.js +0 -7
  192. package/src/assets/web-panel/assets/OrderTableRenderer-BZOiY8Yw.js +0 -1
  193. package/src/assets/web-panel/assets/Projects-BzjvJYMW.js +0 -1
  194. package/src/assets/web-panel/assets/devWarning-CnV02N63.js +0 -1
  195. package/src/assets/web-panel/assets/index-DJ2gkaIH.js +0 -1
  196. package/src/assets/web-panel/assets/index-Dvm_-AOi.js +0 -1
@@ -12,23 +12,65 @@
12
12
  import { BUILT_IN_PROVIDERS } from "./llm-providers.js";
13
13
  import { appendTokenUsage } from "../harness/jsonl-session-store.js";
14
14
 
15
+ // A streaming chat call must not hang forever if the API accepts the connection
16
+ // but then goes silent (TCP up, no bytes). Abort the request if no data arrives
17
+ // for STREAM_STALL_MS — reset on every chunk, so it's a stall detector, not a
18
+ // total-time cap (long but healthy responses are unaffected). The default is
19
+ // generous (and env-overridable) so a slow local model's first token isn't cut
20
+ // off; raise CC_CHAT_STALL_MS if you run very large local models.
21
+ export const STREAM_STALL_MS = Number(process.env.CC_CHAT_STALL_MS) || 180000;
22
+
23
+ function makeStallGuard(stallMs = STREAM_STALL_MS) {
24
+ const controller = new AbortController();
25
+ let timer = null;
26
+ const bump = () => {
27
+ if (timer) clearTimeout(timer);
28
+ timer = setTimeout(() => controller.abort(), stallMs);
29
+ if (timer && typeof timer.unref === "function") timer.unref();
30
+ };
31
+ const stop = () => {
32
+ if (timer) clearTimeout(timer);
33
+ timer = null;
34
+ };
35
+ return {
36
+ signal: controller.signal,
37
+ bump,
38
+ stop,
39
+ stalled: () => controller.signal.aborted,
40
+ };
41
+ }
42
+
15
43
  /**
16
44
  * Stream a response from Ollama.
17
45
  * If `onUsage` is provided, it's called with `{inputTokens, outputTokens}`
18
46
  * derived from Ollama's terminal `prompt_eval_count` / `eval_count` fields.
19
47
  */
20
48
  export async function streamOllama(messages, model, baseUrl, onToken, onUsage) {
21
- const response = await fetch(`${baseUrl}/api/chat`, {
22
- method: "POST",
23
- headers: { "Content-Type": "application/json" },
24
- body: JSON.stringify({
25
- model,
26
- messages,
27
- stream: true,
28
- }),
29
- });
49
+ const guard = makeStallGuard();
50
+ guard.bump();
51
+ let response;
52
+ try {
53
+ response = await fetch(`${baseUrl}/api/chat`, {
54
+ method: "POST",
55
+ headers: { "Content-Type": "application/json" },
56
+ body: JSON.stringify({
57
+ model,
58
+ messages,
59
+ stream: true,
60
+ }),
61
+ signal: guard.signal,
62
+ });
63
+ } catch (e) {
64
+ guard.stop();
65
+ throw guard.stalled()
66
+ ? new Error(
67
+ `Ollama request stalled (no response in ${STREAM_STALL_MS / 1000}s)`,
68
+ )
69
+ : e;
70
+ }
30
71
 
31
72
  if (!response.ok) {
73
+ guard.stop();
32
74
  throw new Error(`Ollama error: ${response.status} ${response.statusText}`);
33
75
  }
34
76
 
@@ -36,31 +78,42 @@ export async function streamOllama(messages, model, baseUrl, onToken, onUsage) {
36
78
  const decoder = new TextDecoder();
37
79
  let fullResponse = "";
38
80
 
39
- while (true) {
40
- const { done, value } = await reader.read();
41
- if (done) break;
81
+ try {
82
+ while (true) {
83
+ const { done, value } = await reader.read();
84
+ if (done) break;
85
+ guard.bump();
42
86
 
43
- const text = decoder.decode(value, { stream: true });
44
- const lines = text.split("\n").filter(Boolean);
87
+ const text = decoder.decode(value, { stream: true });
88
+ const lines = text.split("\n").filter(Boolean);
45
89
 
46
- for (const line of lines) {
47
- try {
48
- const json = JSON.parse(line);
49
- if (json.message?.content) {
50
- fullResponse += json.message.content;
51
- onToken(json.message.content);
52
- }
53
- if (json.done && onUsage) {
54
- const inputTokens = Number(json.prompt_eval_count) || 0;
55
- const outputTokens = Number(json.eval_count) || 0;
56
- if (inputTokens || outputTokens) {
57
- onUsage({ inputTokens, outputTokens });
90
+ for (const line of lines) {
91
+ try {
92
+ const json = JSON.parse(line);
93
+ if (json.message?.content) {
94
+ fullResponse += json.message.content;
95
+ onToken(json.message.content);
58
96
  }
97
+ if (json.done && onUsage) {
98
+ const inputTokens = Number(json.prompt_eval_count) || 0;
99
+ const outputTokens = Number(json.eval_count) || 0;
100
+ if (inputTokens || outputTokens) {
101
+ onUsage({ inputTokens, outputTokens });
102
+ }
103
+ }
104
+ } catch {
105
+ // Partial JSON, skip
59
106
  }
60
- } catch {
61
- // Partial JSON, skip
62
107
  }
63
108
  }
109
+ } catch (e) {
110
+ throw guard.stalled()
111
+ ? new Error(
112
+ `Ollama stream stalled (no data in ${STREAM_STALL_MS / 1000}s)`,
113
+ )
114
+ : e;
115
+ } finally {
116
+ guard.stop();
64
117
  }
65
118
 
66
119
  return fullResponse;
@@ -77,23 +130,37 @@ export async function streamOpenAI(
77
130
  onToken,
78
131
  onUsage,
79
132
  ) {
80
- const response = await fetch(`${baseUrl}/chat/completions`, {
81
- method: "POST",
82
- headers: {
83
- "Content-Type": "application/json",
84
- Authorization: `Bearer ${apiKey}`,
85
- },
86
- body: JSON.stringify({
87
- model,
88
- messages,
89
- stream: true,
90
- // Opt-in token usage in the terminal chunk (OpenAI-compatible).
91
- // Servers that don't understand it simply ignore it.
92
- stream_options: { include_usage: true },
93
- }),
94
- });
133
+ const guard = makeStallGuard();
134
+ guard.bump();
135
+ let response;
136
+ try {
137
+ response = await fetch(`${baseUrl}/chat/completions`, {
138
+ method: "POST",
139
+ headers: {
140
+ "Content-Type": "application/json",
141
+ Authorization: `Bearer ${apiKey}`,
142
+ },
143
+ body: JSON.stringify({
144
+ model,
145
+ messages,
146
+ stream: true,
147
+ // Opt-in token usage in the terminal chunk (OpenAI-compatible).
148
+ // Servers that don't understand it simply ignore it.
149
+ stream_options: { include_usage: true },
150
+ }),
151
+ signal: guard.signal,
152
+ });
153
+ } catch (e) {
154
+ guard.stop();
155
+ throw guard.stalled()
156
+ ? new Error(
157
+ `API request stalled (no response in ${STREAM_STALL_MS / 1000}s)`,
158
+ )
159
+ : e;
160
+ }
95
161
 
96
162
  if (!response.ok) {
163
+ guard.stop();
97
164
  throw new Error(`API error: ${response.status} ${response.statusText}`);
98
165
  }
99
166
 
@@ -101,36 +168,45 @@ export async function streamOpenAI(
101
168
  const decoder = new TextDecoder();
102
169
  let fullResponse = "";
103
170
 
104
- while (true) {
105
- const { done, value } = await reader.read();
106
- if (done) break;
171
+ try {
172
+ while (true) {
173
+ const { done, value } = await reader.read();
174
+ if (done) break;
175
+ guard.bump();
107
176
 
108
- const text = decoder.decode(value, { stream: true });
109
- const lines = text.split("\n").filter(Boolean);
177
+ const text = decoder.decode(value, { stream: true });
178
+ const lines = text.split("\n").filter(Boolean);
110
179
 
111
- for (const line of lines) {
112
- if (line.startsWith("data: ")) {
113
- const data = line.slice(6);
114
- if (data === "[DONE]") continue;
115
- try {
116
- const json = JSON.parse(data);
117
- const content = json.choices?.[0]?.delta?.content;
118
- if (content) {
119
- fullResponse += content;
120
- onToken(content);
121
- }
122
- if (json.usage && onUsage) {
123
- const inputTokens = Number(json.usage.prompt_tokens) || 0;
124
- const outputTokens = Number(json.usage.completion_tokens) || 0;
125
- if (inputTokens || outputTokens) {
126
- onUsage({ inputTokens, outputTokens });
180
+ for (const line of lines) {
181
+ if (line.startsWith("data: ")) {
182
+ const data = line.slice(6);
183
+ if (data === "[DONE]") continue;
184
+ try {
185
+ const json = JSON.parse(data);
186
+ const content = json.choices?.[0]?.delta?.content;
187
+ if (content) {
188
+ fullResponse += content;
189
+ onToken(content);
190
+ }
191
+ if (json.usage && onUsage) {
192
+ const inputTokens = Number(json.usage.prompt_tokens) || 0;
193
+ const outputTokens = Number(json.usage.completion_tokens) || 0;
194
+ if (inputTokens || outputTokens) {
195
+ onUsage({ inputTokens, outputTokens });
196
+ }
127
197
  }
198
+ } catch {
199
+ // Partial data
128
200
  }
129
- } catch {
130
- // Partial data
131
201
  }
132
202
  }
133
203
  }
204
+ } catch (e) {
205
+ throw guard.stalled()
206
+ ? new Error(`API stream stalled (no data in ${STREAM_STALL_MS / 1000}s)`)
207
+ : e;
208
+ } finally {
209
+ guard.stop();
134
210
  }
135
211
 
136
212
  return fullResponse;
@@ -161,23 +237,37 @@ export async function streamAnthropic(
161
237
  }
162
238
  }
163
239
 
164
- const response = await fetch(`${baseUrl}/messages`, {
165
- method: "POST",
166
- headers: {
167
- "Content-Type": "application/json",
168
- "x-api-key": apiKey,
169
- "anthropic-version": "2023-06-01",
170
- },
171
- body: JSON.stringify({
172
- model,
173
- max_tokens: 4096,
174
- stream: true,
175
- ...(system ? { system } : {}),
176
- messages: convo,
177
- }),
178
- });
240
+ const guard = makeStallGuard();
241
+ guard.bump();
242
+ let response;
243
+ try {
244
+ response = await fetch(`${baseUrl}/messages`, {
245
+ method: "POST",
246
+ headers: {
247
+ "Content-Type": "application/json",
248
+ "x-api-key": apiKey,
249
+ "anthropic-version": "2023-06-01",
250
+ },
251
+ body: JSON.stringify({
252
+ model,
253
+ max_tokens: 4096,
254
+ stream: true,
255
+ ...(system ? { system } : {}),
256
+ messages: convo,
257
+ }),
258
+ signal: guard.signal,
259
+ });
260
+ } catch (e) {
261
+ guard.stop();
262
+ throw guard.stalled()
263
+ ? new Error(
264
+ `Anthropic request stalled (no response in ${STREAM_STALL_MS / 1000}s)`,
265
+ )
266
+ : e;
267
+ }
179
268
 
180
269
  if (!response.ok) {
270
+ guard.stop();
181
271
  throw new Error(
182
272
  `Anthropic error: ${response.status} ${response.statusText}`,
183
273
  );
@@ -190,36 +280,48 @@ export async function streamAnthropic(
190
280
  let inputTokens = 0;
191
281
  let outputTokens = 0;
192
282
 
193
- while (true) {
194
- const { done, value } = await reader.read();
195
- if (done) break;
196
- buf += decoder.decode(value, { stream: true });
197
- const lines = buf.split("\n");
198
- buf = lines.pop() || "";
199
- for (const raw of lines) {
200
- const line = raw.trim();
201
- if (!line || !line.startsWith("data:")) continue;
202
- const payload = line.slice(5).trim();
203
- if (!payload) continue;
204
- try {
205
- const obj = JSON.parse(payload);
206
- if (obj.type === "content_block_delta") {
207
- const delta = obj.delta?.text;
208
- if (delta) {
209
- fullResponse += delta;
210
- onToken(delta);
283
+ try {
284
+ while (true) {
285
+ const { done, value } = await reader.read();
286
+ if (done) break;
287
+ guard.bump();
288
+ buf += decoder.decode(value, { stream: true });
289
+ const lines = buf.split("\n");
290
+ buf = lines.pop() || "";
291
+ for (const raw of lines) {
292
+ const line = raw.trim();
293
+ if (!line || !line.startsWith("data:")) continue;
294
+ const payload = line.slice(5).trim();
295
+ if (!payload) continue;
296
+ try {
297
+ const obj = JSON.parse(payload);
298
+ if (obj.type === "content_block_delta") {
299
+ const delta = obj.delta?.text;
300
+ if (delta) {
301
+ fullResponse += delta;
302
+ onToken(delta);
303
+ }
304
+ } else if (obj.type === "message_start") {
305
+ inputTokens =
306
+ Number(obj.message?.usage?.input_tokens) || inputTokens;
307
+ outputTokens =
308
+ Number(obj.message?.usage?.output_tokens) || outputTokens;
309
+ } else if (obj.type === "message_delta") {
310
+ outputTokens = Number(obj.usage?.output_tokens) || outputTokens;
211
311
  }
212
- } else if (obj.type === "message_start") {
213
- inputTokens = Number(obj.message?.usage?.input_tokens) || inputTokens;
214
- outputTokens =
215
- Number(obj.message?.usage?.output_tokens) || outputTokens;
216
- } else if (obj.type === "message_delta") {
217
- outputTokens = Number(obj.usage?.output_tokens) || outputTokens;
312
+ } catch {
313
+ /* skip malformed */
218
314
  }
219
- } catch {
220
- /* skip malformed */
221
315
  }
222
316
  }
317
+ } catch (e) {
318
+ throw guard.stalled()
319
+ ? new Error(
320
+ `Anthropic stream stalled (no data in ${STREAM_STALL_MS / 1000}s)`,
321
+ )
322
+ : e;
323
+ } finally {
324
+ guard.stop();
223
325
  }
224
326
 
225
327
  if (onUsage && (inputTokens || outputTokens)) {
@@ -97,6 +97,7 @@ export class ClaudeCodeAgent extends EventEmitter {
97
97
  timeout = 300_000,
98
98
  context = "",
99
99
  allowedTools = null,
100
+ killGraceMs = 3000,
100
101
  } = options;
101
102
 
102
103
  const fullPrompt = context
@@ -128,10 +129,19 @@ export class ClaudeCodeAgent extends EventEmitter {
128
129
  });
129
130
  this._proc = proc;
130
131
 
132
+ // SIGKILL-escalation timer is hoisted so the close/error handlers can
133
+ // clear it — otherwise, when the process dies promptly from SIGTERM, this
134
+ // inner timer still fires a redundant SIGKILL on a dead pid AND holds the
135
+ // event loop open for the full grace period. unref() is a second guard so
136
+ // it never keeps the process alive on its own.
137
+ let killTimer = null;
131
138
  const timer = setTimeout(() => {
132
139
  timedOut = true;
133
140
  proc.kill("SIGTERM");
134
- setTimeout(() => proc.kill("SIGKILL"), 3000);
141
+ killTimer = setTimeout(() => proc.kill("SIGKILL"), killGraceMs);
142
+ if (killTimer && typeof killTimer.unref === "function") {
143
+ killTimer.unref();
144
+ }
135
145
  }, timeout);
136
146
 
137
147
  proc.stdout.on("data", (data) => {
@@ -146,6 +156,7 @@ export class ClaudeCodeAgent extends EventEmitter {
146
156
 
147
157
  proc.on("close", (code) => {
148
158
  clearTimeout(timer);
159
+ if (killTimer) clearTimeout(killTimer);
149
160
  this._proc = null;
150
161
  const duration = Date.now() - startTime;
151
162
  const rawOutput = outputChunks.join("");
@@ -178,6 +189,7 @@ export class ClaudeCodeAgent extends EventEmitter {
178
189
 
179
190
  proc.on("error", (err) => {
180
191
  clearTimeout(timer);
192
+ if (killTimer) clearTimeout(killTimer);
181
193
  this._proc = null;
182
194
  this.status = AGENT_STATUS.FAILED;
183
195
  this.currentTask = null;
@@ -437,7 +437,7 @@ export function castVote(db, opts) {
437
437
  `Invalid voteType: ${voteType}. Must be one of: ${Object.values(VOTE_TYPE).join(", ")}`,
438
438
  );
439
439
  }
440
- if (typeof voteCount !== "number" || voteCount <= 0) {
440
+ if (!Number.isFinite(voteCount) || voteCount <= 0) {
441
441
  throw new Error("voteCount must be a positive number");
442
442
  }
443
443
 
@@ -686,7 +686,7 @@ export function allocateFundsV2(db, opts) {
686
686
  const { proposalId, recipient, amount, asset = "native", memo } = opts || {};
687
687
  if (!proposalId) throw new Error("proposalId is required");
688
688
  if (!recipient) throw new Error("recipient is required");
689
- if (typeof amount !== "number" || amount <= 0) {
689
+ if (!Number.isFinite(amount) || amount <= 0) {
690
690
  throw new Error("amount must be a positive number");
691
691
  }
692
692
  if (amount > _configV2.maxSingleAllocation) {
@@ -807,7 +807,7 @@ export function getConfigV2() {
807
807
 
808
808
  export function depositToTreasuryV2(db, opts) {
809
809
  const { amount, asset = "native", memo, depositorDid } = opts || {};
810
- if (typeof amount !== "number" || amount <= 0) {
810
+ if (!Number.isFinite(amount) || amount <= 0) {
811
811
  throw new Error("amount must be a positive number");
812
812
  }
813
813
 
@@ -24,6 +24,67 @@ const ASSET_PATTERNS = {
24
24
  linux: { x64: /chainlesschain.*linux.*x64.*\.zip$/i },
25
25
  };
26
26
 
27
+ /**
28
+ * Stream a fetch Response body to `destPath` with two robustness guards for the
29
+ * download/install path:
30
+ * - idle timeout: if no bytes arrive for `idleTimeoutMs`, abort `controller`
31
+ * (which cancels the underlying fetch) so a hung server can't freeze the
32
+ * download forever. The timer resets on every chunk — a stall detector, not
33
+ * a total-time cap — and is unref'd so it never holds the loop open.
34
+ * - size verification: if the server advertised a Content-Length and fewer
35
+ * bytes arrived, throw. A silently truncated download must not look like a
36
+ * successful one (the caller would otherwise extract/install a corrupt file).
37
+ *
38
+ * Exported for tests. Returns { downloadedBytes, totalBytes }.
39
+ */
40
+ export async function streamToFileVerified(
41
+ response,
42
+ destPath,
43
+ {
44
+ controller = null,
45
+ idleTimeoutMs = 60000,
46
+ onProgress = null,
47
+ createWriteStreamImpl = createWriteStream,
48
+ } = {},
49
+ ) {
50
+ const totalBytes = parseInt(
51
+ response.headers.get("content-length") || "0",
52
+ 10,
53
+ );
54
+ let downloadedBytes = 0;
55
+ let idleTimer = null;
56
+ const armIdle = () => {
57
+ if (!controller || !(idleTimeoutMs > 0)) return;
58
+ if (idleTimer) clearTimeout(idleTimer);
59
+ idleTimer = setTimeout(() => controller.abort(), idleTimeoutMs);
60
+ if (idleTimer && typeof idleTimer.unref === "function") idleTimer.unref();
61
+ };
62
+ const reader = response.body.getReader();
63
+ async function* track() {
64
+ try {
65
+ while (true) {
66
+ const { done, value } = await reader.read();
67
+ if (done) return;
68
+ armIdle();
69
+ downloadedBytes += value.byteLength;
70
+ if (onProgress) onProgress(downloadedBytes, totalBytes);
71
+ yield value;
72
+ }
73
+ } finally {
74
+ if (idleTimer) clearTimeout(idleTimer);
75
+ }
76
+ }
77
+ armIdle();
78
+ await pipeline(track(), createWriteStreamImpl(destPath));
79
+ if (idleTimer) clearTimeout(idleTimer);
80
+ if (totalBytes > 0 && downloadedBytes !== totalBytes) {
81
+ throw new Error(
82
+ `Download truncated: received ${downloadedBytes} of ${totalBytes} bytes`,
83
+ );
84
+ }
85
+ return { downloadedBytes, totalBytes };
86
+ }
87
+
27
88
  export async function downloadRelease(version, options = {}) {
28
89
  const binDir = ensureDir(getBinDir());
29
90
 
@@ -41,9 +102,14 @@ export async function downloadRelease(version, options = {}) {
41
102
  const spinner = ora(`Downloading ${assetName}...`).start();
42
103
 
43
104
  try {
105
+ // Abort the fetch if the download stalls (no bytes for idleTimeoutMs) so a
106
+ // hung mirror can't freeze `cc setup` forever; streamToFileVerified also
107
+ // verifies the full body landed (truncation guard).
108
+ const controller = new AbortController();
44
109
  const response = await fetch(assetUrl, {
45
110
  headers: { Accept: "application/octet-stream" },
46
111
  redirect: "follow",
112
+ signal: controller.signal,
47
113
  });
48
114
 
49
115
  if (!response.ok) {
@@ -52,30 +118,16 @@ export async function downloadRelease(version, options = {}) {
52
118
  );
53
119
  }
54
120
 
55
- const totalBytes = parseInt(
56
- response.headers.get("content-length") || "0",
57
- 10,
58
- );
59
- let downloadedBytes = 0;
60
-
61
- // Stream the fetch body to disk via an async generator (stable APIs only —
62
- // avoids the experimental stream.Readable.fromWeb); `pipeline` keeps the
63
- // backpressure + error handling. Progress is tracked as chunks flow through.
64
- const reader = response.body.getReader();
65
- async function* trackProgress() {
66
- while (true) {
67
- const { done, value } = await reader.read();
68
- if (done) return;
69
- downloadedBytes += value.byteLength;
70
- if (totalBytes > 0) {
71
- const pct = ((downloadedBytes / totalBytes) * 100).toFixed(1);
72
- spinner.text = `Downloading ${assetName}... ${pct}% (${formatBytes(downloadedBytes)}/${formatBytes(totalBytes)})`;
121
+ const { downloadedBytes } = await streamToFileVerified(response, destPath, {
122
+ controller,
123
+ idleTimeoutMs: options.idleTimeoutMs,
124
+ onProgress: (downloaded, total) => {
125
+ if (total > 0) {
126
+ const pct = ((downloaded / total) * 100).toFixed(1);
127
+ spinner.text = `Downloading ${assetName}... ${pct}% (${formatBytes(downloaded)}/${formatBytes(total)})`;
73
128
  }
74
- yield value;
75
- }
76
- }
77
-
78
- await pipeline(trackProgress(), createWriteStream(destPath));
129
+ },
130
+ });
79
131
 
80
132
  spinner.succeed(
81
133
  `Downloaded ${assetName} (${formatBytes(downloadedBytes)})`,
@@ -96,11 +148,16 @@ export async function downloadRelease(version, options = {}) {
96
148
 
97
149
  return binDir;
98
150
  } catch (err) {
99
- spinner.fail(`Download failed: ${err.message}`);
151
+ const stalled =
152
+ err && (err.name === "AbortError" || err.code === "ABORT_ERR");
153
+ const msg = stalled
154
+ ? `Download stalled (no data received): ${assetName}`
155
+ : `Download failed: ${err.message}`;
156
+ spinner.fail(msg);
100
157
  if (existsSync(destPath)) {
101
158
  unlinkSync(destPath);
102
159
  }
103
- throw err;
160
+ throw stalled ? new Error(msg) : err;
104
161
  }
105
162
  }
106
163
 
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Headless `/config` directive (Claude-Code 2.1.181 parity — "/config key=value
3
+ * in interactive AND -p modes"). The REPL already handles `/config` for the
4
+ * interactive half; this is the `-p` / piped-stdin half: when the headless
5
+ * prompt is a leading `/config …` slash command, treat it as a one-shot config
6
+ * get / set / show instead of a task for the LLM — no model call, no session,
7
+ * no bootstrap. (`cc config get|set` covers the same ground for pure scripting;
8
+ * this gives the symmetric single-interface form Claude Code exposes.)
9
+ *
10
+ * Pure detection + execution; config-manager does the disk I/O (injected for
11
+ * tests). Secrets stay masked on read and write — the same invariant the REPL
12
+ * `/config` upholds.
13
+ */
14
+ import {
15
+ parseConfigCommand,
16
+ renderConfigGet,
17
+ renderConfigSet,
18
+ renderConfigSummary,
19
+ } from "../repl/config-summary.js";
20
+
21
+ /** Does this headless prompt start with the `/config` slash command? */
22
+ export function isHeadlessConfigCommand(prompt) {
23
+ return /^\/config(?:\s|$)/.test((prompt || "").trim());
24
+ }
25
+
26
+ /**
27
+ * Execute a `/config …` directive. Returns `{ text, isError }`.
28
+ *
29
+ * @param {string} prompt the full prompt (leading `/config …`)
30
+ * @param {object} deps
31
+ * - configManager: { loadConfig, getConfigValue, setConfigValue } (required)
32
+ * - getConfigPath?: () => string|null (shown in the `show` summary)
33
+ */
34
+ export function runConfigDirective(prompt, deps = {}) {
35
+ const cm = deps.configManager;
36
+ if (!cm) return { text: "/config: no config manager", isError: true };
37
+ const getConfigPath = deps.getConfigPath || (() => null);
38
+ const argStr = (prompt || "").trim().replace(/^\/config\b/, "");
39
+ const cmd = parseConfigCommand(argStr);
40
+
41
+ if (cmd.action === "error") {
42
+ return { text: `/config: ${cmd.message}`, isError: true };
43
+ }
44
+ if (cmd.action === "get") {
45
+ return {
46
+ text: renderConfigGet(cmd.key, cm.getConfigValue(cmd.key)),
47
+ isError: false,
48
+ };
49
+ }
50
+ if (cmd.action === "set") {
51
+ cm.setConfigValue(cmd.key, cmd.value);
52
+ return {
53
+ text: renderConfigSet(cmd.key, cm.getConfigValue(cmd.key)),
54
+ isError: false,
55
+ };
56
+ }
57
+ // show
58
+ return {
59
+ text: renderConfigSummary(cm.loadConfig(), { path: getConfigPath() }),
60
+ isError: false,
61
+ };
62
+ }