chainlesschain 0.162.78 → 0.162.80

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 (194) 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-CwebRiRI.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-BzhKY5C_.js → ActionButton-C7h2xsW3.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-CATIz2Jc.js → Analytics-BRdOQzzK.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-eCx64YWg.js → AppLayout-D_i-Jbsu.js} +5 -5
  8. package/src/assets/web-panel/assets/{Audit-CGOHfCHj.js → Audit-AgCF_nLK.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-Dyr6R0Ra.js → Backup-Be9up1Uo.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-CVhBu7NZ.js → BaseInput-2j-7gyTU.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-DOCCKp2k.js → Chat-ZyYadHdk.js} +6 -6
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-DcXCCnjN.js → ChatBubbleRenderer-CwnbmcAg.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-3yjnENud.js → Checkbox-Di0bejSO.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-D06sn8kB.js → Codegen-BgEwqrVh.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-Bjn5vFES.js → Col-FIduoerd.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-BYHpQHmf.js → Community-Bf7olKXg.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-MKRmnUDQ.js → Compact-CsjIF6B3.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-CVtPe8dh.js → Compliance-CQchRaQh.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-BRu5M3dv.js → Cowork-gJzDgX9E.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-D_7eU5Ut.js → Cron-CDjeagXb.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-BFyVp_e9.js → Crosschain-D4UE5bt0.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-DRoG7638.js → DID-OwvsxTzD.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-NfM_v-Oe.js → Dashboard-CbmOlZ1l.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-CGN1Ksu0.js → Dropdown-B6tBUMev.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-DhsY_z_a.js → EmailListRenderer-DnM2-O3n.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-vW_VsKUG.js → FamilyGuardDashboard-DPD9znJH.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-D2ob_c7h.js → Federation-DjUj87Vr.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-Cpem15Pr.js → FormItemContext-DyPgrKLf.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-P6XfmXwT.js → GenericCardRenderer-D3fmbO1W.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-CfSeec1R.js → Git-IvwC_R2h.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-4ipIpqNl.js → Governance-CTVEvxpW.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-CiQY_P9Y.js → Inference-DQlp7Rf1.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-B3Qem78R.js → KnowledgeGraph-Cg8pVh5j.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-De2zWVy6.js → Logs-D7kLXyaK.js} +2 -2
  35. package/src/assets/web-panel/assets/{Marketplace-CTNu4c1A.js → Marketplace-QOj6g-oT.js} +1 -1
  36. package/src/assets/web-panel/assets/{McpTools-Xx25EA2f.js → McpTools-PO9yrTD6.js} +5 -5
  37. package/src/assets/web-panel/assets/{Memory-YzzCCrch.js → Memory-B1FwSFmt.js} +2 -2
  38. package/src/assets/web-panel/assets/{MobileBridge-CJZY98f0.js → MobileBridge-CYiQSoKu.js} +3 -3
  39. package/src/assets/web-panel/assets/{MobileProjects-Dw7yl4KN.js → MobileProjects-BThUga4r.js} +1 -1
  40. package/src/assets/web-panel/assets/{Mtc-D7TzGJhH.js → Mtc-B4pMzmr7.js} +4 -4
  41. package/src/assets/web-panel/assets/{MtcAudit-THyGhf0h.js → MtcAudit-D6-KGsvR.js} +6 -6
  42. package/src/assets/web-panel/assets/{Multisig-RVxuGPUR.js → Multisig-BlZigJMj.js} +3 -3
  43. package/src/assets/web-panel/assets/{NLProgramming-DK7TCzKF.js → NLProgramming-Dhy92OLw.js} +1 -1
  44. package/src/assets/web-panel/assets/{Notes-BaOU0psj.js → Notes-DOSv2Lb0.js} +4 -4
  45. package/src/assets/web-panel/assets/{NotificationSettings-BMivJy85.js → NotificationSettings-FRWsHQfi.js} +1 -1
  46. package/src/assets/web-panel/assets/{OrderTableRenderer-BZOiY8Yw.js → OrderTableRenderer-xRSRWRZg.js} +1 -1
  47. package/src/assets/web-panel/assets/{Organization-zVRtW1n_.js → Organization-DApwYY-B.js} +4 -4
  48. package/src/assets/web-panel/assets/{Overflow-C0YLldFH.js → Overflow-DyJQHxIY.js} +1 -1
  49. package/src/assets/web-panel/assets/{P2P-Xxgzghqp.js → P2P-NKTPd_Bn.js} +2 -2
  50. package/src/assets/web-panel/assets/{PdhVaultBrowser-DH_LO13b.js → PdhVaultBrowser-DCgGt1BE.js} +5 -5
  51. package/src/assets/web-panel/assets/{Permissions-CvAd1VBw.js → Permissions-BX9MCb2z.js} +4 -4
  52. package/src/assets/web-panel/assets/{PersonalDataHub-CWQGgCAK.js → PersonalDataHub-Bcg4az84.js} +3 -3
  53. package/src/assets/web-panel/assets/{Pipeline-BNAoh-Lb.js → Pipeline-DjsbfwbG.js} +1 -1
  54. package/src/assets/web-panel/assets/{Privacy-CNO5pFq-.js → Privacy-kDWl06vo.js} +1 -1
  55. package/src/assets/web-panel/assets/{ProjectInit-WaVVDsm3.js → ProjectInit-yCe-Imkv.js} +2 -2
  56. package/src/assets/web-panel/assets/{ProjectSettings-D1WfkuJ3.js → ProjectSettings-tVcxlGJ5.js} +2 -2
  57. package/src/assets/web-panel/assets/{Projects-BzjvJYMW.js → Projects-DGL4Za4o.js} +1 -1
  58. package/src/assets/web-panel/assets/{Providers-IOOJ4_wy.js → Providers-CciGxskW.js} +1 -1
  59. package/src/assets/web-panel/assets/{QuickAsk-ChHZqVZy.js → QuickAsk-It17rT4F.js} +1 -1
  60. package/src/assets/web-panel/assets/{Recommend-CSiW6Qv9.js → Recommend-CixKdpBl.js} +1 -1
  61. package/src/assets/web-panel/assets/{Reputation-BQe0rkfF.js → Reputation-LB0_D2lx.js} +1 -1
  62. package/src/assets/web-panel/assets/{Row-Dem0Wxxb.js → Row-BoiDd-zb.js} +1 -1
  63. package/src/assets/web-panel/assets/{RssFeed-pBY5G41C.js → RssFeed-yA5FdTgh.js} +2 -2
  64. package/src/assets/web-panel/assets/{Search-CtRepO6B.js → Search-xYldUFeJ.js} +1 -1
  65. package/src/assets/web-panel/assets/{Security-nrSlKpWq.js → Security-BtRMnkFm.js} +4 -4
  66. package/src/assets/web-panel/assets/{Services-DeaDBASi.js → Services-CNKTgE2v.js} +2 -2
  67. package/src/assets/web-panel/assets/{Skeleton-Cz9R-Wjb.js → Skeleton-B9goiUo_.js} +1 -1
  68. package/src/assets/web-panel/assets/{Skills-B3U-XLH3.js → Skills-CUf2Z5Ge.js} +1 -1
  69. package/src/assets/web-panel/assets/{Sla-Bu46dIA_.js → Sla-TPga8c2I.js} +1 -1
  70. package/src/assets/web-panel/assets/{SpeechSettings-C9Z0V0pk.js → SpeechSettings-B8zrmLP6.js} +1 -1
  71. package/src/assets/web-panel/assets/{SyncSettings-Ctj9KHHr.js → SyncSettings-DGyAbZ84.js} +2 -2
  72. package/src/assets/web-panel/assets/{Tasks-D4upQgR_.js → Tasks-DPeVrQNM.js} +1 -1
  73. package/src/assets/web-panel/assets/{Templates-JHsPGU_c.js → Templates-CNLco6pc.js} +1 -1
  74. package/src/assets/web-panel/assets/{Tenant-uoaQL3fB.js → Tenant-CL9Eczo8.js} +1 -1
  75. package/src/assets/web-panel/assets/Terminal-DGLvbp97.js +3 -0
  76. package/src/assets/web-panel/assets/{TimelineRenderer-BTicmSAV.js → TimelineRenderer-Cv0LxMwd.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tokens-Bp3BUe2K.js → Tokens-DLhHgtcS.js} +1 -1
  78. package/src/assets/web-panel/assets/{Trigger-CgoISw5d.js → Trigger-DzDaE-An.js} +1 -1
  79. package/src/assets/web-panel/assets/{Trust-CC29awNT.js → Trust-CRh-fhYe.js} +1 -1
  80. package/src/assets/web-panel/assets/{UkeySign-CB1SB6Nc.js → UkeySign-_xBJ16UC.js} +1 -1
  81. package/src/assets/web-panel/assets/{VideoEditing-D7vptDUg.js → VideoEditing-Z5m_edIa.js} +1 -1
  82. package/src/assets/web-panel/assets/{Wallet-BWfjzF7p.js → Wallet-DgmchNit.js} +4 -4
  83. package/src/assets/web-panel/assets/{WebAuthn-Dzz5OnPc.js → WebAuthn-BxuKxjuf.js} +5 -5
  84. package/src/assets/web-panel/assets/{WorkflowEditor-CiDeVmsG.js → WorkflowEditor-luJ180aM.js} +1 -1
  85. package/src/assets/web-panel/assets/{chat-DQbciNb5.js → chat-DzglnTps.js} +1 -1
  86. package/src/assets/web-panel/assets/{colors-DcLbPJzb.js → colors-DelLNoxZ.js} +1 -1
  87. package/src/assets/web-panel/assets/{compact-item-CvYrR3rc.js → compact-item-BZaabUge.js} +1 -1
  88. package/src/assets/web-panel/assets/{createContext-BR4P7Rgm.js → createContext-DqTSTjmk.js} +1 -1
  89. package/src/assets/web-panel/assets/devWarning-CJLMPKYL.js +1 -0
  90. package/src/assets/web-panel/assets/{hasIn-IQ88RNRJ.js → hasIn-CAeHUQj2.js} +1 -1
  91. package/src/assets/web-panel/assets/index-7CJalvEf.js +1 -0
  92. package/src/assets/web-panel/assets/{index-Bw0Dm_P6.js → index-7FxBHcH8.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-Db5LFFCN.js → index-BAcpfWwI.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-B5W1vQHV.js → index-BAfdWN9t.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BUTN1VlO.js → index-BR-DF81e.js} +3 -3
  96. package/src/assets/web-panel/assets/{index-BmPuR0aA.js → index-BTbN0V4A.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-D8OJdOc_.js → index-Bv2Tp7kz.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BhkZZXtI.js → index-BzLgm3Jm.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-CzERBV9P.js → index-CBZPDGTg.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-UbB2IcFR.js → index-CBtnHlYF.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-eKd1n8pw.js → index-CDw1am9U.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-JqOP7puJ.js → index-CI5cynRw.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BiMlLIZ-.js → index-CKZQVcH1.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CgP5aQmA.js → index-CV4FisuU.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BH2RT15D.js → index-CaSLz8-6.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DgaCUxpi.js → index-Cj7oeTxA.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DdQBxvpt.js → index-Clq1OP4B.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-Bl1TSbTE.js → index-CppTZ4SW.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-BQXs-5db.js → index-D-QuIaEh.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DGwa8mnJ.js → index-D-lVDXUg.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DHIp5msb.js → index-DE4-6oHW.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CCO8yc1h.js → index-DM7xncnU.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-b6FjzfoJ.js → index-DRt2lx0X.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-Bo7HAK6G.js → index-DSATjRyg.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Dpmnk2qv.js → index-DU9QWJO5.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-Mn8_ryOe.js → index-DXvcxNo5.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-Dox9vEhP.js → index-DhMSUhbW.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-bRT7u-51.js → index-Dk1R9vFq.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BHxJnExB.js → index-DrSuq6t6.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-CPOupQSX.js → index-DtfTElxo.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BxY0ozve.js → index-KeadEGaZ.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BvQpTO67.js → index-NZBXGj64.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CiOZ_Whh.js → index-OGKhEFZZ.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CeCWyiFl.js → index-RZ23Wlp8.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-BVb6RI7f.js → index-WzAdJ0PX.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-Cm74AosZ.js → index-Xo2WWPZ4.js} +1 -1
  127. package/src/assets/web-panel/assets/index-_hLbeSOT.js +1 -0
  128. package/src/assets/web-panel/assets/{index-CE2mqX8w.js → index-kJ30C4m8.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-Bu8931Yi.js → index-vLR-ssxc.js} +1 -1
  130. package/src/assets/web-panel/assets/{initDefaultProps-C0arzCLE.js → initDefaultProps-BiHvIjo1.js} +1 -1
  131. package/src/assets/web-panel/assets/{motion-C1K6JxwD.js → motion-COD0OBOe.js} +1 -1
  132. package/src/assets/web-panel/assets/{move-DREsRLHj.js → move-DJNLMhIj.js} +1 -1
  133. package/src/assets/web-panel/assets/{omit-BtPS3EDq.js → omit-4qrDRhlN.js} +1 -1
  134. package/src/assets/web-panel/assets/{pickAttrs-BPz6tHoT.js → pickAttrs-3jv8tAgW.js} +1 -1
  135. package/src/assets/web-panel/assets/{placementArrow-B0CR_CSI.js → placementArrow-N1UVUOH_.js} +1 -1
  136. package/src/assets/web-panel/assets/{responsiveObserve-Ch2ojiNn.js → responsiveObserve-D67_gjCH.js} +1 -1
  137. package/src/assets/web-panel/assets/{slide-9qU9vOhj.js → slide-DiDh7_u4.js} +1 -1
  138. package/src/assets/web-panel/assets/{statusUtils-Cr4fICjV.js → statusUtils-Dzz3tSiz.js} +1 -1
  139. package/src/assets/web-panel/assets/{styleChecker-Cor2-FwV.js → styleChecker-L-tgt7xx.js} +1 -1
  140. package/src/assets/web-panel/assets/{useFlexGapSupport-BINo_rNH.js → useFlexGapSupport-vAgElNal.js} +1 -1
  141. package/src/assets/web-panel/assets/{useFs-Dm1tDNYC.js → useFs-af0c_HYI.js} +1 -1
  142. package/src/assets/web-panel/assets/{usePersonalDataHub-__JgBEkX.js → usePersonalDataHub-V9U2Mbny.js} +1 -1
  143. package/src/assets/web-panel/assets/{vnode-1hQKpRgP.js → vnode-C7zS_LLr.js} +1 -1
  144. package/src/assets/web-panel/assets/{zoom-C1EY9X2J.js → zoom-DdXBDemd.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 +216 -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 +210 -37
  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/Terminal-CWRWr8bq.js +0 -3
  192. package/src/assets/web-panel/assets/devWarning-CnV02N63.js +0 -1
  193. package/src/assets/web-panel/assets/index-DJ2gkaIH.js +0 -1
  194. package/src/assets/web-panel/assets/index-Dvm_-AOi.js +0 -1
@@ -62,11 +62,14 @@ export function buildTools({ root, readOnly = false, deps = _deps }) {
62
62
  const text = (truncated ? buf.slice(0, MAX_READ_BYTES) : buf).toString(
63
63
  "utf-8",
64
64
  );
65
- return ok(truncated ? `${text}\n… [truncated ${buf.length} bytes]` : text);
65
+ return ok(
66
+ truncated ? `${text}\n… [truncated ${buf.length} bytes]` : text,
67
+ );
66
68
  },
67
69
  },
68
70
  list_dir: {
69
- description: "List a directory under the serve root (dirs get trailing /)",
71
+ description:
72
+ "List a directory under the serve root (dirs get trailing /)",
70
73
  inputSchema: {
71
74
  type: "object",
72
75
  properties: { path: { type: "string" } },
@@ -106,12 +109,14 @@ export function buildTools({ root, readOnly = false, deps = _deps }) {
106
109
  return;
107
110
  }
108
111
  for (const e of list) {
109
- if (hits.length >= MAX_SEARCH_RESULTS || ++seen >= MAX_SEARCH_ENTRIES)
112
+ if (
113
+ hits.length >= MAX_SEARCH_RESULTS ||
114
+ ++seen >= MAX_SEARCH_ENTRIES
115
+ )
110
116
  return;
111
117
  const abs = deps.path.join(d, e.name);
112
118
  if (e.isDirectory()) {
113
- if (!SKIP_DIRS.has(e.name) && !e.name.startsWith("."))
114
- walk(abs);
119
+ if (!SKIP_DIRS.has(e.name) && !e.name.startsWith(".")) walk(abs);
115
120
  } else {
116
121
  const rel = deps.path.relative(root, abs).replace(/\\/g, "/");
117
122
  if (rel.toLowerCase().includes(q)) hits.push(rel);
@@ -125,7 +130,8 @@ export function buildTools({ root, readOnly = false, deps = _deps }) {
125
130
  };
126
131
  if (!readOnly) {
127
132
  tools.write_file = {
128
- description: "Write a UTF-8 file under the serve root (creates parent dirs)",
133
+ description:
134
+ "Write a UTF-8 file under the serve root (creates parent dirs)",
129
135
  inputSchema: {
130
136
  type: "object",
131
137
  properties: {
@@ -138,7 +144,9 @@ export function buildTools({ root, readOnly = false, deps = _deps }) {
138
144
  const abs = confine(root, rel, deps);
139
145
  fs.mkdirSync(deps.path.dirname(abs), { recursive: true });
140
146
  fs.writeFileSync(abs, String(content), "utf-8");
141
- return ok(`wrote ${Buffer.byteLength(String(content))} bytes to ${rel}`);
147
+ return ok(
148
+ `wrote ${Buffer.byteLength(String(content))} bytes to ${rel}`,
149
+ );
142
150
  },
143
151
  };
144
152
  }
@@ -163,11 +171,20 @@ export function startMcpServe(opts = {}) {
163
171
  const root = deps.path.resolve(opts.root || process.cwd());
164
172
  const readOnly = Boolean(opts.readOnly);
165
173
  const token =
166
- opts.token === false
167
- ? null
168
- : opts.token || randomBytes(16).toString("hex");
174
+ opts.token === false ? null : opts.token || randomBytes(16).toString("hex");
169
175
  const tools = buildTools({ root, readOnly, deps });
170
176
 
177
+ // Guardrails for the request-collection phase: a JSON-RPC request is small,
178
+ // so cap the body and bound how long we wait for it. Without these a large
179
+ // body grows `raw` unbounded (memory) and a stalled client holds the socket
180
+ // forever (req.on("end") never fires). Both overridable for tests / tuning.
181
+ const maxRequestBytes = Number.isFinite(opts.maxRequestBytes)
182
+ ? opts.maxRequestBytes
183
+ : 1024 * 1024; // 1 MB
184
+ const requestTimeoutMs = Number.isFinite(opts.requestTimeoutMs)
185
+ ? opts.requestTimeoutMs
186
+ : 30000;
187
+
171
188
  const server = http.createServer((req, res) => {
172
189
  const send = (status, body) => {
173
190
  res.writeHead(status, { "Content-Type": "application/json" });
@@ -183,10 +200,49 @@ export function startMcpServe(opts = {}) {
183
200
  }
184
201
  }
185
202
  let raw = "";
203
+ let bytes = 0;
204
+ let aborted = false;
205
+ const collectTimer = setTimeout(() => {
206
+ if (aborted) return;
207
+ aborted = true;
208
+ try {
209
+ send(408, rpcError(null, -32001, "request timeout"));
210
+ } catch {
211
+ /* socket already gone */
212
+ }
213
+ req.destroy();
214
+ }, requestTimeoutMs);
215
+ req.on("error", () => {
216
+ if (aborted) return;
217
+ aborted = true;
218
+ clearTimeout(collectTimer);
219
+ // Request stream errored (client reset/dropped): no response is
220
+ // deliverable on a broken socket — just stop, don't crash the process.
221
+ try {
222
+ if (!res.writableEnded) res.destroy();
223
+ } catch {
224
+ /* ignore */
225
+ }
226
+ });
186
227
  req.on("data", (c) => {
228
+ if (aborted) return;
229
+ bytes += c.length;
230
+ if (bytes > maxRequestBytes) {
231
+ aborted = true;
232
+ clearTimeout(collectTimer);
233
+ try {
234
+ send(413, rpcError(null, -32600, "request too large"));
235
+ } catch {
236
+ /* socket already gone */
237
+ }
238
+ req.destroy();
239
+ return;
240
+ }
187
241
  raw += c;
188
242
  });
189
243
  req.on("end", () => {
244
+ if (aborted) return;
245
+ clearTimeout(collectTimer);
190
246
  let msg;
191
247
  try {
192
248
  msg = JSON.parse(raw);
@@ -224,7 +280,10 @@ export function startMcpServe(opts = {}) {
224
280
  if (method === "tools/call") {
225
281
  const tool = tools[params?.name];
226
282
  if (!tool) {
227
- return send(200, rpcResult(id, fail(`unknown tool: ${params?.name}`)));
283
+ return send(
284
+ 200,
285
+ rpcResult(id, fail(`unknown tool: ${params?.name}`)),
286
+ );
228
287
  }
229
288
  let result;
230
289
  try {
@@ -150,11 +150,30 @@ export function readSecretKey(arg) {
150
150
  }
151
151
 
152
152
  /**
153
- * Read JSON from inline string or file path.
153
+ * Read JSON from an inline string or a file path. Errors name the file (or
154
+ * say it was inline) and carry the underlying parser reason, instead of a
155
+ * bare "Unexpected token …" / "ENOENT …" with no context.
154
156
  */
155
157
  export function readJsonArg(arg) {
158
+ if (arg == null || arg === "") {
159
+ throw new Error("Expected inline JSON or a path to a JSON file");
160
+ }
156
161
  if (fs.existsSync(arg)) {
157
- return JSON.parse(fs.readFileSync(arg, "utf-8"));
162
+ let raw;
163
+ try {
164
+ raw = fs.readFileSync(arg, "utf-8");
165
+ } catch (e) {
166
+ throw new Error(`Cannot read JSON file "${arg}": ${e.message}`);
167
+ }
168
+ try {
169
+ return JSON.parse(raw);
170
+ } catch (e) {
171
+ throw new Error(`Invalid JSON in file "${arg}": ${e.message}`);
172
+ }
173
+ }
174
+ try {
175
+ return JSON.parse(arg);
176
+ } catch (e) {
177
+ throw new Error(`Invalid inline JSON argument: ${e.message}`);
158
178
  }
159
- return JSON.parse(arg);
160
179
  }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Parse a user-supplied JSON CLI option value.
3
+ *
4
+ * Many command actions pass raw `--flag` strings straight to `JSON.parse`,
5
+ * so malformed JSON surfaces as a raw `SyntaxError` — an ugly stack trace for
6
+ * actions without a try/catch, and a cryptic "Unexpected token …" for the rest.
7
+ * This helper turns that into a single, friendly `Invalid JSON for <label>: …`
8
+ * error, and consolidates the duplicated `_parseJsonArg` / `_parseMetaV2`
9
+ * helpers that several command files grew independently.
10
+ *
11
+ * @param {string|undefined|null} value raw option string (e.g. `options.input`)
12
+ * @param {string} label user-facing label for errors (e.g. `"--input"`)
13
+ * @param {*} [fallback] returned when `value` is empty (default `undefined`)
14
+ * @returns the parsed JSON, or `fallback` when `value` is empty
15
+ * @throws {Error} `Invalid JSON for <label>: <reason>` when `value` is non-empty but unparseable
16
+ */
17
+ export function parseJsonOption(value, label, fallback = undefined) {
18
+ if (value === undefined || value === null || value === "") return fallback;
19
+ // Defensive: a Commander custom parser may have already produced an object.
20
+ if (typeof value !== "string") return value;
21
+ try {
22
+ return JSON.parse(value);
23
+ } catch (e) {
24
+ const reason = e && e.message ? e.message : String(e);
25
+ // Friendly one-line message for the non-verbose error boundary, but keep the
26
+ // original SyntaxError reachable: chain it as `cause`, and append its stack
27
+ // to ours so `--verbose` (which prints err.stack) still surfaces the root
28
+ // SyntaxError type + location instead of swallowing it behind the wrapper.
29
+ const err = new Error(`Invalid JSON for ${label}: ${reason}`, { cause: e });
30
+ if (e && e.stack) err.stack += `\nCaused by: ${e.stack}`;
31
+ throw err;
32
+ }
33
+ }
34
+
35
+ export default parseJsonOption;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Parse a user-supplied numeric CLI option value.
3
+ *
4
+ * Command actions often do `parseFloat(options.x)` / `parseInt(options.x)`
5
+ * straight into domain logic. When the flag is malformed (`--weight abc`,
6
+ * `--amount 1,5`) those yield `NaN`, which then flows silently into stored
7
+ * records and math (vote weights, amounts, thresholds) — corrupt data with no
8
+ * error. This helper validates the value is a finite number and otherwise
9
+ * throws a single friendly `Invalid number for <label>: <value>` error
10
+ * (which reads cleanly through the CLI entry boundary).
11
+ *
12
+ * @param {string|number|undefined|null} value raw option (e.g. `options.weight`)
13
+ * @param {string} label user-facing label (e.g. `"--weight"`)
14
+ * @param {*} [fallback] returned when `value` is empty (default `undefined`)
15
+ * @returns {number|*} the parsed finite number, or `fallback` when empty
16
+ * @throws {Error} when `value` is non-empty but not a finite number
17
+ */
18
+ export function parseNumberOption(value, label, fallback = undefined) {
19
+ if (value === undefined || value === null || value === "") return fallback;
20
+ const n = typeof value === "number" ? value : Number(value);
21
+ if (!Number.isFinite(n)) {
22
+ throw new Error(`Invalid number for ${label}: ${JSON.stringify(value)}`);
23
+ }
24
+ return n;
25
+ }
26
+
27
+ export default parseNumberOption;
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Runnability guards for LLM model/provider selection.
3
+ *
4
+ * The task-based auto model-selector (lib/task-model-selector.js) switches the
5
+ * model by detected task type — e.g. a "quick" message on the `anthropic`
6
+ * provider becomes `claude-haiku`. That is WRONG when the target provider has
7
+ * no usable API key: it silently routes you onto a model you can't call and you
8
+ * get a 401. These helpers make selection "runnable-first": never switch onto a
9
+ * provider that has no usable (present) key, and let callers detect that the
10
+ * configured provider itself is not runnable.
11
+ *
12
+ * "Usable key" = a keyless local provider (ollama), OR the active session's
13
+ * configured key, OR the provider's standard env var (`<PROVIDER>_API_KEY`).
14
+ * Empty/whitespace keys never count (a missing or cleared key is not usable).
15
+ */
16
+ import { BUILT_IN_PROVIDERS } from "./llm-providers.js";
17
+ import { selectModelForTask, TaskType } from "./task-model-selector.js";
18
+
19
+ function nonEmpty(v) {
20
+ return typeof v === "string" && v.trim().length > 0;
21
+ }
22
+
23
+ /** Is this an authentication/authorization failure (missing/invalid/expired key)? */
24
+ export function isAuthError(err) {
25
+ if (!err) return false;
26
+ const status =
27
+ typeof err.status === "number"
28
+ ? err.status
29
+ : typeof err.statusCode === "number"
30
+ ? err.statusCode
31
+ : null;
32
+ if (status === 401 || status === 403) return true;
33
+ const msg = String(err.message || err).toLowerCase();
34
+ return /\b401\b|\b403\b|unauthorized|forbidden|authentication failed|api[\s_-]*key required|invalid api[\s_-]*key|incorrect api[\s_-]*key|expired/.test(
35
+ msg,
36
+ );
37
+ }
38
+
39
+ /** Host of a baseUrl, lowercased, or "" — tolerant of a bare host string. */
40
+ function hostOf(url) {
41
+ if (!nonEmpty(url)) return "";
42
+ try {
43
+ return new URL(url).host.toLowerCase();
44
+ } catch {
45
+ return String(url).toLowerCase();
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Infer the provider a baseUrl actually belongs to. Catches a config where the
51
+ * provider field disagrees with the endpoint (e.g. provider "anthropic" but
52
+ * baseUrl points at volces.com → really volcengine). Returns null if unknown.
53
+ */
54
+ export function inferProviderFromBaseUrl(baseUrl) {
55
+ const host = hostOf(baseUrl);
56
+ if (!host) return null;
57
+ for (const [name, def] of Object.entries(BUILT_IN_PROVIDERS)) {
58
+ const defHost = hostOf(def.baseUrl);
59
+ if (defHost && (host === defHost || host.endsWith("." + defHost))) {
60
+ return name;
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+
66
+ function envKey(provider, env) {
67
+ const def = BUILT_IN_PROVIDERS[provider];
68
+ return def && def.apiKeyEnv ? env[def.apiKeyEnv] : null;
69
+ }
70
+
71
+ /** A sensible default model for a provider (its CHAT task model). */
72
+ function defaultModelFor(provider) {
73
+ return selectModelForTask(provider, TaskType.CHAT) || undefined;
74
+ }
75
+
76
+ /**
77
+ * Wrap a chatFn so an AUTH failure (no / wrong / expired key for the resolved
78
+ * provider) self-heals to a provider we can actually run instead of failing the
79
+ * whole turn. Two recovery paths, in order:
80
+ * 1. baseUrl says otherwise — the endpoint belongs to a different provider
81
+ * than the `provider` field; retry with that provider, SAME baseUrl + key
82
+ * (fixes a mislabeled config like provider:anthropic + volces baseUrl).
83
+ * 2. env-keyed fallback — some other built-in provider has its key in the
84
+ * environment; retry with that provider's baseUrl + env key + default model.
85
+ * If neither applies, the original (clear) auth error is rethrown. One hop only;
86
+ * never recurses. Non-auth errors pass straight through.
87
+ *
88
+ * @param {Function} chatFn
89
+ * @param {object} [opts] { env=process.env, onFallback?({from,to,reason}) }
90
+ */
91
+ export function makeRunnableProviderFallback(
92
+ chatFn,
93
+ { env = process.env, onFallback } = {},
94
+ ) {
95
+ const notify = (info) => {
96
+ if (typeof onFallback === "function") {
97
+ try {
98
+ onFallback(info);
99
+ } catch {
100
+ /* notification is best-effort */
101
+ }
102
+ }
103
+ };
104
+ return async function runnableProviderFallback(messages, options = {}) {
105
+ try {
106
+ return await chatFn(messages, options);
107
+ } catch (err) {
108
+ if (!isAuthError(err)) throw err;
109
+ const failed = options.provider;
110
+
111
+ // 1) The endpoint belongs to a different provider than the label.
112
+ const inferred = inferProviderFromBaseUrl(options.baseUrl);
113
+ if (inferred && inferred !== failed) {
114
+ notify({ from: failed, to: inferred, reason: "baseurl-mismatch" });
115
+ return await chatFn(messages, {
116
+ ...options,
117
+ provider: inferred,
118
+ model: defaultModelFor(inferred) || options.model,
119
+ });
120
+ }
121
+
122
+ // 2) Another provider has a usable key in the environment.
123
+ for (const [name, def] of Object.entries(BUILT_IN_PROVIDERS)) {
124
+ if (name === failed) continue;
125
+ if (def.apiKeyEnv && nonEmpty(env[def.apiKeyEnv])) {
126
+ notify({ from: failed, to: name, reason: "env-key" });
127
+ return await chatFn(messages, {
128
+ ...options,
129
+ provider: name,
130
+ baseUrl: def.baseUrl,
131
+ apiKey: envKey(name, env),
132
+ model: defaultModelFor(name) || options.model,
133
+ });
134
+ }
135
+ }
136
+ throw err; // nowhere runnable to fall back to — surface the clear error
137
+ }
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Can we actually call `provider` right now?
143
+ *
144
+ * @param {string} provider
145
+ * @param {object} [opts]
146
+ * @param {string} [opts.apiKey] the session's configured key — only counts
147
+ * when `isActive` (it belongs to the ACTIVE
148
+ * provider, not an arbitrary one).
149
+ * @param {boolean} [opts.isActive=true]
150
+ * @param {object} [opts.env=process.env]
151
+ * @returns {boolean}
152
+ */
153
+ export function hasUsableKey(
154
+ provider,
155
+ { apiKey, isActive = true, env = process.env } = {},
156
+ ) {
157
+ const def = BUILT_IN_PROVIDERS[provider];
158
+ if (!def) return false;
159
+ if (!def.apiKeyEnv) return true; // keyless local provider (e.g. ollama)
160
+ if (isActive && nonEmpty(apiKey)) return true;
161
+ return nonEmpty(env[def.apiKeyEnv]);
162
+ }
163
+
164
+ /**
165
+ * Gate the task-based auto model-switch on runnability. Returns the recommended
166
+ * model ONLY when the (active) provider is runnable; otherwise returns null so
167
+ * the caller keeps the user's configured model instead of switching onto
168
+ * something it can't call.
169
+ *
170
+ * @param {object} args
171
+ * @param {string} args.provider
172
+ * @param {string} [args.currentModel]
173
+ * @param {string|null} [args.recommended] from selectModelForTask()
174
+ * @param {string} [args.apiKey]
175
+ * @param {object} [args.env=process.env]
176
+ * @returns {string|null} a model to switch to, or null to keep the current one
177
+ */
178
+ export function runnableTaskModel({
179
+ provider,
180
+ currentModel,
181
+ recommended,
182
+ apiKey,
183
+ env = process.env,
184
+ } = {}) {
185
+ if (!recommended || recommended === currentModel) return null;
186
+ return hasUsableKey(provider, { apiKey, isActive: true, env })
187
+ ? recommended
188
+ : null;
189
+ }
190
+
191
+ /**
192
+ * Find a provider we can actually run, "runnable-first": keep `provider` when
193
+ * it has a usable key; otherwise fall back to the first built-in provider whose
194
+ * env key is set; otherwise the keyless local provider (ollama). Returns
195
+ * `{ provider, runnable, fellBackFrom?, keyless? }`. Pure given `env`.
196
+ */
197
+ export function pickRunnableProvider({
198
+ provider,
199
+ apiKey,
200
+ env = process.env,
201
+ } = {}) {
202
+ if (provider && hasUsableKey(provider, { apiKey, isActive: true, env })) {
203
+ return { provider, runnable: true };
204
+ }
205
+ for (const [name, def] of Object.entries(BUILT_IN_PROVIDERS)) {
206
+ if (def.apiKeyEnv && nonEmpty(env[def.apiKeyEnv])) {
207
+ return { provider: name, runnable: true, fellBackFrom: provider || null };
208
+ }
209
+ }
210
+ return {
211
+ provider: "ollama",
212
+ runnable: true,
213
+ keyless: true,
214
+ fellBackFrom: provider || null,
215
+ };
216
+ }
@@ -52,6 +52,7 @@ import {
52
52
  detectTaskType,
53
53
  selectModelForTask,
54
54
  } from "../lib/task-model-selector.js";
55
+ import { runnableTaskModel, hasUsableKey } from "../lib/runnable-provider.js";
55
56
  import { CLIPermanentMemory } from "../lib/permanent-memory.js";
56
57
  import { CLIAutonomousAgent, GoalStatus } from "../lib/autonomous-agent.js";
57
58
  import {
@@ -743,7 +744,9 @@ export async function startAgentRepl(options = {}) {
743
744
  _bundleResolved.approvalPolicy.default,
744
745
  );
745
746
  // Mirror it so Shift+Tab cycling starts from the real tier.
746
- const applied = parsePermissionTier(_bundleResolved.approvalPolicy.default);
747
+ const applied = parsePermissionTier(
748
+ _bundleResolved.approvalPolicy.default,
749
+ );
747
750
  if (applied) _sessionTier = applied;
748
751
  } catch (_err) {
749
752
  // Non-critical — invalid policy value is silently ignored
@@ -1307,7 +1310,7 @@ export async function startAgentRepl(options = {}) {
1307
1310
  ` ${chalk.cyan("/theme")} Color theme (/theme <auto|dark|light|mono>; mono = no color)`,
1308
1311
  );
1309
1312
  logger.log(
1310
- ` ${chalk.cyan("/config")} Effective config (provider/model, keys masked)`,
1313
+ ` ${chalk.cyan("/config")} Show config; ${chalk.cyan("/config <key>")} read, ${chalk.cyan("/config <key>=<val>")} set`,
1311
1314
  );
1312
1315
  logger.log(
1313
1316
  ` ${chalk.cyan("/doctor")} Session health check (provider/key/IDE/MCP/hooks)`,
@@ -2695,18 +2698,43 @@ export async function startAgentRepl(options = {}) {
2695
2698
 
2696
2699
  // `/config` — effective configuration (secret-safe): the LLM provider/model
2697
2700
  // in effect, whether keys are set, web-search backend, config path.
2698
- if (trimmed === "/config" || trimmed === "/config ") {
2701
+ // `/config <key>` reads a value; `/config <key>=<value>` (Claude-Code
2702
+ // parity) or `/config <key> <value>` persists one. Secrets stay masked.
2703
+ if (trimmed === "/config" || trimmed.startsWith("/config ")) {
2699
2704
  try {
2700
- const { loadConfig } = await import("../lib/config-manager.js");
2705
+ const cm = await import("../lib/config-manager.js");
2701
2706
  const { getConfigPath } = await import("../lib/paths.js");
2702
- const { renderConfigSummary } = await import("./config-summary.js");
2703
- logger.log(
2704
- renderConfigSummary(loadConfig(), {
2705
- path: getConfigPath(),
2706
- activeProvider: provider,
2707
- activeModel: _curModel || model,
2708
- }),
2709
- );
2707
+ const {
2708
+ renderConfigSummary,
2709
+ parseConfigCommand,
2710
+ renderConfigGet,
2711
+ renderConfigSet,
2712
+ } = await import("./config-summary.js");
2713
+ const cmd = parseConfigCommand(trimmed.slice("/config".length));
2714
+ if (cmd.action === "error") {
2715
+ logger.error(chalk.red(`/config: ${cmd.message}`));
2716
+ } else if (cmd.action === "get") {
2717
+ logger.log(renderConfigGet(cmd.key, cm.getConfigValue(cmd.key)));
2718
+ } else if (cmd.action === "set") {
2719
+ cm.setConfigValue(cmd.key, cmd.value);
2720
+ logger.log(
2721
+ chalk.green("✓ ") +
2722
+ renderConfigSet(cmd.key, cm.getConfigValue(cmd.key)),
2723
+ );
2724
+ logger.log(
2725
+ chalk.gray(
2726
+ " (persisted; provider/model changes apply to new sessions)",
2727
+ ),
2728
+ );
2729
+ } else {
2730
+ logger.log(
2731
+ renderConfigSummary(cm.loadConfig(), {
2732
+ path: getConfigPath(),
2733
+ activeProvider: provider,
2734
+ activeModel: _curModel || model,
2735
+ }),
2736
+ );
2737
+ }
2710
2738
  } catch (err) {
2711
2739
  logger.error(chalk.red(`/config failed: ${err.message}`));
2712
2740
  }
@@ -3080,16 +3108,35 @@ export async function startAgentRepl(options = {}) {
3080
3108
  // Slot-filling failure is non-critical
3081
3109
  }
3082
3110
 
3083
- // Auto-select best model based on task type
3111
+ // Auto-select best model based on task type — but ONLY onto a runnable
3112
+ // provider. The selector maps e.g. "fast" → claude-haiku on anthropic; if
3113
+ // there's no usable key for the provider, never switch there (you'd just
3114
+ // get a 401). Runnable-first: keep the configured (working) model instead.
3084
3115
  let activeModel = model;
3085
3116
  const taskDetection = detectTaskType(promptText);
3086
3117
  if (taskDetection.confidence > 0.3) {
3087
3118
  const recommended = selectModelForTask(provider, taskDetection.taskType);
3088
- if (recommended && recommended !== activeModel) {
3089
- activeModel = recommended;
3119
+ const switchTo = runnableTaskModel({
3120
+ provider,
3121
+ currentModel: activeModel,
3122
+ recommended,
3123
+ apiKey,
3124
+ });
3125
+ if (switchTo) {
3126
+ activeModel = switchTo;
3090
3127
  logger.info(
3091
3128
  chalk.gray(`[auto] ${taskDetection.name} → ${activeModel}`),
3092
3129
  );
3130
+ } else if (
3131
+ recommended &&
3132
+ recommended !== activeModel &&
3133
+ !hasUsableKey(provider, { apiKey })
3134
+ ) {
3135
+ logger.info(
3136
+ chalk.gray(
3137
+ `[auto] ${taskDetection.name}: keeping ${activeModel} — no usable key for "${provider}" (skipping ${recommended})`,
3138
+ ),
3139
+ );
3093
3140
  }
3094
3141
  }
3095
3142
 
@@ -3122,7 +3169,8 @@ export async function startAgentRepl(options = {}) {
3122
3169
  ? {
3123
3170
  onToken: (t) => {
3124
3171
  // Separate the answer from the dimmed reasoning above it (once).
3125
- if (!_liveStreamed && _liveThinkStarted) process.stdout.write("\n");
3172
+ if (!_liveStreamed && _liveThinkStarted)
3173
+ process.stdout.write("\n");
3126
3174
  _liveStreamed = true;
3127
3175
  process.stdout.write(t);
3128
3176
  },
@@ -3143,6 +3191,16 @@ export async function startAgentRepl(options = {}) {
3143
3191
  thinking: reasoning,
3144
3192
  } = await agentLoop(messages, {
3145
3193
  ...liveOpts,
3194
+ // Visible auto-retry feedback (Claude-Code 2.1.181): when the model's
3195
+ // streaming call hits a transient connection drop and retries, tell the
3196
+ // user instead of leaving them staring at a silent pause. To stderr so
3197
+ // it never corrupts the streamed answer on stdout.
3198
+ onStreamRetry: (attempt) =>
3199
+ process.stderr.write(
3200
+ chalk.dim(
3201
+ ` ⟳ connection dropped — retrying (attempt ${attempt})…\n`,
3202
+ ),
3203
+ ),
3146
3204
  signal: _turnAbort.signal,
3147
3205
  provider,
3148
3206
  model: activeModel,
@@ -3242,7 +3300,8 @@ export async function startAgentRepl(options = {}) {
3242
3300
  } else {
3243
3301
  // Phase G #2 — route through StreamRouter so REPL / WS / future
3244
3302
  // streaming providers share one StreamEvent protocol.
3245
- const { streamAgentResponse } = await import("../lib/agent-stream.js");
3303
+ const { streamAgentResponse } =
3304
+ await import("../lib/agent-stream.js");
3246
3305
  process.stdout.write("\n");
3247
3306
  const noStream = options.noStream === true;
3248
3307
  const streamResult = await streamAgentResponse(effectiveResponse, {
@@ -17,6 +17,72 @@ export function maskSecret(v) {
17
17
  return s.length <= 4 ? "set (hidden)" : `set (…${s.slice(-4)})`;
18
18
  }
19
19
 
20
+ /**
21
+ * A config key whose value must never be echoed in plaintext in the
22
+ * (shoulder-surfable) interactive REPL — apiKey / secret / token / password.
23
+ * Matched on the last dotted segment (e.g. `llm.apiKey`, `webSearch.apiKey`).
24
+ */
25
+ const SECRET_KEY_RE = /(api[_-]?key|secret|token|password|passwd)$/i;
26
+ export function isSecretConfigKey(key) {
27
+ if (!key) return false;
28
+ const last = String(key).split(".").pop();
29
+ return SECRET_KEY_RE.test(last);
30
+ }
31
+
32
+ /**
33
+ * Parse the argument string after `/config` into an action.
34
+ * Mirrors Claude Code's `/config key=value` syntax, and also accepts the
35
+ * `/config key value` form. Returns one of:
36
+ * { action: "show" } // `/config`
37
+ * { action: "get", key } // `/config llm.model`
38
+ * { action: "set", key, value } // `/config llm.model=opus` | `/config llm.model opus`
39
+ * { action: "error", message }
40
+ * `value` is the raw string; the caller coerces (true/false/null/number) via
41
+ * config-manager's setConfigValue.
42
+ *
43
+ * @param {string} argStr everything after the literal `/config`
44
+ */
45
+ export function parseConfigCommand(argStr) {
46
+ const s = (argStr || "").trim();
47
+ if (s === "") return { action: "show" };
48
+
49
+ const eq = s.indexOf("=");
50
+ if (eq !== -1) {
51
+ const key = s.slice(0, eq).trim();
52
+ const value = s.slice(eq + 1).trim();
53
+ if (!key) return { action: "error", message: "missing key before '='" };
54
+ return { action: "set", key, value };
55
+ }
56
+
57
+ // `key value` — split on the first run of whitespace.
58
+ const m = s.match(/^(\S+)\s+([\s\S]+)$/);
59
+ if (m) return { action: "set", key: m[1], value: m[2].trim() };
60
+
61
+ // Bare token → read that key.
62
+ return { action: "get", key: s };
63
+ }
64
+
65
+ /** Render a single config value for `/config <key>`, masking secrets. */
66
+ export function renderConfigGet(key, value) {
67
+ if (value === undefined) return `${key} = (unset)`;
68
+ if (isSecretConfigKey(key)) return `${key} = ${maskSecret(value)}`;
69
+ const shown =
70
+ value !== null && typeof value === "object"
71
+ ? JSON.stringify(value)
72
+ : String(value);
73
+ return `${key} = ${shown}`;
74
+ }
75
+
76
+ /** Render the confirmation line after `/config <key>=<value>`. */
77
+ export function renderConfigSet(key, storedValue) {
78
+ if (isSecretConfigKey(key)) return `set ${key} = ${maskSecret(storedValue)}`;
79
+ const shown =
80
+ storedValue !== null && typeof storedValue === "object"
81
+ ? JSON.stringify(storedValue)
82
+ : String(storedValue);
83
+ return `set ${key} = ${shown}`;
84
+ }
85
+
20
86
  /**
21
87
  * @param {object|null} config loaded config.json
22
88
  * @param {object} [opts] { path, activeProvider, activeModel }