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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-BwpTJTN_.js → AIOps-CkRHjW8e.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BtQOoo3l.js → ActionButton-BP3MeNi2.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-Duf_rXZa.js → Analytics-CjbjQsOr.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-XQK3wjvB.js → AppLayout-i0adH1M8.js} +5 -5
- package/src/assets/web-panel/assets/Artifacts-CCCG2GOU.js +1 -0
- package/src/assets/web-panel/assets/{Audit-DNjOGQLh.js → Audit-DrsgvwHC.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-FH9RAfB5.js → BackgroundAgents-DkXNkbZM.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-ZIbrR4-U.js → Backup-DarTWCex.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DSt3NaND.js → BaseInput-DaDzhcto.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CY-wvbjv.js → Chat-C4EzGw6V.js} +5 -5
- package/src/assets/web-panel/assets/ChatBubbleRenderer-lkavIWMS.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-8JVadE_B.js → Checkbox-gddNgaQi.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-CN83nPYI.js → Codegen-BKldFWf9.js} +1 -1
- package/src/assets/web-panel/assets/{Col-xPSvqdN8.js → Col-ChR-brDs.js} +1 -1
- package/src/assets/web-panel/assets/{Community-D78cnIjG.js → Community-DlHtZX86.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CSw6Csbp.js → Compact-BAhfcBpC.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-E_HhjcYR.js → Compliance-gIrKkm6m.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DybeQjmm.js → Cowork-BYnzLBO3.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DS0BJeLY.js → Cron-ByUDIljI.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BOwVgkbs.js → Crosschain-DOdNbHFp.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DRUr9YuX.js → DID-BJHaNWD2.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Cht-yUKW.js → Dashboard-Drm_tClz.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BLr_EJ9X.js → Dropdown-CA1WLzGN.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Drexuh5W.js → EmailListRenderer-Di1SKjNO.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-D9Q2Kq63.js → FamilyGuardDashboard-44Ve7ZFe.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-C-Z1xI-T.js → Federation-CubPwv_N.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DLHPMkQF.js → FormItemContext-CsUFe5bU.js} +1 -1
- package/src/assets/web-panel/assets/GenericCardRenderer-D7xRjPjZ.js +1 -0
- package/src/assets/web-panel/assets/{Git-syATcaBt.js → Git-D-Qx9dhO.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CYe_r8Zf.js → Governance-_YMYaU4n.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-GWxErB7k.js → Inference-J7Yzh-K_.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-qQvVCZoi.js → KnowledgeGraph-D37iI_Mo.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BenAU2W8.js → Logs-oMTSYfGY.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-7agE3Or1.js → MarkdownRenderer-BqaPmC0W.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-D4Jn5fpD.js → Marketplace-DKUqYDaF.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DtAwoqiQ.js → McpTools-BEv3Gx61.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-BI_O50I8.js → Memory-CtA8XD2y.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CQWE2lqx.js → MobileBridge-ug7c1mMj.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-BpX5g9It.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-Dnr16qsn.js → Mtc-WJaMs4i2.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-CQ5Xe4-a.js → MtcAudit-DXnK_-ru.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-Bjr8O-wy.js → Multisig-C4Ev7726.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CEe-KirU.js → NLProgramming-BMFb9_zk.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-spKJIKF9.js → Notes-Bl93nvBX.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-DRp83LFG.js → NotificationSettings-BN2W5cl6.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-h1A0XB2a.js → OrderTableRenderer-Dxc9oN4o.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-BlgB0IYg.js → Organization-CAs48637.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-a_35HqZI.js → Overflow-Bhq27Zoc.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CSNHCNPC.js → P2P-D61-2542.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-C8M1JH-1.js → PdhVaultBrowser-C1j4iUxz.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-DmjJi9Ms.js → Permissions-rJ2-fScM.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-DfuwRyf4.js → PersonalDataHub-DFIlTok6.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-B2-S4ekE.js → Pipeline-BjIgEWoZ.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-B1dOiBLC.js → Privacy-BQoH5wcX.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-_pNmc-DX.js → ProjectInit-CYR_-_Ub.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DmwG2Ii1.js → ProjectSettings-nkatCq-9.js} +2 -2
- package/src/assets/web-panel/assets/Projects-SS9uZKCl.js +1 -0
- package/src/assets/web-panel/assets/{Providers-C6HSjg1d.js → Providers-BnKM9fsF.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CrDmS7l1.js → QrScannerModal-Cfe82dbz.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CfKvWt-x.js → QuickAsk-r1RbPufV.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DMa6Kuzv.js → Recommend-CjCbM2WE.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-DrktUo5x.js → RemoteSession-BuMGPH6V.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-BTPQ1vjL.js → Reputation-Bmj3NY1F.js} +1 -1
- package/src/assets/web-panel/assets/{Row-B6Q7QkMG.js → Row-BIsNcE_1.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-B1IJhAmX.js → RssFeed-BObmojD2.js} +3 -3
- package/src/assets/web-panel/assets/{Search-xbea1kGD.js → Search-BRmbkQNS.js} +1 -1
- package/src/assets/web-panel/assets/{Security-7rs0cO3F.js → Security-_rF3DRpY.js} +2 -2
- package/src/assets/web-panel/assets/{Services-0fjixK0S.js → Services-D0If9ZV4.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-B_rVq9Cu.js → Skeleton-o7zIksUg.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-C5uM90b9.js → Skills-oUR7zi9u.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-U4pYCIZH.js → Sla-fqKyGi03.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-Cdz4yTWx.js → SpeechSettings-B1wZ_gxx.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-Me3aetHv.js → SyncSettings-B3qJWYXq.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-Cf6aNkL_.js → Tasks-BH5_dUfr.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CYatk4tU.js → Templates-CCIVpjRP.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DIMKLw7o.js → Tenant-2GZtTGnB.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-C_cp-uSJ.js → Terminal-BlMkfs_u.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-cUPVNADp.js → TimelineRenderer-U6hi5OMc.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CiaEIXVf.js → Tokens-C-_BerGJ.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DB1WU5lQ.js → Trigger-DaKzBgN3.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-Cfht9Ijy.js → Trust-CLvcsco3.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-K3P4Vt9S.js → UkeySign-BFn_Y1zZ.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DWjhjXRh.js → VideoEditing-B0V18dLT.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-D9-Q1Kan.js → Wallet-BbmP7Imu.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-BDszy-oV.js → WebAuthn-Dewj_0kt.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-DIhij4rA.js → WorkflowEditor-DOxz3Lwa.js} +1 -1
- package/src/assets/web-panel/assets/{chat-D7VCiKbh.js → chat-B1OIpZnh.js} +1 -1
- package/src/assets/web-panel/assets/{colors-BvOd9pVn.js → colors-CSGNVdri.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DoqXBiAO.js → compact-item-8S0vcg0u.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CrKRzUtp.js → createContext-vZyc27TN.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DYEZcY55.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BWAev4WC.js → hasIn-DWw3-UhA.js} +1 -1
- package/src/assets/web-panel/assets/{index-DvDOpsJB.js → index-B4pP2ycr.js} +1 -1
- package/src/assets/web-panel/assets/{index-oY45XrkG.js → index-BDHyovnK.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmJ6xKBO.js → index-BKEmQQvR.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoYrS_L2.js → index-BQssf0EQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-ww1QtLnX.js → index-BZnauCqN.js} +1 -1
- package/src/assets/web-panel/assets/{index-BWWsaimP.js → index-BdyokEau.js} +1 -1
- package/src/assets/web-panel/assets/{index-CoD31Ndm.js → index-BeBv896m.js} +1 -1
- package/src/assets/web-panel/assets/{index-DlGu7As6.js → index-BjC-TrOb.js} +1 -1
- package/src/assets/web-panel/assets/{index-zH-tuuGZ.js → index-BkWd6YL7.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEXfKSiI.js → index-BoIPHP4R.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEPHMSih.js → index-BuB5P0TR.js} +1 -1
- package/src/assets/web-panel/assets/{index-PDxGjKRu.js → index-BwKrTVV0.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkgnGX7v.js → index-C6r27A-o.js} +1 -1
- package/src/assets/web-panel/assets/{index-DV-Rfmfx.js → index-CB2MOS_8.js} +3 -3
- package/src/assets/web-panel/assets/{index-KR1-Em9C.js → index-CFQWZvak.js} +1 -1
- package/src/assets/web-panel/assets/{index-8g5TJCZ3.js → index-CI3slBKI.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCyP071m.js → index-CJgKP2cm.js} +1 -1
- package/src/assets/web-panel/assets/{index-CSb-tnIo.js → index-CLPr2vC_.js} +1 -1
- package/src/assets/web-panel/assets/{index-1poMpMU0.js → index-CbZqw7pg.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZGk8AbZ.js → index-CfEvacz7.js} +1 -1
- package/src/assets/web-panel/assets/{index-BXyl8O23.js → index-CwXBblm-.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHOcVYrx.js → index-D1i_8oop.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bq_ctRpz.js → index-D8qXVfoV.js} +1 -1
- package/src/assets/web-panel/assets/{index-gmxqu5Kk.js → index-DKwl1AkQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-D23L1V76.js → index-DLAvUtOH.js} +1 -1
- package/src/assets/web-panel/assets/{index-CDGLdn12.js → index-DLgkAhEE.js} +1 -1
- package/src/assets/web-panel/assets/{index-YGDy7nNq.js → index-DPgY5GLy.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cx7M7PSF.js → index-Dc40pX3F.js} +1 -1
- package/src/assets/web-panel/assets/{index-JzUmJMvn.js → index-DcfLXAW7.js} +1 -1
- package/src/assets/web-panel/assets/{index-HwHdTusY.js → index-DeCS12Mh.js} +1 -1
- package/src/assets/web-panel/assets/{index-B30NvR21.js → index-Dg0Izipr.js} +1 -1
- package/src/assets/web-panel/assets/index-DmVFfcNR.js +1 -0
- package/src/assets/web-panel/assets/{index-B0vGEbJT.js → index-DtHWHJnj.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bf9YlbDn.js → index-Q6oyLnKC.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bj7EgmOp.js → index-dxocDVD_.js} +1 -1
- package/src/assets/web-panel/assets/{index-ISvPZ05r.js → index-t-q3rkVm.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5kRmj3P.js → index-tr1x96W9.js} +1 -1
- package/src/assets/web-panel/assets/index-x7YyhMUa.js +1 -0
- package/src/assets/web-panel/assets/{index-BN9m4Ux2.js → index-xx5M5Rbj.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-XW-OR3mL.js → initDefaultProps-Di5YFId0.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CeuEHh20.js → motion-BVSN0aeL.js} +1 -1
- package/src/assets/web-panel/assets/{move-Dva79Keg.js → move-DHNiaP2l.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-C0B6bD2j.js → mtc-parser-xJPf3gQD.js} +1 -1
- package/src/assets/web-panel/assets/{omit-AkZvO-a2.js → omit-C7qzUdkN.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-B4r55qGc.js → pickAttrs-pSpjg3Le.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BLH-z7YE.js → placementArrow-cs_jL_UC.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-ksEGZ2Wf.js → responsiveObserve-DkS_Zs4r.js} +1 -1
- package/src/assets/web-panel/assets/{slide-Cth4OD1Z.js → slide-B5O1Kpxo.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-9S83ezxw.js → statusUtils-BX02GnZp.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-7yQmQGZ4.js → styleChecker-EmBYRftd.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DpGnMmPc.js → useFlexGapSupport-ZGzN1cU8.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-C4zDZIXH.js → useFs-CfP7M1m7.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CoLICdUZ.js → usePersonalDataHub-CbIPHgNh.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CKyH-35X.js → vnode-7xh0If_O.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DRDGit17.js → zoom-DmitvTR2.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-manifest.json +17 -3
- package/src/commands/agent.js +72 -6
- package/src/commands/background-session.js +173 -3
- package/src/commands/browse.js +86 -0
- package/src/commands/cloud.js +232 -0
- package/src/commands/doctor.js +61 -6
- package/src/commands/mcp.js +107 -0
- package/src/commands/plugin.js +63 -1
- package/src/commands/routine.js +367 -0
- package/src/commands/session.js +396 -0
- package/src/data/changelog.json +12 -2
- package/src/harness/jsonl-session-store.js +152 -3
- package/src/harness/mcp-client.js +50 -0
- package/src/harness/session-index.js +295 -0
- package/src/harness/session-mirror.js +142 -0
- package/src/harness/transcript-integrity.js +182 -0
- package/src/index.js +8 -0
- package/src/lib/agent-sandbox.js +68 -0
- package/src/lib/agents.js +12 -4
- package/src/lib/api-key-helper.js +62 -0
- package/src/lib/background-agent-supervisor.js +158 -2
- package/src/lib/channels/channel-manager.js +126 -0
- package/src/lib/channels/telegram-channel.js +112 -0
- package/src/lib/channels/webhook-channel.js +140 -0
- package/src/lib/chrome-connector.js +386 -0
- package/src/lib/cloud/bundle.js +110 -0
- package/src/lib/cloud/cloud-client.js +152 -0
- package/src/lib/computer-use/authorization.js +173 -0
- package/src/lib/computer-use/computer-use-server.js +276 -0
- package/src/lib/computer-use/control-backend.js +150 -0
- package/src/lib/dependency-install-policy.js +130 -0
- package/src/lib/doctor-checkup.js +652 -0
- package/src/lib/exit-codes.cjs +77 -0
- package/src/lib/headless-manifest.js +175 -0
- package/src/lib/hook-runner.cjs +70 -4
- package/src/lib/ide-context-redaction.js +130 -0
- package/src/lib/ide-context.js +89 -13
- package/src/lib/jsonl-session-store.js +6 -0
- package/src/lib/llm-config-defaults.js +24 -8
- package/src/lib/lsp/lsp-server-registry.js +48 -21
- package/src/lib/plugin-runtime/governance.js +174 -0
- package/src/lib/pr-link-ledger.js +221 -0
- package/src/lib/project-mcp-trust.js +87 -0
- package/src/lib/routine-store.js +352 -0
- package/src/lib/sensitive-file-guard.js +51 -0
- package/src/lib/session-store-guard.js +40 -0
- package/src/lib/session-usage.js +318 -2
- package/src/lib/sub-agent-context.js +30 -3
- package/src/repl/agent-repl.js +361 -3
- package/src/repl/bg-dashboard.js +355 -0
- package/src/repl/fast-mode.js +103 -0
- package/src/repl/fullscreen-tui.js +197 -0
- package/src/repl/slash-command-registry.js +11 -3
- package/src/repl/voice-dictation.js +132 -0
- package/src/runtime/agent-core.js +344 -7
- package/src/runtime/coding-agent-contract-shared.cjs +107 -2
- package/src/runtime/coding-agent-policy.cjs +17 -0
- package/src/runtime/headless-runner.js +137 -14
- package/src/runtime/headless-stream.js +43 -9
- package/src/runtime/mcp-config.js +32 -1
- package/src/runtime/policies/agent-policy.js +3 -0
- package/src/assets/web-panel/assets/Artifacts-DQvJoQ6E.js +0 -1
- package/src/assets/web-panel/assets/ChatBubbleRenderer-BAV9V-t-.js +0 -1
- package/src/assets/web-panel/assets/GenericCardRenderer-DxxEdn2r.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-CVtTqgtg.js +0 -1
- package/src/assets/web-panel/assets/Projects-COATmHxF.js +0 -1
- package/src/assets/web-panel/assets/devWarning-nDB--2xr.js +0 -1
- package/src/assets/web-panel/assets/index-B1_pRTtB.js +0 -1
- 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 } =
|
|
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 } =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
71
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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(
|
|
198
|
-
command
|
|
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
|
+
}
|