specrails-desktop 2.30.2 → 2.31.0
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 +28 -24
- package/client/dist/assets/{ActivityFeedPage-9DZOvIcY.js → ActivityFeedPage-CAhFRUO4.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-DqyHbzk7.js → AgentBrowserCapture-Ct4G5lsE.js} +1 -1
- package/client/dist/assets/AgentModeCodePane-DQt9WIvu.js +2 -0
- package/client/dist/assets/{AgentModeJobsPane-CTIHMUKN.js → AgentModeJobsPane-podaJVl9.js} +1 -1
- package/client/dist/assets/AgentsPage-DTZdbW80.js +87 -0
- package/client/dist/assets/AnalyticsPage-BskgGPm0.js +1 -0
- package/client/dist/assets/CodePage-CvtYo1DU.js +2 -0
- package/client/dist/assets/{DesktopAnalyticsPage-CkXUFj3F.js → DesktopAnalyticsPage-Dnf99E8e.js} +1 -1
- package/client/dist/assets/{DocsDialog-PhdCJNVj.js → DocsDialog-DHi36rZ4.js} +1 -1
- package/client/dist/assets/{DocsPage-BY-wDthx.js → DocsPage-DIdVrQz1.js} +1 -1
- package/client/dist/assets/{InteractiveJobComposer-C9I0A4KR.js → InteractiveJobComposer-BHeB2inv.js} +1 -1
- package/client/dist/assets/{JobDetailModal-I_RoQJV4.js → JobDetailModal-CF1SKUHH.js} +1 -1
- package/client/dist/assets/JobDetailPage-De3rY18h.js +1 -0
- package/client/dist/assets/{JobsPage-DcllM4Ip.js → JobsPage-CfAqqJTx.js} +1 -1
- package/client/dist/assets/{LoopBuilderPage-DGm_iuff.js → LoopBuilderPage-DSBMuTb3.js} +2 -2
- package/client/dist/assets/{LoopPreviewModal-BC-TeFdD.js → LoopPreviewModal-B-c37kA0.js} +1 -1
- package/client/dist/assets/LoopsPage-DjnNXdmm.js +1 -0
- package/client/dist/assets/{MinimizedChatsContext-Cs34FPTz.js → MinimizedChatsContext-DIsnQMVz.js} +1 -1
- package/client/dist/assets/PluginsPage-DA5pQ_6u.js +2 -0
- package/client/dist/assets/{ProjectSettingsDialog-nEs-ENrE.js → ProjectSettingsDialog-DA23PSi6.js} +1 -1
- package/client/dist/assets/{TemplatePreviewModal-D4qW-J9B.js → TemplatePreviewModal-Cp7IQ-g2.js} +1 -1
- package/client/dist/assets/TicketDetailModal-BPD2i_0R.js +14 -0
- package/client/dist/assets/{Trans-CPISVji8.js → Trans-cWtExGkC.js} +1 -1
- package/client/dist/assets/addspec-4WmLJmBG.js +1 -0
- package/client/dist/assets/addspec-BRlk8jON.js +1 -0
- package/client/dist/assets/addspec-C7XRejU4.js +1 -0
- package/client/dist/assets/addspec-CHW9xf_X.js +1 -0
- package/client/dist/assets/addspec-DTmRrWrF.js +1 -0
- package/client/dist/assets/addspec-DhmnXER8.js +1 -0
- package/client/dist/assets/addspec-Di41DpYV.js +1 -0
- package/client/dist/assets/addspec-x3mizDxc.js +1 -0
- package/client/dist/assets/agent-BKsuIgIT.js +1 -0
- package/client/dist/assets/agent-Bs9NilfI.js +1 -0
- package/client/dist/assets/agent-C23h684q.js +1 -0
- package/client/dist/assets/agent-CqEjy3Z2.js +1 -0
- package/client/dist/assets/agent-D5DZTks1.js +1 -0
- package/client/dist/assets/agent-DE8hUYPl.js +1 -0
- package/client/dist/assets/agent-DsTuJGSc.js +1 -0
- package/client/dist/assets/agent-DwZM05-x.js +1 -0
- package/client/dist/assets/agents-45GbINfh.js +1 -0
- package/client/dist/assets/agents-BHp0_FQt.js +1 -0
- package/client/dist/assets/agents-Bvyi3y6R.js +1 -0
- package/client/dist/assets/agents-DMKFStHL.js +1 -0
- package/client/dist/assets/agents-DaYcxAr1.js +1 -0
- package/client/dist/assets/agents-IUOgqT5t.js +1 -0
- package/client/dist/assets/agents-ms7TwG16.js +1 -0
- package/client/dist/assets/agents-rzPnqoBR.js +1 -0
- package/client/dist/assets/agentstudio-8TQqBenW.js +1 -0
- package/client/dist/assets/agentstudio-B8Z_mmEX.js +1 -0
- package/client/dist/assets/agentstudio-BZR9YmLs.js +1 -0
- package/client/dist/assets/agentstudio-Bge9pAWb.js +1 -0
- package/client/dist/assets/agentstudio-Bz169Vzt.js +1 -0
- package/client/dist/assets/agentstudio-D6L2jpNi.js +1 -0
- package/client/dist/assets/agentstudio-K2-Z7CL0.js +1 -0
- package/client/dist/assets/agentstudio-Zmgzthv8.js +1 -0
- package/client/dist/assets/analytics-B8Qtptbl.js +1 -0
- package/client/dist/assets/analytics-BHqUazhP.js +1 -0
- package/client/dist/assets/analytics-BPptRY4f.js +1 -0
- package/client/dist/assets/analytics-BxDGOIwf.js +1 -0
- package/client/dist/assets/analytics-C9RaQcE3.js +1 -0
- package/client/dist/assets/analytics-CJIwqrEl.js +1 -0
- package/client/dist/assets/analytics-D_OZdQ9O.js +1 -0
- package/client/dist/assets/analytics-We6lUMI8.js +1 -0
- package/client/dist/assets/builder-3bygQtZi.js +1 -0
- package/client/dist/assets/builder-BiUG9Z5K.js +1 -0
- package/client/dist/assets/builder-CGRz_-zP.js +1 -0
- package/client/dist/assets/builder-CWYPw7Cw.js +1 -0
- package/client/dist/assets/builder-CX1437hF.js +1 -0
- package/client/dist/assets/{builder-CwA4T7gP.js → builder-Cowk9jtL.js} +1 -1
- package/client/dist/assets/builder-DwDexYRE.js +1 -0
- package/client/dist/assets/builder-skfnZ1uC.js +1 -0
- package/client/dist/assets/code-B0O8-p8k.js +1 -0
- package/client/dist/assets/code-B1VLZDhS.js +1 -0
- package/client/dist/assets/code-B86IMAfz.js +1 -0
- package/client/dist/assets/code-Bpj4Ddml.js +1 -0
- package/client/dist/assets/code-BykBZ5-e.js +1 -0
- package/client/dist/assets/code-CHPkOkDS.js +1 -0
- package/client/dist/assets/code-CkUuW4m9.js +1 -0
- package/client/dist/assets/code-cAqaqMTx.js +1 -0
- package/client/dist/assets/{dashboard-Dy2Um-US.js → dashboard-BVq6kRJe.js} +1 -1
- package/client/dist/assets/{dashboard-CHCEkds4.js → dashboard-BaKPPmLH.js} +1 -1
- package/client/dist/assets/{dashboard-CrlTV1ms.js → dashboard-Bm8e4zYG.js} +1 -1
- package/client/dist/assets/{dashboard-WhzOQw1S.js → dashboard-Bt6IesnM.js} +1 -1
- package/client/dist/assets/{dashboard-Cslu_GCH.js → dashboard-C_NQZle_.js} +1 -1
- package/client/dist/assets/{dashboard-DweT3Y4R.js → dashboard-CyZKoGoI.js} +1 -1
- package/client/dist/assets/{dashboard-DUU8-_Rg.js → dashboard-HIUdUBQ0.js} +1 -1
- package/client/dist/assets/{dashboard-Cqmmbh3l.js → dashboard-pm4Mr229.js} +1 -1
- package/client/dist/assets/{dist-js-jwoXo0YD.js → dist-js-BaP-xLOy.js} +1 -1
- package/client/dist/assets/{dist-js-Dj72W6qo.js → dist-js-CLlgOa5a.js} +1 -1
- package/client/dist/assets/format-command-C0D9IPmV.js +1 -0
- package/client/dist/assets/{formatDistanceToNow-ByKli7Q_.js → formatDistanceToNow-DUyzS4rr.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-qrcBaLW1.js → getTimezoneOffsetInMilliseconds-BCIwmmTj.js} +1 -1
- package/client/dist/assets/{i18n-Bf9Ckd10.js → i18n-D21Pr8Sj.js} +2 -2
- package/client/dist/assets/index-BdSDUFnA.css +2 -0
- package/client/dist/assets/index-Ci4-JKDr.js +77 -0
- package/client/dist/assets/{jobs-CVFD8rar.js → jobs-BAls3bWq.js} +1 -1
- package/client/dist/assets/{jobs-DoSaFzAa.js → jobs-B_TeSQxb.js} +1 -1
- package/client/dist/assets/jobs-Bgq-PZkC.js +1 -0
- package/client/dist/assets/jobs-Bpks6doU.js +1 -0
- package/client/dist/assets/jobs-CN_zv4Gr.js +1 -0
- package/client/dist/assets/jobs-DUlWSYq2.js +1 -0
- package/client/dist/assets/{jobs-B-sDHEay.js → jobs-DhxKgWFq.js} +1 -1
- package/client/dist/assets/{jobs-lk1_Dq1U.js → jobs-SwYS2M-B.js} +1 -1
- package/client/dist/assets/nav-BHDiSIVu.js +1 -0
- package/client/dist/assets/nav-CMiYAq4i.js +1 -0
- package/client/dist/assets/nav-CqS7NrDW.js +1 -0
- package/client/dist/assets/nav-D-0M7xfv.js +1 -0
- package/client/dist/assets/nav-D65O6u9I.js +1 -0
- package/client/dist/assets/nav-D_LKYvBr.js +1 -0
- package/client/dist/assets/nav-FDTJmU79.js +1 -0
- package/client/dist/assets/nav-VaB-C4k5.js +1 -0
- package/client/dist/assets/provider-capabilities-BvoExILy.js +1 -0
- package/client/dist/assets/settings-BveOJoaI.js +1 -0
- package/client/dist/assets/settings-Cj1C6F5k.js +1 -0
- package/client/dist/assets/settings-CuVwOpe0.js +1 -0
- package/client/dist/assets/settings-D5X4DfRc.js +1 -0
- package/client/dist/assets/settings-DXwxe-BW.js +1 -0
- package/client/dist/assets/settings-Di81Ba_V.js +1 -0
- package/client/dist/assets/settings-jbLjCYC_.js +1 -0
- package/client/dist/assets/settings-wIZNPFnn.js +1 -0
- package/client/dist/assets/setup-BBiHEO7h.js +1 -0
- package/client/dist/assets/setup-BlTWhNIQ.js +1 -0
- package/client/dist/assets/setup-Bp2L_oiP.js +1 -0
- package/client/dist/assets/setup-CInYXp1-.js +1 -0
- package/client/dist/assets/{setup-CITtPjed.js → setup-CYH3bP6K.js} +1 -1
- package/client/dist/assets/setup-DwcQ921P.js +1 -0
- package/client/dist/assets/setup-PBLjuDSs.js +1 -0
- package/client/dist/assets/setup-bx6FtoBp.js +1 -0
- package/client/dist/assets/{spending-DMnCXy84.js → spending-BCyUxoOI.js} +1 -1
- package/client/dist/assets/{tickets-B8BuAaj5.js → tickets-BL9r1G0e.js} +1 -1
- package/client/dist/assets/{tickets-DpmdE8tv.js → tickets-B_NyHJ8f.js} +1 -1
- package/client/dist/assets/{tickets-B_yh4qQk.js → tickets-BarF1wuG.js} +1 -1
- package/client/dist/assets/{tickets-pAUJkDTu.js → tickets-CclVhpQA.js} +1 -1
- package/client/dist/assets/{tickets-D_yDquEw.js → tickets-CqqOHbli.js} +1 -1
- package/client/dist/assets/{tickets-CFk2n2Or.js → tickets-DCkFSiqt.js} +1 -1
- package/client/dist/assets/{tickets-DQp7JS2R.js → tickets-Djn3ccfT.js} +1 -1
- package/client/dist/assets/{tickets-Bs37scwY.js → tickets-MiHBPsy_.js} +1 -1
- package/client/dist/assets/{useBrowserCaptureSession-CkBJShUw.js → useBrowserCaptureSession-CkVCDzer.js} +1 -1
- package/client/dist/assets/{useDesktop-B4pSBIDd.js → useDesktop-DCP_MHp9.js} +1 -1
- package/client/dist/index.html +25 -24
- package/docs/README.md +7 -3
- package/docs/cli.md +3 -3
- package/docs/codex.md +51 -64
- package/docs/creating-specs.md +36 -4
- package/docs/customizing.md +5 -5
- package/docs/gemini.md +35 -47
- package/docs/getting-started.md +15 -9
- package/docs/guide/de/agents/2-profiles-and-the-balanced-default.md +5 -1
- package/docs/guide/de/agents/4-custom-agents-catalog.md +7 -4
- package/docs/guide/de/getting-started/1-what-is-specrails.md +1 -0
- package/docs/guide/de/getting-started/2-installing-and-first-run.md +1 -0
- package/docs/guide/de/getting-started/4-the-dashboard-tour.md +4 -4
- package/docs/guide/de/integrations/1-ai-providers.md +29 -18
- package/docs/guide/de/integrations/2-plugins.md +3 -3
- package/docs/guide/de/integrations/6-agent-chat.md +8 -1
- package/docs/guide/de/pipeline/1-rails-and-jobs.md +9 -6
- package/docs/guide/de/pipeline/2-the-job-detail-view.md +3 -1
- package/docs/guide/de/pipeline/3-batch-implement-and-multi-feature.md +1 -1
- package/docs/guide/de/pipeline/4-picking-an-engine-per-rail.md +13 -13
- package/docs/guide/de/pipeline/5-the-loop-builder.md +6 -2
- package/docs/guide/de/specs/2-add-spec-quick-mode.md +6 -1
- package/docs/guide/en/agents/2-profiles-and-the-balanced-default.md +7 -1
- package/docs/guide/en/agents/4-custom-agents-catalog.md +17 -4
- package/docs/guide/en/getting-started/1-what-is-specrails.md +2 -1
- package/docs/guide/en/getting-started/2-installing-and-first-run.md +2 -1
- package/docs/guide/en/getting-started/4-the-dashboard-tour.md +5 -5
- package/docs/guide/en/insights/3-code-explorer.md +5 -0
- package/docs/guide/en/integrations/1-ai-providers.md +63 -22
- package/docs/guide/en/integrations/2-plugins.md +3 -3
- package/docs/guide/en/integrations/6-agent-chat.md +14 -2
- package/docs/guide/en/pipeline/1-rails-and-jobs.md +19 -7
- package/docs/guide/en/pipeline/2-the-job-detail-view.md +3 -1
- package/docs/guide/en/pipeline/3-batch-implement-and-multi-feature.md +1 -1
- package/docs/guide/en/pipeline/4-picking-an-engine-per-rail.md +25 -11
- package/docs/guide/en/pipeline/5-the-loop-builder.md +7 -2
- package/docs/guide/en/specs/2-add-spec-quick-mode.md +10 -1
- package/docs/guide/es/agents/2-profiles-and-the-balanced-default.md +5 -1
- package/docs/guide/es/agents/4-custom-agents-catalog.md +7 -4
- package/docs/guide/es/getting-started/1-what-is-specrails.md +1 -0
- package/docs/guide/es/getting-started/2-installing-and-first-run.md +1 -0
- package/docs/guide/es/getting-started/4-the-dashboard-tour.md +4 -4
- package/docs/guide/es/integrations/1-ai-providers.md +30 -18
- package/docs/guide/es/integrations/2-plugins.md +3 -3
- package/docs/guide/es/integrations/6-agent-chat.md +8 -1
- package/docs/guide/es/pipeline/1-rails-and-jobs.md +9 -6
- package/docs/guide/es/pipeline/2-the-job-detail-view.md +3 -1
- package/docs/guide/es/pipeline/3-batch-implement-and-multi-feature.md +1 -1
- package/docs/guide/es/pipeline/4-picking-an-engine-per-rail.md +15 -13
- package/docs/guide/es/pipeline/5-the-loop-builder.md +6 -2
- package/docs/guide/es/specs/2-add-spec-quick-mode.md +6 -1
- package/docs/guide/fr/agents/2-profiles-and-the-balanced-default.md +5 -1
- package/docs/guide/fr/agents/4-custom-agents-catalog.md +7 -4
- package/docs/guide/fr/getting-started/1-what-is-specrails.md +1 -0
- package/docs/guide/fr/getting-started/2-installing-and-first-run.md +1 -0
- package/docs/guide/fr/getting-started/4-the-dashboard-tour.md +4 -4
- package/docs/guide/fr/integrations/1-ai-providers.md +29 -18
- package/docs/guide/fr/integrations/2-plugins.md +3 -3
- package/docs/guide/fr/integrations/6-agent-chat.md +8 -1
- package/docs/guide/fr/pipeline/1-rails-and-jobs.md +9 -6
- package/docs/guide/fr/pipeline/2-the-job-detail-view.md +3 -1
- package/docs/guide/fr/pipeline/3-batch-implement-and-multi-feature.md +1 -1
- package/docs/guide/fr/pipeline/4-picking-an-engine-per-rail.md +15 -13
- package/docs/guide/fr/pipeline/5-the-loop-builder.md +6 -2
- package/docs/guide/fr/specs/2-add-spec-quick-mode.md +6 -1
- package/docs/guide/it/agents/2-profiles-and-the-balanced-default.md +5 -1
- package/docs/guide/it/agents/4-custom-agents-catalog.md +7 -4
- package/docs/guide/it/getting-started/1-what-is-specrails.md +1 -0
- package/docs/guide/it/getting-started/2-installing-and-first-run.md +1 -0
- package/docs/guide/it/getting-started/4-the-dashboard-tour.md +4 -4
- package/docs/guide/it/integrations/1-ai-providers.md +28 -18
- package/docs/guide/it/integrations/2-plugins.md +3 -3
- package/docs/guide/it/integrations/6-agent-chat.md +8 -1
- package/docs/guide/it/pipeline/1-rails-and-jobs.md +9 -6
- package/docs/guide/it/pipeline/2-the-job-detail-view.md +3 -1
- package/docs/guide/it/pipeline/3-batch-implement-and-multi-feature.md +1 -1
- package/docs/guide/it/pipeline/4-picking-an-engine-per-rail.md +13 -13
- package/docs/guide/it/pipeline/5-the-loop-builder.md +6 -2
- package/docs/guide/it/specs/2-add-spec-quick-mode.md +6 -1
- package/docs/guide/ja/agents/2-profiles-and-the-balanced-default.md +6 -1
- package/docs/guide/ja/agents/4-custom-agents-catalog.md +7 -4
- package/docs/guide/ja/getting-started/1-what-is-specrails.md +1 -0
- package/docs/guide/ja/getting-started/2-installing-and-first-run.md +1 -0
- package/docs/guide/ja/getting-started/4-the-dashboard-tour.md +4 -4
- package/docs/guide/ja/integrations/1-ai-providers.md +28 -18
- package/docs/guide/ja/integrations/2-plugins.md +3 -3
- package/docs/guide/ja/integrations/6-agent-chat.md +7 -1
- package/docs/guide/ja/pipeline/1-rails-and-jobs.md +9 -6
- package/docs/guide/ja/pipeline/2-the-job-detail-view.md +3 -1
- package/docs/guide/ja/pipeline/3-batch-implement-and-multi-feature.md +1 -1
- package/docs/guide/ja/pipeline/4-picking-an-engine-per-rail.md +14 -13
- package/docs/guide/ja/pipeline/5-the-loop-builder.md +6 -2
- package/docs/guide/ja/specs/2-add-spec-quick-mode.md +6 -1
- package/docs/guide/pt/agents/2-profiles-and-the-balanced-default.md +5 -1
- package/docs/guide/pt/agents/4-custom-agents-catalog.md +7 -4
- package/docs/guide/pt/getting-started/1-what-is-specrails.md +1 -0
- package/docs/guide/pt/getting-started/2-installing-and-first-run.md +1 -0
- package/docs/guide/pt/getting-started/4-the-dashboard-tour.md +4 -4
- package/docs/guide/pt/integrations/1-ai-providers.md +28 -18
- package/docs/guide/pt/integrations/2-plugins.md +3 -3
- package/docs/guide/pt/integrations/6-agent-chat.md +7 -1
- package/docs/guide/pt/pipeline/1-rails-and-jobs.md +9 -6
- package/docs/guide/pt/pipeline/2-the-job-detail-view.md +2 -1
- package/docs/guide/pt/pipeline/3-batch-implement-and-multi-feature.md +1 -1
- package/docs/guide/pt/pipeline/4-picking-an-engine-per-rail.md +14 -13
- package/docs/guide/pt/pipeline/5-the-loop-builder.md +6 -2
- package/docs/guide/pt/specs/2-add-spec-quick-mode.md +6 -1
- package/docs/guide/zh/agents/2-profiles-and-the-balanced-default.md +4 -1
- package/docs/guide/zh/agents/4-custom-agents-catalog.md +7 -4
- package/docs/guide/zh/getting-started/1-what-is-specrails.md +1 -0
- package/docs/guide/zh/getting-started/2-installing-and-first-run.md +1 -0
- package/docs/guide/zh/getting-started/4-the-dashboard-tour.md +4 -4
- package/docs/guide/zh/integrations/1-ai-providers.md +26 -18
- package/docs/guide/zh/integrations/2-plugins.md +3 -3
- package/docs/guide/zh/integrations/6-agent-chat.md +6 -1
- package/docs/guide/zh/pipeline/1-rails-and-jobs.md +9 -6
- package/docs/guide/zh/pipeline/2-the-job-detail-view.md +2 -1
- package/docs/guide/zh/pipeline/3-batch-implement-and-multi-feature.md +1 -1
- package/docs/guide/zh/pipeline/4-picking-an-engine-per-rail.md +12 -12
- package/docs/guide/zh/pipeline/5-the-loop-builder.md +6 -2
- package/docs/guide/zh/specs/2-add-spec-quick-mode.md +6 -1
- package/docs/internals/README.md +4 -1
- package/docs/internals/adding-a-provider.md +32 -28
- package/docs/internals/api-reference.md +37 -13
- package/docs/internals/architecture.md +5 -5
- package/docs/internals/bundled-framework-build-plan.md +3 -3
- package/docs/internals/code-explorer-story.md +4 -0
- package/docs/internals/configuration.md +17 -5
- package/docs/internals/global-artifacts-alignment-contract.md +1 -1
- package/docs/internals/operations-runbook.md +1 -1
- package/docs/internals/profiles.md +37 -45
- package/docs/internals/project-builder.md +16 -0
- package/docs/kimi.md +291 -0
- package/docs/mcp.md +9 -2
- package/docs/platforms/macos.md +17 -7
- package/docs/platforms/windows.md +9 -4
- package/docs/running-pipelines.md +46 -12
- package/docs/terminal.md +1 -1
- package/docs/tracking-cost.md +25 -6
- package/package.json +4 -2
- package/server/dist/agent-chat-manager.js +74 -32
- package/server/dist/agent-chat-router.js +40 -17
- package/server/dist/agent-cwd-manager.js +14 -7
- package/server/dist/agent-generator.js +69 -85
- package/server/dist/agent-mcp-config.js +17 -11
- package/server/dist/agent-operator-prompt.js +29 -20
- package/server/dist/agent-refine-manager.js +58 -38
- package/server/dist/ai-invocations.js +65 -20
- package/server/dist/attachment-manager.js +49 -15
- package/server/dist/blueprint-chat-manager.js +41 -16
- package/server/dist/blueprint-router.js +40 -6
- package/server/dist/chat-manager.js +165 -89
- package/server/dist/code-explorer-router.js +26 -7
- package/server/dist/contract-refine-runner.js +146 -113
- package/server/dist/core-compat.js +57 -2
- package/server/dist/core-package.js +3 -3
- package/server/dist/core-update-manager.js +17 -1
- package/server/dist/db.js +60 -5
- package/server/dist/desktop-analytics.js +44 -6
- package/server/dist/desktop-router.js +127 -11
- package/server/dist/explore-cwd-manager.js +19 -3
- package/server/dist/feature-flags.js +1 -1
- package/server/dist/file-story-manager.js +3 -3
- package/server/dist/file-summary-generator.js +21 -15
- package/server/dist/file-summary-manager.js +6 -6
- package/server/dist/framework-manager.js +19 -2
- package/server/dist/framework-migration.js +27 -12
- package/server/dist/install-config-path.js +1 -1
- package/server/dist/interactive-job-session.js +8 -6
- package/server/dist/loop-command-catalog.js +27 -10
- package/server/dist/loop-executors.js +59 -27
- package/server/dist/loop-factory.js +2 -2
- package/server/dist/loop-run-manager.js +100 -39
- package/server/dist/loops-router.js +1 -1
- package/server/dist/mcp/guide.js +23 -16
- package/server/dist/mcp/tools/agents.js +47 -41
- package/server/dist/mcp/tools/analytics.js +2 -2
- package/server/dist/mcp/tools/loops.js +2 -2
- package/server/dist/mcp/tools/plugins.js +14 -9
- package/server/dist/mcp/tools/rails.js +12 -13
- package/server/dist/mcp/tools/support.js +1 -1
- package/server/dist/mobile/mobile-router.js +5 -1
- package/server/dist/offline-assemble.js +13 -1
- package/server/dist/path-resolver.js +2 -2
- package/server/dist/plugin-manager.js +391 -106
- package/server/dist/plugins/claude-md-mutation.js +5 -4
- package/server/dist/plugins/drift.js +32 -29
- package/server/dist/plugins/rail-integration.js +13 -2
- package/server/dist/plugins/serena/install.js +25 -17
- package/server/dist/plugins/serena/manifest.js +22 -5
- package/server/dist/plugins/serena/verify.js +46 -16
- package/server/dist/plugins-router.js +59 -8
- package/server/dist/profile-manager.js +148 -36
- package/server/dist/profiles-router.js +311 -82
- package/server/dist/project-registry.js +2 -2
- package/server/dist/project-router-chat.js +17 -8
- package/server/dist/project-router-helpers.js +93 -5
- package/server/dist/project-router-jobs.js +76 -12
- package/server/dist/project-router-loop-runs.js +42 -14
- package/server/dist/project-router-settings.js +31 -10
- package/server/dist/project-router-setup.js +4 -5
- package/server/dist/project-router-spending.js +15 -3
- package/server/dist/project-router-tickets.js +187 -181
- package/server/dist/project-router.js +1 -0
- package/server/dist/proposal-manager.js +125 -85
- package/server/dist/providers/claude-adapter.js +14 -0
- package/server/dist/providers/codex-adapter.js +20 -0
- package/server/dist/providers/gemini-adapter.js +14 -0
- package/server/dist/providers/index.js +18 -1
- package/server/dist/providers/kimi-adapter.js +361 -0
- package/server/dist/providers/kimi-skill-prompt.js +332 -0
- package/server/dist/providers/runtime.js +137 -0
- package/server/dist/queue-manager.js +103 -78
- package/server/dist/rail-isolated-launch.js +1 -1
- package/server/dist/rails-router.js +51 -38
- package/server/dist/result-event.js +5 -0
- package/server/dist/schemas/profile.v1.json +3 -4
- package/server/dist/setup-manager.js +222 -101
- package/server/dist/setup-prerequisites.js +7 -1
- package/server/dist/smash-runner.js +104 -57
- package/server/dist/spawn-lifecycle.js +18 -14
- package/server/dist/spec-launcher-manager.js +66 -54
- package/server/dist/spec-models.js +15 -6
- package/server/dist/spending.js +110 -17
- package/server/dist/util/cli-prompt.js +142 -5
- package/server/dist/util/stream-display.js +46 -0
- package/server/dist/workspace-manager.js +100 -37
- package/server/dist/worktree-manager.js +4 -0
- package/server/dist/worktree-overlay.js +12 -3
- package/client/dist/assets/AgentModeCodePane-BAYyFRmY.js +0 -2
- package/client/dist/assets/AgentsPage-DuyJuaFs.js +0 -86
- package/client/dist/assets/AnalyticsPage-BY7gtCx6.js +0 -1
- package/client/dist/assets/CodePage-vyQr40Vl.js +0 -2
- package/client/dist/assets/JobDetailPage-CU4FEnUC.js +0 -1
- package/client/dist/assets/LoopsPage-SqglnXlp.js +0 -1
- package/client/dist/assets/PluginsPage-B89S8o1a.js +0 -2
- package/client/dist/assets/TicketDetailModal-CI47Dq9E.js +0 -14
- package/client/dist/assets/addspec-BXjvkh5q.js +0 -1
- package/client/dist/assets/addspec-C4bwrmpi.js +0 -1
- package/client/dist/assets/addspec-CArHXUDM.js +0 -1
- package/client/dist/assets/addspec-DTKJcXiR.js +0 -1
- package/client/dist/assets/addspec-DbNBbrha.js +0 -1
- package/client/dist/assets/addspec-Rw8uVBC1.js +0 -1
- package/client/dist/assets/addspec-htHGZHOe.js +0 -1
- package/client/dist/assets/addspec-lr_LP6jk.js +0 -1
- package/client/dist/assets/agent-BABjRt0P.js +0 -1
- package/client/dist/assets/agent-BuUpgqVx.js +0 -1
- package/client/dist/assets/agent-CCDZesgU.js +0 -1
- package/client/dist/assets/agent-CVn64zWH.js +0 -1
- package/client/dist/assets/agent-CWeOny0E.js +0 -1
- package/client/dist/assets/agent-DOysmad_.js +0 -1
- package/client/dist/assets/agent-DSsjTBhn.js +0 -1
- package/client/dist/assets/agent-DlZMxfM9.js +0 -1
- package/client/dist/assets/agents-6ZOJXWVk.js +0 -1
- package/client/dist/assets/agents-BZ0XQKjg.js +0 -1
- package/client/dist/assets/agents-BqWklnHR.js +0 -1
- package/client/dist/assets/agents-Bz-thHkB.js +0 -1
- package/client/dist/assets/agents-CJclElx6.js +0 -1
- package/client/dist/assets/agents-D_l9L0VA.js +0 -1
- package/client/dist/assets/agents-DhG7mg6-.js +0 -1
- package/client/dist/assets/agents-u_C0jPB5.js +0 -1
- package/client/dist/assets/agentstudio-BF19hbXj.js +0 -1
- package/client/dist/assets/agentstudio-BIUe1Im2.js +0 -1
- package/client/dist/assets/agentstudio-BLGvxWBx.js +0 -1
- package/client/dist/assets/agentstudio-D9UKiWTj.js +0 -1
- package/client/dist/assets/agentstudio-Drs37nIM.js +0 -1
- package/client/dist/assets/agentstudio-nEnKkcXM.js +0 -1
- package/client/dist/assets/agentstudio-pyFcSBKy.js +0 -1
- package/client/dist/assets/agentstudio-zfTzQh3U.js +0 -1
- package/client/dist/assets/analytics-BC7mwR4s.js +0 -1
- package/client/dist/assets/analytics-BiNg8edP.js +0 -1
- package/client/dist/assets/analytics-C0s_T2GN.js +0 -1
- package/client/dist/assets/analytics-CdsAPAwZ.js +0 -1
- package/client/dist/assets/analytics-Cr1_q-lC.js +0 -1
- package/client/dist/assets/analytics-DZ8Aoq3I.js +0 -1
- package/client/dist/assets/analytics-IEsKZvpW.js +0 -1
- package/client/dist/assets/analytics-SRPf84OT.js +0 -1
- package/client/dist/assets/builder-BH8Fua-g.js +0 -1
- package/client/dist/assets/builder-BoV7DgKd.js +0 -1
- package/client/dist/assets/builder-Cc_Lf79C.js +0 -1
- package/client/dist/assets/builder-CwZ7Bme2.js +0 -1
- package/client/dist/assets/builder-CxW-JFsR.js +0 -1
- package/client/dist/assets/builder-D94lrV7x.js +0 -1
- package/client/dist/assets/builder-cqHpCyGa.js +0 -1
- package/client/dist/assets/code-76gS0IdL.js +0 -1
- package/client/dist/assets/code-BDXEM4ml.js +0 -1
- package/client/dist/assets/code-C1VwxlML.js +0 -1
- package/client/dist/assets/code-CnMZyJpr.js +0 -1
- package/client/dist/assets/code-D20ef9Cf.js +0 -1
- package/client/dist/assets/code-DdOXaWRY.js +0 -1
- package/client/dist/assets/code-Duohuf9l.js +0 -1
- package/client/dist/assets/code-geHmknB9.js +0 -1
- package/client/dist/assets/format-command-Dfa1o2d-.js +0 -1
- package/client/dist/assets/index-Dh9i4CnN.css +0 -2
- package/client/dist/assets/index-F9r8mWP_.js +0 -76
- package/client/dist/assets/jobs-BCtGfgUA.js +0 -1
- package/client/dist/assets/jobs-BHR_EnDI.js +0 -1
- package/client/dist/assets/jobs-BMbZJjJJ.js +0 -1
- package/client/dist/assets/jobs-DzSIR8V6.js +0 -1
- package/client/dist/assets/nav-BRJBxaGt.js +0 -1
- package/client/dist/assets/nav-CAGKv3_F.js +0 -1
- package/client/dist/assets/nav-CNskRJKQ.js +0 -1
- package/client/dist/assets/nav-DY93irBL.js +0 -1
- package/client/dist/assets/nav-DhjvYgqt.js +0 -1
- package/client/dist/assets/nav-Gb-r4wop.js +0 -1
- package/client/dist/assets/nav-TvumAFD2.js +0 -1
- package/client/dist/assets/nav-myFaXRHD.js +0 -1
- package/client/dist/assets/settings-B0ypw9uw.js +0 -1
- package/client/dist/assets/settings-B9ilOSYO.js +0 -1
- package/client/dist/assets/settings-BqlWHRHA.js +0 -1
- package/client/dist/assets/settings-D8dlqt4d.js +0 -1
- package/client/dist/assets/settings-DAZqjX2a.js +0 -1
- package/client/dist/assets/settings-DRh39v1S.js +0 -1
- package/client/dist/assets/settings-Mj1OJbfd.js +0 -1
- package/client/dist/assets/settings-lRReaSYm.js +0 -1
- package/client/dist/assets/setup-3832BJCo.js +0 -1
- package/client/dist/assets/setup-B8J-gECe.js +0 -1
- package/client/dist/assets/setup-BCIOxVsc.js +0 -1
- package/client/dist/assets/setup-BciLj_tU.js +0 -1
- package/client/dist/assets/setup-CTkE3cfh.js +0 -1
- package/client/dist/assets/setup-ajB4rYm-.js +0 -1
- package/client/dist/assets/setup-xFO1wNl0.js +0 -1
|
@@ -11,11 +11,18 @@ const claude_approval_1 = require("./plugins/claude-approval");
|
|
|
11
11
|
const drift_1 = require("./plugins/drift");
|
|
12
12
|
const providers_1 = require("./providers");
|
|
13
13
|
const paths_1 = require("./plugins/paths");
|
|
14
|
-
function
|
|
14
|
+
function providerMcpJsonPath(projectPath, providerId) {
|
|
15
|
+
if (providerId && (0, providers_1.hasAdapter)(providerId)) {
|
|
16
|
+
return (0, providers_1.getAdapter)(providerId).projectMcpPath?.(projectPath) ?? (0, paths_1.mcpJsonPath)(projectPath);
|
|
17
|
+
}
|
|
18
|
+
return (0, paths_1.mcpJsonPath)(projectPath);
|
|
19
|
+
}
|
|
20
|
+
function readMcpServersMap(projectPath, providerId) {
|
|
15
21
|
try {
|
|
16
|
-
|
|
22
|
+
const file = providerMcpJsonPath(projectPath, providerId);
|
|
23
|
+
if (!fs_1.default.existsSync(file))
|
|
17
24
|
return {};
|
|
18
|
-
const raw = fs_1.default.readFileSync(
|
|
25
|
+
const raw = fs_1.default.readFileSync(file, 'utf8');
|
|
19
26
|
if (!raw.trim())
|
|
20
27
|
return {};
|
|
21
28
|
const parsed = JSON.parse(raw);
|
|
@@ -27,6 +34,87 @@ function readMcpServersMap(projectPath) {
|
|
|
27
34
|
}
|
|
28
35
|
const json_mutation_1 = require("./plugins/json-mutation");
|
|
29
36
|
const contributors_1 = require("./plugins/contributors");
|
|
37
|
+
const LEGACY_PROVIDER = 'claude';
|
|
38
|
+
function targetProvider(providerId) {
|
|
39
|
+
return providerId?.trim() || LEGACY_PROVIDER;
|
|
40
|
+
}
|
|
41
|
+
function legacyProvider(providerId, legacyProviderId) {
|
|
42
|
+
return legacyProviderId?.trim() || providerId;
|
|
43
|
+
}
|
|
44
|
+
function legacyProviderState(entry) {
|
|
45
|
+
return {
|
|
46
|
+
installedAt: entry.installedAt,
|
|
47
|
+
installedFiles: [...(entry.installedFiles ?? [])],
|
|
48
|
+
health: entry.health,
|
|
49
|
+
healthReason: entry.healthReason,
|
|
50
|
+
ownedMcpServers: [...(entry.ownedMcpServers ?? [])],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Resolve one provider without pretending a legacy primary install also
|
|
54
|
+
* belongs to every secondary provider. */
|
|
55
|
+
function getProviderState(entry, providerId, legacyProviderId) {
|
|
56
|
+
if (!entry)
|
|
57
|
+
return undefined;
|
|
58
|
+
if (entry.providers)
|
|
59
|
+
return entry.providers[providerId];
|
|
60
|
+
return providerId === legacyProviderId ? legacyProviderState(entry) : undefined;
|
|
61
|
+
}
|
|
62
|
+
/** Materialize the provider map lazily when the first post-migration mutation
|
|
63
|
+
* occurs. Until then legacy state remains byte-compatible on disk. */
|
|
64
|
+
function ensureProviderStates(entry, legacyProviderId) {
|
|
65
|
+
if (!entry.providers) {
|
|
66
|
+
entry.providers = { [legacyProviderId]: legacyProviderState(entry) };
|
|
67
|
+
}
|
|
68
|
+
return entry.providers;
|
|
69
|
+
}
|
|
70
|
+
/** Keep the schema-v1 aggregate fields truthful for older Desktop builds and
|
|
71
|
+
* diagnostics that have not learned the provider map yet. */
|
|
72
|
+
function syncAggregateState(entry) {
|
|
73
|
+
const states = Object.values(entry.providers ?? {});
|
|
74
|
+
if (states.length === 0)
|
|
75
|
+
return;
|
|
76
|
+
entry.installedAt = states
|
|
77
|
+
.map((state) => state.installedAt)
|
|
78
|
+
.sort()[0] ?? entry.installedAt;
|
|
79
|
+
entry.installedFiles = Array.from(new Set(states.flatMap((state) => state.installedFiles ?? [])));
|
|
80
|
+
const degraded = states.find((state) => state.health === 'degraded');
|
|
81
|
+
if (degraded) {
|
|
82
|
+
entry.health = 'degraded';
|
|
83
|
+
entry.healthReason = degraded.healthReason;
|
|
84
|
+
}
|
|
85
|
+
else if (states.every((state) => state.health === 'ok')) {
|
|
86
|
+
entry.health = 'ok';
|
|
87
|
+
delete entry.healthReason;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
entry.health = 'unknown';
|
|
91
|
+
delete entry.healthReason;
|
|
92
|
+
}
|
|
93
|
+
entry.ownedMcpServers = Array.from(new Set(states.flatMap((state) => state.ownedMcpServers ?? [])));
|
|
94
|
+
}
|
|
95
|
+
function supportsProvider(plugin, providerId) {
|
|
96
|
+
const declared = plugin.manifest.providerSupport;
|
|
97
|
+
return declared === undefined
|
|
98
|
+
? providerId === LEGACY_PROVIDER
|
|
99
|
+
: Object.prototype.hasOwnProperty.call(declared, providerId);
|
|
100
|
+
}
|
|
101
|
+
function sameInstructionsFile(leftProviderId, rightProviderId) {
|
|
102
|
+
if (!(0, providers_1.hasAdapter)(leftProviderId) || !(0, providers_1.hasAdapter)(rightProviderId))
|
|
103
|
+
return false;
|
|
104
|
+
return (0, providers_1.getAdapter)(leftProviderId).instructionsFilename === (0, providers_1.getAdapter)(rightProviderId).instructionsFilename;
|
|
105
|
+
}
|
|
106
|
+
/** A single managed instructions block may be shared by providers that declare
|
|
107
|
+
* the same project-relative instructions path. Removing or deactivating one
|
|
108
|
+
* provider must not remove that block while another such provider still has
|
|
109
|
+
* the plugin active. Codex (`AGENTS.md`) and Kimi
|
|
110
|
+
* (`.kimi-code/AGENTS.md`) are intentionally independent. */
|
|
111
|
+
function hasOtherActiveContributor(entry, providerId) {
|
|
112
|
+
if (!entry.providers)
|
|
113
|
+
return false;
|
|
114
|
+
return Object.entries(entry.providers).some(([candidate, state]) => candidate !== providerId &&
|
|
115
|
+
state.active !== false &&
|
|
116
|
+
sameInstructionsFile(candidate, providerId));
|
|
117
|
+
}
|
|
30
118
|
class PluginNotFoundError extends Error {
|
|
31
119
|
constructor(name) {
|
|
32
120
|
super(`plugin not found in registry: ${name}`);
|
|
@@ -103,33 +191,35 @@ class PluginManager {
|
|
|
103
191
|
});
|
|
104
192
|
}
|
|
105
193
|
// ─── Catalog ───────────────────────────────────────────────────────────────
|
|
106
|
-
async listAvailable(projectPath, providerId) {
|
|
194
|
+
async listAvailable(projectPath, providerId, legacyProviderId) {
|
|
195
|
+
const provider = targetProvider(providerId);
|
|
196
|
+
const primaryProvider = legacyProvider(provider, legacyProviderId);
|
|
107
197
|
const state = this.getProjectState(projectPath);
|
|
108
198
|
const entries = [];
|
|
109
199
|
// Bundled plugins, regardless of install state.
|
|
110
200
|
for (const plugin of this.registry.byName.values()) {
|
|
111
201
|
const m = plugin.manifest;
|
|
112
202
|
const stateEntry = state.plugins[m.name];
|
|
203
|
+
const providerState = getProviderState(stateEntry, provider, primaryProvider);
|
|
113
204
|
let status;
|
|
114
205
|
// Provider applicability: a plugin is `not-applicable` when the
|
|
115
206
|
// project's provider is registered, providerSupport is declared, and
|
|
116
207
|
// there's no entry for this provider. Plugins that don't declare
|
|
117
208
|
// providerSupport at all default to claude-compatible (preserves
|
|
118
209
|
// pre-§14 behaviour for unchanged manifests).
|
|
119
|
-
const supportsThisProvider =
|
|
120
|
-
? true
|
|
121
|
-
: m.providerSupport === undefined
|
|
122
|
-
? providerId === 'claude'
|
|
123
|
-
: providerId in m.providerSupport;
|
|
210
|
+
const supportsThisProvider = supportsProvider(plugin, provider);
|
|
124
211
|
if (!supportsThisProvider) {
|
|
125
212
|
status = 'not-applicable';
|
|
126
213
|
}
|
|
127
|
-
else if (!
|
|
214
|
+
else if (!providerState) {
|
|
128
215
|
status = 'not-installed';
|
|
129
216
|
}
|
|
130
|
-
else if (
|
|
217
|
+
else if (providerState.health === 'degraded') {
|
|
131
218
|
status = 'degraded';
|
|
132
219
|
}
|
|
220
|
+
else if (providerState.active === false) {
|
|
221
|
+
status = 'deactivated';
|
|
222
|
+
}
|
|
133
223
|
else {
|
|
134
224
|
// Plugin install lives in two files:
|
|
135
225
|
// (a) state.json — the app's record that the plugin is installed
|
|
@@ -139,14 +229,14 @@ class PluginManager {
|
|
|
139
229
|
// (b) check is skipped because the registration lives outside the
|
|
140
230
|
// project filesystem (CODEX_HOME).
|
|
141
231
|
let allKeysPresent = true;
|
|
142
|
-
if (
|
|
232
|
+
if ((0, providers_1.hasAdapter)(provider) && (0, providers_1.getAdapter)(provider).mcpRegistration === 'cli-add') {
|
|
143
233
|
// For codex we trust state.json — `codex mcp list` against the
|
|
144
234
|
// per-project CODEX_HOME is the source of truth, but it requires a
|
|
145
235
|
// subprocess which is too expensive for a catalog listing call.
|
|
146
236
|
allKeysPresent = true;
|
|
147
237
|
}
|
|
148
238
|
else {
|
|
149
|
-
const mcpServers = readMcpServersMap(projectPath);
|
|
239
|
+
const mcpServers = readMcpServersMap(projectPath, provider);
|
|
150
240
|
for (const server of m.owns.mcpServers ?? []) {
|
|
151
241
|
if (!(server in mcpServers)) {
|
|
152
242
|
allKeysPresent = false;
|
|
@@ -160,18 +250,22 @@ class PluginManager {
|
|
|
160
250
|
// affordance when our project-scoped install is being shadowed.
|
|
161
251
|
const conflicts = [];
|
|
162
252
|
const cachedDisabled = [];
|
|
163
|
-
|
|
164
|
-
for (const
|
|
165
|
-
|
|
166
|
-
conflicts.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
cachedDisabled.
|
|
253
|
+
if (provider === 'claude') {
|
|
254
|
+
for (const server of m.owns.mcpServers ?? []) {
|
|
255
|
+
for (const key of (0, claude_approval_1.findEnabledMarketplaceKeys)(server)) {
|
|
256
|
+
if (!conflicts.includes(key))
|
|
257
|
+
conflicts.push(key);
|
|
258
|
+
}
|
|
259
|
+
for (const key of (0, claude_approval_1.findInstalledButNotEnabledMarketplaceKeys)(server)) {
|
|
260
|
+
if (!cachedDisabled.includes(key))
|
|
261
|
+
cachedDisabled.push(key);
|
|
262
|
+
}
|
|
171
263
|
}
|
|
172
264
|
}
|
|
173
265
|
// Drift detection: only meaningful when actually installed.
|
|
174
|
-
const updateAvailable =
|
|
266
|
+
const updateAvailable = providerState
|
|
267
|
+
? (0, drift_1.detectMcpDrift)(projectPath, plugin, provider)
|
|
268
|
+
: false;
|
|
175
269
|
entries.push({
|
|
176
270
|
name: m.name,
|
|
177
271
|
version: m.version,
|
|
@@ -181,9 +275,10 @@ class PluginManager {
|
|
|
181
275
|
requirements: m.requirements ?? [],
|
|
182
276
|
owns: m.owns,
|
|
183
277
|
status,
|
|
184
|
-
installedAt:
|
|
185
|
-
health:
|
|
186
|
-
healthReason:
|
|
278
|
+
installedAt: providerState?.installedAt,
|
|
279
|
+
health: providerState?.health,
|
|
280
|
+
healthReason: providerState?.healthReason,
|
|
281
|
+
providerId: provider,
|
|
187
282
|
marketplaceConflicts: conflicts.length > 0 ? conflicts : undefined,
|
|
188
283
|
marketplaceCachedButDisabled: cachedDisabled.length > 0 ? cachedDisabled : undefined,
|
|
189
284
|
updateAvailable: updateAvailable || undefined,
|
|
@@ -193,6 +288,9 @@ class PluginManager {
|
|
|
193
288
|
for (const [name, entry] of Object.entries(state.plugins)) {
|
|
194
289
|
if (this.registry.byName.has(name))
|
|
195
290
|
continue;
|
|
291
|
+
const providerState = getProviderState(entry, provider, primaryProvider);
|
|
292
|
+
if (!providerState)
|
|
293
|
+
continue;
|
|
196
294
|
entries.push({
|
|
197
295
|
name,
|
|
198
296
|
version: entry.version,
|
|
@@ -201,24 +299,34 @@ class PluginManager {
|
|
|
201
299
|
requirements: [],
|
|
202
300
|
owns: {},
|
|
203
301
|
status: 'orphan',
|
|
204
|
-
installedAt:
|
|
205
|
-
health:
|
|
206
|
-
healthReason:
|
|
302
|
+
installedAt: providerState.installedAt,
|
|
303
|
+
health: providerState.health,
|
|
304
|
+
healthReason: providerState.healthReason,
|
|
305
|
+
providerId: provider,
|
|
207
306
|
});
|
|
208
307
|
}
|
|
209
308
|
return entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
210
309
|
}
|
|
211
310
|
// ─── Preview install ───────────────────────────────────────────────────────
|
|
212
|
-
async previewInstall(projectPath, projectId, name) {
|
|
311
|
+
async previewInstall(projectPath, projectId, name, providerId, slug) {
|
|
213
312
|
const plugin = this.registry.byName.get(name);
|
|
214
313
|
if (!plugin)
|
|
215
314
|
throw new PluginNotFoundError(name);
|
|
315
|
+
const provider = targetProvider(providerId);
|
|
316
|
+
if (!supportsProvider(plugin, provider)) {
|
|
317
|
+
throw new PluginInstallError(`plugin '${name}' is not applicable for provider '${provider}'.`);
|
|
318
|
+
}
|
|
216
319
|
let files;
|
|
217
320
|
if (plugin.previewInstall) {
|
|
218
|
-
files = await plugin.previewInstall({
|
|
321
|
+
files = await plugin.previewInstall({
|
|
322
|
+
projectPath,
|
|
323
|
+
projectId,
|
|
324
|
+
providerId: provider,
|
|
325
|
+
slug,
|
|
326
|
+
});
|
|
219
327
|
}
|
|
220
328
|
else {
|
|
221
|
-
files = this._derivePreviewFiles(projectPath, plugin);
|
|
329
|
+
files = this._derivePreviewFiles(projectPath, plugin, provider);
|
|
222
330
|
}
|
|
223
331
|
const requirements = await Promise.all((plugin.manifest.requirements ?? []).map(async (req) => {
|
|
224
332
|
if (this._options.checkPrerequisite) {
|
|
@@ -236,25 +344,26 @@ class PluginManager {
|
|
|
236
344
|
platformNote,
|
|
237
345
|
};
|
|
238
346
|
}
|
|
239
|
-
_derivePreviewFiles(projectPath, plugin) {
|
|
347
|
+
_derivePreviewFiles(projectPath, plugin, providerId) {
|
|
240
348
|
const out = [];
|
|
241
349
|
const m = plugin.manifest;
|
|
242
350
|
// .mcp.json
|
|
243
351
|
if ((m.owns.mcpServers ?? []).length > 0) {
|
|
244
|
-
const
|
|
352
|
+
const mcpFile = providerMcpJsonPath(projectPath, providerId);
|
|
353
|
+
const mcpExists = fs_1.default.existsSync(mcpFile);
|
|
245
354
|
out.push({
|
|
246
|
-
path:
|
|
355
|
+
path: path_1.default.relative(projectPath, mcpFile),
|
|
247
356
|
op: mcpExists ? 'modify' : 'create',
|
|
248
357
|
summary: `+ mcpServers.${(m.owns.mcpServers ?? []).join(', mcpServers.')}`,
|
|
249
358
|
});
|
|
250
359
|
}
|
|
251
360
|
// Agent fragments
|
|
252
|
-
for (const frag of m.owns.agentFragments ?? []) {
|
|
361
|
+
for (const frag of providerId && providerId !== 'claude' ? [] : (m.owns.agentFragments ?? [])) {
|
|
253
362
|
const exists = fs_1.default.existsSync(path_1.default.join(projectPath, frag));
|
|
254
363
|
out.push({ path: frag, op: exists ? 'modify' : 'create' });
|
|
255
364
|
}
|
|
256
365
|
// Shared-file contributors (CLAUDE.md today, more in the future).
|
|
257
|
-
for (const rel of (0, contributors_1.contributorPaths)(plugin)) {
|
|
366
|
+
for (const rel of (0, contributors_1.contributorPaths)(plugin, providerId)) {
|
|
258
367
|
const exists = fs_1.default.existsSync(path_1.default.join(projectPath, rel));
|
|
259
368
|
out.push({
|
|
260
369
|
path: rel,
|
|
@@ -272,32 +381,34 @@ class PluginManager {
|
|
|
272
381
|
return out;
|
|
273
382
|
}
|
|
274
383
|
// ─── Install ───────────────────────────────────────────────────────────────
|
|
275
|
-
async install(projectPath, projectId, name, broadcast, providerId, slug) {
|
|
384
|
+
async install(projectPath, projectId, name, broadcast, providerId, slug, legacyProviderId) {
|
|
276
385
|
const plugin = this.registry.byName.get(name);
|
|
277
386
|
if (!plugin)
|
|
278
387
|
throw new PluginNotFoundError(name);
|
|
388
|
+
const provider = targetProvider(providerId);
|
|
389
|
+
const primaryProvider = legacyProvider(provider, legacyProviderId);
|
|
279
390
|
// Provider applicability gate: refuse to install a plugin that has no
|
|
280
391
|
// providerSupport entry for this project's provider. Plugins that omit
|
|
281
392
|
// providerSupport altogether default to claude-compatible.
|
|
282
|
-
if (
|
|
393
|
+
if (!supportsProvider(plugin, provider)) {
|
|
283
394
|
const declared = plugin.manifest.providerSupport;
|
|
284
|
-
|
|
285
|
-
throw new PluginInstallError(`plugin '${name}' is not applicable for provider '${providerId}'. Declared providers: ${Object.keys(declared).join(', ')}.`);
|
|
286
|
-
}
|
|
395
|
+
throw new PluginInstallError(`plugin '${name}' is not applicable for provider '${provider}'. Declared providers: ${declared ? Object.keys(declared).join(', ') : LEGACY_PROVIDER}.`);
|
|
287
396
|
}
|
|
288
397
|
const state = this.getProjectState(projectPath);
|
|
289
|
-
|
|
398
|
+
const existingEntry = state.plugins[name];
|
|
399
|
+
if (getProviderState(existingEntry, provider, primaryProvider)) {
|
|
290
400
|
throw new PluginAlreadyInstalledError(name);
|
|
401
|
+
}
|
|
291
402
|
// Check for ownership conflicts with user-authored `.mcp.json` entries.
|
|
292
403
|
// Only meaningful for `project-json` MCP registration providers (claude
|
|
293
404
|
// today). Codex registers via `codex mcp add` against per-project
|
|
294
405
|
// CODEX_HOME, which the plugin's install path checks via `codex mcp list`.
|
|
295
|
-
const adapter =
|
|
296
|
-
? (0, providers_1.getAdapter)(
|
|
406
|
+
const adapter = (0, providers_1.hasAdapter)(provider)
|
|
407
|
+
? (0, providers_1.getAdapter)(provider)
|
|
297
408
|
: null;
|
|
298
409
|
const usesProjectJsonMcp = adapter === null || adapter.mcpRegistration === 'project-json';
|
|
299
410
|
if (usesProjectJsonMcp) {
|
|
300
|
-
const mcpFile = (
|
|
411
|
+
const mcpFile = providerMcpJsonPath(projectPath, provider);
|
|
301
412
|
if (fs_1.default.existsSync(mcpFile)) {
|
|
302
413
|
const raw = fs_1.default.readFileSync(mcpFile, 'utf8');
|
|
303
414
|
let parsed;
|
|
@@ -307,12 +418,12 @@ class PluginManager {
|
|
|
307
418
|
catch {
|
|
308
419
|
// A hand-edited / broken `.mcp.json` should yield an actionable 409,
|
|
309
420
|
// not an opaque 500 with a raw "Unexpected token" SyntaxError.
|
|
310
|
-
throw new PluginInstallError(`cannot install '${name}': '${
|
|
421
|
+
throw new PluginInstallError(`cannot install '${name}': '${mcpFile}' is not valid JSON; fix it first.`);
|
|
311
422
|
}
|
|
312
423
|
const servers = parsed.mcpServers ?? {};
|
|
313
424
|
for (const key of plugin.manifest.owns.mcpServers ?? []) {
|
|
314
425
|
if (key in servers) {
|
|
315
|
-
throw new PluginInstallError(`cannot install '${name}': '${
|
|
426
|
+
throw new PluginInstallError(`cannot install '${name}': '${mcpFile}' already has a 'mcpServers.${key}' entry. Remove it first.`);
|
|
316
427
|
}
|
|
317
428
|
}
|
|
318
429
|
}
|
|
@@ -320,14 +431,15 @@ class PluginManager {
|
|
|
320
431
|
// Snapshot pre-install state of every file the plugin might touch — we
|
|
321
432
|
// need exact bytes to roll back if install/verify fails.
|
|
322
433
|
const targetPaths = [
|
|
323
|
-
(
|
|
434
|
+
providerMcpJsonPath(projectPath, provider),
|
|
324
435
|
(0, paths_1.stateFilePath)(projectPath),
|
|
325
|
-
...(plugin.manifest.owns.agentFragments ?? [])
|
|
436
|
+
...(provider !== 'claude' ? [] : (plugin.manifest.owns.agentFragments ?? []))
|
|
437
|
+
.map((f) => path_1.default.join(projectPath, f)),
|
|
326
438
|
// Include the shared instructions file (CLAUDE.md / AGENTS.md) so a failed
|
|
327
439
|
// install rolls it back too — otherwise an applyContributors write that
|
|
328
440
|
// survives a later failure leaves an orphaned managed block with no state
|
|
329
441
|
// entry, which uninstall can never remove (breaks byte-identical restore).
|
|
330
|
-
...(0, contributors_1.contributorPaths)(plugin,
|
|
442
|
+
...(0, contributors_1.contributorPaths)(plugin, provider).map((rel) => path_1.default.join(projectPath, rel)),
|
|
331
443
|
];
|
|
332
444
|
const preState = new Map();
|
|
333
445
|
for (const p of targetPaths) {
|
|
@@ -339,6 +451,7 @@ class PluginManager {
|
|
|
339
451
|
type: 'plugin.install_progress',
|
|
340
452
|
projectId,
|
|
341
453
|
name,
|
|
454
|
+
providerId: provider,
|
|
342
455
|
line,
|
|
343
456
|
timestamp: new Date().toISOString(),
|
|
344
457
|
};
|
|
@@ -348,7 +461,7 @@ class PluginManager {
|
|
|
348
461
|
projectPath,
|
|
349
462
|
projectId,
|
|
350
463
|
slug,
|
|
351
|
-
providerId,
|
|
464
|
+
providerId: provider,
|
|
352
465
|
recordInstalledFile: (rel) => { installedFiles.push(rel); },
|
|
353
466
|
log: onLog,
|
|
354
467
|
};
|
|
@@ -356,7 +469,7 @@ class PluginManager {
|
|
|
356
469
|
await plugin.install(ctx);
|
|
357
470
|
// Verify immediately. A degraded result also triggers rollback because
|
|
358
471
|
// the spec requires verify-pass before we commit state.
|
|
359
|
-
const verify = await this._runVerify(plugin, projectPath, projectId);
|
|
472
|
+
const verify = await this._runVerify(plugin, projectPath, projectId, provider, slug);
|
|
360
473
|
if (!verify.ok) {
|
|
361
474
|
throw new PluginInstallError(`verify failed after install: ${verify.reason ?? 'unknown'}`);
|
|
362
475
|
}
|
|
@@ -366,31 +479,64 @@ class PluginManager {
|
|
|
366
479
|
// the merged `.mcp.json` entries instead of leaving them loaded forever.
|
|
367
480
|
const ownedMcpServers = [...(plugin.manifest.owns.mcpServers ?? [])];
|
|
368
481
|
await this.lockedUpdateState(projectPath, (s) => {
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
installedAt
|
|
372
|
-
installedFiles,
|
|
482
|
+
const installedAt = new Date().toISOString();
|
|
483
|
+
const providerEntry = {
|
|
484
|
+
installedAt,
|
|
485
|
+
installedFiles: [...installedFiles],
|
|
486
|
+
active: true,
|
|
373
487
|
health: 'ok',
|
|
488
|
+
ownedMcpServers,
|
|
374
489
|
};
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
490
|
+
const current = s.plugins[name];
|
|
491
|
+
if (!current) {
|
|
492
|
+
const entry = {
|
|
493
|
+
version: plugin.manifest.version,
|
|
494
|
+
installedAt,
|
|
495
|
+
installedFiles: [...installedFiles],
|
|
496
|
+
health: 'ok',
|
|
497
|
+
};
|
|
498
|
+
if (ownedMcpServers.length > 0)
|
|
499
|
+
entry.ownedMcpServers = ownedMcpServers;
|
|
500
|
+
// A primary-provider-only install stays in the historical shape.
|
|
501
|
+
// The provider map is materialised only when it is required, keeping
|
|
502
|
+
// state byte-compatible for existing single-provider projects.
|
|
503
|
+
if (provider !== primaryProvider) {
|
|
504
|
+
entry.providers = { [provider]: providerEntry };
|
|
505
|
+
}
|
|
506
|
+
s.plugins[name] = entry;
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const providerStates = ensureProviderStates(current, primaryProvider);
|
|
510
|
+
if (providerStates[provider]) {
|
|
511
|
+
throw new PluginAlreadyInstalledError(name);
|
|
512
|
+
}
|
|
513
|
+
current.version = plugin.manifest.version;
|
|
514
|
+
providerStates[provider] = providerEntry;
|
|
515
|
+
syncAggregateState(current);
|
|
378
516
|
});
|
|
379
517
|
// No additional approval write needed: any server in `.mcp.json` loads
|
|
380
518
|
// automatically when Claude opens the project. Install IS active.
|
|
381
519
|
// Apply shared-file contributors (CLAUDE.md block today, more in the
|
|
382
520
|
// future). Each contributor is per-plugin and idempotent.
|
|
383
|
-
const sharedTouched = await (0, contributors_1.applyContributors)(plugin, projectPath,
|
|
521
|
+
const sharedTouched = await (0, contributors_1.applyContributors)(plugin, projectPath, provider);
|
|
384
522
|
if (sharedTouched.length > 0) {
|
|
385
523
|
for (const p of sharedTouched) {
|
|
386
524
|
if (!installedFiles.includes(p))
|
|
387
525
|
installedFiles.push(p);
|
|
388
526
|
}
|
|
389
527
|
await this.lockedUpdateState(projectPath, (s) => {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
else
|
|
528
|
+
const entry = s.plugins[name];
|
|
529
|
+
if (!entry)
|
|
393
530
|
return false;
|
|
531
|
+
if (!entry.providers && provider === primaryProvider) {
|
|
532
|
+
entry.installedFiles = [...installedFiles];
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const providerState = getProviderState(entry, provider, primaryProvider);
|
|
536
|
+
if (!providerState)
|
|
537
|
+
return false;
|
|
538
|
+
ensureProviderStates(entry, primaryProvider)[provider].installedFiles = [...installedFiles];
|
|
539
|
+
syncAggregateState(entry);
|
|
394
540
|
});
|
|
395
541
|
}
|
|
396
542
|
}
|
|
@@ -417,10 +563,10 @@ class PluginManager {
|
|
|
417
563
|
// CODEX_HOME, NOT the snapshotted filesystem — the file rollback above
|
|
418
564
|
// can't undo it. Run the plugin's uninstall so a verify-failed codex
|
|
419
565
|
// install leaves no orphaned MCP registration (best-effort).
|
|
420
|
-
if (
|
|
566
|
+
if ((0, providers_1.hasAdapter)(provider) && (0, providers_1.getAdapter)(provider).mcpRegistration === 'cli-add') {
|
|
421
567
|
try {
|
|
422
568
|
await plugin.uninstall({
|
|
423
|
-
projectPath, projectId, slug, providerId,
|
|
569
|
+
projectPath, projectId, slug, providerId: provider,
|
|
424
570
|
recordInstalledFile: () => { }, log: onLog,
|
|
425
571
|
});
|
|
426
572
|
}
|
|
@@ -433,15 +579,19 @@ class PluginManager {
|
|
|
433
579
|
projectId,
|
|
434
580
|
name,
|
|
435
581
|
version: plugin.manifest.version,
|
|
582
|
+
providerId: provider,
|
|
436
583
|
timestamp: new Date().toISOString(),
|
|
437
584
|
};
|
|
438
585
|
broadcast(msg);
|
|
439
586
|
}
|
|
440
587
|
// ─── Uninstall ─────────────────────────────────────────────────────────────
|
|
441
|
-
async uninstall(projectPath, projectId, name, broadcast, providerId, slug) {
|
|
588
|
+
async uninstall(projectPath, projectId, name, broadcast, providerId, slug, legacyProviderId) {
|
|
589
|
+
const provider = targetProvider(providerId);
|
|
590
|
+
const primaryProvider = legacyProvider(provider, legacyProviderId);
|
|
442
591
|
const state = this.getProjectState(projectPath);
|
|
443
592
|
const entry = state.plugins[name];
|
|
444
|
-
|
|
593
|
+
const providerState = getProviderState(entry, provider, primaryProvider);
|
|
594
|
+
if (!entry || !providerState)
|
|
445
595
|
throw new PluginNotInstalledError(name);
|
|
446
596
|
const plugin = this.registry.byName.get(name);
|
|
447
597
|
const onLog = (line) => {
|
|
@@ -449,19 +599,22 @@ class PluginManager {
|
|
|
449
599
|
type: 'plugin.install_progress',
|
|
450
600
|
projectId,
|
|
451
601
|
name,
|
|
602
|
+
providerId: provider,
|
|
452
603
|
line,
|
|
453
604
|
timestamp: new Date().toISOString(),
|
|
454
605
|
});
|
|
455
606
|
};
|
|
456
607
|
if (plugin) {
|
|
457
|
-
// Revert
|
|
608
|
+
// Revert provider instruction contributors first so a partial uninstall doesn't
|
|
458
609
|
// leave dangling instructions referencing missing tools.
|
|
459
|
-
|
|
610
|
+
if (!hasOtherActiveContributor(entry, provider)) {
|
|
611
|
+
await (0, contributors_1.revertContributors)(plugin, projectPath, provider);
|
|
612
|
+
}
|
|
460
613
|
await plugin.uninstall({
|
|
461
614
|
projectPath,
|
|
462
615
|
projectId,
|
|
463
616
|
slug,
|
|
464
|
-
providerId,
|
|
617
|
+
providerId: provider,
|
|
465
618
|
recordInstalledFile: () => { },
|
|
466
619
|
log: onLog,
|
|
467
620
|
});
|
|
@@ -472,15 +625,22 @@ class PluginManager {
|
|
|
472
625
|
// mcpServers keys were persisted in state at install time (BUG-PLUGIN-04),
|
|
473
626
|
// so even with the plugin code gone we can surgically strip the merged
|
|
474
627
|
// `.mcp.json` entries instead of leaving them loaded by Claude forever.
|
|
475
|
-
const ownedMcpServers =
|
|
628
|
+
const ownedMcpServers = providerState.ownedMcpServers ?? [];
|
|
476
629
|
if (ownedMcpServers.length > 0) {
|
|
477
630
|
try {
|
|
478
|
-
await PluginManager.removeMcpServers(projectPath, ownedMcpServers);
|
|
631
|
+
await PluginManager.removeMcpServers(projectPath, ownedMcpServers, provider);
|
|
479
632
|
}
|
|
480
633
|
catch { /* best-effort: leave .mcp.json untouched on failure */ }
|
|
481
634
|
}
|
|
482
635
|
const root = path_1.default.resolve(projectPath);
|
|
483
|
-
|
|
636
|
+
const remainingFiles = new Set(Object.entries(entry.providers ?? {})
|
|
637
|
+
.filter(([candidate]) => candidate !== provider)
|
|
638
|
+
.flatMap(([, candidateState]) => candidateState.installedFiles ?? []));
|
|
639
|
+
for (const rel of providerState.installedFiles ?? []) {
|
|
640
|
+
// A managed contributor path may be referenced by more than one
|
|
641
|
+
// provider. Never unlink a file still owned by another provider install.
|
|
642
|
+
if (remainingFiles.has(rel))
|
|
643
|
+
continue;
|
|
484
644
|
const abs = path_1.default.resolve(projectPath, rel);
|
|
485
645
|
// M5: installedFiles comes from state.json, which a hostile repo can
|
|
486
646
|
// ship. Without containment, `rel` of "../../../Users/victim/x" (or an
|
|
@@ -499,12 +659,29 @@ class PluginManager {
|
|
|
499
659
|
}
|
|
500
660
|
}
|
|
501
661
|
await this.lockedUpdateState(projectPath, (s) => {
|
|
502
|
-
|
|
662
|
+
const current = s.plugins[name];
|
|
663
|
+
if (!current)
|
|
664
|
+
return false;
|
|
665
|
+
if (!current.providers && provider === primaryProvider) {
|
|
666
|
+
delete s.plugins[name];
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const providers = ensureProviderStates(current, primaryProvider);
|
|
670
|
+
if (!providers[provider])
|
|
671
|
+
return false;
|
|
672
|
+
delete providers[provider];
|
|
673
|
+
if (Object.keys(providers).length === 0) {
|
|
674
|
+
delete s.plugins[name];
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
syncAggregateState(current);
|
|
678
|
+
}
|
|
503
679
|
});
|
|
504
680
|
broadcast({
|
|
505
681
|
type: 'plugin.uninstalled',
|
|
506
682
|
projectId,
|
|
507
683
|
name,
|
|
684
|
+
providerId: provider,
|
|
508
685
|
timestamp: new Date().toISOString(),
|
|
509
686
|
});
|
|
510
687
|
}
|
|
@@ -514,29 +691,65 @@ class PluginManager {
|
|
|
514
691
|
* `owns.mcpServers` keys are touched; user entries are preserved. Used to
|
|
515
692
|
* resolve drift surfaced by `updateAvailable`.
|
|
516
693
|
*/
|
|
517
|
-
async updateMcpEntry(projectPath, projectId, name, broadcast, providerId) {
|
|
694
|
+
async updateMcpEntry(projectPath, projectId, name, broadcast, providerId, slug, legacyProviderId) {
|
|
518
695
|
const plugin = this.registry.byName.get(name);
|
|
519
696
|
if (!plugin)
|
|
520
697
|
throw new PluginNotFoundError(name);
|
|
698
|
+
const provider = targetProvider(providerId);
|
|
699
|
+
const primaryProvider = legacyProvider(provider, legacyProviderId);
|
|
521
700
|
const state = this.getProjectState(projectPath);
|
|
522
|
-
|
|
701
|
+
const entry = state.plugins[name];
|
|
702
|
+
if (!getProviderState(entry, provider, primaryProvider)) {
|
|
523
703
|
throw new PluginNotInstalledError(name);
|
|
704
|
+
}
|
|
524
705
|
const expected = plugin.expectedMcpEntry?.();
|
|
525
706
|
if (!expected) {
|
|
526
707
|
throw new PluginInstallError(`'${name}' does not declare expectedMcpEntry; cannot update`);
|
|
527
708
|
}
|
|
528
709
|
const owned = plugin.manifest.owns.mcpServers ?? [];
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
710
|
+
if ((0, providers_1.hasAdapter)(provider) && (0, providers_1.getAdapter)(provider).mcpRegistration === 'cli-add') {
|
|
711
|
+
const lifecycle = {
|
|
712
|
+
projectPath,
|
|
713
|
+
projectId,
|
|
714
|
+
slug,
|
|
715
|
+
providerId: provider,
|
|
716
|
+
recordInstalledFile: () => { },
|
|
717
|
+
log: () => { },
|
|
718
|
+
};
|
|
719
|
+
await plugin.uninstall(lifecycle);
|
|
720
|
+
await plugin.install(lifecycle);
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
const entries = {};
|
|
724
|
+
for (const key of owned)
|
|
725
|
+
entries[key] = expected;
|
|
726
|
+
await PluginManager.mergeMcpServers(projectPath, entries, provider);
|
|
727
|
+
}
|
|
533
728
|
// Refresh shared-file contributions too: a drift may exist in CLAUDE.md
|
|
534
729
|
// even when the .mcp.json entry matches.
|
|
535
|
-
await (0, contributors_1.applyContributors)(plugin, projectPath,
|
|
730
|
+
await (0, contributors_1.applyContributors)(plugin, projectPath, provider);
|
|
731
|
+
await this.lockedUpdateState(projectPath, (s) => {
|
|
732
|
+
const current = s.plugins[name];
|
|
733
|
+
if (!current)
|
|
734
|
+
return false;
|
|
735
|
+
if (!current.providers && provider === primaryProvider) {
|
|
736
|
+
current.health = 'unknown';
|
|
737
|
+
delete current.healthReason;
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const providerState = getProviderState(current, provider, primaryProvider);
|
|
741
|
+
if (!providerState)
|
|
742
|
+
return false;
|
|
743
|
+
const mutable = ensureProviderStates(current, primaryProvider)[provider];
|
|
744
|
+
mutable.health = 'unknown';
|
|
745
|
+
delete mutable.healthReason;
|
|
746
|
+
syncAggregateState(current);
|
|
747
|
+
});
|
|
536
748
|
broadcast({
|
|
537
749
|
type: 'plugin.health_changed',
|
|
538
750
|
projectId,
|
|
539
751
|
name,
|
|
752
|
+
providerId: provider,
|
|
540
753
|
status: 'unknown',
|
|
541
754
|
reason: 'updated',
|
|
542
755
|
timestamp: new Date().toISOString(),
|
|
@@ -556,13 +769,17 @@ class PluginManager {
|
|
|
556
769
|
* Plugin install state survives across toggles. Uninstall is the only
|
|
557
770
|
* action that clears state.json + custom-*.md fragments.
|
|
558
771
|
*/
|
|
559
|
-
async setActive(projectPath, projectId, name, active, broadcast, providerId) {
|
|
772
|
+
async setActive(projectPath, projectId, name, active, broadcast, providerId, slug, legacyProviderId) {
|
|
560
773
|
const plugin = this.registry.byName.get(name);
|
|
561
774
|
if (!plugin)
|
|
562
775
|
throw new PluginNotFoundError(name);
|
|
776
|
+
const provider = targetProvider(providerId);
|
|
777
|
+
const primaryProvider = legacyProvider(provider, legacyProviderId);
|
|
563
778
|
const state = this.getProjectState(projectPath);
|
|
564
|
-
|
|
779
|
+
const entry = state.plugins[name];
|
|
780
|
+
if (!entry || !getProviderState(entry, provider, primaryProvider)) {
|
|
565
781
|
throw new PluginNotInstalledError(name);
|
|
782
|
+
}
|
|
566
783
|
const owned = plugin.manifest.owns.mcpServers ?? [];
|
|
567
784
|
if (owned.length === 0) {
|
|
568
785
|
throw new PluginInstallError(`'${name}' owns no mcpServers; cannot toggle activation`);
|
|
@@ -572,47 +789,100 @@ class PluginManager {
|
|
|
572
789
|
if (!expected) {
|
|
573
790
|
throw new PluginInstallError(`'${name}' does not declare expectedMcpEntry; cannot activate`);
|
|
574
791
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
792
|
+
if ((0, providers_1.hasAdapter)(provider) && (0, providers_1.getAdapter)(provider).mcpRegistration === 'cli-add') {
|
|
793
|
+
await plugin.install({
|
|
794
|
+
projectPath,
|
|
795
|
+
projectId,
|
|
796
|
+
slug,
|
|
797
|
+
providerId: provider,
|
|
798
|
+
recordInstalledFile: () => { },
|
|
799
|
+
log: () => { },
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
const entries = {};
|
|
804
|
+
for (const k of owned)
|
|
805
|
+
entries[k] = expected;
|
|
806
|
+
await PluginManager.mergeMcpServers(projectPath, entries, provider);
|
|
807
|
+
}
|
|
808
|
+
await (0, contributors_1.applyContributors)(plugin, projectPath, provider);
|
|
580
809
|
}
|
|
581
810
|
else {
|
|
582
|
-
|
|
583
|
-
|
|
811
|
+
if ((0, providers_1.hasAdapter)(provider) && (0, providers_1.getAdapter)(provider).mcpRegistration === 'cli-add') {
|
|
812
|
+
await plugin.uninstall({
|
|
813
|
+
projectPath,
|
|
814
|
+
projectId,
|
|
815
|
+
slug,
|
|
816
|
+
providerId: provider,
|
|
817
|
+
recordInstalledFile: () => { },
|
|
818
|
+
log: () => { },
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
await PluginManager.removeMcpServers(projectPath, owned, provider);
|
|
823
|
+
}
|
|
824
|
+
if (!hasOtherActiveContributor(entry, provider)) {
|
|
825
|
+
await (0, contributors_1.revertContributors)(plugin, projectPath, provider);
|
|
826
|
+
}
|
|
584
827
|
}
|
|
828
|
+
await this.lockedUpdateState(projectPath, (s) => {
|
|
829
|
+
const current = s.plugins[name];
|
|
830
|
+
if (!current)
|
|
831
|
+
return false;
|
|
832
|
+
if (!current.providers && provider === primaryProvider) {
|
|
833
|
+
// Legacy state has no explicit active field. Materialise only when a
|
|
834
|
+
// value is needed, preserving legacy reads while making the toggle
|
|
835
|
+
// durable.
|
|
836
|
+
const providerState = ensureProviderStates(current, primaryProvider)[provider];
|
|
837
|
+
providerState.active = active;
|
|
838
|
+
providerState.health = active ? 'ok' : 'unknown';
|
|
839
|
+
delete providerState.healthReason;
|
|
840
|
+
syncAggregateState(current);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const providerState = getProviderState(current, provider, primaryProvider);
|
|
844
|
+
if (!providerState)
|
|
845
|
+
return false;
|
|
846
|
+
const mutable = ensureProviderStates(current, primaryProvider)[provider];
|
|
847
|
+
mutable.active = active;
|
|
848
|
+
mutable.health = active ? 'ok' : 'unknown';
|
|
849
|
+
delete mutable.healthReason;
|
|
850
|
+
syncAggregateState(current);
|
|
851
|
+
});
|
|
585
852
|
broadcast({
|
|
586
853
|
type: 'plugin.health_changed',
|
|
587
854
|
projectId,
|
|
588
855
|
name,
|
|
856
|
+
providerId: provider,
|
|
589
857
|
status: active ? 'ok' : 'unknown',
|
|
590
858
|
reason: active ? 'activated' : 'deactivated',
|
|
591
859
|
timestamp: new Date().toISOString(),
|
|
592
860
|
});
|
|
593
861
|
}
|
|
594
862
|
/** Drop a state.json entry for a plugin no longer in the registry. */
|
|
595
|
-
async removeOrphan(projectPath, projectId, name, broadcast) {
|
|
863
|
+
async removeOrphan(projectPath, projectId, name, broadcast, providerId, slug, legacyProviderId) {
|
|
596
864
|
if (this.registry.byName.has(name)) {
|
|
597
865
|
throw new PluginInstallError(`'${name}' is not orphan; it is still bundled. Use uninstall instead.`);
|
|
598
866
|
}
|
|
599
|
-
return this.uninstall(projectPath, projectId, name, broadcast);
|
|
867
|
+
return this.uninstall(projectPath, projectId, name, broadcast, providerId, slug, legacyProviderId);
|
|
600
868
|
}
|
|
601
869
|
// ─── Verify ────────────────────────────────────────────────────────────────
|
|
602
|
-
async verify(projectPath, projectId, name, broadcast) {
|
|
870
|
+
async verify(projectPath, projectId, name, broadcast, providerId, legacyProviderId, slug) {
|
|
603
871
|
const plugin = this.registry.byName.get(name);
|
|
604
872
|
if (!plugin)
|
|
605
873
|
throw new PluginNotFoundError(name);
|
|
606
|
-
const
|
|
607
|
-
|
|
874
|
+
const provider = targetProvider(providerId);
|
|
875
|
+
const primaryProvider = legacyProvider(provider, legacyProviderId);
|
|
876
|
+
const result = await this._runVerify(plugin, projectPath, projectId, provider, slug);
|
|
877
|
+
await this._cacheHealth(projectPath, projectId, name, result, broadcast, provider, primaryProvider);
|
|
608
878
|
return result;
|
|
609
879
|
}
|
|
610
|
-
async _runVerify(plugin, projectPath, projectId) {
|
|
880
|
+
async _runVerify(plugin, projectPath, projectId, providerId, slug) {
|
|
611
881
|
const timeout = plugin.manifest.verifyTimeoutMs ?? this._options.defaultVerifyTimeoutMs;
|
|
612
882
|
const checkedAt = new Date().toISOString();
|
|
613
883
|
try {
|
|
614
884
|
const result = await Promise.race([
|
|
615
|
-
plugin.verify({ projectPath, projectId }),
|
|
885
|
+
plugin.verify({ projectPath, projectId, providerId, slug }),
|
|
616
886
|
new Promise((resolve) => setTimeout(() => resolve({ __timeout: true }), timeout).unref?.()),
|
|
617
887
|
]);
|
|
618
888
|
if ('__timeout' in result) {
|
|
@@ -624,7 +894,9 @@ class PluginManager {
|
|
|
624
894
|
return { ok: false, reason: `verify-exception: ${err?.message ?? String(err)}`, checkedAt };
|
|
625
895
|
}
|
|
626
896
|
}
|
|
627
|
-
async _cacheHealth(projectPath, projectId, name, result, broadcast) {
|
|
897
|
+
async _cacheHealth(projectPath, projectId, name, result, broadcast, providerId, legacyProviderId) {
|
|
898
|
+
const provider = targetProvider(providerId);
|
|
899
|
+
const primaryProvider = legacyProvider(provider, legacyProviderId);
|
|
628
900
|
const newHealth = result.ok ? 'ok' : 'degraded';
|
|
629
901
|
// Read → compare → write under ONE lock so concurrent verifies on the same
|
|
630
902
|
// project (resolvePluginsForSpawn runs every installed plugin's verify in
|
|
@@ -636,11 +908,22 @@ class PluginManager {
|
|
|
636
908
|
const entry = state.plugins[name];
|
|
637
909
|
if (!entry)
|
|
638
910
|
return false;
|
|
639
|
-
const
|
|
911
|
+
const providerState = getProviderState(entry, provider, primaryProvider);
|
|
912
|
+
if (!providerState)
|
|
913
|
+
return false;
|
|
914
|
+
const changed = providerState.health !== newHealth || providerState.healthReason !== result.reason;
|
|
640
915
|
if (!changed)
|
|
641
916
|
return false; // nothing to persist — avoids per-spawn write churn (verify runs on every rail spawn)
|
|
642
|
-
entry.
|
|
643
|
-
|
|
917
|
+
if (!entry.providers && provider === primaryProvider) {
|
|
918
|
+
entry.health = newHealth;
|
|
919
|
+
entry.healthReason = result.reason;
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
const mutable = ensureProviderStates(entry, primaryProvider)[provider];
|
|
923
|
+
mutable.health = newHealth;
|
|
924
|
+
mutable.healthReason = result.reason;
|
|
925
|
+
syncAggregateState(entry);
|
|
926
|
+
}
|
|
644
927
|
didChange = true;
|
|
645
928
|
});
|
|
646
929
|
if (!didChange)
|
|
@@ -650,6 +933,7 @@ class PluginManager {
|
|
|
650
933
|
type: 'plugin.health_changed',
|
|
651
934
|
projectId,
|
|
652
935
|
name,
|
|
936
|
+
providerId: provider,
|
|
653
937
|
status: newHealth,
|
|
654
938
|
reason: result.reason,
|
|
655
939
|
timestamp: new Date().toISOString(),
|
|
@@ -659,8 +943,9 @@ class PluginManager {
|
|
|
659
943
|
}
|
|
660
944
|
// ─── Surgical helpers exposed to plugins ───────────────────────────────────
|
|
661
945
|
/** Helper: surgically merge `mcpServers.<key>` entries into .mcp.json. */
|
|
662
|
-
static async mergeMcpServers(projectPath, entries) {
|
|
663
|
-
|
|
946
|
+
static async mergeMcpServers(projectPath, entries, providerId) {
|
|
947
|
+
const mcpFile = providerMcpJsonPath(projectPath, providerId);
|
|
948
|
+
await (0, json_mutation_1.surgicalMergeJson)(mcpFile, (current) => {
|
|
664
949
|
const next = (current ?? {});
|
|
665
950
|
// BUG-PLUGIN-05: `mcpServers` MUST be a plain object. The old `?? {}`
|
|
666
951
|
// fallback only covered null/undefined — if it was a JSON array (or any
|
|
@@ -673,7 +958,7 @@ class PluginManager {
|
|
|
673
958
|
raw === null ||
|
|
674
959
|
(typeof raw === 'object' && !Array.isArray(raw));
|
|
675
960
|
if (!isPlainObject) {
|
|
676
|
-
throw new PluginInstallError(`cannot merge mcpServers into '${
|
|
961
|
+
throw new PluginInstallError(`cannot merge mcpServers into '${mcpFile}': ` +
|
|
677
962
|
`'mcpServers' must be a JSON object but is ${Array.isArray(raw) ? 'an array' : typeof raw}; fix it first.`);
|
|
678
963
|
}
|
|
679
964
|
const servers = (raw ?? {});
|
|
@@ -684,10 +969,10 @@ class PluginManager {
|
|
|
684
969
|
});
|
|
685
970
|
}
|
|
686
971
|
/** Helper: remove specific `mcpServers.<key>` entries from .mcp.json. */
|
|
687
|
-
static async removeMcpServers(projectPath, keys) {
|
|
972
|
+
static async removeMcpServers(projectPath, keys, providerId) {
|
|
688
973
|
if (keys.length === 0)
|
|
689
974
|
return;
|
|
690
|
-
await (0, json_mutation_1.surgicalRemoveKeys)((
|
|
975
|
+
await (0, json_mutation_1.surgicalRemoveKeys)(providerMcpJsonPath(projectPath, providerId), keys.map((k) => `mcpServers.${k}`));
|
|
691
976
|
}
|
|
692
977
|
}
|
|
693
978
|
exports.PluginManager = PluginManager;
|