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,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSPClient — one connection to a single language server over stdio.
|
|
3
|
+
*
|
|
4
|
+
* Speaks JSON-RPC 2.0 framed by `jsonrpc-stream.js`. Responsibilities:
|
|
5
|
+
* - spawn the server process (stdio pipes, UTF-8)
|
|
6
|
+
* - the `initialize` / `initialized` handshake
|
|
7
|
+
* - correlate `request` id → Promise, dispatch `notification`s to listeners
|
|
8
|
+
* - request timeouts, graceful `shutdown`/`exit`, and process-level
|
|
9
|
+
* `error`/`exit` propagation
|
|
10
|
+
*
|
|
11
|
+
* Testability: all process spawning goes through `_deps.spawn` so tests inject a
|
|
12
|
+
* fake stdio pair (see cli-dev.md `_deps` pattern — `vi.mock("child_process")`
|
|
13
|
+
* does not intercept this file). Nothing here is language-specific.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
17
|
+
import { EventEmitter } from "events";
|
|
18
|
+
import { encodeMessage, MessageBuffer } from "./jsonrpc-stream.js";
|
|
19
|
+
|
|
20
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
21
|
+
const DEFAULT_INIT_TIMEOUT_MS = 30_000;
|
|
22
|
+
|
|
23
|
+
// Injectable so unit tests can supply a fake child process.
|
|
24
|
+
export const _deps = { spawn: nodeSpawn };
|
|
25
|
+
|
|
26
|
+
export class LSPClient extends EventEmitter {
|
|
27
|
+
/**
|
|
28
|
+
* @param {object} opts
|
|
29
|
+
* @param {string} opts.command server executable
|
|
30
|
+
* @param {string[]} [opts.args] server args (e.g. ["--stdio"])
|
|
31
|
+
* @param {string} opts.rootPath absolute project root
|
|
32
|
+
* @param {object} [opts.env] extra env vars
|
|
33
|
+
* @param {number} [opts.requestTimeoutMs]
|
|
34
|
+
* @param {object} [opts.capabilities] client capabilities override
|
|
35
|
+
*/
|
|
36
|
+
constructor(opts = {}) {
|
|
37
|
+
super();
|
|
38
|
+
this.command = opts.command;
|
|
39
|
+
this.args = Array.isArray(opts.args) ? opts.args : [];
|
|
40
|
+
this.rootPath = opts.rootPath;
|
|
41
|
+
this.env = opts.env || {};
|
|
42
|
+
this.requestTimeoutMs = opts.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS;
|
|
43
|
+
this.capabilitiesOverride = opts.capabilities || null;
|
|
44
|
+
|
|
45
|
+
this._child = null;
|
|
46
|
+
this._buffer = new MessageBuffer();
|
|
47
|
+
this._nextId = 1;
|
|
48
|
+
/** @type {Map<number, {resolve,reject,timer}>} */
|
|
49
|
+
this._pending = new Map();
|
|
50
|
+
this._initialized = false;
|
|
51
|
+
this._closed = false;
|
|
52
|
+
/** server-reported capabilities from `initialize` */
|
|
53
|
+
this.serverCapabilities = null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get running() {
|
|
57
|
+
return Boolean(this._child) && !this._closed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Spawn the process and run the initialize handshake. Resolves when ready. */
|
|
61
|
+
async start({ initTimeoutMs = DEFAULT_INIT_TIMEOUT_MS } = {}) {
|
|
62
|
+
if (this._child) throw new Error("LSPClient already started");
|
|
63
|
+
|
|
64
|
+
// shell:false — never route the server command through a shell (prevents
|
|
65
|
+
// command injection and GBK codepage surprises on Windows). Callers pass a
|
|
66
|
+
// resolved executable path; see lsp-server-registry.js. `.cmd`/`.bat` shims
|
|
67
|
+
// can't be spawned directly with shell:false since Node's CVE-2024-27980
|
|
68
|
+
// fix, so buildSpawnCommand rewrites them to an explicit `cmd.exe /d /s /c`
|
|
69
|
+
// invocation with verbatim args — still no shell, still injection-safe.
|
|
70
|
+
const spawnSpec = buildSpawnCommand(this.command, this.args);
|
|
71
|
+
this._child = _deps.spawn(spawnSpec.file, spawnSpec.args, {
|
|
72
|
+
cwd: this.rootPath,
|
|
73
|
+
env: { ...process.env, ...this.env },
|
|
74
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
75
|
+
shell: false,
|
|
76
|
+
windowsHide: true,
|
|
77
|
+
windowsVerbatimArguments: spawnSpec.windowsVerbatimArguments,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
this._child.on("error", (err) => {
|
|
81
|
+
this._failAll(err);
|
|
82
|
+
this.emit("error", err);
|
|
83
|
+
});
|
|
84
|
+
this._child.on("exit", (code, signal) => {
|
|
85
|
+
const wasClosed = this._closed;
|
|
86
|
+
this._closed = true;
|
|
87
|
+
this._failAll(
|
|
88
|
+
new Error(`language server exited (code=${code}, signal=${signal})`),
|
|
89
|
+
);
|
|
90
|
+
if (!wasClosed) this.emit("exit", { code, signal });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// UTF-8 decode explicitly (encoding.md) — never rely on system default.
|
|
94
|
+
this._child.stdout.on("data", (chunk) => {
|
|
95
|
+
this._buffer.append(chunk);
|
|
96
|
+
for (const msg of this._buffer.readAll()) this._dispatch(msg);
|
|
97
|
+
});
|
|
98
|
+
// Surface server diagnostics/logs without corrupting the message channel.
|
|
99
|
+
this._child.stderr.on("data", (chunk) => {
|
|
100
|
+
this.emit("stderr", chunk.toString("utf8"));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const initResult = await this.request(
|
|
104
|
+
"initialize",
|
|
105
|
+
this._initializeParams(),
|
|
106
|
+
{ timeoutMs: initTimeoutMs },
|
|
107
|
+
);
|
|
108
|
+
this.serverCapabilities = initResult?.capabilities || {};
|
|
109
|
+
this.notify("initialized", {});
|
|
110
|
+
this._initialized = true;
|
|
111
|
+
return this.serverCapabilities;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_initializeParams() {
|
|
115
|
+
const rootUri = pathToFileUri(this.rootPath);
|
|
116
|
+
return {
|
|
117
|
+
processId: process.pid,
|
|
118
|
+
clientInfo: { name: "chainlesschain-cc", version: "1" },
|
|
119
|
+
rootUri,
|
|
120
|
+
rootPath: this.rootPath,
|
|
121
|
+
workspaceFolders: [{ uri: rootUri, name: "workspace" }],
|
|
122
|
+
capabilities: this.capabilitiesOverride || defaultClientCapabilities(),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Send a JSON-RPC request and await its result. */
|
|
127
|
+
request(method, params, { timeoutMs } = {}) {
|
|
128
|
+
if (this._closed) {
|
|
129
|
+
return Promise.reject(new Error("LSPClient is closed"));
|
|
130
|
+
}
|
|
131
|
+
const id = this._nextId++;
|
|
132
|
+
const payload = { jsonrpc: "2.0", id, method, params };
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const timer = setTimeout(() => {
|
|
135
|
+
this._pending.delete(id);
|
|
136
|
+
reject(
|
|
137
|
+
new Error(
|
|
138
|
+
`LSP request "${method}" timed out after ${timeoutMs || this.requestTimeoutMs}ms`,
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
}, timeoutMs || this.requestTimeoutMs);
|
|
142
|
+
// Don't keep the event loop alive purely for a pending LSP request.
|
|
143
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
144
|
+
this._pending.set(id, { resolve, reject, timer });
|
|
145
|
+
this._write(payload);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Send a JSON-RPC notification (no response expected). */
|
|
150
|
+
notify(method, params) {
|
|
151
|
+
if (this._closed) return;
|
|
152
|
+
this._write({ jsonrpc: "2.0", method, params });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
_write(payload) {
|
|
156
|
+
if (!this._child || !this._child.stdin.writable) return;
|
|
157
|
+
try {
|
|
158
|
+
this._child.stdin.write(encodeMessage(payload));
|
|
159
|
+
} catch (err) {
|
|
160
|
+
this.emit("error", err);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_dispatch(msg) {
|
|
165
|
+
if (msg == null) return;
|
|
166
|
+
// Response to one of our requests.
|
|
167
|
+
if (Object.prototype.hasOwnProperty.call(msg, "id") && msg.method == null) {
|
|
168
|
+
const entry = this._pending.get(msg.id);
|
|
169
|
+
if (!entry) return; // late/duplicate response — ignore
|
|
170
|
+
this._pending.delete(msg.id);
|
|
171
|
+
clearTimeout(entry.timer);
|
|
172
|
+
if (msg.error) {
|
|
173
|
+
const e = new Error(msg.error.message || "LSP error");
|
|
174
|
+
e.code = msg.error.code;
|
|
175
|
+
e.data = msg.error.data;
|
|
176
|
+
entry.reject(e);
|
|
177
|
+
} else {
|
|
178
|
+
entry.resolve(msg.result);
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
// Server → client request (e.g. workspace/configuration). We don't act on
|
|
183
|
+
// these yet, but must reply so the server doesn't block; reply null.
|
|
184
|
+
if (msg.method != null && Object.prototype.hasOwnProperty.call(msg, "id")) {
|
|
185
|
+
this._write({ jsonrpc: "2.0", id: msg.id, result: null });
|
|
186
|
+
this.emit("serverRequest", msg);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// Notification from the server (diagnostics, logs, progress…).
|
|
190
|
+
if (msg.method != null) {
|
|
191
|
+
this.emit("notification", msg.method, msg.params);
|
|
192
|
+
this.emit(`notify:${msg.method}`, msg.params);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
_failAll(err) {
|
|
197
|
+
for (const [, entry] of this._pending) {
|
|
198
|
+
clearTimeout(entry.timer);
|
|
199
|
+
entry.reject(err);
|
|
200
|
+
}
|
|
201
|
+
this._pending.clear();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Graceful LSP shutdown → exit → kill fallback. Idempotent. */
|
|
205
|
+
async stop({ graceTimeoutMs = 2000 } = {}) {
|
|
206
|
+
if (!this._child || this._closed) {
|
|
207
|
+
this._closed = true;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
if (this._initialized) {
|
|
212
|
+
await this.request("shutdown", null, { timeoutMs: graceTimeoutMs });
|
|
213
|
+
this.notify("exit", null);
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
// Server unresponsive — fall through to hard kill.
|
|
217
|
+
} finally {
|
|
218
|
+
this._closed = true;
|
|
219
|
+
this._failAll(new Error("LSPClient stopped"));
|
|
220
|
+
try {
|
|
221
|
+
this._child.kill();
|
|
222
|
+
} catch {
|
|
223
|
+
/* already dead */
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Compute the actual (file, args) to spawn without a shell. On Windows a
|
|
231
|
+
* `.cmd`/`.bat` batch shim cannot be spawned with shell:false (EINVAL since
|
|
232
|
+
* Node's CVE-2024-27980 fix), so route it through `cmd.exe /d /s /c "<cmd>" …`
|
|
233
|
+
* with windowsVerbatimArguments so cmd.exe — not Node — parses the line. The
|
|
234
|
+
* command path is double-quoted (handles spaces) and our args are constant, so
|
|
235
|
+
* there is no injection surface. Everything else spawns verbatim.
|
|
236
|
+
*/
|
|
237
|
+
export function buildSpawnCommand(command, args = []) {
|
|
238
|
+
if (process.platform === "win32" && /\.(cmd|bat)$/i.test(command)) {
|
|
239
|
+
const comspec = process.env.ComSpec || "cmd.exe";
|
|
240
|
+
// Always quote the command path (a batch path can contain spaces/&); quote
|
|
241
|
+
// args only when they carry shell-significant characters. Verbatim args mean
|
|
242
|
+
// cmd.exe — not Node — parses this line, so our own quoting is authoritative.
|
|
243
|
+
const quotedCmd = `"${String(command).replace(/"/g, '""')}"`;
|
|
244
|
+
const quotedArgs = args.map((a) =>
|
|
245
|
+
/[\s"&|<>^]/.test(a) ? `"${String(a).replace(/"/g, '""')}"` : a,
|
|
246
|
+
);
|
|
247
|
+
return {
|
|
248
|
+
file: comspec,
|
|
249
|
+
args: ["/d", "/s", "/c", [quotedCmd, ...quotedArgs].join(" ")],
|
|
250
|
+
windowsVerbatimArguments: true,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return { file: command, args, windowsVerbatimArguments: false };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** LSP wants a `file://` URI; build one that round-trips on Windows and POSIX. */
|
|
257
|
+
export function pathToFileUri(p) {
|
|
258
|
+
if (!p) return "";
|
|
259
|
+
let normalized = String(p).replace(/\\/g, "/");
|
|
260
|
+
// Windows drive path → file:///C:/...
|
|
261
|
+
if (/^[a-zA-Z]:\//.test(normalized)) {
|
|
262
|
+
normalized = "/" + normalized;
|
|
263
|
+
}
|
|
264
|
+
const encoded = normalized
|
|
265
|
+
.split("/")
|
|
266
|
+
.map((seg) => encodeURIComponent(seg))
|
|
267
|
+
.join("/");
|
|
268
|
+
return "file://" + encoded;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Canonicalize a `file://` URI so keys match regardless of who produced them.
|
|
273
|
+
* Windows language servers echo back a lowercase drive letter
|
|
274
|
+
* (`file:///c%3A/…`) while `path.resolve` yields uppercase (`C%3A`), so a Map
|
|
275
|
+
* keyed on the raw URI misses. Lowercasing the drive letter reconciles both.
|
|
276
|
+
*/
|
|
277
|
+
export function normalizeUri(uri) {
|
|
278
|
+
if (!uri) return uri;
|
|
279
|
+
return String(uri).replace(
|
|
280
|
+
/^(file:\/\/\/)([A-Za-z])(%3[Aa]|:)/,
|
|
281
|
+
(_m, p, drive, colon) => p + drive.toLowerCase() + colon,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Convert a `file://` URI back to a native filesystem path. */
|
|
286
|
+
export function fileUriToPath(uri) {
|
|
287
|
+
if (!uri) return "";
|
|
288
|
+
let p = String(uri).replace(/^file:\/\//, "");
|
|
289
|
+
p = decodeURIComponent(p);
|
|
290
|
+
// /C:/... → C:/... on Windows
|
|
291
|
+
if (/^\/[a-zA-Z]:\//.test(p)) p = p.slice(1);
|
|
292
|
+
return process.platform === "win32" ? p.replace(/\//g, "\\") : p;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function defaultClientCapabilities() {
|
|
296
|
+
return {
|
|
297
|
+
textDocument: {
|
|
298
|
+
synchronization: { dynamicRegistration: false, didSave: true },
|
|
299
|
+
definition: { dynamicRegistration: false, linkSupport: false },
|
|
300
|
+
references: { dynamicRegistration: false },
|
|
301
|
+
hover: {
|
|
302
|
+
dynamicRegistration: false,
|
|
303
|
+
contentFormat: ["markdown", "plaintext"],
|
|
304
|
+
},
|
|
305
|
+
documentSymbol: {
|
|
306
|
+
dynamicRegistration: false,
|
|
307
|
+
hierarchicalDocumentSymbolSupport: true,
|
|
308
|
+
},
|
|
309
|
+
publishDiagnostics: { relatedInformation: true },
|
|
310
|
+
rename: { dynamicRegistration: false, prepareSupport: true },
|
|
311
|
+
},
|
|
312
|
+
workspace: {
|
|
313
|
+
workspaceFolders: true,
|
|
314
|
+
symbol: { dynamicRegistration: false },
|
|
315
|
+
configuration: true,
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSPManager — pools language servers by (projectRoot, languageId), syncs open
|
|
3
|
+
* documents, translates positions, and recovers from server crashes.
|
|
4
|
+
*
|
|
5
|
+
* One `LSPClient` is expensive (spawns a process + indexes the project), so we
|
|
6
|
+
* lazily start and reuse one per (root, language) family. The manager owns:
|
|
7
|
+
* - server lifecycle (start / reuse / bounded crash-loop restart / disposeAll)
|
|
8
|
+
* - textDocument/didOpen + didChange (servers only answer about open docs)
|
|
9
|
+
* - diagnostics collection (servers push them via publishDiagnostics)
|
|
10
|
+
* - 1-based (user) ⇄ 0-based (LSP) position conversion, done HERE only
|
|
11
|
+
*
|
|
12
|
+
* The high-level `code-intelligence.js` calls into this; nothing above the
|
|
13
|
+
* manager should touch raw LSP positions/URIs.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from "fs";
|
|
17
|
+
import path from "path";
|
|
18
|
+
import { LSPClient, pathToFileUri, normalizeUri } from "./lsp-client.js";
|
|
19
|
+
import { languageIdForFile, resolveServer } from "./lsp-server-registry.js";
|
|
20
|
+
|
|
21
|
+
export const _deps = {
|
|
22
|
+
readFileSync: fs.readFileSync,
|
|
23
|
+
existsSync: fs.existsSync,
|
|
24
|
+
createClient: (opts) => new LSPClient(opts),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class LSPManager {
|
|
28
|
+
/**
|
|
29
|
+
* @param {object} [opts]
|
|
30
|
+
* @param {string} [opts.projectRoot] default root; per-file root inferred otherwise
|
|
31
|
+
* @param {number} [opts.maxRestarts] crashes within the window before a server
|
|
32
|
+
* is quarantined (degrade to text search) instead of re-spawned. Default 3.
|
|
33
|
+
* @param {number} [opts.restartWindowMs] sliding window for the crash count.
|
|
34
|
+
* A server that stays healthy for this long has its crash history pruned and
|
|
35
|
+
* can restart fresh. Default 30000.
|
|
36
|
+
* @param {() => number} [opts.now] injected clock (deterministic crash-loop tests)
|
|
37
|
+
*/
|
|
38
|
+
constructor(opts = {}) {
|
|
39
|
+
this.projectRoot = opts.projectRoot || process.cwd();
|
|
40
|
+
/** key `${root}::${serverId}` → { client, languageId, root, restarts } */
|
|
41
|
+
this._servers = new Map();
|
|
42
|
+
/** open document uri → { version, languageId } */
|
|
43
|
+
this._openDocs = new Map();
|
|
44
|
+
/** uri → latest diagnostics array */
|
|
45
|
+
this._diagnostics = new Map();
|
|
46
|
+
/** uri → { count, at } publish bookkeeping (for settle/readiness) */
|
|
47
|
+
this._diagMeta = new Map();
|
|
48
|
+
// Crash-loop guard: a language server that dies is lazily re-spawned on the
|
|
49
|
+
// next request (ensureFor), but a server that crashes on startup would then
|
|
50
|
+
// thrash — re-spawned on EVERY request forever. Bound it: at most
|
|
51
|
+
// `maxRestarts` crashes inside a sliding `restartWindowMs`, after which the
|
|
52
|
+
// server is quarantined (callers degrade to text search) until the window
|
|
53
|
+
// clears. `restarts` was previously a dead field; this makes it real.
|
|
54
|
+
this.maxRestarts = Number.isFinite(opts.maxRestarts) ? opts.maxRestarts : 3;
|
|
55
|
+
this.restartWindowMs = Number.isFinite(opts.restartWindowMs)
|
|
56
|
+
? opts.restartWindowMs
|
|
57
|
+
: 30000;
|
|
58
|
+
this._now = typeof opts.now === "function" ? opts.now : () => Date.now();
|
|
59
|
+
/** key → array of recent crash timestamps (survives entry re-spawn) */
|
|
60
|
+
this._crashLog = new Map();
|
|
61
|
+
this._disposed = false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Recent crash timestamps for `key`, pruned to the sliding window. */
|
|
65
|
+
_recentCrashes(key) {
|
|
66
|
+
const cutoff = this._now() - this.restartWindowMs;
|
|
67
|
+
const pruned = (this._crashLog.get(key) || []).filter((t) => t >= cutoff);
|
|
68
|
+
if (pruned.length) this._crashLog.set(key, pruned);
|
|
69
|
+
else this._crashLog.delete(key);
|
|
70
|
+
return pruned;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Ensure a server is running for `filePath` and the file is open. Returns
|
|
75
|
+
* `{ client, uri, languageId }`, or `{ unavailable: true, reason }` when no
|
|
76
|
+
* server is installed (caller degrades to text search).
|
|
77
|
+
*/
|
|
78
|
+
async ensureFor(
|
|
79
|
+
filePath,
|
|
80
|
+
{ root, waitReady = false, readyTimeoutMs = 8000 } = {},
|
|
81
|
+
) {
|
|
82
|
+
if (this._disposed) throw new Error("LSPManager disposed");
|
|
83
|
+
const abs = path.resolve(filePath);
|
|
84
|
+
const languageId = languageIdForFile(abs);
|
|
85
|
+
if (!languageId) {
|
|
86
|
+
return {
|
|
87
|
+
unavailable: true,
|
|
88
|
+
reason: `no language mapping for ${path.extname(abs) || "file"}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const projectRoot = root || this._inferRoot(abs);
|
|
92
|
+
const resolved = resolveServer(languageId, projectRoot);
|
|
93
|
+
if (!resolved) {
|
|
94
|
+
return {
|
|
95
|
+
unavailable: true,
|
|
96
|
+
reason: `no language server installed for ${languageId} (install one, e.g. typescript-language-server)`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const key = `${projectRoot}::${resolved.id}`;
|
|
101
|
+
let entry = this._servers.get(key);
|
|
102
|
+
if (!entry || !entry.client.running) {
|
|
103
|
+
// A dead/absent server is (re)started — but if it has crash-looped past
|
|
104
|
+
// the cap inside the window, quarantine it so we don't re-spawn on every
|
|
105
|
+
// request. The caller degrades to text search until the window clears.
|
|
106
|
+
const crashes = this._recentCrashes(key);
|
|
107
|
+
if (crashes.length >= this.maxRestarts) {
|
|
108
|
+
const retryInMs = this.restartWindowMs - (this._now() - crashes[0]);
|
|
109
|
+
return {
|
|
110
|
+
unavailable: true,
|
|
111
|
+
reason:
|
|
112
|
+
`${resolved.id} crash-looped (${crashes.length} crashes in ` +
|
|
113
|
+
`${Math.round(this.restartWindowMs / 1000)}s) — quarantined, ` +
|
|
114
|
+
`retrying in ~${Math.max(0, Math.round(retryInMs / 1000))}s`,
|
|
115
|
+
quarantined: true,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
entry = await this._startServer(key, resolved, projectRoot);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const uri = pathToFileUri(abs);
|
|
122
|
+
this._ensureOpen(entry.client, uri, abs, languageId);
|
|
123
|
+
|
|
124
|
+
// One-shot callers (the `cc code-intel` CLI) spawn a cold server per run;
|
|
125
|
+
// the server publishes an empty diagnostics set BEFORE it finishes loading
|
|
126
|
+
// the tsconfig project, so an immediate definition/references/diagnostics
|
|
127
|
+
// query sees a partial project. Waiting for the first publish for THIS doc
|
|
128
|
+
// is a reliable "project loaded far enough" signal. Long-lived callers (the
|
|
129
|
+
// agent session) keep the server warm and pass waitReady:false.
|
|
130
|
+
if (waitReady) {
|
|
131
|
+
await this._waitForFirstPublish(uri, readyTimeoutMs);
|
|
132
|
+
}
|
|
133
|
+
return { client: entry.client, uri, languageId, root: projectRoot };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Resolve once the server has published diagnostics for `uri` at least once, or timeout. */
|
|
137
|
+
_waitForFirstPublish(rawUri, timeoutMs) {
|
|
138
|
+
const uri = normalizeUri(rawUri);
|
|
139
|
+
if ((this._diagMeta.get(uri)?.count || 0) > 0) return Promise.resolve();
|
|
140
|
+
return new Promise((resolve) => {
|
|
141
|
+
const started = Date.now();
|
|
142
|
+
const timer = setInterval(() => {
|
|
143
|
+
if (
|
|
144
|
+
(this._diagMeta.get(uri)?.count || 0) > 0 ||
|
|
145
|
+
Date.now() - started >= timeoutMs
|
|
146
|
+
) {
|
|
147
|
+
clearInterval(timer);
|
|
148
|
+
resolve();
|
|
149
|
+
}
|
|
150
|
+
}, 40);
|
|
151
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async _startServer(key, resolved, projectRoot) {
|
|
156
|
+
const client = _deps.createClient({
|
|
157
|
+
command: resolved.command,
|
|
158
|
+
args: resolved.args,
|
|
159
|
+
rootPath: projectRoot,
|
|
160
|
+
});
|
|
161
|
+
// Collect pushed diagnostics keyed by document, tracking publish count/time
|
|
162
|
+
// so callers can wait for the project to load (first publish) and settle
|
|
163
|
+
// (last publish) rather than racing the initial empty set tsserver emits.
|
|
164
|
+
client.on("notify:textDocument/publishDiagnostics", (params) => {
|
|
165
|
+
if (params && params.uri) {
|
|
166
|
+
const key = normalizeUri(params.uri);
|
|
167
|
+
this._diagnostics.set(key, params.diagnostics || []);
|
|
168
|
+
const prev = this._diagMeta.get(key) || { count: 0 };
|
|
169
|
+
this._diagMeta.set(key, { count: prev.count + 1, at: Date.now() });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
const entry = {
|
|
173
|
+
client,
|
|
174
|
+
serverId: resolved.id,
|
|
175
|
+
root: projectRoot,
|
|
176
|
+
// How many times THIS key has already been (re)started — crash history
|
|
177
|
+
// survives entry replacement via _crashLog, so this reflects it.
|
|
178
|
+
restarts: (this._crashLog.get(key) || []).length,
|
|
179
|
+
};
|
|
180
|
+
// On crash: record the crash timestamp (bounds the re-spawn loop via the
|
|
181
|
+
// crash-loop guard in ensureFor) and drop this project's open docs so they
|
|
182
|
+
// re-sync against the fresh server on the next ensureFor.
|
|
183
|
+
client.on("exit", () => {
|
|
184
|
+
const log = this._crashLog.get(key) || [];
|
|
185
|
+
log.push(this._now());
|
|
186
|
+
this._crashLog.set(key, log);
|
|
187
|
+
for (const [uri, meta] of this._openDocs) {
|
|
188
|
+
if (meta.root === projectRoot) this._openDocs.delete(uri);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
await client.start();
|
|
192
|
+
this._servers.set(key, entry);
|
|
193
|
+
return entry;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
_ensureOpen(client, uri, abs, languageId) {
|
|
197
|
+
const existing = this._openDocs.get(uri);
|
|
198
|
+
let text = "";
|
|
199
|
+
try {
|
|
200
|
+
text = _deps.readFileSync(abs, "utf8");
|
|
201
|
+
} catch {
|
|
202
|
+
text = "";
|
|
203
|
+
}
|
|
204
|
+
if (!existing) {
|
|
205
|
+
client.notify("textDocument/didOpen", {
|
|
206
|
+
textDocument: { uri, languageId, version: 1, text },
|
|
207
|
+
});
|
|
208
|
+
this._openDocs.set(uri, {
|
|
209
|
+
version: 1,
|
|
210
|
+
languageId,
|
|
211
|
+
root: client.rootPath,
|
|
212
|
+
});
|
|
213
|
+
} else {
|
|
214
|
+
// File may have changed on disk since first open — push the latest text so
|
|
215
|
+
// definitions/diagnostics reflect current state (edit-freshness).
|
|
216
|
+
const version = existing.version + 1;
|
|
217
|
+
client.notify("textDocument/didChange", {
|
|
218
|
+
textDocument: { uri, version },
|
|
219
|
+
contentChanges: [{ text }],
|
|
220
|
+
});
|
|
221
|
+
this._openDocs.set(uri, { ...existing, version });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Notify the server that a document changed on disk (after an edit). */
|
|
226
|
+
async didChangeFile(filePath, { root } = {}) {
|
|
227
|
+
const abs = path.resolve(filePath);
|
|
228
|
+
const uri = pathToFileUri(abs);
|
|
229
|
+
if (!this._openDocs.has(uri)) {
|
|
230
|
+
// Not open yet — opening it (via ensureFor) will read fresh text anyway.
|
|
231
|
+
return this.ensureFor(abs, { root });
|
|
232
|
+
}
|
|
233
|
+
const ready = await this.ensureFor(abs, { root });
|
|
234
|
+
return ready;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Latest diagnostics for a file (may be empty). Server pushes asynchronously. */
|
|
238
|
+
getDiagnostics(filePath) {
|
|
239
|
+
const uri = normalizeUri(pathToFileUri(path.resolve(filePath)));
|
|
240
|
+
return this._diagnostics.get(uri) || [];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Wait for diagnostics for `filePath` to publish AND settle, then return the
|
|
245
|
+
* latest set. tsserver publishes an initial (often empty) set the moment a doc
|
|
246
|
+
* opens, then republishes once the project is loaded — resolving on the first
|
|
247
|
+
* publish would return stale/empty results. So we wait for at least one
|
|
248
|
+
* publish, then a `settleMs` quiet window with no further publishes (bounded
|
|
249
|
+
* by `timeoutMs`), and return whatever was published last.
|
|
250
|
+
*/
|
|
251
|
+
async waitForDiagnostics(
|
|
252
|
+
filePath,
|
|
253
|
+
{ timeoutMs = 3000, settleMs = 600 } = {},
|
|
254
|
+
) {
|
|
255
|
+
const uri = normalizeUri(pathToFileUri(path.resolve(filePath)));
|
|
256
|
+
return new Promise((resolve) => {
|
|
257
|
+
const started = Date.now();
|
|
258
|
+
const timer = setInterval(() => {
|
|
259
|
+
const meta = this._diagMeta.get(uri);
|
|
260
|
+
const elapsed = Date.now() - started;
|
|
261
|
+
const settled =
|
|
262
|
+
meta && meta.count > 0 && Date.now() - meta.at >= settleMs;
|
|
263
|
+
if (settled || elapsed >= timeoutMs) {
|
|
264
|
+
clearInterval(timer);
|
|
265
|
+
resolve(this._diagnostics.get(uri) || []);
|
|
266
|
+
}
|
|
267
|
+
}, 50);
|
|
268
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Infer the project root for a file: nearest ancestor with package.json / .git / go.mod / Cargo.toml. */
|
|
273
|
+
_inferRoot(abs) {
|
|
274
|
+
let dir = path.dirname(abs);
|
|
275
|
+
const markers = [
|
|
276
|
+
"package.json",
|
|
277
|
+
".git",
|
|
278
|
+
"go.mod",
|
|
279
|
+
"Cargo.toml",
|
|
280
|
+
"tsconfig.json",
|
|
281
|
+
"pyproject.toml",
|
|
282
|
+
];
|
|
283
|
+
for (let i = 0; i < 12; i++) {
|
|
284
|
+
for (const m of markers) {
|
|
285
|
+
if (_deps.existsSync(path.join(dir, m))) return dir;
|
|
286
|
+
}
|
|
287
|
+
const parent = path.dirname(dir);
|
|
288
|
+
if (parent === dir) break;
|
|
289
|
+
dir = parent;
|
|
290
|
+
}
|
|
291
|
+
return this.projectRoot;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Shut down every server and clear state. Idempotent. */
|
|
295
|
+
async disposeAll() {
|
|
296
|
+
this._disposed = true;
|
|
297
|
+
const clients = [...this._servers.values()].map((e) => e.client);
|
|
298
|
+
this._servers.clear();
|
|
299
|
+
this._openDocs.clear();
|
|
300
|
+
this._diagnostics.clear();
|
|
301
|
+
this._diagMeta.clear();
|
|
302
|
+
await Promise.all(clients.map((c) => c.stop().catch(() => {})));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
get activeServerCount() {
|
|
306
|
+
return this._servers.size;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ---- position helpers (the ONLY place that converts between coordinate spaces) ----
|
|
311
|
+
|
|
312
|
+
/** 1-based user position → 0-based LSP position. */
|
|
313
|
+
export function toLspPosition({ line, col }) {
|
|
314
|
+
return {
|
|
315
|
+
line: Math.max(0, (line || 1) - 1),
|
|
316
|
+
character: Math.max(0, (col || 1) - 1),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** 0-based LSP position → 1-based user position. */
|
|
321
|
+
export function fromLspPosition(pos) {
|
|
322
|
+
return { line: (pos?.line || 0) + 1, col: (pos?.character || 0) + 1 };
|
|
323
|
+
}
|