chainlesschain 0.162.77 → 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 (194) hide show
  1. package/README.md +37 -1
  2. package/bin/chainlesschain.js +20 -1
  3. package/package.json +2 -2
  4. package/src/assets/web-panel/assets/{AIOps--t6qElO3.js → AIOps-D89fD_7T.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-vRiifAww.js → ActionButton-d75flwX8.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-BfSlGprA.js → Analytics-gvvBEb4T.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-B6L2I5ld.js → AppLayout-DzHOVS64.js} +4 -4
  8. package/src/assets/web-panel/assets/{Audit-Dwpk7vO2.js → Audit-15K7MhRC.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-D7UuR3M8.js → Backup-BgVHsglI.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-BzMDBTyZ.js → BaseInput-BRnvd4sF.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-BxYUynGU.js → Chat-B2vtXu7s.js} +6 -6
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BYyCFqO0.js → ChatBubbleRenderer-d1EFM3dh.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-DgpLcrjA.js → Checkbox-B5uV5Jhr.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-N5dSkUbq.js → Codegen-cVDMBVGV.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-C1nf7jzT.js → Col-DtL6q7Hw.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-BuX21CEd.js → Community-DSyPZkqQ.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-M37IFQe_.js → Compact-CzfV_q4O.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-B_OBnXJx.js → Compliance-B_xhhLE1.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-BcIn7Sb2.js → Cowork-Y49Qf0JX.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-BtmA9AMI.js → Cron-B7JG-oLj.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-DlAiW2Iy.js → Crosschain-CukEkQtf.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-91Skenc5.js → DID-Bd4UVKdn.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-BTzcKrw1.js → Dashboard-DRCQl7H1.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-VNCZqfkL.js → Dropdown-De47CuRY.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-B_o62liK.js → EmailListRenderer-C-wAkceB.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BqceYpym.js → FamilyGuardDashboard-DhLF_E0k.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-BJFcp8S0.js → Federation-DAipUS1m.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-DwsPen2h.js → FormItemContext-BCFyAd2j.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-BOU0nfJP.js → GenericCardRenderer-wMdxrEV9.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-C_92Ngor.js → Git-CcwP7HUN.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-SIBJc353.js → Governance-VF760JcB.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-DcEwRh8C.js → Inference-DnR-GIn2.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-C4LQ8MJI.js → KnowledgeGraph-j-ewVKWO.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-BXz2tihi.js → Logs-DgB7wSor.js} +2 -2
  35. package/src/assets/web-panel/assets/{Marketplace-DqXKQJ2n.js → Marketplace-fiKjzVWE.js} +1 -1
  36. package/src/assets/web-panel/assets/{McpTools-CXVGQSUd.js → McpTools-BjXSMQrd.js} +5 -5
  37. package/src/assets/web-panel/assets/{Memory-Bp8huWkt.js → Memory-C0L5lnSS.js} +2 -2
  38. package/src/assets/web-panel/assets/{MobileBridge-B3duw6FB.js → MobileBridge-C0Rgg1Su.js} +3 -3
  39. package/src/assets/web-panel/assets/{MobileProjects-Cn5YO60O.js → MobileProjects-BPA50hQb.js} +1 -1
  40. package/src/assets/web-panel/assets/{Mtc-CE_M_-1m.js → Mtc-BUsHuAjB.js} +2 -2
  41. package/src/assets/web-panel/assets/{MtcAudit-Dev-y1Ei.js → MtcAudit-dlH8Q_7U.js} +2 -2
  42. package/src/assets/web-panel/assets/{Multisig-RILPE0-i.js → Multisig-L3_9beuW.js} +3 -3
  43. package/src/assets/web-panel/assets/{NLProgramming-DsBeFM0w.js → NLProgramming-BG4sXy27.js} +1 -1
  44. package/src/assets/web-panel/assets/{Notes-MONi8b1b.js → Notes-felgIvGS.js} +3 -3
  45. package/src/assets/web-panel/assets/{NotificationSettings-mXNink9t.js → NotificationSettings-DHDT96AK.js} +1 -1
  46. package/src/assets/web-panel/assets/{OrderTableRenderer-Bvtv8CHQ.js → OrderTableRenderer-BYrkEfvR.js} +1 -1
  47. package/src/assets/web-panel/assets/{Organization-_7rEqDWP.js → Organization-DdtOLkHG.js} +4 -4
  48. package/src/assets/web-panel/assets/{Overflow-CuqybwI0.js → Overflow-DLw5Pni7.js} +1 -1
  49. package/src/assets/web-panel/assets/{P2P-BvYN2SXJ.js → P2P-B-mZ5EXz.js} +2 -2
  50. package/src/assets/web-panel/assets/{PdhVaultBrowser-BWGnb4i0.js → PdhVaultBrowser-DeOmNCwK.js} +3 -3
  51. package/src/assets/web-panel/assets/{Permissions-D2vyDMm6.js → Permissions-BeQMX71K.js} +4 -4
  52. package/src/assets/web-panel/assets/{PersonalDataHub-BWaT70CA.js → PersonalDataHub-D5PoqtQI.js} +2 -2
  53. package/src/assets/web-panel/assets/{Pipeline-T_Pztk-K.js → Pipeline-B0f5sqKF.js} +1 -1
  54. package/src/assets/web-panel/assets/{Privacy-BTChzeM8.js → Privacy-Dj_gcxio.js} +1 -1
  55. package/src/assets/web-panel/assets/{ProjectInit-BQQBICYM.js → ProjectInit-DmfPgied.js} +2 -2
  56. package/src/assets/web-panel/assets/{ProjectSettings-BOLO_fhW.js → ProjectSettings-BmBfocrv.js} +2 -2
  57. package/src/assets/web-panel/assets/{Projects-BfqkBhzi.js → Projects-BUif48cc.js} +1 -1
  58. package/src/assets/web-panel/assets/{Providers-DpWEAYh2.js → Providers-B0ztEAOV.js} +1 -1
  59. package/src/assets/web-panel/assets/{QuickAsk-MX8kg-uO.js → QuickAsk-BtMG4jH5.js} +1 -1
  60. package/src/assets/web-panel/assets/{Recommend-UOCp-h2X.js → Recommend-DQKN1En8.js} +1 -1
  61. package/src/assets/web-panel/assets/{Reputation-H_vE2kEf.js → Reputation-D34CvUxg.js} +1 -1
  62. package/src/assets/web-panel/assets/{Row-dupAolkD.js → Row-nnuDNx31.js} +1 -1
  63. package/src/assets/web-panel/assets/{RssFeed-DVoerh51.js → RssFeed-DHx9x8_B.js} +2 -2
  64. package/src/assets/web-panel/assets/{Search-5zxC_iWb.js → Search-CH-JYtn_.js} +1 -1
  65. package/src/assets/web-panel/assets/{Security-vg3XxdEZ.js → Security-nNKBmzm4.js} +3 -3
  66. package/src/assets/web-panel/assets/{Services-CWjum0P4.js → Services-xYk_lDxy.js} +2 -2
  67. package/src/assets/web-panel/assets/{Skeleton-BkVnAX7_.js → Skeleton-DTYyRduA.js} +1 -1
  68. package/src/assets/web-panel/assets/{Skills-2oMJr3sU.js → Skills-DS19-9sF.js} +1 -1
  69. package/src/assets/web-panel/assets/{Sla-Bv8uLcr_.js → Sla-Cr_ir42Y.js} +1 -1
  70. package/src/assets/web-panel/assets/{SpeechSettings-BfvawAGM.js → SpeechSettings-Ch5Iq7Wy.js} +1 -1
  71. package/src/assets/web-panel/assets/{SyncSettings-DzWmsU8K.js → SyncSettings-CTblfa_E.js} +2 -2
  72. package/src/assets/web-panel/assets/{Tasks-cbtuyShK.js → Tasks-z7lz3hjG.js} +1 -1
  73. package/src/assets/web-panel/assets/{Templates-PP2omXNk.js → Templates-DGoMFd4r.js} +1 -1
  74. package/src/assets/web-panel/assets/{Tenant-Cab9qgCx.js → Tenant-DQUPvHxx.js} +1 -1
  75. package/src/assets/web-panel/assets/{Terminal-9kFA0Vf8.js → Terminal-Wt2wrbE6.js} +2 -2
  76. package/src/assets/web-panel/assets/{TimelineRenderer-Cx8kncEg.js → TimelineRenderer-DGTc2UqL.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tokens-D19NKMv_.js → Tokens-DPVnuARF.js} +1 -1
  78. package/src/assets/web-panel/assets/{Trigger-DbRaTJsm.js → Trigger-DV5MPXC1.js} +1 -1
  79. package/src/assets/web-panel/assets/{Trust-8EFY9ZrM.js → Trust-BBkMKqwS.js} +1 -1
  80. package/src/assets/web-panel/assets/{UkeySign--wxy-nr4.js → UkeySign-B_2E0qev.js} +1 -1
  81. package/src/assets/web-panel/assets/{VideoEditing-BlRqqo2c.js → VideoEditing-CA-qKeVb.js} +1 -1
  82. package/src/assets/web-panel/assets/{Wallet-B2x_RfaK.js → Wallet-Ds4WURh-.js} +3 -3
  83. package/src/assets/web-panel/assets/{WebAuthn-DISokPYb.js → WebAuthn-BApiHjzz.js} +5 -5
  84. package/src/assets/web-panel/assets/{WorkflowEditor-DkuLYA12.js → WorkflowEditor-DFzmAnzV.js} +1 -1
  85. package/src/assets/web-panel/assets/{chat-MMwBSP3l.js → chat-DZ6sHPit.js} +1 -1
  86. package/src/assets/web-panel/assets/{colors-CPP3K6Jb.js → colors-DgbwhHtE.js} +1 -1
  87. package/src/assets/web-panel/assets/{compact-item-2ruYr7FB.js → compact-item-C4QVYSzd.js} +1 -1
  88. package/src/assets/web-panel/assets/{createContext-BPpiGxaR.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-CtP3Uqy-.js → hasIn-CPtuUtBl.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-DpBaxHIL.js → index-4g6pGxnX.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-9SEWDDhb.js → index-B91hax9S.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-C2N_-y_W.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-2cESIQ4O.js → index-BCb7MYMS.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-D05imMj-.js → index-BIAxMDe9.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-Bq8gB01i.js → index-BJiGrT6V.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BM_RMGj5.js → index-BOKVSmKh.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-COSm6DcE.js → index-BWJGry74.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Bf1tNEoB.js → index-BaVYCMJa.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DyCZ3fz1.js → index-Bd0JznCS.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-oaNSovPm.js → index-BpBCKK6W.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DSNoOIwN.js → index-BsXF9cn5.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DIQO-prS.js → index-BxclR5gc.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-C6qsWs-4.js → index-C0LyE6R6.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-COPqJgcW.js → index-CBAgy5pf.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BLAT0M1t.js → index-CJ550XXg.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-D7-Zo0uW.js → index-CTlz3MP6.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-Dc1ysHqq.js → index-CaC4gaQZ.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-6Sly6NDj.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-Bjthf-eJ.js → index-D2KRfjEI.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-Bfr1HIma.js → index-DA0ePxNn.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-Byddazfj.js → index-DC5iP1VB.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BkqTFggQ.js → index-DCGmeXfl.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-BM7y07U3.js → index-DHnWI0jj.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CZrah8Gb.js → index-DN4qpkKQ.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-DS_ISOVj.js → index-DTLaRwKx.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DO_a65ut.js → index-DdFjcgqH.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-Dy85x86R.js → index-DopuoTzG.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-DXGebJnR.js → index-DpDAGvyU.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-CUQp8MdV.js → index-EGVlUtH-.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-Cru4qTvK.js → index-IFh9qCi0.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-oD_rJWBp.js → index-UEeNpaDD.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DzFVlsbg.js → index-ZCyyy3Zt.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-h-zAHKNr.js → index-aKZjRwjv.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-D5g378QF.js → index-jBlY6-bF.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CZyr02ib.js → index-kbcTc6Pf.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-P_R2pJ9q.js → index-x9bXmSNB.js} +1 -1
  130. package/src/assets/web-panel/assets/{initDefaultProps-BrJf63uk.js → initDefaultProps-BDsxioXk.js} +1 -1
  131. package/src/assets/web-panel/assets/{motion-BD8UoaTG.js → motion-erZ2fiIQ.js} +1 -1
  132. package/src/assets/web-panel/assets/{move-DbunBKV-.js → move-BFiiw4WK.js} +1 -1
  133. package/src/assets/web-panel/assets/{omit-CkGNCa-h.js → omit-BRMVd4pM.js} +1 -1
  134. package/src/assets/web-panel/assets/{pickAttrs-DTvTrHC2.js → pickAttrs-CGjoFXsq.js} +1 -1
  135. package/src/assets/web-panel/assets/{placementArrow-Cb7oz9vL.js → placementArrow-DU_Bnf7w.js} +1 -1
  136. package/src/assets/web-panel/assets/{responsiveObserve-O7lB6MAE.js → responsiveObserve-DPdkYupg.js} +1 -1
  137. package/src/assets/web-panel/assets/{slide-C24m1SKv.js → slide-JfHiG96y.js} +1 -1
  138. package/src/assets/web-panel/assets/{statusUtils-D_-9GtZ3.js → statusUtils-Dy-1guyd.js} +1 -1
  139. package/src/assets/web-panel/assets/{styleChecker-CrAYF9jD.js → styleChecker-Chaljnux.js} +1 -1
  140. package/src/assets/web-panel/assets/{useFlexGapSupport-CmgeoVPN.js → useFlexGapSupport-DOHIHBgY.js} +1 -1
  141. package/src/assets/web-panel/assets/{useFs-DzJSzGsy.js → useFs-BxSoQhIY.js} +1 -1
  142. package/src/assets/web-panel/assets/{usePersonalDataHub-Bgqky5YK.js → usePersonalDataHub-MOR76PYB.js} +1 -1
  143. package/src/assets/web-panel/assets/{vnode-ErTJLgr4.js → vnode-BBGIyeYD.js} +1 -1
  144. package/src/assets/web-panel/assets/{zoom-wlf3cppM.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/agent-core.js +2 -1
  175. package/src/lib/chat-core.js +209 -107
  176. package/src/lib/claude-code-bridge.js +13 -1
  177. package/src/lib/dao-governance.js +3 -3
  178. package/src/lib/downloader.js +82 -25
  179. package/src/lib/headless-config-command.js +62 -0
  180. package/src/lib/json-schema-output.js +55 -11
  181. package/src/lib/mcp-oauth.js +110 -21
  182. package/src/lib/mcp-serve.js +70 -11
  183. package/src/lib/multisig-runtime.js +22 -3
  184. package/src/lib/parse-json-option.js +35 -0
  185. package/src/lib/parse-number-option.js +27 -0
  186. package/src/lib/runnable-provider.js +97 -0
  187. package/src/repl/agent-repl.js +76 -17
  188. package/src/repl/config-summary.js +66 -0
  189. package/src/runtime/agent-core.js +189 -36
  190. package/src/runtime/headless-runner.js +49 -1
  191. package/src/runtime/headless-stream.js +34 -0
  192. package/src/assets/web-panel/assets/devWarning-D7iybGpP.js +0 -1
  193. package/src/assets/web-panel/assets/index-BzJrOJ0f.js +0 -1
  194. package/src/assets/web-panel/assets/index-QN-iyhAl.js +0 -1
@@ -34,21 +34,32 @@ export function validateAgainstSchema(value, schema, path = "$") {
34
34
  if (schema.type) {
35
35
  const want = Array.isArray(schema.type) ? schema.type : [schema.type];
36
36
  const got = typeOf(value);
37
- const ok = want.some((t) => t === got || (t === "number" && got === "integer"));
37
+ const ok = want.some(
38
+ (t) => t === got || (t === "number" && got === "integer"),
39
+ );
38
40
  if (!ok) {
39
41
  errors.push(`${path}: expected type ${want.join("|")}, got ${got}`);
40
42
  return errors; // type mismatch — deeper checks are noise
41
43
  }
42
44
  }
43
- if (schema.enum && !schema.enum.some((e) => JSON.stringify(e) === JSON.stringify(value))) {
44
- errors.push(`${path}: value not in enum [${schema.enum.map((e) => JSON.stringify(e)).join(", ")}]`);
45
+ if (
46
+ schema.enum &&
47
+ !schema.enum.some((e) => JSON.stringify(e) === JSON.stringify(value))
48
+ ) {
49
+ errors.push(
50
+ `${path}: value not in enum [${schema.enum.map((e) => JSON.stringify(e)).join(", ")}]`,
51
+ );
45
52
  }
46
- if (schema.const !== undefined && JSON.stringify(schema.const) !== JSON.stringify(value)) {
53
+ if (
54
+ schema.const !== undefined &&
55
+ JSON.stringify(schema.const) !== JSON.stringify(value)
56
+ ) {
47
57
  errors.push(`${path}: must equal const ${JSON.stringify(schema.const)}`);
48
58
  }
49
59
  if (typeOf(value) === "object" && !Array.isArray(value)) {
50
60
  for (const req of schema.required || []) {
51
- if (!(req in value)) errors.push(`${path}: missing required property "${req}"`);
61
+ if (!(req in value))
62
+ errors.push(`${path}: missing required property "${req}"`);
52
63
  }
53
64
  const props = schema.properties || {};
54
65
  for (const [k, v] of Object.entries(value)) {
@@ -61,7 +72,9 @@ export function validateAgainstSchema(value, schema, path = "$") {
61
72
  }
62
73
  if (Array.isArray(value) && schema.items) {
63
74
  value.forEach((item, i) => {
64
- errors.push(...validateAgainstSchema(item, schema.items, `${path}[${i}]`));
75
+ errors.push(
76
+ ...validateAgainstSchema(item, schema.items, `${path}[${i}]`),
77
+ );
65
78
  });
66
79
  }
67
80
  return errors;
@@ -76,10 +89,12 @@ export function extractJsonPayload(text) {
76
89
  if (fence) tries.push(fence[1].trim());
77
90
  const firstObj = raw.indexOf("{");
78
91
  const lastObj = raw.lastIndexOf("}");
79
- if (firstObj !== -1 && lastObj > firstObj) tries.push(raw.slice(firstObj, lastObj + 1));
92
+ if (firstObj !== -1 && lastObj > firstObj)
93
+ tries.push(raw.slice(firstObj, lastObj + 1));
80
94
  const firstArr = raw.indexOf("[");
81
95
  const lastArr = raw.lastIndexOf("]");
82
- if (firstArr !== -1 && lastArr > firstArr) tries.push(raw.slice(firstArr, lastArr + 1));
96
+ if (firstArr !== -1 && lastArr > firstArr)
97
+ tries.push(raw.slice(firstArr, lastArr + 1));
83
98
  for (const candidate of tries) {
84
99
  if (!candidate) continue;
85
100
  try {
@@ -111,6 +126,35 @@ export function buildRetryPrompt(originalPrompt, raw, errors) {
111
126
  ].join("\n");
112
127
  }
113
128
 
129
+ /**
130
+ * Load + parse a --json-schema file, raising errors that name the file and
131
+ * the underlying cause instead of a bare `ENOENT …` or `Unexpected token …`
132
+ * stack (which is all the user would otherwise see).
133
+ *
134
+ * @param {{ readFileSync: Function }} fs
135
+ * @param {string} schemaFile
136
+ */
137
+ export function loadSchemaFile(fs, schemaFile) {
138
+ if (!schemaFile) {
139
+ throw new Error(
140
+ "No schema provided: pass --json-schema <file> or a schema object",
141
+ );
142
+ }
143
+ let raw;
144
+ try {
145
+ raw = fs.readFileSync(schemaFile, "utf-8");
146
+ } catch (e) {
147
+ throw new Error(`Cannot read schema file "${schemaFile}": ${e.message}`);
148
+ }
149
+ try {
150
+ return JSON.parse(raw);
151
+ } catch (e) {
152
+ throw new Error(
153
+ `Invalid JSON in schema file "${schemaFile}": ${e.message}`,
154
+ );
155
+ }
156
+ }
157
+
114
158
  /**
115
159
  * Run a headless turn constrained to a schema, retrying on validation
116
160
  * failure. Prints the validated JSON to writeOut; returns the exit code.
@@ -124,8 +168,7 @@ export async function runJsonSchemaConstrained(cfg = {}) {
124
168
  const writeErr = cfg.writeErr || ((s) => process.stderr.write(s));
125
169
  const maxAttempts = cfg.maxAttempts || MAX_ATTEMPTS;
126
170
 
127
- const schema =
128
- cfg.schema || JSON.parse(fs.readFileSync(cfg.schemaFile, "utf-8"));
171
+ const schema = cfg.schema || loadSchemaFile(fs, cfg.schemaFile);
129
172
  const instruction = buildSchemaInstruction(schema);
130
173
  const base = cfg.baseOptions || {};
131
174
 
@@ -151,7 +194,8 @@ export async function runJsonSchemaConstrained(cfg = {}) {
151
194
  writeErr,
152
195
  },
153
196
  );
154
- const raw = String(outcome?.result ?? captured ?? "").trim() || captured.trim();
197
+ const raw =
198
+ String(outcome?.result ?? captured ?? "").trim() || captured.trim();
155
199
  lastRaw = raw;
156
200
  const parsed = extractJsonPayload(raw);
157
201
  if (parsed.ok) {
@@ -39,7 +39,11 @@ export const _deps = {
39
39
  };
40
40
 
41
41
  const base64url = (buf) =>
42
- Buffer.from(buf).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
42
+ Buffer.from(buf)
43
+ .toString("base64")
44
+ .replace(/\+/g, "-")
45
+ .replace(/\//g, "_")
46
+ .replace(/=+$/, "");
43
47
 
44
48
  /** RFC 7636 PKCE pair (S256). */
45
49
  export function generatePkce() {
@@ -53,6 +57,52 @@ export function randomState(bytes = 16) {
53
57
  return base64url(_deps.randomBytes(bytes));
54
58
  }
55
59
 
60
+ /** Minimal HTML-escape for values reflected into the callback page. */
61
+ function escapeHtml(s) {
62
+ return String(s).replace(
63
+ /[&<>"']/g,
64
+ (c) =>
65
+ ({
66
+ "&": "&amp;",
67
+ "<": "&lt;",
68
+ ">": "&gt;",
69
+ '"': "&quot;",
70
+ "'": "&#39;",
71
+ })[c],
72
+ );
73
+ }
74
+
75
+ /**
76
+ * The HTML shown in the user's browser after the OAuth redirect hits our
77
+ * localhost callback. On success it AUTO-CLOSES the tab (Claude-Code 2.1.181
78
+ * parity) — best-effort, since window.close() only acts on script-opened
79
+ * windows, so the page still tells the user they may close it manually. The
80
+ * provider-supplied `error` is HTML-escaped (it is reflected into the page).
81
+ * Pure → unit-testable.
82
+ *
83
+ * @param {string|null|undefined} error the `error` query param, if any
84
+ */
85
+ export function renderCallbackPage(error) {
86
+ const ok = !error;
87
+ const title = ok ? "Authorized" : "Authorization failed";
88
+ const body = ok
89
+ ? "You can close this tab and return to the terminal."
90
+ : `The provider returned an error: ${escapeHtml(error)}`;
91
+ const autoClose = ok
92
+ ? "<script>setTimeout(function(){try{window.close();}catch(e){}},800);</script>"
93
+ : "";
94
+ return (
95
+ '<!doctype html><html><head><meta charset="utf-8"><title>' +
96
+ title +
97
+ "</title></head>" +
98
+ '<body style="font-family:system-ui,-apple-system,sans-serif;text-align:center;margin-top:18vh;color:#222">' +
99
+ `<h2 style="color:${ok ? "#16794a" : "#b00020"}">${title}</h2>` +
100
+ `<p style="color:#555">${body}</p>` +
101
+ autoClose +
102
+ "</body></html>"
103
+ );
104
+ }
105
+
56
106
  async function fetchJson(url, opts) {
57
107
  const res = await _deps.fetch(url, opts);
58
108
  if (!res || !res.ok) {
@@ -63,7 +113,9 @@ async function fetchJson(url, opts) {
63
113
  } catch {
64
114
  /* ignore */
65
115
  }
66
- const err = new Error(`HTTP ${status}${body ? `: ${body.slice(0, 200)}` : ""}`);
116
+ const err = new Error(
117
+ `HTTP ${status}${body ? `: ${body.slice(0, 200)}` : ""}`,
118
+ );
67
119
  err.status = res ? res.status : null;
68
120
  throw err;
69
121
  }
@@ -76,7 +128,10 @@ async function fetchJson(url, opts) {
76
128
  * the authorization-server doc directly at the origin.
77
129
  * @returns {Promise<{issuer?,authorization_endpoint,token_endpoint,registration_endpoint?,scopes_supported?}>}
78
130
  */
79
- export async function discoverAuthMetadata(serverUrl, { resourceMetadataUrl } = {}) {
131
+ export async function discoverAuthMetadata(
132
+ serverUrl,
133
+ { resourceMetadataUrl } = {},
134
+ ) {
80
135
  const origin = new URL(serverUrl).origin;
81
136
  // 1. protected-resource metadata (RFC 9728) → authorization_servers[]
82
137
  let authServer = origin;
@@ -84,7 +139,10 @@ export async function discoverAuthMetadata(serverUrl, { resourceMetadataUrl } =
84
139
  const prUrl =
85
140
  resourceMetadataUrl || `${origin}/.well-known/oauth-protected-resource`;
86
141
  const pr = await fetchJson(prUrl);
87
- if (Array.isArray(pr.authorization_servers) && pr.authorization_servers[0]) {
142
+ if (
143
+ Array.isArray(pr.authorization_servers) &&
144
+ pr.authorization_servers[0]
145
+ ) {
88
146
  authServer = String(pr.authorization_servers[0]).replace(/\/$/, "");
89
147
  }
90
148
  } catch {
@@ -118,9 +176,14 @@ export async function discoverAuthMetadata(serverUrl, { resourceMetadataUrl } =
118
176
  }
119
177
 
120
178
  /** RFC 7591 dynamic client registration → client_id (public client). */
121
- export async function registerClient(metadata, { redirectUri, clientName = "chainlesschain-cli" } = {}) {
179
+ export async function registerClient(
180
+ metadata,
181
+ { redirectUri, clientName = "chainlesschain-cli" } = {},
182
+ ) {
122
183
  if (!metadata.registration_endpoint) {
123
- throw new Error("server has no registration_endpoint and no --client-id was given");
184
+ throw new Error(
185
+ "server has no registration_endpoint and no --client-id was given",
186
+ );
124
187
  }
125
188
  const reg = await fetchJson(metadata.registration_endpoint, {
126
189
  method: "POST",
@@ -133,12 +196,16 @@ export async function registerClient(metadata, { redirectUri, clientName = "chai
133
196
  token_endpoint_auth_method: "none",
134
197
  }),
135
198
  });
136
- if (!reg.client_id) throw new Error("registration did not return a client_id");
199
+ if (!reg.client_id)
200
+ throw new Error("registration did not return a client_id");
137
201
  return { clientId: reg.client_id, clientSecret: reg.client_secret || null };
138
202
  }
139
203
 
140
204
  /** Build the authorize URL (Authorization Code + PKCE). */
141
- export function buildAuthorizeUrl(metadata, { clientId, redirectUri, scope, codeChallenge, state, resource }) {
205
+ export function buildAuthorizeUrl(
206
+ metadata,
207
+ { clientId, redirectUri, scope, codeChallenge, state, resource },
208
+ ) {
142
209
  const u = new URL(metadata.authorization_endpoint);
143
210
  u.searchParams.set("response_type", "code");
144
211
  u.searchParams.set("client_id", clientId);
@@ -157,7 +224,10 @@ function _tokenExpiresAt(tok) {
157
224
  }
158
225
 
159
226
  /** Exchange an authorization code for tokens. */
160
- export async function exchangeCodeForToken(metadata, { code, codeVerifier, clientId, clientSecret, redirectUri, resource }) {
227
+ export async function exchangeCodeForToken(
228
+ metadata,
229
+ { code, codeVerifier, clientId, clientSecret, redirectUri, resource },
230
+ ) {
161
231
  const body = new URLSearchParams({
162
232
  grant_type: "authorization_code",
163
233
  code,
@@ -182,7 +252,10 @@ export async function exchangeCodeForToken(metadata, { code, codeVerifier, clien
182
252
  }
183
253
 
184
254
  /** Refresh an access token. */
185
- export async function refreshAccessToken(metadata, { refreshToken, clientId, clientSecret, resource }) {
255
+ export async function refreshAccessToken(
256
+ metadata,
257
+ { refreshToken, clientId, clientSecret, resource },
258
+ ) {
186
259
  const body = new URLSearchParams({
187
260
  grant_type: "refresh_token",
188
261
  refresh_token: refreshToken,
@@ -249,7 +322,11 @@ export function deleteStoredToken(serverUrl) {
249
322
  if (!(key in store)) return false;
250
323
  delete store[key];
251
324
  try {
252
- _deps.fs.writeFileSync(file, JSON.stringify(store, null, 2) + "\n", "utf-8");
325
+ _deps.fs.writeFileSync(
326
+ file,
327
+ JSON.stringify(store, null, 2) + "\n",
328
+ "utf-8",
329
+ );
253
330
  } catch {
254
331
  /* best-effort */
255
332
  }
@@ -292,8 +369,7 @@ function defaultOpenBrowser(url) {
292
369
  const platform = process.platform;
293
370
  const cmd =
294
371
  platform === "win32" ? "cmd" : platform === "darwin" ? "open" : "xdg-open";
295
- const args =
296
- platform === "win32" ? ["/c", "start", "", url] : [url];
372
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
297
373
  try {
298
374
  const child = spawn(cmd, args, { stdio: "ignore", detached: true });
299
375
  child.unref?.();
@@ -304,7 +380,12 @@ function defaultOpenBrowser(url) {
304
380
  }
305
381
 
306
382
  /** Wait for the OAuth redirect on a localhost callback server; resolve {code,state}. */
307
- function waitForCallback({ port, host = "127.0.0.1", path = "/callback", timeout = 300_000 }) {
383
+ function waitForCallback({
384
+ port,
385
+ host = "127.0.0.1",
386
+ path = "/callback",
387
+ timeout = 300_000,
388
+ }) {
308
389
  return new Promise((resolve, reject) => {
309
390
  const server = _deps.createServer((req, res) => {
310
391
  let u;
@@ -323,10 +404,8 @@ function waitForCallback({ port, host = "127.0.0.1", path = "/callback", timeout
323
404
  const code = u.searchParams.get("code");
324
405
  const state = u.searchParams.get("state");
325
406
  const error = u.searchParams.get("error");
326
- res.writeHead(200, { "content-type": "text/html" });
327
- res.end(
328
- `<html><body style="font-family:sans-serif"><h3>${error ? "Authorization failed" : "Authorized — you can close this tab."}</h3></body></html>`,
329
- );
407
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
408
+ res.end(renderCallbackPage(error));
330
409
  server.close();
331
410
  clearTimeout(timer);
332
411
  if (error) reject(new Error(`authorization error: ${error}`));
@@ -377,13 +456,22 @@ export async function authorizeInteractive(serverUrl, opts = {}) {
377
456
  const authorizeUrl = buildAuthorizeUrl(metadata, {
378
457
  clientId,
379
458
  redirectUri,
380
- scope: scope || (metadata.scopes_supported ? metadata.scopes_supported.join(" ") : undefined),
459
+ scope:
460
+ scope ||
461
+ (metadata.scopes_supported
462
+ ? metadata.scopes_supported.join(" ")
463
+ : undefined),
381
464
  codeChallenge: pkce.challenge,
382
465
  state,
383
466
  resource: serverUrl,
384
467
  });
385
468
 
386
- const callbackPromise = waitForCallback({ port, host, path: redirectPath, timeout });
469
+ const callbackPromise = waitForCallback({
470
+ port,
471
+ host,
472
+ path: redirectPath,
473
+ timeout,
474
+ });
387
475
  const opened = _deps.openBrowser(authorizeUrl);
388
476
  writeOut(
389
477
  (opened
@@ -393,7 +481,8 @@ export async function authorizeInteractive(serverUrl, opts = {}) {
393
481
  );
394
482
 
395
483
  const { code, state: returnedState } = await callbackPromise;
396
- if (returnedState !== state) throw new Error("OAuth state mismatch (possible CSRF)");
484
+ if (returnedState !== state)
485
+ throw new Error("OAuth state mismatch (possible CSRF)");
397
486
 
398
487
  const tok = await exchangeCodeForToken(metadata, {
399
488
  code,
@@ -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;