chainlesschain 0.162.158 → 0.162.160

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 (219) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/assets/{AIOps-BwpTJTN_.js → AIOps-CkRHjW8e.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-BtQOoo3l.js → ActionButton-BP3MeNi2.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-Duf_rXZa.js → Analytics-CjbjQsOr.js} +2 -2
  6. package/src/assets/web-panel/assets/{AppLayout-XQK3wjvB.js → AppLayout-i0adH1M8.js} +5 -5
  7. package/src/assets/web-panel/assets/Artifacts-CCCG2GOU.js +1 -0
  8. package/src/assets/web-panel/assets/{Audit-DNjOGQLh.js → Audit-DrsgvwHC.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-FH9RAfB5.js → BackgroundAgents-DkXNkbZM.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-ZIbrR4-U.js → Backup-DarTWCex.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-DSt3NaND.js → BaseInput-DaDzhcto.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-CY-wvbjv.js → Chat-C4EzGw6V.js} +5 -5
  13. package/src/assets/web-panel/assets/ChatBubbleRenderer-lkavIWMS.js +1 -0
  14. package/src/assets/web-panel/assets/{Checkbox-8JVadE_B.js → Checkbox-gddNgaQi.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-CN83nPYI.js → Codegen-BKldFWf9.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-xPSvqdN8.js → Col-ChR-brDs.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-D78cnIjG.js → Community-DlHtZX86.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-CSw6Csbp.js → Compact-BAhfcBpC.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-E_HhjcYR.js → Compliance-gIrKkm6m.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-DybeQjmm.js → Cowork-BYnzLBO3.js} +3 -3
  21. package/src/assets/web-panel/assets/{Cron-DS0BJeLY.js → Cron-ByUDIljI.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-BOwVgkbs.js → Crosschain-DOdNbHFp.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-DRUr9YuX.js → DID-BJHaNWD2.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-Cht-yUKW.js → Dashboard-Drm_tClz.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-BLr_EJ9X.js → Dropdown-CA1WLzGN.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-Drexuh5W.js → EmailListRenderer-Di1SKjNO.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-D9Q2Kq63.js → FamilyGuardDashboard-44Ve7ZFe.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-C-Z1xI-T.js → Federation-CubPwv_N.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-DLHPMkQF.js → FormItemContext-CsUFe5bU.js} +1 -1
  30. package/src/assets/web-panel/assets/GenericCardRenderer-D7xRjPjZ.js +1 -0
  31. package/src/assets/web-panel/assets/{Git-syATcaBt.js → Git-D-Qx9dhO.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-CYe_r8Zf.js → Governance-_YMYaU4n.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-GWxErB7k.js → Inference-J7Yzh-K_.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-qQvVCZoi.js → KnowledgeGraph-D37iI_Mo.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-BenAU2W8.js → Logs-oMTSYfGY.js} +2 -2
  36. package/src/assets/web-panel/assets/{MarkdownRenderer-7agE3Or1.js → MarkdownRenderer-BqaPmC0W.js} +1 -1
  37. package/src/assets/web-panel/assets/{Marketplace-D4Jn5fpD.js → Marketplace-DKUqYDaF.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-DtAwoqiQ.js → McpTools-BEv3Gx61.js} +4 -4
  39. package/src/assets/web-panel/assets/{Memory-BI_O50I8.js → Memory-CtA8XD2y.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-CQWE2lqx.js → MobileBridge-ug7c1mMj.js} +1 -1
  41. package/src/assets/web-panel/assets/MobileProjects-BpX5g9It.js +1 -0
  42. package/src/assets/web-panel/assets/{Mtc-Dnr16qsn.js → Mtc-WJaMs4i2.js} +2 -2
  43. package/src/assets/web-panel/assets/{MtcAudit-CQ5Xe4-a.js → MtcAudit-DXnK_-ru.js} +2 -2
  44. package/src/assets/web-panel/assets/{Multisig-Bjr8O-wy.js → Multisig-C4Ev7726.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-CEe-KirU.js → NLProgramming-BMFb9_zk.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-spKJIKF9.js → Notes-Bl93nvBX.js} +4 -4
  47. package/src/assets/web-panel/assets/{NotificationSettings-DRp83LFG.js → NotificationSettings-BN2W5cl6.js} +1 -1
  48. package/src/assets/web-panel/assets/{OrderTableRenderer-h1A0XB2a.js → OrderTableRenderer-Dxc9oN4o.js} +1 -1
  49. package/src/assets/web-panel/assets/{Organization-BlgB0IYg.js → Organization-CAs48637.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-a_35HqZI.js → Overflow-Bhq27Zoc.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-CSNHCNPC.js → P2P-D61-2542.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-C8M1JH-1.js → PdhVaultBrowser-C1j4iUxz.js} +5 -5
  53. package/src/assets/web-panel/assets/{Permissions-DmjJi9Ms.js → Permissions-rJ2-fScM.js} +3 -3
  54. package/src/assets/web-panel/assets/{PersonalDataHub-DfuwRyf4.js → PersonalDataHub-DFIlTok6.js} +2 -2
  55. package/src/assets/web-panel/assets/{Pipeline-B2-S4ekE.js → Pipeline-BjIgEWoZ.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-B1dOiBLC.js → Privacy-BQoH5wcX.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-_pNmc-DX.js → ProjectInit-CYR_-_Ub.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-DmwG2Ii1.js → ProjectSettings-nkatCq-9.js} +2 -2
  59. package/src/assets/web-panel/assets/Projects-SS9uZKCl.js +1 -0
  60. package/src/assets/web-panel/assets/{Providers-C6HSjg1d.js → Providers-BnKM9fsF.js} +1 -1
  61. package/src/assets/web-panel/assets/{QrScannerModal-CrDmS7l1.js → QrScannerModal-Cfe82dbz.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-CfKvWt-x.js → QuickAsk-r1RbPufV.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-DMa6Kuzv.js → Recommend-CjCbM2WE.js} +1 -1
  64. package/src/assets/web-panel/assets/{RemoteSession-DrktUo5x.js → RemoteSession-BuMGPH6V.js} +2 -2
  65. package/src/assets/web-panel/assets/{Reputation-BTPQ1vjL.js → Reputation-Bmj3NY1F.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-B6Q7QkMG.js → Row-BIsNcE_1.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-B1IJhAmX.js → RssFeed-BObmojD2.js} +3 -3
  68. package/src/assets/web-panel/assets/{Search-xbea1kGD.js → Search-BRmbkQNS.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-7rs0cO3F.js → Security-_rF3DRpY.js} +2 -2
  70. package/src/assets/web-panel/assets/{Services-0fjixK0S.js → Services-D0If9ZV4.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-B_rVq9Cu.js → Skeleton-o7zIksUg.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-C5uM90b9.js → Skills-oUR7zi9u.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-U4pYCIZH.js → Sla-fqKyGi03.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-Cdz4yTWx.js → SpeechSettings-B1wZ_gxx.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-Me3aetHv.js → SyncSettings-B3qJWYXq.js} +2 -2
  76. package/src/assets/web-panel/assets/{Tasks-Cf6aNkL_.js → Tasks-BH5_dUfr.js} +1 -1
  77. package/src/assets/web-panel/assets/{Templates-CYatk4tU.js → Templates-CCIVpjRP.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-DIMKLw7o.js → Tenant-2GZtTGnB.js} +1 -1
  79. package/src/assets/web-panel/assets/{Terminal-C_cp-uSJ.js → Terminal-BlMkfs_u.js} +2 -2
  80. package/src/assets/web-panel/assets/{TimelineRenderer-cUPVNADp.js → TimelineRenderer-U6hi5OMc.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tokens-CiaEIXVf.js → Tokens-C-_BerGJ.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger-DB1WU5lQ.js → Trigger-DaKzBgN3.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-Cfht9Ijy.js → Trust-CLvcsco3.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-K3P4Vt9S.js → UkeySign-BFn_Y1zZ.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-DWjhjXRh.js → VideoEditing-B0V18dLT.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-D9-Q1Kan.js → Wallet-BbmP7Imu.js} +4 -4
  87. package/src/assets/web-panel/assets/{WebAuthn-BDszy-oV.js → WebAuthn-Dewj_0kt.js} +5 -5
  88. package/src/assets/web-panel/assets/{WorkflowEditor-DIhij4rA.js → WorkflowEditor-DOxz3Lwa.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-D7VCiKbh.js → chat-B1OIpZnh.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-BvOd9pVn.js → colors-CSGNVdri.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-DoqXBiAO.js → compact-item-8S0vcg0u.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-CrKRzUtp.js → createContext-vZyc27TN.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-DYEZcY55.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-BWAev4WC.js → hasIn-DWw3-UhA.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-DvDOpsJB.js → index-B4pP2ycr.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-oY45XrkG.js → index-BDHyovnK.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-CmJ6xKBO.js → index-BKEmQQvR.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BoYrS_L2.js → index-BQssf0EQ.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-ww1QtLnX.js → index-BZnauCqN.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BWWsaimP.js → index-BdyokEau.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CoD31Ndm.js → index-BeBv896m.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-DlGu7As6.js → index-BjC-TrOb.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-zH-tuuGZ.js → index-BkWd6YL7.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CEXfKSiI.js → index-BoIPHP4R.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-CEPHMSih.js → index-BuB5P0TR.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-PDxGjKRu.js → index-BwKrTVV0.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BkgnGX7v.js → index-C6r27A-o.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-DV-Rfmfx.js → index-CB2MOS_8.js} +3 -3
  109. package/src/assets/web-panel/assets/{index-KR1-Em9C.js → index-CFQWZvak.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-8g5TJCZ3.js → index-CI3slBKI.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BCyP071m.js → index-CJgKP2cm.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CSb-tnIo.js → index-CLPr2vC_.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-1poMpMU0.js → index-CbZqw7pg.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BZGk8AbZ.js → index-CfEvacz7.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BXyl8O23.js → index-CwXBblm-.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-BHOcVYrx.js → index-D1i_8oop.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-Bq_ctRpz.js → index-D8qXVfoV.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-gmxqu5Kk.js → index-DKwl1AkQ.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-D23L1V76.js → index-DLAvUtOH.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-CDGLdn12.js → index-DLgkAhEE.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-YGDy7nNq.js → index-DPgY5GLy.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-Cx7M7PSF.js → index-Dc40pX3F.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-JzUmJMvn.js → index-DcfLXAW7.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-HwHdTusY.js → index-DeCS12Mh.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-B30NvR21.js → index-Dg0Izipr.js} +1 -1
  126. package/src/assets/web-panel/assets/index-DmVFfcNR.js +1 -0
  127. package/src/assets/web-panel/assets/{index-B0vGEbJT.js → index-DtHWHJnj.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-Bf9YlbDn.js → index-Q6oyLnKC.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-Bj7EgmOp.js → index-dxocDVD_.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-ISvPZ05r.js → index-t-q3rkVm.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-C5kRmj3P.js → index-tr1x96W9.js} +1 -1
  132. package/src/assets/web-panel/assets/index-x7YyhMUa.js +1 -0
  133. package/src/assets/web-panel/assets/{index-BN9m4Ux2.js → index-xx5M5Rbj.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-XW-OR3mL.js → initDefaultProps-Di5YFId0.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-CeuEHh20.js → motion-BVSN0aeL.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-Dva79Keg.js → move-DHNiaP2l.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-C0B6bD2j.js → mtc-parser-xJPf3gQD.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-AkZvO-a2.js → omit-C7qzUdkN.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-B4r55qGc.js → pickAttrs-pSpjg3Le.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-BLH-z7YE.js → placementArrow-cs_jL_UC.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-ksEGZ2Wf.js → responsiveObserve-DkS_Zs4r.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-Cth4OD1Z.js → slide-B5O1Kpxo.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-9S83ezxw.js → statusUtils-BX02GnZp.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-7yQmQGZ4.js → styleChecker-EmBYRftd.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-DpGnMmPc.js → useFlexGapSupport-ZGzN1cU8.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-C4zDZIXH.js → useFs-CfP7M1m7.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-CoLICdUZ.js → usePersonalDataHub-CbIPHgNh.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-CKyH-35X.js → vnode-7xh0If_O.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-DRDGit17.js → zoom-DmitvTR2.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/command-manifest.json +17 -3
  152. package/src/commands/agent.js +72 -6
  153. package/src/commands/background-session.js +173 -3
  154. package/src/commands/browse.js +86 -0
  155. package/src/commands/cloud.js +232 -0
  156. package/src/commands/doctor.js +61 -6
  157. package/src/commands/mcp.js +107 -0
  158. package/src/commands/plugin.js +63 -1
  159. package/src/commands/routine.js +367 -0
  160. package/src/commands/session.js +396 -0
  161. package/src/data/changelog.json +12 -2
  162. package/src/harness/jsonl-session-store.js +152 -3
  163. package/src/harness/mcp-client.js +50 -0
  164. package/src/harness/session-index.js +295 -0
  165. package/src/harness/session-mirror.js +142 -0
  166. package/src/harness/transcript-integrity.js +182 -0
  167. package/src/index.js +8 -0
  168. package/src/lib/agent-sandbox.js +68 -0
  169. package/src/lib/agents.js +12 -4
  170. package/src/lib/api-key-helper.js +62 -0
  171. package/src/lib/background-agent-supervisor.js +158 -2
  172. package/src/lib/channels/channel-manager.js +126 -0
  173. package/src/lib/channels/telegram-channel.js +112 -0
  174. package/src/lib/channels/webhook-channel.js +140 -0
  175. package/src/lib/chrome-connector.js +386 -0
  176. package/src/lib/cloud/bundle.js +110 -0
  177. package/src/lib/cloud/cloud-client.js +152 -0
  178. package/src/lib/computer-use/authorization.js +173 -0
  179. package/src/lib/computer-use/computer-use-server.js +276 -0
  180. package/src/lib/computer-use/control-backend.js +150 -0
  181. package/src/lib/dependency-install-policy.js +130 -0
  182. package/src/lib/doctor-checkup.js +652 -0
  183. package/src/lib/exit-codes.cjs +77 -0
  184. package/src/lib/headless-manifest.js +175 -0
  185. package/src/lib/hook-runner.cjs +70 -4
  186. package/src/lib/ide-context-redaction.js +130 -0
  187. package/src/lib/ide-context.js +89 -13
  188. package/src/lib/jsonl-session-store.js +6 -0
  189. package/src/lib/llm-config-defaults.js +24 -8
  190. package/src/lib/lsp/lsp-server-registry.js +48 -21
  191. package/src/lib/plugin-runtime/governance.js +174 -0
  192. package/src/lib/pr-link-ledger.js +221 -0
  193. package/src/lib/project-mcp-trust.js +87 -0
  194. package/src/lib/routine-store.js +352 -0
  195. package/src/lib/sensitive-file-guard.js +51 -0
  196. package/src/lib/session-store-guard.js +40 -0
  197. package/src/lib/session-usage.js +318 -2
  198. package/src/lib/sub-agent-context.js +30 -3
  199. package/src/repl/agent-repl.js +361 -3
  200. package/src/repl/bg-dashboard.js +355 -0
  201. package/src/repl/fast-mode.js +103 -0
  202. package/src/repl/fullscreen-tui.js +197 -0
  203. package/src/repl/slash-command-registry.js +11 -3
  204. package/src/repl/voice-dictation.js +132 -0
  205. package/src/runtime/agent-core.js +344 -7
  206. package/src/runtime/coding-agent-contract-shared.cjs +107 -2
  207. package/src/runtime/coding-agent-policy.cjs +17 -0
  208. package/src/runtime/headless-runner.js +137 -14
  209. package/src/runtime/headless-stream.js +43 -9
  210. package/src/runtime/mcp-config.js +32 -1
  211. package/src/runtime/policies/agent-policy.js +3 -0
  212. package/src/assets/web-panel/assets/Artifacts-DQvJoQ6E.js +0 -1
  213. package/src/assets/web-panel/assets/ChatBubbleRenderer-BAV9V-t-.js +0 -1
  214. package/src/assets/web-panel/assets/GenericCardRenderer-DxxEdn2r.js +0 -1
  215. package/src/assets/web-panel/assets/MobileProjects-CVtTqgtg.js +0 -1
  216. package/src/assets/web-panel/assets/Projects-COATmHxF.js +0 -1
  217. package/src/assets/web-panel/assets/devWarning-nDB--2xr.js +0 -1
  218. package/src/assets/web-panel/assets/index-B1_pRTtB.js +0 -1
  219. package/src/assets/web-panel/assets/index-TMM9FuW-.js +0 -1
@@ -38,24 +38,40 @@ export async function reconcileConfigLlmProvider(cfgLlm = {}) {
38
38
  if (!cfgLlm || !cfgLlm.provider || !cfgLlm.baseUrl) {
39
39
  return { llm: cfgLlm, changed: false };
40
40
  }
41
- const { inferProviderFromBaseUrl, modelForeignToProvider } = await import(
42
- "./runnable-provider.js"
43
- );
41
+ const { inferProviderFromBaseUrl, modelForeignToProvider } =
42
+ await import("./runnable-provider.js");
44
43
  const inferred = inferProviderFromBaseUrl(cfgLlm.baseUrl);
45
44
  if (!inferred || inferred === cfgLlm.provider) {
46
45
  return { llm: cfgLlm, changed: false };
47
46
  }
48
47
  const corrected = { ...cfgLlm, provider: inferred };
49
48
  if (modelForeignToProvider(inferred, cfgLlm.model)) {
50
- const { selectModelForTask, TaskType } = await import(
51
- "./task-model-selector.js"
52
- );
49
+ const { selectModelForTask, TaskType } =
50
+ await import("./task-model-selector.js");
53
51
  const def = selectModelForTask(inferred, TaskType.CHAT);
54
52
  if (def) corrected.model = def;
55
53
  }
56
54
  return { llm: corrected, changed: true };
57
55
  }
58
56
 
57
+ import { resolveApiKeyFromHelper } from "./api-key-helper.js";
58
+
59
+ /**
60
+ * Key backfill order: config llm.apiKey (plaintext, legacy) first, then
61
+ * llm.apiKeyHelper (external command → OS credential store). The helper only
62
+ * runs when no key is otherwise present, so configured setups never pay a
63
+ * subprocess spawn.
64
+ */
65
+ function backfillApiKey(options, cfgLlm) {
66
+ if (options.apiKey) return;
67
+ if (cfgLlm.apiKey) {
68
+ options.apiKey = cfgLlm.apiKey;
69
+ } else if (cfgLlm.apiKeyHelper) {
70
+ const key = resolveApiKeyFromHelper(cfgLlm.apiKeyHelper);
71
+ if (key) options.apiKey = key;
72
+ }
73
+ }
74
+
59
75
  export function applyConfigLlmDefaults(options = {}, cfgLlm = {}, opts = {}) {
60
76
  // The IDE chat panel pins --provider/--model (read from config) but DROPS
61
77
  // --base-url/--api-key. With an explicit --provider the block below bails, so
@@ -71,7 +87,7 @@ export function applyConfigLlmDefaults(options = {}, cfgLlm = {}, opts = {}) {
71
87
  options.provider === cfgLlm.provider
72
88
  ) {
73
89
  if (!options.baseUrl && cfgLlm.baseUrl) options.baseUrl = cfgLlm.baseUrl;
74
- if (!options.apiKey && cfgLlm.apiKey) options.apiKey = cfgLlm.apiKey;
90
+ backfillApiKey(options, cfgLlm);
75
91
  if (!options.model && !opts.explicitModel && cfgLlm.model) {
76
92
  options.model = cfgLlm.model;
77
93
  }
@@ -85,6 +101,6 @@ export function applyConfigLlmDefaults(options = {}, cfgLlm = {}, opts = {}) {
85
101
  options.model = cfgLlm.model;
86
102
  }
87
103
  if (!options.baseUrl && cfgLlm.baseUrl) options.baseUrl = cfgLlm.baseUrl;
88
- if (!options.apiKey && cfgLlm.apiKey) options.apiKey = cfgLlm.apiKey;
104
+ backfillApiKey(options, cfgLlm);
89
105
  return options;
90
106
  }
@@ -67,8 +67,12 @@ function tsServer() {
67
67
  };
68
68
  }
69
69
 
70
- // Registry of plugin-contributed servers, keyed by languageId. Wins over
71
- // builtins if a plugin explicitly overrides.
70
+ // Registry of plugin-contributed servers, keyed by languageId → ordered list of
71
+ // defs (registration order). Multiple plugins may contribute a server for the
72
+ // same languageId/extension; they are tried in order and then fall through to
73
+ // the builtin, so one plugin's server being uninstalled or failing to resolve
74
+ // does NOT knock out code intelligence for that extension — the next candidate
75
+ // (another plugin, or the builtin) still serves it.
72
76
  const pluginServers = new Map();
73
77
  // Plugin-contributed extension→languageId overrides, kept SEPARATE from the
74
78
  // builtin EXTENSION_LANGUAGE map so `_resetPluginServers` (used by
@@ -86,12 +90,18 @@ export function registerLanguageServer(def) {
86
90
  if (!def || !def.languageId || !def.command) {
87
91
  throw new Error("registerLanguageServer requires { languageId, command }");
88
92
  }
89
- pluginServers.set(def.languageId, {
93
+ const entry = {
90
94
  id: def.id || def.command,
91
95
  bins: [def.command],
92
96
  argsFor: () => (Array.isArray(def.args) ? def.args : []),
93
97
  fromPlugin: true,
94
- });
98
+ };
99
+ const existing = pluginServers.get(def.languageId) || [];
100
+ // De-dupe by server id so re-registering the same server (e.g. a reload)
101
+ // doesn't stack duplicates, but distinct plugins for one language coexist.
102
+ const list = existing.filter((e) => e.id !== entry.id);
103
+ list.push(entry);
104
+ pluginServers.set(def.languageId, list);
95
105
  if (Array.isArray(def.extensions)) {
96
106
  for (const ext of def.extensions) {
97
107
  pluginExtensions.set(
@@ -123,17 +133,22 @@ export function languageIdForFile(filePath) {
123
133
  * @returns {{ languageId, id, command, args } | null}
124
134
  */
125
135
  export function resolveServer(languageId, projectRoot) {
126
- const def = pluginServers.get(languageId) || BUILTIN_SERVERS[languageId];
127
- if (!def) return null;
128
- for (const bin of def.bins) {
129
- const command = resolveBin(bin, projectRoot);
130
- if (command) {
131
- return {
132
- languageId,
133
- id: def.id,
134
- command,
135
- args: def.argsFor(bin),
136
- };
136
+ // Try plugin-contributed servers in registration order, then the builtin.
137
+ // The first candidate whose binary actually resolves wins; a plugin server
138
+ // that isn't installed falls through instead of shadowing the builtin.
139
+ const candidates = [...(pluginServers.get(languageId) || [])];
140
+ if (BUILTIN_SERVERS[languageId]) candidates.push(BUILTIN_SERVERS[languageId]);
141
+ for (const def of candidates) {
142
+ for (const bin of def.bins) {
143
+ const command = resolveBin(bin, projectRoot);
144
+ if (command) {
145
+ return {
146
+ languageId,
147
+ id: def.id,
148
+ command,
149
+ args: def.argsFor(bin),
150
+ };
151
+ }
137
152
  }
138
153
  }
139
154
  return null;
@@ -185,17 +200,29 @@ export function resolveBin(bin, projectRoot) {
185
200
  export function probeServers(projectRoot) {
186
201
  const seen = new Set();
187
202
  const out = [];
188
- const all = { ...BUILTIN_SERVERS };
189
- for (const [lid, def] of pluginServers) all[lid] = def;
190
- for (const [languageId, def] of Object.entries(all)) {
203
+ // One (languageId, def) pair per candidate: every plugin server plus each
204
+ // builtin. Probing each independently means an unavailable plugin server is
205
+ // reported as its own row without hiding the builtin's availability.
206
+ const pairs = [];
207
+ for (const [languageId, defs] of pluginServers) {
208
+ for (const def of defs) pairs.push([languageId, def]);
209
+ }
210
+ for (const [languageId, def] of Object.entries(BUILTIN_SERVERS)) {
211
+ pairs.push([languageId, def]);
212
+ }
213
+ for (const [languageId, def] of pairs) {
191
214
  if (seen.has(def.id)) continue; // one row per server family
192
215
  seen.add(def.id);
193
- const resolved = resolveServer(languageId, projectRoot);
216
+ let command = null;
217
+ for (const bin of def.bins) {
218
+ command = resolveBin(bin, projectRoot);
219
+ if (command) break;
220
+ }
194
221
  out.push({
195
222
  languageId,
196
223
  id: def.id,
197
- available: Boolean(resolved),
198
- command: resolved ? resolved.command : null,
224
+ available: Boolean(command),
225
+ command,
199
226
  });
200
227
  }
201
228
  return out;
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Plugin governance (gap-2026-07-11 P2#13): dependency version constraints and
3
+ * "is this plugin actually in use" classification.
4
+ *
5
+ * Two failure modes this addresses:
6
+ * 1. A plugin that needs another plugin (or a minimum host version) installs
7
+ * fine and then misbehaves at runtime because the dependency is absent or
8
+ * too old. `checkPluginDependencies` validates a manifest `dependencies`
9
+ * map (name → semver range) against what's installed, before load.
10
+ * 2. An "unused plugin" reaper that only counts tool invocations wrongly
11
+ * flags a plugin whose entire value is passive — an LSP server, an MCP
12
+ * server, hooks, or monitors — because those never show up as an explicit
13
+ * tool call. `isPluginInUse` treats passive contributions as usage.
14
+ *
15
+ * Pure functions only; semver is the sole dependency.
16
+ */
17
+
18
+ import semver from "semver";
19
+
20
+ /**
21
+ * Passive component kinds — a plugin contributing any of these is "in use"
22
+ * even with zero explicit tool/skill invocations, because they work in the
23
+ * background (a language server, MCP tools surfaced on demand, lifecycle hooks,
24
+ * background monitors).
25
+ */
26
+ export const PASSIVE_COMPONENT_KINDS = Object.freeze([
27
+ "lsp",
28
+ "mcp",
29
+ "hooks",
30
+ "monitors",
31
+ ]);
32
+
33
+ /**
34
+ * Parse a manifest's `dependencies` field into a normalized map. Accepts:
35
+ * { "other-plugin": "^1.2.0", "host": ">=0.162.0" }
36
+ * A reserved key `host` (or `cc`) constrains the CLI version itself. Returns
37
+ * `{ deps, hostRange, errors }`; bad ranges are collected, not thrown. Pure.
38
+ */
39
+ export function parsePluginDependencies(manifest) {
40
+ const errors = [];
41
+ const deps = {};
42
+ let hostRange = null;
43
+ const raw = manifest && manifest.dependencies;
44
+ if (raw == null) return { deps, hostRange, errors };
45
+ if (typeof raw !== "object" || Array.isArray(raw)) {
46
+ errors.push("dependencies must be an object of name → semver range");
47
+ return { deps, hostRange, errors };
48
+ }
49
+ for (const [name, range] of Object.entries(raw)) {
50
+ if (typeof range !== "string" || range.trim() === "") {
51
+ errors.push(`dependency "${name}": range must be a non-empty string`);
52
+ continue;
53
+ }
54
+ if (!semver.validRange(range)) {
55
+ errors.push(`dependency "${name}": invalid semver range "${range}"`);
56
+ continue;
57
+ }
58
+ if (name === "host" || name === "cc") {
59
+ hostRange = range;
60
+ } else {
61
+ deps[name] = range;
62
+ }
63
+ }
64
+ return { deps, hostRange, errors };
65
+ }
66
+
67
+ /**
68
+ * Check a plugin's declared dependencies against installed plugins + the host
69
+ * version. `installed` is a map of pluginName → version string. Returns
70
+ * `{ ok, missing, mismatched, hostMismatch }`. Pure.
71
+ *
72
+ * @param {object} manifest the depending plugin's manifest
73
+ * @param {object} opts { installed, hostVersion }
74
+ */
75
+ export function checkPluginDependencies(
76
+ manifest,
77
+ { installed = {}, hostVersion = null } = {},
78
+ ) {
79
+ const { deps, hostRange } = parsePluginDependencies(manifest);
80
+ const missing = [];
81
+ const mismatched = [];
82
+ for (const [name, range] of Object.entries(deps)) {
83
+ const have = installed[name];
84
+ if (have == null) {
85
+ missing.push({ name, range });
86
+ continue;
87
+ }
88
+ const coerced = semver.valid(have) || semver.coerce(have)?.version;
89
+ if (
90
+ !coerced ||
91
+ !semver.satisfies(coerced, range, { includePrerelease: true })
92
+ ) {
93
+ mismatched.push({ name, range, have });
94
+ }
95
+ }
96
+ let hostMismatch = null;
97
+ if (hostRange && hostVersion) {
98
+ const hv = semver.valid(hostVersion) || semver.coerce(hostVersion)?.version;
99
+ if (!hv || !semver.satisfies(hv, hostRange, { includePrerelease: true })) {
100
+ hostMismatch = { range: hostRange, have: hostVersion };
101
+ }
102
+ }
103
+ return {
104
+ ok: missing.length === 0 && mismatched.length === 0 && !hostMismatch,
105
+ missing,
106
+ mismatched,
107
+ hostMismatch,
108
+ };
109
+ }
110
+
111
+ /** Human-readable one-liners for a dependency check result. Pure. */
112
+ export function formatDependencyIssues(name, result) {
113
+ const lines = [];
114
+ for (const m of result.missing) {
115
+ lines.push(`${name}: missing dependency ${m.name}@${m.range}`);
116
+ }
117
+ for (const m of result.mismatched) {
118
+ lines.push(
119
+ `${name}: dependency ${m.name}@${m.range} not satisfied by installed ${m.have}`,
120
+ );
121
+ }
122
+ if (result.hostMismatch) {
123
+ lines.push(
124
+ `${name}: requires cc ${result.hostMismatch.range}, running ${result.hostMismatch.have}`,
125
+ );
126
+ }
127
+ return lines;
128
+ }
129
+
130
+ /**
131
+ * Whether a plugin should be considered "in use". A plugin is in use when it
132
+ * contributes any passive component (LSP/MCP/hooks/monitors) OR when one of its
133
+ * named skills/agents/tools appears in `invokedNames`. Pure.
134
+ *
135
+ * @param {object} plugin parsed manifest with `.components` + `.name`
136
+ * @param {object} opts { invokedNames?: Set|Array }
137
+ * @returns {{ inUse, reason }}
138
+ */
139
+ export function isPluginInUse(plugin, { invokedNames = [] } = {}) {
140
+ const components = (plugin && plugin.components) || {};
141
+ for (const kind of PASSIVE_COMPONENT_KINDS) {
142
+ if (hasComponent(components[kind])) {
143
+ return { inUse: true, reason: `passive ${kind}` };
144
+ }
145
+ }
146
+ const invoked =
147
+ invokedNames instanceof Set ? invokedNames : new Set(invokedNames || []);
148
+ const contributed = [
149
+ ...componentNames(components.skills),
150
+ ...componentNames(components.agents),
151
+ ];
152
+ for (const n of contributed) {
153
+ if (invoked.has(n)) return { inUse: true, reason: `invoked ${n}` };
154
+ }
155
+ return { inUse: false, reason: "no passive component and never invoked" };
156
+ }
157
+
158
+ function hasComponent(c) {
159
+ if (!c) return false;
160
+ if (Array.isArray(c)) return c.length > 0;
161
+ if (typeof c === "object") {
162
+ if (typeof c.count === "number") return c.count > 0;
163
+ if (c.file) return true;
164
+ if (c.inline) return true;
165
+ }
166
+ return false;
167
+ }
168
+
169
+ function componentNames(c) {
170
+ if (!Array.isArray(c)) return [];
171
+ return c
172
+ .map((e) => (typeof e === "string" ? e : e && (e.name || e.id)))
173
+ .filter(Boolean);
174
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * PR / session linking (gap-2026-07-11 P1#9) — Claude-Code agent-view parity:
3
+ * remember which pull requests a session created, viewed, commented on,
4
+ * merged, or pushed to, so `cc session show` and the background-agent
5
+ * dashboard can display `#123 open` next to the session.
6
+ *
7
+ * Sources:
8
+ * - successful `gh pr create/view/merge/comment/checkout/…` tool commands —
9
+ * their stdout carries the canonical https://github.com/o/r/pull/N URL;
10
+ * - `git push` — best-effort `gh pr list --head <branch>` lookup (async,
11
+ * 3s timeout, silently skipped when gh is unavailable).
12
+ *
13
+ * Ledger: ~/.chainlesschain/pr-links.json
14
+ * { "<sessionId>": [{ number, repo, url, state, action, updatedAt }] }
15
+ * Everything here is best-effort — a ledger failure must never break a tool
16
+ * result or a session listing.
17
+ */
18
+
19
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
20
+ import { execFile } from "node:child_process";
21
+ import { join } from "node:path";
22
+ import { getHomeDir } from "./paths.js";
23
+
24
+ export const _deps = { execFile, now: () => Date.now() };
25
+
26
+ const MAX_LINKS_PER_SESSION = 20;
27
+ const MAX_SESSIONS = 500;
28
+
29
+ export function prLinkLedgerPath() {
30
+ return join(getHomeDir(), "pr-links.json");
31
+ }
32
+
33
+ export function readPrLinkLedger() {
34
+ try {
35
+ const p = prLinkLedgerPath();
36
+ if (!existsSync(p)) return {};
37
+ const parsed = JSON.parse(readFileSync(p, "utf-8"));
38
+ return parsed && typeof parsed === "object" ? parsed : {};
39
+ } catch {
40
+ return {}; // corrupt ledger degrades to empty, never throws
41
+ }
42
+ }
43
+
44
+ function writeLedger(ledger) {
45
+ const ids = Object.keys(ledger);
46
+ if (ids.length > MAX_SESSIONS) {
47
+ // Evict the oldest sessions by their newest link's updatedAt.
48
+ const newest = (id) =>
49
+ Math.max(0, ...(ledger[id] || []).map((l) => l.updatedAt || 0));
50
+ for (const id of ids
51
+ .sort((a, b) => newest(a) - newest(b))
52
+ .slice(0, ids.length - MAX_SESSIONS)) {
53
+ delete ledger[id];
54
+ }
55
+ }
56
+ writeFileSync(prLinkLedgerPath(), JSON.stringify(ledger, null, 2), "utf-8");
57
+ }
58
+
59
+ const PR_URL_RE = /https:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/(\d+)/g;
60
+ const GH_PR_CMD_RE =
61
+ /^\s*gh\s+pr\s+(create|view|merge|comment|checkout|edit|ready|close|reopen|review)\b/;
62
+ const GH_PR_NUMBER_RE =
63
+ /^\s*gh\s+pr\s+(?:view|merge|comment|checkout|edit|ready|close|reopen|review)\s+(?:--\S+\s+)*#?(\d+)\b/;
64
+
65
+ /** True when the shell command is a `gh pr …` subcommand we track. */
66
+ export function isGhPrCommand(command) {
67
+ return GH_PR_CMD_RE.test(String(command || ""));
68
+ }
69
+
70
+ /**
71
+ * Extract PR refs from a tracked `gh pr` command + its output. Pure.
72
+ * @returns {Array<{number:number, repo:string|null, url:string|null, action:string}>}
73
+ */
74
+ export function parsePrRefs(command, output) {
75
+ const cmd = String(command || "");
76
+ const match = cmd.match(GH_PR_CMD_RE);
77
+ if (!match) return [];
78
+ const action = match[1];
79
+ const refs = new Map(); // repo#number → ref
80
+
81
+ for (const m of String(output || "").matchAll(PR_URL_RE)) {
82
+ const repo = `${m[1]}/${m[2]}`;
83
+ const number = Number(m[3]);
84
+ refs.set(`${repo}#${number}`, {
85
+ number,
86
+ repo,
87
+ url: m[0],
88
+ action,
89
+ });
90
+ }
91
+ // `gh pr merge 123` style — explicit number, repo unknown from text alone.
92
+ if (refs.size === 0) {
93
+ const numbered = cmd.match(GH_PR_NUMBER_RE);
94
+ if (numbered) {
95
+ const number = Number(numbered[1]);
96
+ refs.set(`?#${number}`, { number, repo: null, url: null, action });
97
+ }
98
+ }
99
+ return [...refs.values()];
100
+ }
101
+
102
+ const TERMINAL_ACTIONS = { merge: "merged", close: "closed", reopen: "open" };
103
+
104
+ /** Merge one PR ref into a session's link list (dedupe by repo#number). */
105
+ export function recordPrLink(sessionId, ref, options = {}) {
106
+ if (!sessionId || !ref || !Number.isFinite(Number(ref.number))) return null;
107
+ const now = typeof options.now === "number" ? options.now : _deps.now();
108
+ const ledger = readPrLinkLedger();
109
+ const links = Array.isArray(ledger[sessionId]) ? ledger[sessionId] : [];
110
+ const key = (l) => `${l.repo || "?"}#${l.number}`;
111
+ const incoming = {
112
+ number: Number(ref.number),
113
+ repo: ref.repo || null,
114
+ url: ref.url || null,
115
+ state: ref.state || TERMINAL_ACTIONS[ref.action] || "open",
116
+ action: ref.action || null,
117
+ updatedAt: now,
118
+ };
119
+ const existing = links.find((l) => key(l) === key(incoming));
120
+ if (existing) {
121
+ Object.assign(existing, {
122
+ ...incoming,
123
+ url: incoming.url || existing.url,
124
+ repo: incoming.repo || existing.repo,
125
+ });
126
+ } else {
127
+ links.push(incoming);
128
+ if (links.length > MAX_LINKS_PER_SESSION) {
129
+ links.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
130
+ links.length = MAX_LINKS_PER_SESSION;
131
+ }
132
+ }
133
+ ledger[sessionId] = links;
134
+ writeLedger(ledger);
135
+ return incoming;
136
+ }
137
+
138
+ /** Links for a session, newest first. */
139
+ export function getPrLinks(sessionId) {
140
+ if (!sessionId) return [];
141
+ const links = readPrLinkLedger()[sessionId];
142
+ return Array.isArray(links)
143
+ ? [...links].sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
144
+ : [];
145
+ }
146
+
147
+ function ghPrForBranch(cwd, timeoutMs = 3000) {
148
+ const run = (file, fileArgs) =>
149
+ new Promise((resolve) => {
150
+ _deps.execFile(
151
+ file,
152
+ fileArgs,
153
+ { cwd, encoding: "utf8", timeout: timeoutMs, windowsHide: true },
154
+ (err, stdout) => resolve(err ? null : String(stdout || "").trim()),
155
+ );
156
+ });
157
+ return (async () => {
158
+ const branch = await run("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
159
+ if (!branch || branch === "HEAD") return [];
160
+ const out = await run("gh", [
161
+ "pr",
162
+ "list",
163
+ "--head",
164
+ branch,
165
+ "--json",
166
+ "number,url,state",
167
+ ]);
168
+ if (!out) return [];
169
+ try {
170
+ const rows = JSON.parse(out);
171
+ return (Array.isArray(rows) ? rows : []).map((r) => {
172
+ const urlMatch = String(r.url || "").match(
173
+ /github\.com\/([\w.-]+)\/([\w.-]+)\/pull\//,
174
+ );
175
+ return {
176
+ number: Number(r.number),
177
+ url: r.url || null,
178
+ repo: urlMatch ? `${urlMatch[1]}/${urlMatch[2]}` : null,
179
+ state: String(r.state || "open").toLowerCase(),
180
+ action: "push",
181
+ };
182
+ });
183
+ } catch {
184
+ return [];
185
+ }
186
+ })();
187
+ }
188
+
189
+ /**
190
+ * Best-effort hook for the run_shell tool: record PR links produced by this
191
+ * command. Fire-and-forget — resolves quietly on every failure path.
192
+ */
193
+ export async function recordFromShellCommand({
194
+ sessionId,
195
+ command,
196
+ output,
197
+ cwd,
198
+ }) {
199
+ try {
200
+ if (!sessionId || !command) return;
201
+ if (isGhPrCommand(command)) {
202
+ for (const ref of parsePrRefs(command, output)) {
203
+ recordPrLink(sessionId, ref);
204
+ }
205
+ return;
206
+ }
207
+ if (/^\s*git\s+push\b/.test(String(command))) {
208
+ const refs = await ghPrForBranch(cwd || process.cwd());
209
+ for (const ref of refs) recordPrLink(sessionId, ref);
210
+ }
211
+ } catch {
212
+ /* never surface ledger failures into tool results */
213
+ }
214
+ }
215
+
216
+ /** One-line render for panels: "#123 open · #98 merged". */
217
+ export function formatPrLinks(links) {
218
+ return (links || [])
219
+ .map((l) => `#${l.number}${l.state ? ` ${l.state}` : ""}`)
220
+ .join(" · ");
221
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Project `.mcp.json` fingerprint trust (gap-analysis 2026-07-11 P1 "MCP 生命
3
+ * 周期" — 配置指纹变化后重新信任).
4
+ *
5
+ * `--project-mcp` opts a run into a checked-in `.mcp.json`, which can spawn
6
+ * arbitrary commands. That consent is for the file AS THE USER SAW IT — a
7
+ * later commit (or a compromised dependency's postinstall) editing the file
8
+ * must not ride the standing opt-in. So the first successful load records a
9
+ * sha256 fingerprint per absolute file path; a later load whose content no
10
+ * longer matches is REFUSED (fail-closed) until the user re-trusts via
11
+ * `CC_PROJECT_MCP_TRUST=1` (one-shot) or `cc mcp trust-project`.
12
+ *
13
+ * Store: ~/.chainlesschain/trusted-project-mcp.json { [absPath]: {fingerprint, trustedAt} }
14
+ */
15
+
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import { createHash } from "node:crypto";
19
+ import { getHomeDir } from "./paths.js";
20
+
21
+ export const _deps = { fs };
22
+
23
+ function storePath(opts = {}) {
24
+ return (
25
+ opts.storePath ||
26
+ process.env.CC_PROJECT_MCP_TRUST_STORE ||
27
+ path.join(getHomeDir(), "trusted-project-mcp.json")
28
+ );
29
+ }
30
+
31
+ export function projectMcpFingerprint(content) {
32
+ return createHash("sha256").update(String(content), "utf-8").digest("hex");
33
+ }
34
+
35
+ function readStore(opts) {
36
+ try {
37
+ const raw = _deps.fs.readFileSync(storePath(opts), "utf-8");
38
+ const data = JSON.parse(raw);
39
+ return data && typeof data === "object" ? data : {};
40
+ } catch {
41
+ return {};
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Check a project .mcp.json against the trust store.
47
+ * @returns {{status:"first-use"|"trusted"|"changed", fingerprint:string}}
48
+ */
49
+ export function checkProjectMcpTrust(file, content, opts = {}) {
50
+ const fingerprint = projectMcpFingerprint(content);
51
+ const record = readStore(opts)[path.resolve(file)];
52
+ if (!record || !record.fingerprint) {
53
+ return { status: "first-use", fingerprint };
54
+ }
55
+ return {
56
+ status: record.fingerprint === fingerprint ? "trusted" : "changed",
57
+ fingerprint,
58
+ };
59
+ }
60
+
61
+ /** Record (or re-record) the trusted fingerprint for a file. */
62
+ export function recordProjectMcpTrust(file, content, opts = {}) {
63
+ const target = storePath(opts);
64
+ const store = readStore(opts);
65
+ store[path.resolve(file)] = {
66
+ fingerprint: projectMcpFingerprint(content),
67
+ trustedAt: new Date(
68
+ typeof opts.now === "number" ? opts.now : Date.now(),
69
+ ).toISOString(),
70
+ };
71
+ try {
72
+ _deps.fs.mkdirSync(path.dirname(target), { recursive: true });
73
+ _deps.fs.writeFileSync(target, JSON.stringify(store, null, 2), {
74
+ encoding: "utf-8",
75
+ mode: 0o600,
76
+ });
77
+ return true;
78
+ } catch {
79
+ return false; // best-effort — a failed record just re-prompts next run
80
+ }
81
+ }
82
+
83
+ /** Truthy CC_PROJECT_MCP_TRUST → the user explicitly re-trusts this run. */
84
+ export function projectMcpRetrustRequested(env = process.env) {
85
+ const raw = String(env.CC_PROJECT_MCP_TRUST || "").toLowerCase();
86
+ return raw === "1" || raw === "true";
87
+ }