chainlesschain 0.162.148 → 0.162.149
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/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-DwpCCp64.js → AIOps-BJif14yR.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-C9pBYocA.js → ActionButton-CXek6gJY.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BWAkeXxK.js → Analytics-BgQhdAJi.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-CivFGH6B.js → AppLayout-DTYl5NtR.js} +3 -3
- package/src/assets/web-panel/assets/{Audit-Cm8hRpZm.js → Audit-BzATQAeR.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DXdFMsE8.js → Backup-BzdPEQhL.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-uAwSTeql.js → BaseInput-BM0KVh8E.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-KU8eeJJE.js → Chat-D7fHXHkz.js} +4 -4
- package/src/assets/web-panel/assets/ChatBubbleRenderer-Wm34RR-b.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-C6AMDkjd.js → Checkbox-CAiSQ9vO.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-t2moDxcO.js → Codegen-Df40CrEe.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DCcsRRhN.js → Col-DIT2fNFU.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DtB-8SAC.js → Community-CSTIbW2k.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CZm8THYA.js → Compact-DtqXZZUi.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-LiQgC7g1.js → Compliance-CUtzw6o7.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CA8SAxht.js → Cowork-CezmlnmU.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-CtRaF1wD.js → Cron-P4BITarg.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-6-br6Hub.js → Crosschain-BRRiYwvq.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Do2EccrL.js → DID-L5ijB9i4.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-wrXcbzGe.js → Dashboard-CFxro4ob.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-p3FsT245.js → Dropdown-BmAFCQ71.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-D95A2L8q.js → EmailListRenderer-DJBCEuon.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-cLuJW2zo.js → FamilyGuardDashboard-z4oLq6eT.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-Brgq_cbN.js → Federation-DsVCpHMF.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DWLCkNgh.js → FormItemContext-DP3bP5th.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BBqUfQd6.js → GenericCardRenderer-BAXP2Gc6.js} +1 -1
- package/src/assets/web-panel/assets/{Git-8F090w4I.js → Git-I3g_bAw9.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CCogEbxb.js → Governance-jHS5-ioS.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CfLD7v7C.js → Inference-CPL7sfx9.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DDuHJewd.js → KnowledgeGraph-Jf236h4C.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CE8KSEVC.js → Logs-Cm2tcSKg.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-DmwpZmhT.js → Marketplace-CZK82rhi.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-Bf7AazU8.js → McpTools-CZCeji7a.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-D0QU_00S.js → Memory-3l2KVS9k.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-yXS9xdhx.js → MobileBridge-VYdpoLh6.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-BuDUoGUP.js → MobileProjects-CiB9cmm1.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BYyHy28p.js → Mtc-wCFZbBuL.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-CK0aqpiZ.js → MtcAudit-1a3THIyG.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-mNimKYeb.js → Multisig-DMzLB2hG.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-AioAJ0YA.js → NLProgramming-D9wZbf6l.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CVVNCLHE.js → Notes-hrFe2ZM-.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-C4ABj-TK.js → NotificationSettings-CHGX5Jwm.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-CRIFE2Tj.js +1 -0
- package/src/assets/web-panel/assets/{Organization-uozl_gWK.js → Organization-By7FkhtY.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-BCZOM2hK.js → Overflow--khGSzJn.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CArcaiaj.js → P2P-CAG9dH35.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-Sgq2Srr8.js → PdhVaultBrowser-B3SQZmK9.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-BYrQrXnH.js → Permissions-D35ec6JA.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BzHStAbz.js → PersonalDataHub-CxBF7hW_.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-k7KqxX1M.js → Pipeline-s6EtOO1s.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-IumTUWqx.js → Privacy-BbJYmWmO.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BRyImYTf.js → ProjectInit-VGQq1jMT.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BFIqTBFk.js → ProjectSettings-B1ew3Teh.js} +2 -2
- package/src/assets/web-panel/assets/Projects-CGcNIs_g.js +1 -0
- package/src/assets/web-panel/assets/{Providers-CyyFnUPs.js → Providers-mrO47FIK.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-C82cRx-X.js → QrScannerModal-IJF2mHyw.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DGxDF521.js → QuickAsk-CU0wN_Z0.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Cns-Am1y.js → Recommend-DhN37R6G.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-R7OqAIlg.js → RemoteSession-D_xOX_hu.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-D7mgPIYV.js → Reputation-D_Ssv7uH.js} +1 -1
- package/src/assets/web-panel/assets/{Row-B3lEURyd.js → Row-Kx5dKxX7.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-9_UVrpBt.js → RssFeed-BlGwqZkY.js} +2 -2
- package/src/assets/web-panel/assets/{Search-ClZeO80q.js → Search-hLFQzxpb.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BNNJczEp.js → Security-DMlvsVt4.js} +3 -3
- package/src/assets/web-panel/assets/{Services-C5SjrWUj.js → Services-Bd0Qsj2m.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-Ci_obQ-g.js → Skeleton-DFclq9X3.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DdebN15P.js → Skills-BNtln2qY.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-OinZP2vi.js → Sla-Dch5H19G.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-CxzbNF51.js → SpeechSettings-2UfQcU1g.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-D06N_66b.js → SyncSettings-CD9YQr4i.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BdEdW0AZ.js → Tasks-BNu-7MzI.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-F1ctKmPa.js → Templates-D1n9CaIR.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CVz1Urot.js → Tenant-DenL3iBW.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-ZzU0Io1Y.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-Doitzjmf.js → TimelineRenderer-D87IfukO.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BpyVRpVA.js → Tokens-BkiZc8E3.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-nWhWYTbU.js → Trigger-BNRSytGN.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DK_G-wLx.js → Trust-Bwbd4X2m.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-B5yBUojO.js → UkeySign-B8O2bs0o.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-C5D51qAe.js → VideoEditing-C2fL8zmH.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CLbL9K7v.js → Wallet-BORYDdE1.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DZUelI3V.js → WebAuthn-C0V5S2FM.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-8RPxt4KW.js → WorkflowEditor-2dR0fcHL.js} +1 -1
- package/src/assets/web-panel/assets/{chat-4N4tOA3d.js → chat-BmuAFAn8.js} +1 -1
- package/src/assets/web-panel/assets/{colors-zbbGVrua.js → colors-CN3smMcK.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-D7iMkbnE.js → compact-item-DCnxb4kW.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CBzPJv-w.js → createContext-vlR43pHn.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DxwmsL4j.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-bHdrQSqZ.js → hasIn-D04tHYFd.js} +1 -1
- package/src/assets/web-panel/assets/{index-DbE5D0WL.js → index-0DnaZGkC.js} +1 -1
- package/src/assets/web-panel/assets/{index-308gCGh7.js → index-430S68MN.js} +1 -1
- package/src/assets/web-panel/assets/{index-kmh1TxRa.js → index-B78xL3s2.js} +1 -1
- package/src/assets/web-panel/assets/{index-QFObYeo1.js → index-BTPEZTk1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZb8F8YG.js → index-BXRg8GFQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DE9j5YgT.js → index-BasvsWDM.js} +1 -1
- package/src/assets/web-panel/assets/{index-CiG1IZao.js → index-BeGDEXFs.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bqmx24kh.js → index-BqhASEL4.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCr0geV9.js → index-BudvKQfG.js} +1 -1
- package/src/assets/web-panel/assets/{index-S74FpAIn.js → index-C-6NO5il.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyoNgbXl.js → index-C5Sxb1sE.js} +1 -1
- package/src/assets/web-panel/assets/{index-BtbbdLnf.js → index-CP9m9Ggr.js} +3 -3
- package/src/assets/web-panel/assets/{index-CkxK86vf.js → index-CTOCnIQ7.js} +1 -1
- package/src/assets/web-panel/assets/index-Cg2ldRfU.js +1 -0
- package/src/assets/web-panel/assets/{index-BTIjjXry.js → index-ClhAHDK3.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7b1hGry.js → index-CnXebZLL.js} +1 -1
- package/src/assets/web-panel/assets/{index-CPxkoKgA.js → index-CngCC8hC.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ci009ijp.js → index-Cr-A0FYJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CdmMoYN1.js → index-CsAsCPJ6.js} +1 -1
- package/src/assets/web-panel/assets/{index-BBNr77E1.js → index-D28DeAAB.js} +1 -1
- package/src/assets/web-panel/assets/{index-B1s0Bz9O.js → index-D5LZVZ0L.js} +1 -1
- package/src/assets/web-panel/assets/{index-oDKNgCsP.js → index-D65_XseV.js} +1 -1
- package/src/assets/web-panel/assets/{index-QNAG4JtR.js → index-D8vwp4qp.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQfu1U78.js → index-DDwLgQY9.js} +1 -1
- package/src/assets/web-panel/assets/{index-BnBOpqv3.js → index-DIpY0qM5.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQ6NcfWz.js → index-DTWg-18U.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4XKpj6c.js → index-DTtRscUa.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1YDh7Wr.js → index-DYAtmqiv.js} +1 -1
- package/src/assets/web-panel/assets/{index-CvRMA9uT.js → index-D_n8_vfI.js} +1 -1
- package/src/assets/web-panel/assets/{index-CahryTX0.js → index-DbADHwhr.js} +1 -1
- package/src/assets/web-panel/assets/{index-BJ4ZGyW0.js → index-DbVsQBOH.js} +1 -1
- package/src/assets/web-panel/assets/index-Dwzo-dtJ.js +1 -0
- package/src/assets/web-panel/assets/{index-xYCm6W2h.js → index-HhNjnCfe.js} +1 -1
- package/src/assets/web-panel/assets/{index-BS4_Mwk6.js → index-ToUh2b1-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DISWAYce.js → index-TslMTwNy.js} +1 -1
- package/src/assets/web-panel/assets/{index-DmqlJed-.js → index-_FIkN3jd.js} +1 -1
- package/src/assets/web-panel/assets/{index-CqdPyWm5.js → index-hpvvtAZ3.js} +1 -1
- package/src/assets/web-panel/assets/{index-BsKehmmS.js → index-jzR7Ujuw.js} +1 -1
- package/src/assets/web-panel/assets/{index-DUyjUkY7.js → index-ljuBiQW9.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BD55yNBt.js → initDefaultProps-C3wUb4je.js} +1 -1
- package/src/assets/web-panel/assets/{motion-B5Bbe77U.js → motion-D-jYFUNA.js} +1 -1
- package/src/assets/web-panel/assets/{move-CTvIqHEH.js → move-tqb8nwJ2.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BkNyPQKB.js → mtc-parser-DSOjMJMz.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BaXJXgHA.js → omit-CJ7VimIW.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-Bw6-YTxH.js → pickAttrs-qXiG1iT3.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BLdbkLqJ.js → placementArrow-CkGBcy1Z.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-IVYkzntU.js → responsiveObserve-D3WvTlLO.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DFMFwwtY.js → slide-BEE2ftge.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CURYRtZ1.js → statusUtils-CsfBpxiG.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Cfz63s1r.js → styleChecker-xPrIiJPI.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-COJL0KE3.js → useFlexGapSupport-C5Yd-SCR.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-YLLojQQf.js → useFs-D_MDS8HI.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BF_21qUC.js → usePersonalDataHub-BqJh-usA.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-9ZpkA7bb.js → vnode-chm_p-gz.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DhtAfhl8.js → zoom-3-YDNz0Z.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/assets/web-panel/remote-session-sw.js +43 -0
- package/src/command-manifest.json +22 -1
- package/src/commands/agent.js +44 -5
- package/src/commands/codeintel.js +252 -0
- package/src/commands/eval.js +248 -0
- package/src/commands/plugin.js +354 -0
- package/src/commands/team.js +456 -0
- package/src/gateways/ws/message-dispatcher.js +45 -2
- package/src/gateways/ws/remote-session-protocol.js +37 -0
- package/src/gateways/ws/ws-server.js +1 -1
- package/src/harness/prompt-compressor.js +27 -1
- package/src/harness/remote-command-ledger.js +158 -0
- package/src/harness/remote-session-push-apns.js +246 -0
- package/src/harness/remote-session-push-errors.js +15 -0
- package/src/harness/remote-session-push-fcm.js +9 -25
- package/src/harness/remote-session-push-huawei.js +174 -0
- package/src/harness/remote-session-push-oppo.js +171 -0
- package/src/harness/remote-session-push-senders.js +42 -0
- package/src/harness/remote-session-push-vivo.js +179 -0
- package/src/harness/remote-session-push-web.js +299 -0
- package/src/harness/remote-session-push-xiaomi.js +99 -0
- package/src/index.js +6 -0
- package/src/lib/agent-core.js +2 -0
- package/src/lib/agent-sandbox.js +161 -5
- package/src/lib/agent-team/task-lease.js +434 -0
- package/src/lib/agent-team/team-budget.js +165 -0
- package/src/lib/agent-team/team-mailbox.js +106 -0
- package/src/lib/agent-team/team-runner.js +266 -0
- package/src/lib/agent-team/team-worktree.js +204 -0
- package/src/lib/agents.js +18 -0
- package/src/lib/async-hook-supervisor.cjs +305 -0
- package/src/lib/eval/runner.js +163 -0
- package/src/lib/eval/tasks.js +456 -0
- package/src/lib/eval/trend.js +185 -0
- package/src/lib/lsp/__tests__/code-intelligence.integration.test.js +129 -0
- package/src/lib/lsp/__tests__/code-intelligence.test.js +228 -0
- package/src/lib/lsp/__tests__/jsonrpc-stream.test.js +104 -0
- package/src/lib/lsp/__tests__/lsp-client.test.js +258 -0
- package/src/lib/lsp/__tests__/lsp-manager.test.js +264 -0
- package/src/lib/lsp/__tests__/lsp-server-registry.test.js +128 -0
- package/src/lib/lsp/code-intelligence.js +356 -0
- package/src/lib/lsp/jsonrpc-stream.js +139 -0
- package/src/lib/lsp/lsp-client.js +318 -0
- package/src/lib/lsp/lsp-manager.js +323 -0
- package/src/lib/lsp/lsp-server-registry.js +196 -0
- package/src/lib/plugin-monitor-supervisor.js +238 -0
- package/src/lib/plugin-runtime/agents.js +51 -0
- package/src/lib/plugin-runtime/bin.js +100 -0
- package/src/lib/plugin-runtime/hooks.js +100 -0
- package/src/lib/plugin-runtime/install.js +388 -0
- package/src/lib/plugin-runtime/lsp.js +75 -0
- package/src/lib/plugin-runtime/manifest.js +458 -0
- package/src/lib/plugin-runtime/mcp.js +89 -0
- package/src/lib/plugin-runtime/monitors.js +100 -0
- package/src/lib/plugin-runtime/policy.js +144 -0
- package/src/lib/plugin-runtime/reload.js +79 -0
- package/src/lib/plugin-runtime/scopes.js +197 -0
- package/src/lib/plugin-runtime/settings.js +119 -0
- package/src/lib/plugin-runtime/signature.js +107 -0
- package/src/lib/plugin-runtime/skills.js +43 -0
- package/src/lib/plugin-runtime/trust.js +140 -0
- package/src/lib/sandbox-egress-proxy.js +162 -0
- package/src/lib/sandbox-network-policy.js +134 -0
- package/src/lib/sandbox-v2.js +10 -4
- package/src/lib/settings-hook-events.cjs +78 -6
- package/src/lib/settings-hooks.cjs +29 -3
- package/src/lib/settings-loader.cjs +35 -1
- package/src/lib/skill-loader.js +18 -0
- package/src/lib/telemetry/span-recorder.js +237 -0
- package/src/repl/agent-repl.js +400 -4
- package/src/runtime/agent-core.js +546 -52
- package/src/runtime/coding-agent-contract-shared.cjs +82 -8
- package/src/runtime/coding-agent-policy.cjs +32 -7
- package/src/runtime/fallback-model.js +134 -7
- package/src/runtime/headless-runner.js +353 -108
- package/src/runtime/mcp-config.js +46 -0
- package/src/assets/web-panel/assets/ChatBubbleRenderer-DCNfbdlx.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-DIp8Xcbd.js +0 -1
- package/src/assets/web-panel/assets/Projects-Dpid9CDg.js +0 -1
- package/src/assets/web-panel/assets/Terminal-BvAK_Zel.js +0 -3
- package/src/assets/web-panel/assets/devWarning-C2Z5LRgq.js +0 -1
- package/src/assets/web-panel/assets/index-BGp6Yr-Y.js +0 -1
- package/src/assets/web-panel/assets/index-BXiw9dQf.js +0 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSP server registry — maps a language id to the language-server command that
|
|
3
|
+
* serves it, and probes whether that command is actually installed.
|
|
4
|
+
*
|
|
5
|
+
* Design constraint (see optimization plan): the CLI must NOT depend on any
|
|
6
|
+
* language server as an npm dependency (they are heavy and language-specific).
|
|
7
|
+
* Instead we DETECT servers on PATH / `node_modules/.bin`, and when none is
|
|
8
|
+
* available the caller degrades gracefully to text search. Plugins may register
|
|
9
|
+
* extra servers via `.lsp.json`.
|
|
10
|
+
*
|
|
11
|
+
* Pure + injectable: filesystem/exec probing goes through `_deps` so the
|
|
12
|
+
* detection logic is unit-testable without a real toolchain.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
import path from "path";
|
|
17
|
+
import { execFileSync } from "child_process";
|
|
18
|
+
|
|
19
|
+
export const _deps = {
|
|
20
|
+
existsSync: fs.existsSync,
|
|
21
|
+
execFileSync,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Extension → LSP languageId. Kept small and explicit; unknown extensions get
|
|
25
|
+
// no LSP (text-search fallback).
|
|
26
|
+
const EXTENSION_LANGUAGE = {
|
|
27
|
+
".ts": "typescript",
|
|
28
|
+
".tsx": "typescriptreact",
|
|
29
|
+
".mts": "typescript",
|
|
30
|
+
".cts": "typescript",
|
|
31
|
+
".js": "javascript",
|
|
32
|
+
".jsx": "javascriptreact",
|
|
33
|
+
".mjs": "javascript",
|
|
34
|
+
".cjs": "javascript",
|
|
35
|
+
".py": "python",
|
|
36
|
+
".pyi": "python",
|
|
37
|
+
".go": "go",
|
|
38
|
+
".rs": "rust",
|
|
39
|
+
".java": "java",
|
|
40
|
+
".vue": "vue",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// languageId → server definition. `bin` names are tried in order; the first one
|
|
44
|
+
// resolvable wins. `args` is passed verbatim. Several languageIds can share one
|
|
45
|
+
// server family (TS server handles JS too).
|
|
46
|
+
const BUILTIN_SERVERS = {
|
|
47
|
+
typescript: tsServer(),
|
|
48
|
+
typescriptreact: tsServer(),
|
|
49
|
+
javascript: tsServer(),
|
|
50
|
+
javascriptreact: tsServer(),
|
|
51
|
+
python: {
|
|
52
|
+
id: "pyright",
|
|
53
|
+
bins: ["pyright-langserver", "pylsp"],
|
|
54
|
+
// pyright wants --stdio; pylsp defaults to stdio with no args. Resolved per-bin.
|
|
55
|
+
argsFor: (bin) => (bin.startsWith("pyright") ? ["--stdio"] : []),
|
|
56
|
+
},
|
|
57
|
+
go: { id: "gopls", bins: ["gopls"], argsFor: () => [] },
|
|
58
|
+
rust: { id: "rust-analyzer", bins: ["rust-analyzer"], argsFor: () => [] },
|
|
59
|
+
java: { id: "jdtls", bins: ["jdtls"], argsFor: () => [] },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function tsServer() {
|
|
63
|
+
return {
|
|
64
|
+
id: "typescript-language-server",
|
|
65
|
+
bins: ["typescript-language-server"],
|
|
66
|
+
argsFor: () => ["--stdio"],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Registry of plugin-contributed servers, keyed by languageId. Wins over
|
|
71
|
+
// builtins if a plugin explicitly overrides.
|
|
72
|
+
const pluginServers = new Map();
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Register an extra language server (from a plugin `.lsp.json`).
|
|
76
|
+
* @param {object} def { languageId, extensions?, command, args?, id? }
|
|
77
|
+
*/
|
|
78
|
+
export function registerLanguageServer(def) {
|
|
79
|
+
if (!def || !def.languageId || !def.command) {
|
|
80
|
+
throw new Error("registerLanguageServer requires { languageId, command }");
|
|
81
|
+
}
|
|
82
|
+
pluginServers.set(def.languageId, {
|
|
83
|
+
id: def.id || def.command,
|
|
84
|
+
bins: [def.command],
|
|
85
|
+
argsFor: () => (Array.isArray(def.args) ? def.args : []),
|
|
86
|
+
fromPlugin: true,
|
|
87
|
+
});
|
|
88
|
+
if (Array.isArray(def.extensions)) {
|
|
89
|
+
for (const ext of def.extensions) {
|
|
90
|
+
EXTENSION_LANGUAGE[ext.startsWith(".") ? ext : "." + ext] =
|
|
91
|
+
def.languageId;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Test/reset hook — clears plugin registrations. */
|
|
97
|
+
export function _resetPluginServers() {
|
|
98
|
+
pluginServers.clear();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Map a file path to its LSP languageId, or null if unsupported. */
|
|
102
|
+
export function languageIdForFile(filePath) {
|
|
103
|
+
const ext = path.extname(String(filePath || "")).toLowerCase();
|
|
104
|
+
return EXTENSION_LANGUAGE[ext] || null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Resolve a runnable server command for a languageId within a project root, or
|
|
109
|
+
* null if none is installed. Checks (in order): project `node_modules/.bin`,
|
|
110
|
+
* then PATH (via `resolveBin`).
|
|
111
|
+
*
|
|
112
|
+
* @returns {{ languageId, id, command, args } | null}
|
|
113
|
+
*/
|
|
114
|
+
export function resolveServer(languageId, projectRoot) {
|
|
115
|
+
const def = pluginServers.get(languageId) || BUILTIN_SERVERS[languageId];
|
|
116
|
+
if (!def) return null;
|
|
117
|
+
for (const bin of def.bins) {
|
|
118
|
+
const command = resolveBin(bin, projectRoot);
|
|
119
|
+
if (command) {
|
|
120
|
+
return {
|
|
121
|
+
languageId,
|
|
122
|
+
id: def.id,
|
|
123
|
+
command,
|
|
124
|
+
args: def.argsFor(bin),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Find an executable by name: project-local `node_modules/.bin` first (so a
|
|
133
|
+
* repo-pinned server wins), then the system PATH. Returns an absolute path, or
|
|
134
|
+
* null. On Windows the `.cmd`/`.exe` variants are probed too.
|
|
135
|
+
*/
|
|
136
|
+
export function resolveBin(bin, projectRoot) {
|
|
137
|
+
const isWin = process.platform === "win32";
|
|
138
|
+
const candidates = isWin ? [bin + ".cmd", bin + ".exe", bin] : [bin];
|
|
139
|
+
|
|
140
|
+
// 1) project node_modules/.bin (walk up a couple levels for monorepos)
|
|
141
|
+
if (projectRoot) {
|
|
142
|
+
let dir = projectRoot;
|
|
143
|
+
for (let i = 0; i < 4; i++) {
|
|
144
|
+
const binDir = path.join(dir, "node_modules", ".bin");
|
|
145
|
+
for (const c of candidates) {
|
|
146
|
+
const full = path.join(binDir, c);
|
|
147
|
+
if (_deps.existsSync(full)) return full;
|
|
148
|
+
}
|
|
149
|
+
const parent = path.dirname(dir);
|
|
150
|
+
if (parent === dir) break;
|
|
151
|
+
dir = parent;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 2) PATH
|
|
156
|
+
const pathEnv = process.env.PATH || process.env.Path || "";
|
|
157
|
+
const sep = isWin ? ";" : ":";
|
|
158
|
+
for (const entry of pathEnv.split(sep)) {
|
|
159
|
+
if (!entry) continue;
|
|
160
|
+
for (const c of candidates) {
|
|
161
|
+
const full = path.join(entry, c);
|
|
162
|
+
if (_deps.existsSync(full)) return full;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Report which builtin servers are available on this machine — used by
|
|
170
|
+
* `cc doctor` and `cc code-intel status` so the user sees why LSP is or isn't
|
|
171
|
+
* active without guessing.
|
|
172
|
+
* @returns {Array<{ languageId, id, available, command }>}
|
|
173
|
+
*/
|
|
174
|
+
export function probeServers(projectRoot) {
|
|
175
|
+
const seen = new Set();
|
|
176
|
+
const out = [];
|
|
177
|
+
const all = { ...BUILTIN_SERVERS };
|
|
178
|
+
for (const [lid, def] of pluginServers) all[lid] = def;
|
|
179
|
+
for (const [languageId, def] of Object.entries(all)) {
|
|
180
|
+
if (seen.has(def.id)) continue; // one row per server family
|
|
181
|
+
seen.add(def.id);
|
|
182
|
+
const resolved = resolveServer(languageId, projectRoot);
|
|
183
|
+
out.push({
|
|
184
|
+
languageId,
|
|
185
|
+
id: def.id,
|
|
186
|
+
available: Boolean(resolved),
|
|
187
|
+
command: resolved ? resolved.command : null,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** All extensions the registry can currently map (for docs/diagnostics). */
|
|
194
|
+
export function supportedExtensions() {
|
|
195
|
+
return Object.keys(EXTENSION_LANGUAGE).sort();
|
|
196
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PluginMonitorSupervisor — runs installed plugins' background monitors
|
|
3
|
+
* (Phase 3.3i / Phase 6 down payment). Two modes:
|
|
4
|
+
*
|
|
5
|
+
* - longRunning: spawn the command ONCE and keep it alive (e.g. `tail -f`).
|
|
6
|
+
* - interval: re-run the command every `intervalMs`, capturing each run's
|
|
7
|
+
* output (e.g. a periodic health/log check).
|
|
8
|
+
*
|
|
9
|
+
* Captured stdout/stderr lines are buffered as structured records; the agent
|
|
10
|
+
* loop can `drainOutputs()` them and inject them into the next turn's context
|
|
11
|
+
* (Phase 6: "后台结果可在下一轮注入 additionalContext"). The supervisor owns
|
|
12
|
+
* its children and reaps ALL of them on `stopAll()` plus a `process.once('exit')`
|
|
13
|
+
* backstop, so no monitor process leaks past session end.
|
|
14
|
+
*
|
|
15
|
+
* Guardrails (Phase 6 "去重、并发上限、超时、进程回收"): a per-monitor id
|
|
16
|
+
* dedupes double-starts, `maxConcurrent` caps simultaneous interval runs, and
|
|
17
|
+
* `timeoutMs` kills a hung interval run. Commands run with shell:false (argv,
|
|
18
|
+
* never a shell string) — no injection.
|
|
19
|
+
*
|
|
20
|
+
* spawn is injected via `_deps` so the whole thing is unit-testable without
|
|
21
|
+
* real processes.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { spawn } from "node:child_process";
|
|
25
|
+
|
|
26
|
+
export const _deps = { spawn };
|
|
27
|
+
|
|
28
|
+
const DEFAULT_INTERVAL_MS = 60_000;
|
|
29
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
30
|
+
const DEFAULT_MAX_CONCURRENT = 8;
|
|
31
|
+
const OUTPUT_RING = 200; // max buffered output records before dropping oldest
|
|
32
|
+
|
|
33
|
+
export class PluginMonitorSupervisor {
|
|
34
|
+
constructor(opts = {}) {
|
|
35
|
+
this.maxConcurrent = opts.maxConcurrent || DEFAULT_MAX_CONCURRENT;
|
|
36
|
+
this.defaultIntervalMs = opts.defaultIntervalMs || DEFAULT_INTERVAL_MS;
|
|
37
|
+
this.timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
38
|
+
this._spawn = opts.spawn || _deps.spawn;
|
|
39
|
+
// id -> { desc, timer?, child?, running }
|
|
40
|
+
this._monitors = new Map();
|
|
41
|
+
this._outputs = []; // structured records awaiting drain
|
|
42
|
+
this._runningIntervalRuns = 0;
|
|
43
|
+
this._stopped = false;
|
|
44
|
+
this._exitHook = () => this.stopAll();
|
|
45
|
+
this._exitHookInstalled = false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** True when at least one monitor is registered and not stopped. */
|
|
49
|
+
get size() {
|
|
50
|
+
return this._monitors.size;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Start a batch of monitor descriptors (from collectPluginMonitors). Skips a
|
|
55
|
+
* monitor whose id is already running (dedupe). Returns the ids started.
|
|
56
|
+
* @param {Array<object>} monitors
|
|
57
|
+
* @returns {string[]} ids actually started
|
|
58
|
+
*/
|
|
59
|
+
start(monitors = []) {
|
|
60
|
+
if (this._stopped) return [];
|
|
61
|
+
const started = [];
|
|
62
|
+
for (const desc of monitors) {
|
|
63
|
+
if (!desc || !desc.id) continue;
|
|
64
|
+
if (this._monitors.has(desc.id)) continue; // dedupe double-start
|
|
65
|
+
const entry = { desc, timer: null, child: null, running: false };
|
|
66
|
+
this._monitors.set(desc.id, entry);
|
|
67
|
+
if (desc.mode === "longRunning") {
|
|
68
|
+
this._spawnLongRunning(entry);
|
|
69
|
+
} else {
|
|
70
|
+
// interval: run once immediately, then on a cadence.
|
|
71
|
+
const ms = desc.intervalMs || this.defaultIntervalMs;
|
|
72
|
+
this._runIntervalOnce(entry);
|
|
73
|
+
entry.timer = setInterval(() => this._runIntervalOnce(entry), ms);
|
|
74
|
+
if (typeof entry.timer.unref === "function") entry.timer.unref();
|
|
75
|
+
}
|
|
76
|
+
started.push(desc.id);
|
|
77
|
+
}
|
|
78
|
+
if (started.length > 0) this._installExitHook();
|
|
79
|
+
return started;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
_installExitHook() {
|
|
83
|
+
if (this._exitHookInstalled) return;
|
|
84
|
+
this._exitHookInstalled = true;
|
|
85
|
+
// Backstop: if the caller forgets stopAll(), reap on process exit so no
|
|
86
|
+
// monitor process outlives the session.
|
|
87
|
+
process.once("exit", this._exitHook);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_record(desc, stream, text) {
|
|
91
|
+
const lines = String(text)
|
|
92
|
+
.split(/\r?\n/)
|
|
93
|
+
.filter((l) => l.length > 0);
|
|
94
|
+
for (const line of lines) {
|
|
95
|
+
this._outputs.push({
|
|
96
|
+
id: desc.id,
|
|
97
|
+
plugin: desc.plugin,
|
|
98
|
+
monitor: desc.name,
|
|
99
|
+
stream,
|
|
100
|
+
line,
|
|
101
|
+
});
|
|
102
|
+
if (this._outputs.length > OUTPUT_RING) this._outputs.shift();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_spawnLongRunning(entry) {
|
|
107
|
+
const { desc } = entry;
|
|
108
|
+
let child;
|
|
109
|
+
try {
|
|
110
|
+
child = this._spawn(desc.command, desc.args, {
|
|
111
|
+
cwd: desc.cwd,
|
|
112
|
+
env: desc.env ? { ...process.env, ...desc.env } : process.env,
|
|
113
|
+
shell: false,
|
|
114
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
115
|
+
});
|
|
116
|
+
} catch (err) {
|
|
117
|
+
this._record(desc, "error", `spawn failed: ${err.message}`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
entry.child = child;
|
|
121
|
+
child.stdout?.on("data", (d) =>
|
|
122
|
+
this._record(desc, "stdout", d.toString("utf8")),
|
|
123
|
+
);
|
|
124
|
+
child.stderr?.on("data", (d) =>
|
|
125
|
+
this._record(desc, "stderr", d.toString("utf8")),
|
|
126
|
+
);
|
|
127
|
+
child.on("error", (err) => this._record(desc, "error", err.message));
|
|
128
|
+
child.on("exit", (code) => {
|
|
129
|
+
entry.child = null;
|
|
130
|
+
this._record(desc, "exit", `longRunning monitor exited (code ${code})`);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_runIntervalOnce(entry) {
|
|
135
|
+
const { desc } = entry;
|
|
136
|
+
if (this._stopped) return;
|
|
137
|
+
if (entry.running) return; // previous run still in flight — skip this tick
|
|
138
|
+
if (this._runningIntervalRuns >= this.maxConcurrent) return; // cap
|
|
139
|
+
entry.running = true;
|
|
140
|
+
this._runningIntervalRuns++;
|
|
141
|
+
let child;
|
|
142
|
+
try {
|
|
143
|
+
child = this._spawn(desc.command, desc.args, {
|
|
144
|
+
cwd: desc.cwd,
|
|
145
|
+
env: desc.env ? { ...process.env, ...desc.env } : process.env,
|
|
146
|
+
shell: false,
|
|
147
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
148
|
+
});
|
|
149
|
+
} catch (err) {
|
|
150
|
+
this._record(desc, "error", `spawn failed: ${err.message}`);
|
|
151
|
+
entry.running = false;
|
|
152
|
+
this._runningIntervalRuns--;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
entry.child = child;
|
|
156
|
+
let done = false;
|
|
157
|
+
const finish = () => {
|
|
158
|
+
if (done) return;
|
|
159
|
+
done = true;
|
|
160
|
+
if (killTimer) clearTimeout(killTimer);
|
|
161
|
+
entry.child = null;
|
|
162
|
+
entry.running = false;
|
|
163
|
+
this._runningIntervalRuns--;
|
|
164
|
+
};
|
|
165
|
+
const killTimer = setTimeout(() => {
|
|
166
|
+
this._record(
|
|
167
|
+
desc,
|
|
168
|
+
"error",
|
|
169
|
+
`interval run timed out after ${this.timeoutMs}ms`,
|
|
170
|
+
);
|
|
171
|
+
try {
|
|
172
|
+
child.kill("SIGTERM");
|
|
173
|
+
} catch {
|
|
174
|
+
/* already gone */
|
|
175
|
+
}
|
|
176
|
+
}, this.timeoutMs);
|
|
177
|
+
if (typeof killTimer.unref === "function") killTimer.unref();
|
|
178
|
+
child.stdout?.on("data", (d) =>
|
|
179
|
+
this._record(desc, "stdout", d.toString("utf8")),
|
|
180
|
+
);
|
|
181
|
+
child.stderr?.on("data", (d) =>
|
|
182
|
+
this._record(desc, "stderr", d.toString("utf8")),
|
|
183
|
+
);
|
|
184
|
+
child.on("error", (err) => {
|
|
185
|
+
this._record(desc, "error", err.message);
|
|
186
|
+
finish();
|
|
187
|
+
});
|
|
188
|
+
child.on("exit", () => finish());
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Take and clear all buffered monitor output records (for injection into the
|
|
193
|
+
* next agent turn). Returns [] when nothing has been captured.
|
|
194
|
+
*/
|
|
195
|
+
drainOutputs() {
|
|
196
|
+
const out = this._outputs;
|
|
197
|
+
this._outputs = [];
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Peek at buffered output without clearing it. */
|
|
202
|
+
peekOutputs() {
|
|
203
|
+
return this._outputs.slice();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Ids of every registered monitor. */
|
|
207
|
+
ids() {
|
|
208
|
+
return [...this._monitors.keys()];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Stop and reap EVERYTHING: clear interval timers, kill all children. Safe to
|
|
213
|
+
* call more than once. After this, `start` is a no-op.
|
|
214
|
+
*/
|
|
215
|
+
stopAll() {
|
|
216
|
+
this._stopped = true;
|
|
217
|
+
for (const entry of this._monitors.values()) {
|
|
218
|
+
if (entry.timer) {
|
|
219
|
+
clearInterval(entry.timer);
|
|
220
|
+
entry.timer = null;
|
|
221
|
+
}
|
|
222
|
+
if (entry.child) {
|
|
223
|
+
try {
|
|
224
|
+
entry.child.kill("SIGTERM");
|
|
225
|
+
} catch {
|
|
226
|
+
/* already gone */
|
|
227
|
+
}
|
|
228
|
+
entry.child = null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
this._monitors.clear();
|
|
232
|
+
this._runningIntervalRuns = 0;
|
|
233
|
+
if (this._exitHookInstalled) {
|
|
234
|
+
process.removeListener("exit", this._exitHook);
|
|
235
|
+
this._exitHookInstalled = false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discover installed plugins' agent-definition directories so `discoverAgents`
|
|
3
|
+
* can expose them as named subagents (Phase 3.3h — plugin Agent component
|
|
4
|
+
* reaches the agent chain).
|
|
5
|
+
*
|
|
6
|
+
* A plugin ships subagents under `<pluginRoot>/agents/<name>.md` (the same
|
|
7
|
+
* markdown-with-frontmatter shape as `.claude/agents/*.md`), so each installed
|
|
8
|
+
* plugin's `agents/` directory becomes an extra agent-scan layer. Only plugins
|
|
9
|
+
* whose manifest fully validated (`manifest.ok`) contribute.
|
|
10
|
+
*
|
|
11
|
+
* NOT trust-gated: an agent definition is declarative (a system prompt + an
|
|
12
|
+
* optional tool ALLOW-list + model — it can only narrow, never expand,
|
|
13
|
+
* capability) and runs only when the user invokes it BY NAME (`cc agents run …`),
|
|
14
|
+
* exactly like a skill. It is never auto-executed at startup (unlike a hook), so
|
|
15
|
+
* it carries the same trust as a skill's SKILL.md, which is also not gated. The
|
|
16
|
+
* code-bearing components (hooks/LSP/MCP) remain trust-gated.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from "fs";
|
|
20
|
+
import path from "path";
|
|
21
|
+
import { discoverPlugins } from "./scopes.js";
|
|
22
|
+
|
|
23
|
+
export const _deps = { existsSync: fs.existsSync };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} [opts] { cwd, scopes }
|
|
27
|
+
* @returns {Array<{ name, scope, version, dir }>} one entry per plugin that has
|
|
28
|
+
* an `agents/` directory; `dir` is that directory (a scan root for
|
|
29
|
+
* discoverAgents' walkMd).
|
|
30
|
+
*/
|
|
31
|
+
export function discoverPluginAgentLayers(opts = {}) {
|
|
32
|
+
let plugins = [];
|
|
33
|
+
try {
|
|
34
|
+
plugins = discoverPlugins({ cwd: opts.cwd, scopes: opts.scopes });
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const p of plugins) {
|
|
40
|
+
if (!p.manifest || p.manifest.ok !== true) continue;
|
|
41
|
+
const agentsDir = path.join(p.root, "agents");
|
|
42
|
+
if (!_deps.existsSync(agentsDir)) continue;
|
|
43
|
+
out.push({
|
|
44
|
+
name: p.name,
|
|
45
|
+
scope: p.scope,
|
|
46
|
+
version: p.version,
|
|
47
|
+
dir: agentsDir,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin `bin` component — put a trusted plugin's executables on PATH for the
|
|
3
|
+
* session (Phase 3.3n). A plugin ships helper executables under `bin/`; making
|
|
4
|
+
* them resolvable means the agent's `run_shell` (and monitors/hooks, which
|
|
5
|
+
* inherit the process env) can invoke `some-plugin-tool` by name.
|
|
6
|
+
*
|
|
7
|
+
* Wiring: prepend the trusted plugins' bin dirs to `process.env.PATH` at session
|
|
8
|
+
* start and RESTORE the original PATH at session end — session-scoped, so no
|
|
9
|
+
* plugin executable stays resolvable after the session. Every child process
|
|
10
|
+
* inherits process.env, so this one mutation covers run_shell (foreground +
|
|
11
|
+
* background), the monitor supervisor, and hook commands uniformly.
|
|
12
|
+
*
|
|
13
|
+
* SECURITY: an executable on PATH runs code, so — like hooks/LSP/MCP/monitors —
|
|
14
|
+
* only TRUSTED plugins contribute (user/local auto-trusted; project needs
|
|
15
|
+
* `cc plugin trust`). An untrusted cloned-repo plugin can NOT shadow a system
|
|
16
|
+
* command or get its binary invoked.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import path from "path";
|
|
20
|
+
import { discoverPlugins } from "./scopes.js";
|
|
21
|
+
import { partitionByTrust, warnUntrustedOnce } from "./trust.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Collect trusted, installed plugins' `bin/` directories.
|
|
25
|
+
* @param {object} [opts] { cwd, scopes }
|
|
26
|
+
* @returns {Array<{plugin, scope, version, dir}>}
|
|
27
|
+
*/
|
|
28
|
+
export function collectPluginBinDirs(opts = {}) {
|
|
29
|
+
let plugins = [];
|
|
30
|
+
try {
|
|
31
|
+
plugins = discoverPlugins({ cwd: opts.cwd, scopes: opts.scopes });
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
const { trusted, skipped } = partitionByTrust(plugins);
|
|
36
|
+
warnUntrustedOnce(
|
|
37
|
+
skipped
|
|
38
|
+
.filter((p) => p.manifest?.components?.bin?.length)
|
|
39
|
+
.map((p) => p.name),
|
|
40
|
+
"bin",
|
|
41
|
+
);
|
|
42
|
+
const out = [];
|
|
43
|
+
for (const p of trusted) {
|
|
44
|
+
if (!p.manifest || p.manifest.ok !== true) continue;
|
|
45
|
+
const bins = p.manifest.components?.bin;
|
|
46
|
+
if (!Array.isArray(bins) || bins.length === 0) continue;
|
|
47
|
+
// Every bin entry's absPath is a file inside the plugin's bin dir; collect
|
|
48
|
+
// the unique parent directories (usually just `<root>/bin`).
|
|
49
|
+
const dirs = new Set(bins.map((b) => path.dirname(b.absPath)));
|
|
50
|
+
for (const dir of dirs) {
|
|
51
|
+
out.push({
|
|
52
|
+
plugin: p.name,
|
|
53
|
+
scope: p.scope,
|
|
54
|
+
version: p.version,
|
|
55
|
+
dir,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Prepend trusted plugins' bin dirs to `env.PATH` (defaults to process.env).
|
|
64
|
+
* Returns a `restore()` that puts PATH back, plus the dirs added. Idempotent per
|
|
65
|
+
* call — a dir already on PATH is not added twice.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} [opts] { cwd, scopes, env }
|
|
68
|
+
* @returns {{ added: string[], restore: () => void }}
|
|
69
|
+
*/
|
|
70
|
+
export function applyPluginBinPath(opts = {}) {
|
|
71
|
+
const env = opts.env || process.env;
|
|
72
|
+
const dirs = collectPluginBinDirs({ cwd: opts.cwd, scopes: opts.scopes }).map(
|
|
73
|
+
(b) => b.dir,
|
|
74
|
+
);
|
|
75
|
+
const prevPath = env.PATH;
|
|
76
|
+
if (dirs.length === 0) {
|
|
77
|
+
return { added: [], restore: () => {} };
|
|
78
|
+
}
|
|
79
|
+
const existing = new Set(
|
|
80
|
+
String(prevPath || "")
|
|
81
|
+
.split(path.delimiter)
|
|
82
|
+
.filter(Boolean),
|
|
83
|
+
);
|
|
84
|
+
const added = dirs.filter((d) => !existing.has(d));
|
|
85
|
+
if (added.length === 0) {
|
|
86
|
+
return { added: [], restore: () => {} };
|
|
87
|
+
}
|
|
88
|
+
env.PATH = [...added, prevPath].filter(Boolean).join(path.delimiter);
|
|
89
|
+
let restored = false;
|
|
90
|
+
return {
|
|
91
|
+
added,
|
|
92
|
+
restore: () => {
|
|
93
|
+
if (restored) return;
|
|
94
|
+
restored = true;
|
|
95
|
+
// Restore only if nothing else re-touched PATH out from under us; either
|
|
96
|
+
// way, put the recorded previous value back (session-scoped semantics).
|
|
97
|
+
env.PATH = prevPath;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge installed plugins' `hooks/hooks.json` into the effective settings-hook
|
|
3
|
+
* map (Phase 3.3c — plugin Hook component reaches the agent lifecycle).
|
|
4
|
+
*
|
|
5
|
+
* A plugin ships lifecycle hooks in the same shape as `.claude/settings.json`
|
|
6
|
+
* `hooks`: a map of event name → array of `{ matcher?, hooks: [{type,command}] }`
|
|
7
|
+
* entries, optionally wrapped under a top-level `hooks` key. Those entries are
|
|
8
|
+
* concatenated onto whatever the user's settings already declared, so a plugin
|
|
9
|
+
* ADDS hooks without being able to silently replace the user's.
|
|
10
|
+
*
|
|
11
|
+
* Only plugins whose manifest fully validated (`manifest.ok`) contribute. Note:
|
|
12
|
+
* a hook can run a shell command, so a plugin's hooks carry the same trust as
|
|
13
|
+
* the user installing it (same as an npm package's lifecycle scripts) and run
|
|
14
|
+
* through the existing hook-runner (which applies the project-hook trust model).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from "fs";
|
|
18
|
+
import { discoverPlugins } from "./scopes.js";
|
|
19
|
+
import { partitionByTrust, warnUntrustedOnce } from "./trust.js";
|
|
20
|
+
|
|
21
|
+
export const _deps = { readFileSync: fs.readFileSync };
|
|
22
|
+
|
|
23
|
+
/** Accept either `{ hooks: {Event:[...]} }` (plugin wrap) or `{Event:[...]}`. */
|
|
24
|
+
function normalizeHookMap(parsed) {
|
|
25
|
+
if (
|
|
26
|
+
parsed &&
|
|
27
|
+
typeof parsed === "object" &&
|
|
28
|
+
!Array.isArray(parsed) &&
|
|
29
|
+
parsed.hooks &&
|
|
30
|
+
typeof parsed.hooks === "object" &&
|
|
31
|
+
!Array.isArray(parsed.hooks)
|
|
32
|
+
) {
|
|
33
|
+
return parsed.hooks;
|
|
34
|
+
}
|
|
35
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
36
|
+
? parsed
|
|
37
|
+
: {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Collect + merge every installed plugin's hook entries into one event map.
|
|
42
|
+
* @param {object} [opts] { cwd, scopes }
|
|
43
|
+
* @returns {Record<string, Array>} event name → concatenated hook entries
|
|
44
|
+
*/
|
|
45
|
+
export function collectPluginHooks(opts = {}) {
|
|
46
|
+
let plugins = [];
|
|
47
|
+
try {
|
|
48
|
+
plugins = discoverPlugins({ cwd: opts.cwd, scopes: opts.scopes });
|
|
49
|
+
} catch {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
// A hook runs a shell command — gate it behind trust so a cloned repo's
|
|
53
|
+
// project plugin can't run commands the moment the agent starts.
|
|
54
|
+
const { trusted, skipped } = partitionByTrust(plugins);
|
|
55
|
+
warnUntrustedOnce(
|
|
56
|
+
skipped.filter((p) => p.manifest?.components?.hooks).map((p) => p.name),
|
|
57
|
+
"hooks",
|
|
58
|
+
);
|
|
59
|
+
const merged = {};
|
|
60
|
+
for (const p of trusted) {
|
|
61
|
+
if (!p.manifest || p.manifest.ok !== true) continue;
|
|
62
|
+
const h = p.manifest.components?.hooks;
|
|
63
|
+
if (!h || !h.absPath) continue;
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(_deps.readFileSync(h.absPath, "utf8"));
|
|
67
|
+
} catch {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const map = normalizeHookMap(parsed);
|
|
71
|
+
for (const [event, entries] of Object.entries(map)) {
|
|
72
|
+
if (!Array.isArray(entries)) continue;
|
|
73
|
+
merged[event] = (merged[event] || []).concat(entries);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return merged;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Return a settings-hook map that ADDS the installed plugins' hooks to the
|
|
81
|
+
* user's own. Returns the input unchanged when no plugin contributes hooks.
|
|
82
|
+
*
|
|
83
|
+
* @param {object|null} settingsHooks the user's loaded settings hooks
|
|
84
|
+
* @param {object} [opts] { cwd, scopes }
|
|
85
|
+
*/
|
|
86
|
+
export function mergePluginHooks(settingsHooks, opts = {}) {
|
|
87
|
+
const plugin = collectPluginHooks(opts);
|
|
88
|
+
const events = Object.keys(plugin);
|
|
89
|
+
if (events.length === 0) return settingsHooks;
|
|
90
|
+
const out =
|
|
91
|
+
settingsHooks && typeof settingsHooks === "object"
|
|
92
|
+
? { ...settingsHooks }
|
|
93
|
+
: {};
|
|
94
|
+
for (const event of events) {
|
|
95
|
+
out[event] = (Array.isArray(out[event]) ? out[event] : []).concat(
|
|
96
|
+
plugin[event],
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|