coding-tool-x 3.8.4 → 3.8.6
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/CHANGELOG.md +15 -0
- package/README.md +86 -15
- package/dist/web/assets/AgentsDrawer-Db9wAyaZ.js +1 -0
- package/dist/web/assets/AgentsDrawer-Pw2k-ZAa.css +1 -0
- package/dist/web/assets/Analytics-C4gMzv7g.js +1 -0
- package/dist/web/assets/Analytics-CyHhR126.css +1 -0
- package/dist/web/assets/AssetPathField-Bd_nMFu3.js +1 -0
- package/dist/web/assets/ChatHistoryDrawer-B2qBQs9L.css +1 -0
- package/dist/web/assets/ChatHistoryDrawer-CwzJHyNP.js +1 -0
- package/dist/web/assets/CollapseItem-CTCfSiue.js +1 -0
- package/dist/web/assets/CommandsDrawer-BZJxlPvV.css +1 -0
- package/dist/web/assets/CommandsDrawer-vDghB1U9.js +1 -0
- package/dist/web/assets/ConfigExportDrawer-C-XDvlHc.js +1 -0
- package/dist/web/assets/ConfigExportDrawer-dwruRKWr.css +1 -0
- package/dist/web/assets/ConfigTemplates-CYY8XRED.js +1 -0
- package/dist/web/assets/ConfigTemplatesDrawer-BfDi8yO3.css +1 -0
- package/dist/web/assets/ConfigTemplatesDrawer-CbsJp76X.js +1 -0
- package/dist/web/assets/ConfigTemplatesPanel-CWyPtk67.css +1 -0
- package/dist/web/assets/ConfigTemplatesPanel-liMSTh7s.js +1 -0
- package/dist/web/assets/Divider-D28g1gwj.js +1 -0
- package/dist/web/assets/DrawerContent-ChXgZ1Xm.js +1 -0
- package/dist/web/assets/FavoritesDrawer-CG9eI9Ay.css +1 -0
- package/dist/web/assets/FavoritesDrawer-D6QSiYaL.js +1 -0
- package/dist/web/assets/GatewayConvertDrawer-CuNlf7AC.css +1 -0
- package/dist/web/assets/GatewayConvertDrawer-RvET6iNG.js +1 -0
- package/dist/web/assets/Home-CSuFuJ9Y.css +1 -0
- package/dist/web/assets/Home-P8eseZcf.js +1 -0
- package/dist/web/assets/MarkdownEditor-CYmLGYju.js +1 -0
- package/dist/web/assets/MarkdownEditor-qfVgSre_.css +1 -0
- package/dist/web/assets/McpDrawer-C0dnsZhQ.css +1 -0
- package/dist/web/assets/McpDrawer-Ch3bkKIq.js +1 -0
- package/dist/web/assets/McpFormDrawer-BRbDcawn.css +1 -0
- package/dist/web/assets/McpFormDrawer-BcTlgSpN.js +1 -0
- package/dist/web/assets/PluginManager-Dh2o0xkZ.js +1 -0
- package/dist/web/assets/PluginsDrawer-DM3QjprE.css +1 -0
- package/dist/web/assets/PluginsDrawer-deKQ5u3I.js +1 -0
- package/dist/web/assets/PluginsPanel-BofsgQgj.js +1 -0
- package/dist/web/assets/PluginsPanel-D6ZqITcR.css +1 -0
- package/dist/web/assets/ProjectList-677RLZ8O.css +1 -0
- package/dist/web/assets/ProjectList-DhSWJHTA.js +1 -0
- package/dist/web/assets/PromptsDrawer-BIgiYCEQ.css +1 -0
- package/dist/web/assets/PromptsDrawer-Cfb2FeeU.js +1 -0
- package/dist/web/assets/RecentSessionsDrawer-3eXamJhr.js +1 -0
- package/dist/web/assets/RecentSessionsDrawer-CKFsOYDM.css +1 -0
- package/dist/web/assets/SessionCard-CW0eHURI.js +1 -0
- package/dist/web/assets/SessionCard-aM2NHu6i.css +1 -0
- package/dist/web/assets/SessionList-BpYRz1cX.css +1 -0
- package/dist/web/assets/SessionList-DQfyDGWU.js +1 -0
- package/dist/web/assets/SettingsDrawer-Dw5NUgHC.css +1 -0
- package/dist/web/assets/SettingsDrawer-u_vwUuPS.js +1 -0
- package/dist/web/assets/SkillManager-C0GCT2Kb.css +1 -0
- package/dist/web/assets/SkillManager-DHMprJLA.js +1 -0
- package/dist/web/assets/SkillsDrawer-BCNme-aJ.css +1 -0
- package/dist/web/assets/SkillsDrawer-CkF3BpyC.js +1 -0
- package/dist/web/assets/SkillsPanel-B3Nm5j32.css +1 -0
- package/dist/web/assets/SkillsPanel-DWE4pFGx.js +1 -0
- package/dist/web/assets/SpeedTestDrawer-BgfG1Ked.js +1 -0
- package/dist/web/assets/SpeedTestDrawer-Dc0WToJ_.css +1 -0
- package/dist/web/assets/Upload-CNwaGOws.js +1 -0
- package/dist/web/assets/WorkspaceDrawer-Br5CeCzL.js +1 -0
- package/dist/web/assets/WorkspaceDrawer-CGnMu9p1.css +1 -0
- package/dist/web/assets/WorkspaceManager-B2O8-qIw.js +1 -0
- package/dist/web/assets/WorkspacePanel-5oDKit3c.js +1 -0
- package/dist/web/assets/WorkspacePanel-D0-zHTHP.css +1 -0
- package/dist/web/assets/_plugin-vue_export-helper-BeLZJZij.js +1 -0
- package/dist/web/assets/client-UDwXBP9i.js +1 -0
- package/dist/web/assets/clipboard-B46yyoz_.js +1 -0
- package/dist/web/assets/headers-cf-QapUq.js +1 -0
- package/dist/web/assets/icons-B2Vg-IOC.js +1 -0
- package/dist/web/assets/index-BMAUPq5K.js +2 -0
- package/dist/web/assets/index-Bi1MQHYX.css +1 -0
- package/dist/web/assets/light-Cp0t7v32.js +1 -0
- package/dist/web/assets/markdown-u082bbtj.js +1 -0
- package/dist/web/assets/platforms-CBkB45JP.js +1 -0
- package/dist/web/assets/platforms-FTpjxAT6.js +1 -0
- package/dist/web/assets/project-config-B2baPzYL.js +1 -0
- package/dist/web/assets/sessions-Cmy31slD.js +1 -0
- package/dist/web/assets/sessions-yC_oVVh8.js +1 -0
- package/dist/web/assets/skill-repo-input-B_fkGlUK.js +1 -0
- package/dist/web/assets/use-lock-html-scroll-DXIqmUW4.js +1 -0
- package/dist/web/assets/use-message-DoLM8C1C.js +1 -0
- package/dist/web/assets/useResponsiveDrawer-jbmqZuJs.js +1 -0
- package/dist/web/assets/vendors-D0c_0KLP.js +1 -0
- package/dist/web/assets/vue-vendor-BqZbbXHA.js +1 -0
- package/dist/web/assets/workspaces-mjyPVGS8.js +1 -0
- package/dist/web/assets/xml-CgY3RMlX.js +1 -0
- package/dist/web/index.html +12 -8
- package/docs/native-log-memory-2026-09-16.md +43 -0
- package/docs/superpowers/plans/2026-08-26-configuration-driven-cli-platform.md +1578 -0
- package/docs/superpowers/plans/2026-08-30-project-scoped-config-control.md +1260 -0
- package/docs/superpowers/plans/2026-09-01-omp-skill-refresh-boundary.md +213 -0
- package/docs/superpowers/plans/2026-09-01-realtime-logs-panel.md +125 -0
- package/docs/superpowers/plans/2026-09-02-config-driven-platform-boundary.md +86 -0
- package/docs/superpowers/plans/2026-09-02-platform-implementation-drivers.md +508 -0
- package/docs/superpowers/plans/2026-09-03-cli-platform-correctness-boundary.md +760 -0
- package/docs/superpowers/plans/2026-09-03-dependency-security-remediation.md +133 -0
- package/docs/superpowers/plans/2026-09-04-model-catalog-pricing-sync.md +1078 -0
- package/docs/superpowers/plans/2026-09-05-codex-project-scan-500.md +128 -0
- package/docs/superpowers/plans/2026-09-06-project-skill-path-fix.md +97 -0
- package/docs/superpowers/specs/2026-08-26-configuration-driven-cli-platform-design.md +408 -0
- package/docs/superpowers/specs/2026-08-30-project-scoped-config-control-design.md +467 -0
- package/docs/superpowers/specs/2026-09-01-claude-session-content-repair-design.md +111 -0
- package/docs/superpowers/specs/2026-09-01-omp-skill-refresh-boundary-design.md +83 -0
- package/docs/superpowers/specs/2026-09-01-realtime-logs-panel-design.md +27 -0
- package/docs/superpowers/specs/2026-09-02-config-driven-platform-boundary-design.md +64 -0
- package/docs/superpowers/specs/2026-09-02-platform-implementation-driver-boundary-design.md +123 -0
- package/docs/superpowers/specs/2026-09-03-cli-platform-correctness-boundary-design.md +249 -0
- package/docs/superpowers/specs/2026-09-03-dependency-security-remediation-design.md +87 -0
- package/docs/superpowers/specs/2026-09-04-model-catalog-pricing-sync-design.md +301 -0
- package/docs/superpowers/specs/2026-09-05-codex-project-scan-500-design.md +24 -0
- package/package.json +8 -4
- package/src/commands/channels.js +88 -191
- package/src/commands/cli-type.js +19 -11
- package/src/commands/daemon.js +104 -32
- package/src/commands/doctor.js +16 -9
- package/src/commands/export-config.js +23 -5
- package/src/commands/list.js +130 -52
- package/src/commands/logs.js +44 -27
- package/src/commands/platform-command-registry.js +78 -0
- package/src/commands/plugin.js +11 -5
- package/src/commands/port-config.js +34 -52
- package/src/commands/proxy-control.js +207 -145
- package/src/commands/proxy.js +2 -2
- package/src/commands/resume.js +87 -91
- package/src/commands/search.js +137 -67
- package/src/commands/stats.js +30 -22
- package/src/commands/switch.js +1 -1
- package/src/commands/toggle-proxy.js +94 -111
- package/src/commands/ui.js +67 -6
- package/src/commands/workspace.js +90 -8
- package/src/config/default.js +6 -53
- package/src/config/loader.js +61 -24
- package/src/config/model-metadata.js +49 -59
- package/src/config/model-metadata.json +4095 -99
- package/src/config/paths.js +181 -8
- package/src/index.js +134 -71
- package/src/platforms/access.js +196 -0
- package/src/{server/services → platforms}/agents-service.js +354 -197
- package/src/platforms/channel-auth-registry.js +165 -0
- package/src/platforms/channel-auth-service.js +107 -0
- package/src/{server/services → platforms}/commands-service.js +195 -115
- package/src/platforms/driver-registry.js +59 -0
- package/src/platforms/drivers/claude/api-operations.js +62 -0
- package/src/{server/services/channels.js → platforms/drivers/claude/channels-implementation.js} +131 -33
- package/src/platforms/drivers/claude/channels.js +39 -0
- package/src/{server/services/config-sync-service.js → platforms/drivers/claude/config-sync.js} +29 -5
- package/src/{server/services → platforms/drivers/claude}/health-check.js +4 -4
- package/src/platforms/drivers/claude/health.js +22 -0
- package/src/platforms/drivers/claude/mcp.js +9 -0
- package/src/{server/services/settings-manager.js → platforms/drivers/claude/native-config-implementation.js} +13 -4
- package/src/platforms/drivers/claude/native-config.js +46 -0
- package/src/platforms/drivers/claude/native-logs.js +72 -0
- package/src/{server/services/claude-openai-gateway.js → platforms/drivers/claude/openai-gateway.js} +8 -65
- package/src/platforms/drivers/claude/project-config.js +21 -0
- package/src/platforms/drivers/claude/projects.js +62 -0
- package/src/platforms/drivers/claude/prompts.js +9 -0
- package/src/{server/proxy-server.js → platforms/drivers/claude/proxy-implementation.js} +41 -278
- package/src/platforms/drivers/claude/proxy.js +16 -0
- package/src/platforms/drivers/claude/resource-sync.js +9 -0
- package/src/platforms/drivers/claude/session-history-adapter.js +313 -0
- package/src/{server/services/sessions.js → platforms/drivers/claude/sessions-implementation.js} +145 -35
- package/src/platforms/drivers/claude/sessions.js +76 -0
- package/src/{server/services/claude-statistics-service.js → platforms/drivers/claude/statistics-implementation.js} +2 -2
- package/src/platforms/drivers/claude/statistics.js +16 -0
- package/src/{server/services/claude-wire.js → platforms/drivers/claude/wire.js} +1 -1
- package/src/platforms/drivers/codex/api-operations.js +9 -0
- package/src/{server/services/codex-channels.js → platforms/drivers/codex/channels-implementation.js} +149 -14
- package/src/platforms/drivers/codex/channels.js +45 -0
- package/src/{server/services/codex-config.js → platforms/drivers/codex/config.js} +12 -2
- package/src/{server/services/codex-env-manager.js → platforms/drivers/codex/env-manager.js} +1 -1
- package/src/platforms/drivers/codex/mcp.js +9 -0
- package/src/{server/services/codex-settings-manager.js → platforms/drivers/codex/native-config-implementation.js} +15 -6
- package/src/platforms/drivers/codex/native-config.js +47 -0
- package/src/platforms/drivers/codex/native-logs.js +166 -0
- package/src/platforms/drivers/codex/project-config.js +21 -0
- package/src/platforms/drivers/codex/projects.js +26 -0
- package/src/platforms/drivers/codex/prompts.js +9 -0
- package/src/{server/codex-proxy-server.js → platforms/drivers/codex/proxy-implementation.js} +30 -233
- package/src/platforms/drivers/codex/proxy.js +16 -0
- package/src/platforms/drivers/codex/resource-sync.js +9 -0
- package/src/{server/services/session-history-adapters/codex.js → platforms/drivers/codex/session-history-adapter.js} +37 -23
- package/src/{server/services/codex-sessions.js → platforms/drivers/codex/sessions-implementation.js} +81 -75
- package/src/platforms/drivers/codex/sessions.js +36 -0
- package/src/{server/services/codex-statistics-service.js → platforms/drivers/codex/statistics-implementation.js} +2 -2
- package/src/platforms/drivers/codex/statistics.js +16 -0
- package/src/platforms/drivers/gemini/api-operations.js +9 -0
- package/src/{server/services/gemini-channels.js → platforms/drivers/gemini/channels-implementation.js} +202 -37
- package/src/platforms/drivers/gemini/channels.js +39 -0
- package/src/{server/services/gemini-config.js → platforms/drivers/gemini/config.js} +12 -2
- package/src/platforms/drivers/gemini/mcp.js +9 -0
- package/src/{server/services/gemini-settings-manager.js → platforms/drivers/gemini/native-config-implementation.js} +14 -5
- package/src/platforms/drivers/gemini/native-config.js +39 -0
- package/src/platforms/drivers/gemini/native-logs.js +180 -0
- package/src/platforms/drivers/gemini/project-config.js +21 -0
- package/src/platforms/drivers/gemini/projects.js +26 -0
- package/src/platforms/drivers/gemini/prompts.js +9 -0
- package/src/{server/gemini-proxy-server.js → platforms/drivers/gemini/proxy-implementation.js} +36 -267
- package/src/platforms/drivers/gemini/proxy.js +16 -0
- package/src/platforms/drivers/gemini/resource-sync.js +9 -0
- package/src/{server/services/session-history-adapters/gemini.js → platforms/drivers/gemini/session-history-adapter.js} +70 -18
- package/src/{server/services/gemini-sessions.js → platforms/drivers/gemini/sessions-implementation.js} +53 -37
- package/src/platforms/drivers/gemini/sessions.js +47 -0
- package/src/{server/services/gemini-statistics-service.js → platforms/drivers/gemini/statistics-implementation.js} +2 -2
- package/src/platforms/drivers/gemini/statistics.js +16 -0
- package/src/{server/services/gemini-wire.js → platforms/drivers/gemini/wire.js} +1 -1
- package/src/platforms/drivers/generic-filesystem.js +166 -0
- package/src/platforms/drivers/generic-jsonl.js +141 -0
- package/src/platforms/drivers/generic-mcp.js +44 -0
- package/src/platforms/drivers/generic-openai-compatible.js +45 -0
- package/src/platforms/drivers/generic-prompt.js +41 -0
- package/src/platforms/drivers/legacy.js +677 -0
- package/src/platforms/drivers/native-config-snapshot.js +125 -0
- package/src/platforms/drivers/native-log-utils.js +1292 -0
- package/src/platforms/drivers/notification-hooks.js +578 -0
- package/src/{server/api/omp-config.js → platforms/drivers/omp/api-operations.js} +52 -64
- package/src/{server/services/omp-auth-provider-worker.js → platforms/drivers/omp/auth-provider-worker.js} +1 -1
- package/src/{server/services/omp-auth-providers.js → platforms/drivers/omp/auth-providers.js} +1 -1
- package/src/{server/services/omp-channels.js → platforms/drivers/omp/channels-implementation.js} +166 -39
- package/src/platforms/drivers/omp/channels.js +58 -0
- package/src/{server/services/omp-config.js → platforms/drivers/omp/config.js} +53 -13
- package/src/{server/services/omp-gateway-routing.js → platforms/drivers/omp/gateway-routing.js} +14 -7
- package/src/{server/services/omp-gateway.js → platforms/drivers/omp/gateway.js} +4 -262
- package/src/platforms/drivers/omp/mcp.js +9 -0
- package/src/{server/services/omp-settings-manager.js → platforms/drivers/omp/native-config-implementation.js} +75 -10
- package/src/platforms/drivers/omp/native-config.js +46 -0
- package/src/platforms/drivers/omp/native-logs.js +52 -0
- package/src/{server/services/omp-native-plugin-adapter.js → platforms/drivers/omp/native-plugin-adapter.js} +91 -4
- package/src/platforms/drivers/omp/project-config.js +29 -0
- package/src/platforms/drivers/omp/projects.js +20 -0
- package/src/platforms/drivers/omp/prompts.js +9 -0
- package/src/{server/omp-proxy-server.js → platforms/drivers/omp/proxy-implementation.js} +6 -15
- package/src/platforms/drivers/omp/proxy.js +16 -0
- package/src/platforms/drivers/omp/resource-sync.js +9 -0
- package/src/{server/services/session-history-adapters/omp.js → platforms/drivers/omp/session-history-adapter.js} +42 -18
- package/src/{server/services/omp-sessions.js → platforms/drivers/omp/sessions-implementation.js} +130 -94
- package/src/platforms/drivers/omp/sessions.js +36 -0
- package/src/{server/services/omp-skill-discovery.js → platforms/drivers/omp/skill-discovery.js} +159 -32
- package/src/{server/services/omp-skill-settings-service.js → platforms/drivers/omp/skill-settings.js} +1 -1
- package/src/{server/services/omp-statistics-service.js → platforms/drivers/omp/statistics-implementation.js} +3 -3
- package/src/platforms/drivers/omp/statistics.js +16 -0
- package/src/platforms/drivers/opencode/api-operations.js +9 -0
- package/src/{server/services/opencode-channels.js → platforms/drivers/opencode/channels-implementation.js} +28 -9
- package/src/platforms/drivers/opencode/channels.js +40 -0
- package/src/platforms/drivers/opencode/conversion.js +41 -0
- package/src/{server/services/opencode-gateway-converter.js → platforms/drivers/opencode/gateway-converter.js} +1 -1
- package/src/platforms/drivers/opencode/mcp.js +9 -0
- package/src/{server/services/opencode-settings-manager.js → platforms/drivers/opencode/native-config-implementation.js} +17 -6
- package/src/platforms/drivers/opencode/native-config.js +26 -0
- package/src/platforms/drivers/opencode/native-logs.js +133 -0
- package/src/platforms/drivers/opencode/project-config.js +98 -0
- package/src/platforms/drivers/opencode/projects.js +20 -0
- package/src/platforms/drivers/opencode/prompts.js +9 -0
- package/src/{server/opencode-proxy-server.js → platforms/drivers/opencode/proxy-implementation.js} +27 -362
- package/src/platforms/drivers/opencode/proxy.js +16 -0
- package/src/platforms/drivers/opencode/resource-sync.js +9 -0
- package/src/{server/services/opencode-sessions.js → platforms/drivers/opencode/sessions-implementation.js} +211 -64
- package/src/platforms/drivers/opencode/sessions.js +48 -0
- package/src/{server/services/opencode-statistics-service.js → platforms/drivers/opencode/statistics-implementation.js} +2 -2
- package/src/platforms/drivers/opencode/statistics.js +16 -0
- package/src/platforms/drivers/secure-file-driver.js +272 -0
- package/src/platforms/drivers/unsupported.js +7 -0
- package/src/platforms/manifest-schema.js +398 -0
- package/src/platforms/manifests/claude.json +489 -0
- package/src/platforms/manifests/codex.json +448 -0
- package/src/platforms/manifests/gemini.json +447 -0
- package/src/platforms/manifests/omp.json +496 -0
- package/src/platforms/manifests/opencode.json +422 -0
- package/src/{server/services → platforms}/native-oauth-adapters.js +98 -418
- package/src/{server/services → platforms}/notification-hooks.js +190 -1023
- package/src/platforms/oauth-credentials-service.js +635 -0
- package/src/platforms/path-resolver.js +179 -0
- package/src/platforms/platform-path-context.js +305 -0
- package/src/platforms/registry.js +248 -0
- package/src/platforms/remote-notification-providers.js +678 -0
- package/src/platforms/runtime.js +259 -0
- package/src/{server/services/session-history-adapters/index.js → platforms/session-history-adapters.js} +8 -1
- package/src/plugins/plugin-loader.js +2 -2
- package/src/plugins/plugin-manager.js +2 -2
- package/src/plugins/registry.js +27 -10
- package/src/reset-config.js +21 -5
- package/src/server/api/agents.js +46 -72
- package/src/server/api/commands.js +16 -6
- package/src/server/api/config-registry.js +206 -59
- package/src/server/api/config-sync.js +3 -2
- package/src/server/api/config-templates.js +20 -4
- package/src/server/api/config.js +111 -102
- package/src/server/api/convert.js +54 -178
- package/src/server/api/dashboard.js +168 -102
- package/src/server/api/hooks.js +1 -1
- package/src/server/api/mcp.js +224 -84
- package/src/server/api/platform-api-config.js +102 -0
- package/src/server/api/platform-route-factory.js +215 -0
- package/src/server/api/platform-routes.js +26 -0
- package/src/server/api/platforms.js +110 -0
- package/src/server/api/plugins.js +57 -57
- package/src/server/api/project-config.js +168 -0
- package/src/server/api/prompts.js +58 -45
- package/src/server/api/settings.js +55 -8
- package/src/server/api/skills.js +262 -133
- package/src/server/api/ui-config.js +2 -2
- package/src/server/api/validation-errors.js +18 -4
- package/src/server/api/workspaces.js +47 -12
- package/src/server/index.js +164 -213
- package/src/server/platform-context.js +81 -0
- package/src/server/services/channel-balance.js +133 -24
- package/src/server/services/channel-health.js +21 -6
- package/src/server/services/channel-scheduler.js +29 -39
- package/src/server/services/channel-sync-utils.js +23 -13
- package/src/server/services/config-export-service.js +350 -319
- package/src/server/services/config-registry-service.js +280 -54
- package/src/server/services/config-sync-manager.js +172 -944
- package/src/server/services/config-templates-service.js +419 -353
- package/src/server/services/control-manifest-store.js +206 -0
- package/src/server/services/dashboard-snapshot-worker.js +656 -100
- package/src/server/services/effective-control-service.js +557 -0
- package/src/server/services/local-resource-index.js +314 -0
- package/src/server/services/mcp-service.js +607 -977
- package/src/server/services/model-detector.js +15 -10
- package/src/server/services/native-log-channel-resolver.js +214 -0
- package/src/server/services/native-log-observer.js +291 -0
- package/src/server/services/network-access.js +12 -2
- package/src/server/services/platform-catalog.js +82 -0
- package/src/server/services/platform-resolution.js +32 -29
- package/src/server/services/plugins-service.js +376 -122
- package/src/server/services/project-config-service.js +550 -0
- package/src/server/services/project-path-validation.js +31 -0
- package/src/server/services/prompts-service.js +267 -233
- package/src/server/services/proxy-runtime.js +7 -4
- package/src/server/services/remote-credential-cache.js +43 -0
- package/src/server/services/repo-scanner-base.js +182 -75
- package/src/server/services/request-logger.js +7 -4
- package/src/server/services/session-history-index.js +876 -214
- package/src/server/services/session-history-worker.js +228 -33
- package/src/server/services/session-snapshot-worker.js +124 -34
- package/src/server/services/session-snapshots.js +4 -2
- package/src/server/services/skill-artifact-store.js +355 -0
- package/src/server/services/skill-format-adapters.js +65 -0
- package/src/server/services/skill-projection-service.js +323 -0
- package/src/server/services/skill-refresh-task-service.js +275 -0
- package/src/server/services/skill-service.js +2020 -677
- package/src/server/services/snapshot-cache.js +30 -8
- package/src/server/services/speed-test.js +8 -8
- package/src/server/services/sqlite-connection.js +19 -3
- package/src/server/services/statistics-service.js +240 -57
- package/src/server/services/ui-config.js +107 -76
- package/src/server/services/usage-log-utils.js +98 -0
- package/src/server/services/web-build.js +1 -1
- package/src/server/services/workspace-service.js +59 -29
- package/src/server/utils/pricing.js +10 -19
- package/src/server/websocket-server.js +153 -28
- package/src/{server/services/base → shared}/base-channel-service.js +156 -28
- package/src/shared/capability-driver.js +104 -0
- package/src/shared/channel-auth-policy.js +27 -0
- package/src/shared/channel-driver.js +161 -0
- package/src/shared/driver-factories/api.js +180 -0
- package/src/shared/driver-factories/mcp.js +244 -0
- package/src/shared/driver-factories/projects.js +121 -0
- package/src/shared/driver-factories/prompts.js +58 -0
- package/src/shared/driver-factories/resource-sync.js +375 -0
- package/src/shared/driver-result.js +78 -0
- package/src/shared/mcp-format.js +189 -0
- package/src/shared/platforms.js +104 -167
- package/src/shared/project-config.js +512 -0
- package/src/shared/proxy-driver.js +57 -0
- package/src/{server/services/base → shared}/response-usage-parser.js +8 -4
- package/src/ui/menu.js +91 -67
- package/src/utils/port-helper.js +35 -19
- package/src/utils/session.js +1 -1
- package/dist/web/assets/Analytics-COEUXmd4.js +0 -25
- package/dist/web/assets/Analytics-RNn1BUbG.css +0 -1
- package/dist/web/assets/ConfigTemplates-DLLkS53H.js +0 -1
- package/dist/web/assets/Home-DCEyLzwX.js +0 -1
- package/dist/web/assets/Home-DYVy4cGZ.css +0 -1
- package/dist/web/assets/PluginManager-Oe1ZHoMO.js +0 -1
- package/dist/web/assets/ProjectList-CWjuAFof.js +0 -1
- package/dist/web/assets/ProjectList-VeMNXfgO.css +0 -1
- package/dist/web/assets/SessionList-C4chCPxK.css +0 -1
- package/dist/web/assets/SessionList-v3ehGg9U.js +0 -1
- package/dist/web/assets/SkillManager-BsX8Qpj0.css +0 -1
- package/dist/web/assets/SkillManager-DUF_qP5S.js +0 -1
- package/dist/web/assets/WorkspaceManager-CjAWBosO.js +0 -1
- package/dist/web/assets/icons-nGdSKQn2.js +0 -1
- package/dist/web/assets/index-D5hyHPj7.js +0 -2
- package/dist/web/assets/index-bJRG3RXH.css +0 -1
- package/dist/web/assets/markdown-BNJfEgCA.js +0 -1
- package/dist/web/assets/markdown-BfC0goYb.css +0 -10
- package/dist/web/assets/naive-ui-BALVpMgd.js +0 -1
- package/dist/web/assets/vendors-DQA4Hw9v.js +0 -7
- package/dist/web/assets/vue-vendor-B5RqjEjQ.js +0 -45
- package/src/config/model-pricing.js +0 -35
- package/src/server/api/channels.js +0 -439
- package/src/server/api/claude-hooks.js +0 -56
- package/src/server/api/claude-statistics.js +0 -57
- package/src/server/api/codex-channels.js +0 -491
- package/src/server/api/codex-projects.js +0 -117
- package/src/server/api/codex-proxy.js +0 -293
- package/src/server/api/codex-sessions.js +0 -652
- package/src/server/api/codex-statistics.js +0 -57
- package/src/server/api/gemini-channels.js +0 -392
- package/src/server/api/gemini-projects.js +0 -103
- package/src/server/api/gemini-proxy.js +0 -231
- package/src/server/api/gemini-sessions.js +0 -550
- package/src/server/api/gemini-statistics.js +0 -57
- package/src/server/api/health-check.js +0 -31
- package/src/server/api/oauth-credentials.js +0 -198
- package/src/server/api/omp-channels.js +0 -575
- package/src/server/api/omp-projects.js +0 -88
- package/src/server/api/omp-proxy.js +0 -125
- package/src/server/api/omp-sessions.js +0 -386
- package/src/server/api/omp-statistics.js +0 -40
- package/src/server/api/opencode-channels.js +0 -471
- package/src/server/api/opencode-projects.js +0 -110
- package/src/server/api/opencode-proxy.js +0 -354
- package/src/server/api/opencode-sessions.js +0 -466
- package/src/server/api/opencode-statistics.js +0 -57
- package/src/server/api/projects.js +0 -120
- package/src/server/api/proxy.js +0 -330
- package/src/server/api/sessions.js +0 -810
- package/src/server/services/oauth-credentials-service.js +0 -945
- package/src/server/services/omp-session-log-observer.js +0 -138
- package/src/server/services/opencode-gateway-adapters.js +0 -43
- package/src/server/services/project-snapshots.js +0 -59
- package/src/server/services/proxy-log-helper.js +0 -298
- package/src/server/services/session-cache.js +0 -127
- package/src/server/services/session-converter.js +0 -582
- package/src/server/services/session-history-adapters/claude.js +0 -235
- /package/src/{server/services/claude-token-recovery.js → platforms/drivers/claude/token-recovery.js} +0 -0
- /package/src/{server/services/codex-parser.js → platforms/drivers/codex/parser.js} +0 -0
- /package/src/{server/services/codex-speed-test-template.json → platforms/drivers/codex/speed-test-template.json} +0 -0
- /package/src/{server/services/codex-wire.js → platforms/drivers/codex/wire.js} +0 -0
- /package/src/{server/services/omp-auth-gateway-client.js → platforms/drivers/omp/auth-gateway-client.js} +0 -0
- /package/src/{server/services/opencode-normalization.js → platforms/drivers/opencode/normalization.js} +0 -0
- /package/src/{server/services → shared}/config-artifact-paths.js +0 -0
- /package/src/{server/services/base → shared}/proxy-utils.js +0 -0
|
@@ -0,0 +1,1260 @@
|
|
|
1
|
+
# Project-Scoped Config Control Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** 在现有项目历史会话页中,为每个项目提供原生项目指令、Skills 和 MCP 的读取、编辑、安装、测试与移除能力。
|
|
6
|
+
|
|
7
|
+
**Architecture:** 项目原生文件是事实来源,规范化 `realpath` 是项目配置身份;工作区只负责项目分组和路径白名单。新增 `ProjectConfigService` 作为深模块,使用平台 Manifest 声明项目文件和能力,并把格式差异隔离在项目配置适配器中。现有用户级服务和 API 语义保持不变。
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node.js CommonJS、Express、`fs`、`@iarna/toml`、`js-yaml`、Vue 3、Naive UI、Pinia、Vitest、Vite。
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
**Spec:** `docs/superpowers/specs/2026-08-30-project-scoped-config-control-design.md`
|
|
14
|
+
The plan remains one end-to-end plan because all slices share the `ProjectConfigService` contract: the UI, templates and platform-specific resources cannot be independently complete without that seam. Each task still produces a focused, testable increment.
|
|
15
|
+
|
|
16
|
+
## File Map
|
|
17
|
+
|
|
18
|
+
### Platform metadata
|
|
19
|
+
|
|
20
|
+
- Modify `src/platforms/manifest-schema.js`: validate `projectResources` metadata.
|
|
21
|
+
- Modify `src/platforms/manifests/claude.json`: declare Claude project instruction, Skills and MCP locations.
|
|
22
|
+
- Modify `src/platforms/manifests/codex.json`: declare `AGENTS.md`, `.agents/skills/` and `.codex/config.toml`.
|
|
23
|
+
- Modify `src/platforms/manifests/gemini.json`: declare `GEMINI.md`, `.gemini/skills/` and `.gemini/settings.json`.
|
|
24
|
+
- Modify `src/platforms/manifests/opencode.json`: declare `.opencode/AGENTS.md`, `.opencode/skills/` and `.opencode/opencode.json`.
|
|
25
|
+
- Modify `src/platforms/manifests/omp.json`: declare `.omp/skills/`, `.omp/mcp.json` and no instruction file.
|
|
26
|
+
- Modify `src/platforms/registry.js`: expose only safe project-relative metadata through `getPublicDefinition()`.
|
|
27
|
+
|
|
28
|
+
### Project configuration module
|
|
29
|
+
|
|
30
|
+
- Create `src/server/services/project-config-adapters/shared.js`: safe project-root resolution, atomic text/JSON/TOML writes, and target containment checks.
|
|
31
|
+
- Create `src/server/services/project-config-adapters/index.js`: platform adapter registry.
|
|
32
|
+
- Create `src/server/services/project-config-adapters/claude.js`: Claude instruction, Skills and MCP format behavior.
|
|
33
|
+
- Create `src/server/services/project-config-adapters/codex.js`: Codex instruction, `.agents/skills/` and TOML MCP behavior.
|
|
34
|
+
- Create `src/server/services/project-config-adapters/gemini.js`: Gemini instruction, Skills and settings JSON behavior.
|
|
35
|
+
- Create `src/server/services/project-config-adapters/opencode.js`: OpenCode instruction, Skills and MCP schema behavior.
|
|
36
|
+
- Create `src/server/services/project-config-adapters/omp.js`: OMP Skills and MCP behavior, unsupported instruction.
|
|
37
|
+
- Create `src/server/services/project-config-service.js`: project-level application use cases and snapshot DTO.
|
|
38
|
+
- Create `tests/unit/services/project-config-service.test.js`: project path, instruction, adapter and snapshot contracts.
|
|
39
|
+
|
|
40
|
+
### Skills and MCP integration
|
|
41
|
+
|
|
42
|
+
- Modify `src/server/services/skill-service.js`: resolve project install roots and preserve scope through all Skill operations.
|
|
43
|
+
- Modify `src/server/api/skills.js`: validate and forward `scope=project&cwd` for every project Skill operation.
|
|
44
|
+
- Modify `src/web/src/api/skills.js`: send project scope options without changing user-level defaults.
|
|
45
|
+
- Modify `src/server/services/mcp-service.js`: consume extracted codecs while preserving existing global exports and behavior.
|
|
46
|
+
- Create `src/server/services/mcp-format.js`: shared generic/Codex/Gemini/OpenCode/OMP MCP conversion functions.
|
|
47
|
+
- Modify `tests/unit/services/skill-service.test.js`: project Skill scope and cache isolation.
|
|
48
|
+
- Modify `tests/unit/api/skills-api.test.js`: project Skill request validation.
|
|
49
|
+
- Modify `tests/unit/services/mcp-service.test.js`: codec compatibility and project test isolation.
|
|
50
|
+
|
|
51
|
+
### HTTP and workspace security
|
|
52
|
+
|
|
53
|
+
- Create `src/server/api/project-config.js`: aggregate, instruction and project MCP routes.
|
|
54
|
+
- Modify `src/server/index.js`: mount `/api/project-config`.
|
|
55
|
+
- Modify `src/server/services/project-path-validation.js`: lazy workspace lookup and file-under-known-root checks.
|
|
56
|
+
- Modify `src/server/api/workspaces.js`: constrain `/read-file` to known project/workspace roots.
|
|
57
|
+
- Create `tests/unit/api/project-config-api.test.js`: route status, validation and mutation contracts.
|
|
58
|
+
- Modify `tests/unit/api/workspaces-api.test.js`: reject allowed basenames outside known roots.
|
|
59
|
+
|
|
60
|
+
### Web UI
|
|
61
|
+
|
|
62
|
+
- Create `src/web/src/api/project-config.js`: aggregate, instruction and project MCP client functions.
|
|
63
|
+
- Create `src/web/src/components/ProjectConfigDrawer.vue`: project configuration shell and platform tabs.
|
|
64
|
+
- Create `src/web/src/components/ProjectInstructionPanel.vue`: instruction file editor.
|
|
65
|
+
- Create `src/web/src/components/ProjectMcpPanel.vue`: project MCP list, add/edit/test/remove actions.
|
|
66
|
+
- Modify `src/web/src/components/SkillsPanel.vue`: accept explicit `scope` and project path.
|
|
67
|
+
- Modify `src/web/src/components/SkillCreateModal.vue`: create Skills in project scope.
|
|
68
|
+
- Modify `src/web/src/components/SkillDetailDrawer.vue`: read/write project Skill files.
|
|
69
|
+
- Modify `src/web/src/components/McpFormDrawer.vue`: support a project save transport while preserving global mode.
|
|
70
|
+
- Modify `src/web/src/views/SessionList.vue`: add the project configuration entry and pass canonical project path.
|
|
71
|
+
- Create `src/web/src/components/__tests__/ProjectConfigDrawer.test.js`: drawer props, tabs and unsupported state.
|
|
72
|
+
- Create `src/web/src/components/__tests__/ProjectInstructionPanel.test.js`: instruction load/save behavior.
|
|
73
|
+
|
|
74
|
+
### Template and documentation integration
|
|
75
|
+
|
|
76
|
+
- Modify `src/server/services/config-templates-service.js`: route project template writes through `ProjectConfigService`.
|
|
77
|
+
- Modify `src/server/services/workspace-service.js`: make a newly created workspace visible to controlled template validation before applying a template, with rollback on fatal failure.
|
|
78
|
+
- Modify `tests/unit/services/config-templates-service.test.js`: template calls use project resource semantics.
|
|
79
|
+
- Modify `tests/unit/services/workspace-service.test.js`: workspace template registration and rollback.
|
|
80
|
+
- Modify `README.md`: document the project configuration entry and native project file locations.
|
|
81
|
+
|
|
82
|
+
## Shared Contracts
|
|
83
|
+
|
|
84
|
+
All tasks use these contracts. Do not introduce a second naming scheme.
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
const PROJECT_SCOPES = new Set(['user', 'project']);
|
|
88
|
+
|
|
89
|
+
// projectPath is always the canonical realpath after service validation.
|
|
90
|
+
const projectOptions = {
|
|
91
|
+
scope: 'project',
|
|
92
|
+
cwd: '/absolute/canonical/project'
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Every project resource operation returns one of these statuses.
|
|
96
|
+
const RESOURCE_STATUSES = new Set(['ok', 'unsupported', 'invalid', 'failed']);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The aggregate response is:
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
{
|
|
103
|
+
success: true,
|
|
104
|
+
projectPath: '/absolute/canonical/project',
|
|
105
|
+
platform: 'codex',
|
|
106
|
+
instruction: {
|
|
107
|
+
supported: true,
|
|
108
|
+
path: 'AGENTS.md',
|
|
109
|
+
exists: true,
|
|
110
|
+
content: '# Project rules',
|
|
111
|
+
updatedAt: 1730000000000
|
|
112
|
+
},
|
|
113
|
+
skills: {
|
|
114
|
+
supported: true,
|
|
115
|
+
project: [],
|
|
116
|
+
inherited: []
|
|
117
|
+
},
|
|
118
|
+
mcp: {
|
|
119
|
+
supported: true,
|
|
120
|
+
path: '.codex/config.toml',
|
|
121
|
+
servers: []
|
|
122
|
+
},
|
|
123
|
+
capabilities: {
|
|
124
|
+
instruction: true,
|
|
125
|
+
skills: true,
|
|
126
|
+
mcp: true
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Every implementation task follows this local loop: add a behavior test, run the focused test and observe the expected failure, implement only that behavior, rerun the focused test, then commit the task. Do not run formatters, linters, or the project-wide suite inside an individual task.
|
|
132
|
+
|
|
133
|
+
### Task 1: Add project resource metadata to platform manifests
|
|
134
|
+
|
|
135
|
+
**Files:**
|
|
136
|
+
- Modify: `src/platforms/manifest-schema.js`
|
|
137
|
+
- Modify: `src/platforms/manifests/claude.json`
|
|
138
|
+
- Modify: `src/platforms/manifests/codex.json`
|
|
139
|
+
- Modify: `src/platforms/manifests/gemini.json`
|
|
140
|
+
- Modify: `src/platforms/manifests/opencode.json`
|
|
141
|
+
- Modify: `src/platforms/manifests/omp.json`
|
|
142
|
+
- Modify: `src/platforms/registry.js`
|
|
143
|
+
- Test: `tests/unit/platforms/manifest-schema.test.js`
|
|
144
|
+
- Test: `tests/unit/platforms/registry.test.js`
|
|
145
|
+
|
|
146
|
+
- [ ] **Step 1: Write failing manifest validation tests.** Add these cases to `tests/unit/platforms/manifest-schema.test.js`:
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
test('accepts project resource metadata with safe relative paths', () => {
|
|
150
|
+
const result = validateManifest({
|
|
151
|
+
key: 'demo-cli',
|
|
152
|
+
label: 'Demo CLI',
|
|
153
|
+
command: 'demo',
|
|
154
|
+
projectResources: {
|
|
155
|
+
instruction: { path: 'AGENTS.md' },
|
|
156
|
+
skills: { canonicalRoot: '.agents/skills', readRoots: ['.agents/skills'] },
|
|
157
|
+
mcp: { path: '.codex/config.toml', format: 'codex-toml' }
|
|
158
|
+
},
|
|
159
|
+
capabilities: {}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(result.valid).toBe(true);
|
|
163
|
+
expect(result.errors).toEqual([]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('rejects malformed project resource metadata', () => {
|
|
167
|
+
const result = validateManifest({
|
|
168
|
+
key: 'unsafe-cli',
|
|
169
|
+
label: 'Unsafe CLI',
|
|
170
|
+
command: 'unsafe',
|
|
171
|
+
projectResources: {
|
|
172
|
+
instruction: { path: '../AGENTS.md' },
|
|
173
|
+
skills: { canonicalRoot: '.agents/skills', readRoots: ['.agents/skills'] },
|
|
174
|
+
mcp: { path: '.mcp.json', format: 'unknown-format' }
|
|
175
|
+
},
|
|
176
|
+
capabilities: {}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(result.valid).toBe(false);
|
|
180
|
+
expect(normalizeManifestError(result.errors)).toContain('projectResources');
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
- [ ] **Step 2: Run the focused schema test and verify it fails.**
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
npx vitest run tests/unit/platforms/manifest-schema.test.js
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Expected: FAIL because `projectResources` is currently rejected as an unknown manifest field.
|
|
191
|
+
|
|
192
|
+
- [ ] **Step 3: Add the schema contract.** In `src/platforms/manifest-schema.js`, add `projectResources` to the top-level properties with this exact shape:
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
projectResources: {
|
|
196
|
+
type: 'object',
|
|
197
|
+
additionalProperties: false,
|
|
198
|
+
required: ['instruction', 'skills', 'mcp'],
|
|
199
|
+
properties: {
|
|
200
|
+
instruction: {
|
|
201
|
+
type: 'object',
|
|
202
|
+
additionalProperties: false,
|
|
203
|
+
required: ['path'],
|
|
204
|
+
properties: {
|
|
205
|
+
path: { type: ['string', 'null'] }
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
skills: {
|
|
209
|
+
type: 'object',
|
|
210
|
+
additionalProperties: false,
|
|
211
|
+
required: ['canonicalRoot', 'readRoots'],
|
|
212
|
+
properties: {
|
|
213
|
+
canonicalRoot: { type: 'string', minLength: 1 },
|
|
214
|
+
readRoots: {
|
|
215
|
+
type: 'array',
|
|
216
|
+
minItems: 1,
|
|
217
|
+
items: { type: 'string', minLength: 1 }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
mcp: {
|
|
222
|
+
type: 'object',
|
|
223
|
+
additionalProperties: false,
|
|
224
|
+
required: ['path', 'format'],
|
|
225
|
+
properties: {
|
|
226
|
+
path: { type: ['string', 'null'] },
|
|
227
|
+
format: { enum: ['none', 'claude-json', 'codex-toml', 'gemini-json', 'opencode-json', 'omp-json'] }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Validate every non-null path with a manifest-level relative-path check in `validateManifest`; reject absolute paths, NUL bytes, `..` segments and Windows drive paths. `instruction.path` may be null only when the platform has no standalone instruction file. `mcp.path` may be null only with `format: 'none'`.
|
|
235
|
+
|
|
236
|
+
- [ ] **Step 4: Add the five built-in metadata objects.** Add these exact top-level fields to the corresponding manifest files:
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
"projectResources": {
|
|
240
|
+
"instruction": { "path": "CLAUDE.md" },
|
|
241
|
+
"skills": { "canonicalRoot": ".claude/skills", "readRoots": [".claude/skills"] },
|
|
242
|
+
"mcp": { "path": ".mcp.json", "format": "claude-json" }
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
"projectResources": {
|
|
248
|
+
"instruction": { "path": "AGENTS.md" },
|
|
249
|
+
"skills": { "canonicalRoot": ".agents/skills", "readRoots": [".agents/skills", ".codex/skills"] },
|
|
250
|
+
"mcp": { "path": ".codex/config.toml", "format": "codex-toml" }
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
```json
|
|
255
|
+
"projectResources": {
|
|
256
|
+
"instruction": { "path": "GEMINI.md" },
|
|
257
|
+
"skills": { "canonicalRoot": ".gemini/skills", "readRoots": [".gemini/skills"] },
|
|
258
|
+
"mcp": { "path": ".gemini/settings.json", "format": "gemini-json" }
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
```json
|
|
263
|
+
"projectResources": {
|
|
264
|
+
"instruction": { "path": ".opencode/AGENTS.md" },
|
|
265
|
+
"skills": { "canonicalRoot": ".opencode/skills", "readRoots": [".opencode/skills", ".claude/skills", ".agents/skills"] },
|
|
266
|
+
"mcp": { "path": ".opencode/opencode.json", "format": "opencode-json" }
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
```json
|
|
271
|
+
"projectResources": {
|
|
272
|
+
"instruction": { "path": null },
|
|
273
|
+
"skills": { "canonicalRoot": ".omp/skills", "readRoots": [".omp/skills"] },
|
|
274
|
+
"mcp": { "path": ".omp/mcp.json", "format": "omp-json" }
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
- [ ] **Step 5: Expose safe metadata from the registry.** Extend `getPublicDefinition()` in `src/platforms/registry.js` so it returns `projectResources` with only relative paths and format names. It must not return resolved home paths, driver IDs, environment values or absolute paths.
|
|
279
|
+
|
|
280
|
+
- [ ] **Step 6: Verify schema and registry tests.**
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
npx vitest run tests/unit/platforms/manifest-schema.test.js tests/unit/platforms/registry.test.js
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Expected: PASS; the five built-in manifests load and the public definition contains no absolute path.
|
|
287
|
+
|
|
288
|
+
- [ ] **Step 7: Commit.**
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
git add src/platforms/manifest-schema.js src/platforms/manifests src/platforms/registry.js tests/unit/platforms/manifest-schema.test.js tests/unit/platforms/registry.test.js
|
|
292
|
+
git commit -m "feat: declare project resource mappings"
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Task 2: Build the project configuration service and safe adapters
|
|
296
|
+
|
|
297
|
+
**Files:**
|
|
298
|
+
- Create: `src/server/services/project-config-adapters/shared.js`
|
|
299
|
+
- Create: `src/server/services/project-config-adapters/index.js`
|
|
300
|
+
- Create: `src/server/services/project-config-adapters/claude.js`
|
|
301
|
+
- Create: `src/server/services/project-config-adapters/codex.js`
|
|
302
|
+
- Create: `src/server/services/project-config-adapters/gemini.js`
|
|
303
|
+
- Create: `src/server/services/project-config-adapters/opencode.js`
|
|
304
|
+
- Create: `src/server/services/project-config-adapters/omp.js`
|
|
305
|
+
- Create: `src/server/services/project-config-service.js`
|
|
306
|
+
- Modify: `src/server/services/project-path-validation.js`
|
|
307
|
+
- Test: `tests/unit/services/project-config-service.test.js`
|
|
308
|
+
|
|
309
|
+
- [ ] **Step 1: Write failing service tests for canonical paths and instruction files.** Use injected dependencies so tests never touch the real home directory:
|
|
310
|
+
|
|
311
|
+
```js
|
|
312
|
+
const fs = require('fs');
|
|
313
|
+
const os = require('os');
|
|
314
|
+
const path = require('path');
|
|
315
|
+
const { BUILT_IN_MANIFESTS, createPlatformRegistry } = require('../../../src/platforms/registry');
|
|
316
|
+
const { ProjectConfigService } = require('../../../src/server/services/project-config-service');
|
|
317
|
+
|
|
318
|
+
function makeRegistry() {
|
|
319
|
+
return createPlatformRegistry({ builtIns: BUILT_IN_MANIFESTS, userFile: { platforms: [] } });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let projectDir;
|
|
323
|
+
let service;
|
|
324
|
+
|
|
325
|
+
beforeEach(() => {
|
|
326
|
+
projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'project-config-'));
|
|
327
|
+
service = new ProjectConfigService({
|
|
328
|
+
validateProjectPath: vi.fn(async () => fs.realpathSync(projectDir)),
|
|
329
|
+
registry: makeRegistry(),
|
|
330
|
+
fsImpl: fs
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
afterEach(() => {
|
|
335
|
+
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('writes and reads the Codex project instruction file', async () => {
|
|
339
|
+
await service.writeInstruction(projectDir, 'codex', '# Rules');
|
|
340
|
+
|
|
341
|
+
await expect(service.readInstruction(projectDir, 'codex')).resolves.toEqual(expect.objectContaining({
|
|
342
|
+
supported: true,
|
|
343
|
+
path: 'AGENTS.md',
|
|
344
|
+
exists: true,
|
|
345
|
+
content: '# Rules'
|
|
346
|
+
}));
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('rejects a path outside the validated project root', async () => {
|
|
350
|
+
await expect(service.writeInstruction('/tmp/other-project', 'codex', '# Rules'))
|
|
351
|
+
.rejects.toThrow('Invalid project path');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('returns unsupported for OMP instruction files', async () => {
|
|
355
|
+
await expect(service.readInstruction(projectDir, 'omp')).resolves.toEqual(expect.objectContaining({
|
|
356
|
+
supported: false,
|
|
357
|
+
path: null
|
|
358
|
+
}));
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
`ProjectConfigService` must normalize validator failures to an `Invalid project path` error prefix. The registry fixture must load the five built-in manifests and no filesystem paths outside the temporary project.
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
- [ ] **Step 2: Run the focused service test and verify it fails.**
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
npx vitest run tests/unit/services/project-config-service.test.js
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Expected: FAIL because the service and adapter registry do not exist.
|
|
372
|
+
|
|
373
|
+
- [ ] **Step 3: Remove the path-validation cycle before wiring the service.** Change `src/server/services/project-path-validation.js` to load `workspace-service` lazily inside `getKnownProjectPaths()`:
|
|
374
|
+
|
|
375
|
+
```js
|
|
376
|
+
function getWorkspaceService() {
|
|
377
|
+
return require('./workspace-service');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function getKnownProjectPaths() {
|
|
381
|
+
const workspaceService = getWorkspaceService();
|
|
382
|
+
const known = new Set();
|
|
383
|
+
const add = candidate => {
|
|
384
|
+
const resolved = realDirectory(candidate);
|
|
385
|
+
if (resolved) known.add(resolved);
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
add(process.cwd());
|
|
389
|
+
for (const workspace of workspaceService.listWorkspaces()) {
|
|
390
|
+
add(workspace.path);
|
|
391
|
+
for (const project of workspace.projects || []) {
|
|
392
|
+
add(project.sourcePath);
|
|
393
|
+
if (workspace.path && project.name) add(path.join(workspace.path, project.name));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
const projects = await workspaceService.getAllAvailableProjects();
|
|
399
|
+
for (const project of projects || []) add(project.fullPath || project.path);
|
|
400
|
+
} catch {
|
|
401
|
+
// Workspace configuration remains an authoritative fallback.
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return known;
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Do not retain a top-level `require('./workspace-service')` binding.
|
|
409
|
+
|
|
410
|
+
- [ ] **Step 4: Implement safe shared adapter helpers.** `shared.js` must export these functions:
|
|
411
|
+
|
|
412
|
+
```js
|
|
413
|
+
function resolveProjectTarget(projectRoot, relativePath) {}
|
|
414
|
+
function assertExistingProjectRoot(projectRoot) {}
|
|
415
|
+
function readTextFile(projectRoot, relativePath) {}
|
|
416
|
+
function writeTextFileAtomic(projectRoot, relativePath, content) {}
|
|
417
|
+
function deleteProjectFile(projectRoot, relativePath) {}
|
|
418
|
+
function redactSecrets(value) {}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
`resolveProjectTarget` must use `normalizeSafeRelativePath(..., { allowHiddenSegments: true })`, resolve under `projectRoot`, reject root itself, and reject a symlink in any existing path component. `writeTextFileAtomic` must create only the parent directory under the project root, write `${target}.tmp-${process.pid}-${Date.now()}`, then rename it to the target. It must remove the temporary file when the write fails.
|
|
422
|
+
|
|
423
|
+
- [ ] **Step 5: Implement the adapter registry and instruction behavior.** Each platform module exports `createAdapter({ manifest, fsImpl })` and implements the same interface:
|
|
424
|
+
|
|
425
|
+
```js
|
|
426
|
+
{
|
|
427
|
+
describe(),
|
|
428
|
+
readInstruction(projectRoot),
|
|
429
|
+
writeInstruction(projectRoot, content),
|
|
430
|
+
deleteInstruction(projectRoot),
|
|
431
|
+
listSkillRoots(projectRoot),
|
|
432
|
+
readProjectMcp(projectRoot),
|
|
433
|
+
upsertProjectMcp(projectRoot, id, spec),
|
|
434
|
+
removeProjectMcp(projectRoot, id)
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Task 2 only needs the instruction methods and a `listSkillRoots` descriptor. MCP methods may return `unsupported` until Task 4 adds format handlers. The OMP adapter must return `{ supported: false, path: null }` for instruction reads and reject instruction writes with status `unsupported`.
|
|
439
|
+
|
|
440
|
+
- [ ] **Step 6: Implement `ProjectConfigService` with injected dependencies.** The constructor must accept:
|
|
441
|
+
|
|
442
|
+
```js
|
|
443
|
+
new ProjectConfigService({
|
|
444
|
+
registry,
|
|
445
|
+
adapters,
|
|
446
|
+
validateProjectPath,
|
|
447
|
+
skillServiceFactory,
|
|
448
|
+
mcpClientFactory,
|
|
449
|
+
fsImpl
|
|
450
|
+
})
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Use `validateKnownProjectCwd` by default and normalize validator failures to an `Invalid project path` error prefix. Public methods must canonicalize the returned path once and pass that same path to the adapter. Implement these methods with the exact names used by later tasks:
|
|
454
|
+
|
|
455
|
+
```text
|
|
456
|
+
getAdapter
|
|
457
|
+
getSnapshot
|
|
458
|
+
readInstruction
|
|
459
|
+
writeInstruction
|
|
460
|
+
deleteInstruction
|
|
461
|
+
listProjectSkills
|
|
462
|
+
installProjectSkill
|
|
463
|
+
removeProjectSkill
|
|
464
|
+
listProjectMcp
|
|
465
|
+
upsertProjectMcp
|
|
466
|
+
removeProjectMcp
|
|
467
|
+
testProjectMcp
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Task 2 implements `readInstruction`, `writeInstruction`, `deleteInstruction`, `getSnapshot`, and `getAdapter`. The initial snapshot returns instruction state plus empty supported/unsupported Skills and MCP sections without throwing on a supported empty project; Tasks 3–4 fill those sections through the same service.
|
|
471
|
+
|
|
472
|
+
- [ ] **Step 7: Run the focused service test.**
|
|
473
|
+
|
|
474
|
+
```bash
|
|
475
|
+
npx vitest run tests/unit/services/project-config-service.test.js
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Expected: PASS, including path rejection, atomic instruction write and OMP unsupported behavior.
|
|
479
|
+
|
|
480
|
+
- [ ] **Step 8: Commit.**
|
|
481
|
+
|
|
482
|
+
```bash
|
|
483
|
+
git add src/server/services/project-config-adapters src/server/services/project-config-service.js src/server/services/project-path-validation.js tests/unit/services/project-config-service.test.js
|
|
484
|
+
git commit -m "feat: add project config service"
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### Task 3: Add project-scoped Skills without changing user scope
|
|
488
|
+
|
|
489
|
+
**Files:**
|
|
490
|
+
- Modify: `src/server/services/skill-service.js`
|
|
491
|
+
- Modify: `src/server/services/project-config-service.js`
|
|
492
|
+
- Modify: `src/server/api/skills.js`
|
|
493
|
+
- Modify: `src/web/src/api/skills.js`
|
|
494
|
+
- Test: `tests/unit/services/skill-service.test.js`
|
|
495
|
+
- Test: `tests/unit/services/project-config-service.test.js`
|
|
496
|
+
- Test: `tests/unit/api/skills-api.test.js`
|
|
497
|
+
|
|
498
|
+
- [ ] **Step 1: Add failing tests for target roots and cache isolation.** Add a temporary project fixture and assert the existing user install directory remains empty:
|
|
499
|
+
|
|
500
|
+
```js
|
|
501
|
+
test('installs and lists a Skill in project scope', async () => {
|
|
502
|
+
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-project-'));
|
|
503
|
+
const service = makeSkillService({
|
|
504
|
+
platform: 'codex',
|
|
505
|
+
projectRoot,
|
|
506
|
+
userInstallDir: path.join(projectRoot, 'fake-user-skills')
|
|
507
|
+
});
|
|
508
|
+
const repo = makeLocalSkillRepo(projectRoot, 'repo-skill');
|
|
509
|
+
|
|
510
|
+
await service.installSkill('repo-skill', { provider: 'local', localPath: repo }, null, {
|
|
511
|
+
scope: 'project',
|
|
512
|
+
cwd: projectRoot
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
expect(fs.existsSync(path.join(projectRoot, '.agents', 'skills', 'repo-skill', 'SKILL.md'))).toBe(true);
|
|
516
|
+
expect(fs.existsSync(path.join(projectRoot, 'fake-user-skills', 'repo-skill'))).toBe(false);
|
|
517
|
+
await expect(service.listSkills(false, { scope: 'project', cwd: projectRoot }))
|
|
518
|
+
.resolves.toEqual(expect.arrayContaining([
|
|
519
|
+
expect.objectContaining({ directory: 'repo-skill', sourceScope: 'project', installed: true })
|
|
520
|
+
]));
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test('separates user and project Skill cache entries', async () => {
|
|
524
|
+
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-cache-project-'));
|
|
525
|
+
const service = makeSkillService({ platform: 'claude', projectRoot });
|
|
526
|
+
|
|
527
|
+
await service.listSkills(false, { scope: 'user' });
|
|
528
|
+
await service.listSkills(false, { scope: 'project', cwd: projectRoot });
|
|
529
|
+
|
|
530
|
+
expect([...service._preparedSkillsCache.keys()]).toEqual(expect.arrayContaining(['', projectRoot]));
|
|
531
|
+
});
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
- [ ] **Step 2: Run the focused Skill test and verify it fails.**
|
|
535
|
+
|
|
536
|
+
```bash
|
|
537
|
+
npx vitest run tests/unit/services/skill-service.test.js
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Expected: FAIL because install and list currently resolve only `this.installDir`, and `scope` is not propagated through project operations.
|
|
541
|
+
|
|
542
|
+
- [ ] **Step 3: Add one manifest-backed target resolver to `SkillService`.** Extend the constructor to accept an optional `{ registry }` second argument while preserving `new SkillService(platform)`, and read `projectResources.skills` from `registry.resolve(this.platform)`. Add this method adjacent to `resolveInstallPath`:
|
|
543
|
+
|
|
544
|
+
```js
|
|
545
|
+
resolveScopeOptions(options = {}) {
|
|
546
|
+
const scope = options.scope || 'user';
|
|
547
|
+
if (scope === 'user') return { scope, roots: [this.installDir], writeRoot: this.installDir };
|
|
548
|
+
if (scope !== 'project') throw new Error('Invalid scope: expected "user" or "project"');
|
|
549
|
+
if (!options.cwd) throw new Error('Project scope requires a valid cwd');
|
|
550
|
+
|
|
551
|
+
const projectRoot = path.resolve(options.cwd);
|
|
552
|
+
const mapping = this.registry.resolve(this.platform)?.projectResources?.skills;
|
|
553
|
+
if (!mapping) throw new Error(`Project Skills are not supported for ${this.platform}`);
|
|
554
|
+
|
|
555
|
+
const roots = mapping.readRoots.map(relativeRoot =>
|
|
556
|
+
resolveInsideRoot(projectRoot, relativeRoot, 'project skill root', {
|
|
557
|
+
allowHiddenSegments: true,
|
|
558
|
+
allowRoot: true
|
|
559
|
+
})
|
|
560
|
+
);
|
|
561
|
+
const writeRoot = resolveInsideRoot(projectRoot, mapping.canonicalRoot, 'project skill root', {
|
|
562
|
+
allowHiddenSegments: true,
|
|
563
|
+
allowRoot: true
|
|
564
|
+
});
|
|
565
|
+
return { scope, roots, writeRoot };
|
|
566
|
+
}
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
Use `roots` for reads and `writeRoot` for create/install/remove. `resolveScopeOptions` must also check the canonical project root with the same realpath/symlink rules as `ProjectConfigService`. The implementation must not contain a second platform-to-path mapping.
|
|
570
|
+
|
|
571
|
+
- [ ] **Step 4: Thread `{ scope, cwd }` through all Skill operations and the project service facade.** Preserve existing argument order and add an optional final `options` object to `installSkill`, `installLocalSkill`, `createCustomSkill`, `createSkillWithFiles`, `getSkillFiles`, `getSkillFileContent`, `addSkillFiles`, `deleteSkillFile`, `updateSkillFile`, `getSkillDetail`, and `uninstallSkill`. Every call to `resolveInstallPath` must use the same scope options. `scanLocalDir` must set `sourceScope` to the actual scope instead of hard-coding `'user'`.
|
|
572
|
+
|
|
573
|
+
`ProjectConfigService.listProjectSkills()` must call the SkillService with `{ scope: 'project', cwd: canonicalProjectPath }` and return `project` plus `inherited` arrays. `installProjectSkill()` must choose `installLocalSkill` when the template or UI item has no repository source and choose `installSkill` when it has `repo/fullDirectory`. `removeProjectSkill()` must call only the project-scoped uninstall path. No facade method may call a user-scoped install or remove operation for a project request.
|
|
574
|
+
|
|
575
|
+
`listSkills` must use `platform + scope + canonical cwd` for its prepared and inflight keys. It must return project resources with `scope: 'project'`, keep user resources as `scope: 'user'`, and preserve project-over-user deduplication metadata.
|
|
576
|
+
|
|
577
|
+
- [ ] **Step 5: Extend API validation without changing defaults.** In `src/server/api/skills.js`, validate `scope` and `cwd` for list, install, install-local, create, create-with-files, detail, and every file CRUD route. A missing scope remains user scope. A project scope without a valid `cwd` returns a 400 error. Pass `{ scope, cwd }` to the service for both project and user requests so the service owns target resolution.
|
|
578
|
+
|
|
579
|
+
- [ ] **Step 6: Extend the Web API helper.** In `src/web/src/api/skills.js`, add an options argument to every affected function and serialize only present fields:
|
|
580
|
+
|
|
581
|
+
```js
|
|
582
|
+
function scopeParams(options = {}) {
|
|
583
|
+
return {
|
|
584
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
585
|
+
...(options.scope ? { scope: options.scope } : {})
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
Keep existing calls with no options byte-for-byte equivalent at the HTTP level.
|
|
591
|
+
|
|
592
|
+
- [ ] **Step 7: Run service and API tests.**
|
|
593
|
+
|
|
594
|
+
```bash
|
|
595
|
+
npx vitest run tests/unit/services/skill-service.test.js tests/unit/services/project-config-service.test.js tests/unit/api/skills-api.test.js
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
Expected: PASS; user-scope tests still pass, project Skills write only under the canonical project root, the project facade never calls a user target, and traversal requests return 400.
|
|
599
|
+
|
|
600
|
+
- [ ] **Step 8: Commit.**
|
|
601
|
+
|
|
602
|
+
```bash
|
|
603
|
+
git add src/server/services/skill-service.js src/server/services/project-config-service.js src/server/api/skills.js src/web/src/api/skills.js tests/unit/services/skill-service.test.js tests/unit/services/project-config-service.test.js tests/unit/api/skills-api.test.js
|
|
604
|
+
git commit -m "feat: support project-scoped skills"
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### Task 4: Extract MCP codecs and implement project MCP operations
|
|
608
|
+
|
|
609
|
+
**Files:**
|
|
610
|
+
- Create: `src/server/services/mcp-format.js`
|
|
611
|
+
- Modify: `src/server/services/mcp-service.js`
|
|
612
|
+
- Modify: `src/server/services/project-config-adapters/claude.js`
|
|
613
|
+
- Modify: `src/server/services/project-config-adapters/codex.js`
|
|
614
|
+
- Modify: `src/server/services/project-config-adapters/gemini.js`
|
|
615
|
+
- Modify: `src/server/services/project-config-adapters/opencode.js`
|
|
616
|
+
- Modify: `src/server/services/project-config-adapters/omp.js`
|
|
617
|
+
- Modify: `src/server/services/project-config-service.js`
|
|
618
|
+
- Test: `tests/unit/services/mcp-service.test.js`
|
|
619
|
+
- Test: `tests/unit/services/project-config-service.test.js`
|
|
620
|
+
|
|
621
|
+
- [ ] **Step 1: Write failing codec and project MCP tests.** Add tests for all native containers:
|
|
622
|
+
|
|
623
|
+
```js
|
|
624
|
+
const fs = require('fs');
|
|
625
|
+
const os = require('os');
|
|
626
|
+
const path = require('path');
|
|
627
|
+
const toml = require('@iarna/toml');
|
|
628
|
+
const { BUILT_IN_MANIFESTS, createPlatformRegistry } = require('../../../src/platforms/registry');
|
|
629
|
+
const { ProjectConfigService } = require('../../../src/server/services/project-config-service');
|
|
630
|
+
|
|
631
|
+
const projectRoots = [];
|
|
632
|
+
let service;
|
|
633
|
+
|
|
634
|
+
function makeRegistry() {
|
|
635
|
+
return createPlatformRegistry({ builtIns: BUILT_IN_MANIFESTS, userFile: { platforms: [] } });
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function makeProjectRoot() {
|
|
639
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'project-mcp-'));
|
|
640
|
+
projectRoots.push(root);
|
|
641
|
+
return root;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function writeFile(filePath, content) {
|
|
645
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
646
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function makeProjectConfigService(overrides = {}) {
|
|
650
|
+
return new ProjectConfigService({
|
|
651
|
+
registry: makeRegistry(),
|
|
652
|
+
validateProjectPath: async projectPath => fs.realpathSync(projectPath),
|
|
653
|
+
...overrides
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
beforeEach(() => {
|
|
658
|
+
service = makeProjectConfigService();
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
afterEach(() => {
|
|
662
|
+
for (const root of projectRoots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
|
|
663
|
+
});
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
```js
|
|
667
|
+
test('project MCP updates only the target Codex server and preserves other config', async () => {
|
|
668
|
+
const projectRoot = makeProjectRoot();
|
|
669
|
+
writeFile(path.join(projectRoot, '.codex', 'config.toml'), [
|
|
670
|
+
'model = "gpt-5"',
|
|
671
|
+
'',
|
|
672
|
+
'[mcp_servers.existing]',
|
|
673
|
+
'command = "existing"',
|
|
674
|
+
'',
|
|
675
|
+
'[custom]',
|
|
676
|
+
'value = "preserve"',
|
|
677
|
+
''
|
|
678
|
+
].join('\n'));
|
|
679
|
+
|
|
680
|
+
const result = await service.upsertProjectMcp(projectRoot, 'codex', 'new-server', {
|
|
681
|
+
type: 'stdio',
|
|
682
|
+
command: 'node',
|
|
683
|
+
args: ['server.js'],
|
|
684
|
+
env: { TOKEN: 'secret' }
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
expect(result).toEqual(expect.objectContaining({ id: 'new-server', scope: 'project' }));
|
|
688
|
+
const config = toml.parse(fs.readFileSync(path.join(projectRoot, '.codex', 'config.toml'), 'utf8'));
|
|
689
|
+
expect(config.model).toBe('gpt-5');
|
|
690
|
+
expect(config.custom.value).toBe('preserve');
|
|
691
|
+
expect(config.mcp_servers.existing.command).toBe('existing');
|
|
692
|
+
expect(config.mcp_servers['new-server'].command).toBe('node');
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
test('project MCP testing uses project cwd and returns no secret values', async () => {
|
|
696
|
+
const projectRoot = makeProjectRoot();
|
|
697
|
+
const client = { connect: vi.fn(), listTools: vi.fn(async () => [{ name: 'tool' }]), close: vi.fn() };
|
|
698
|
+
const service = makeProjectConfigService({
|
|
699
|
+
mcpClientFactory: (spec) => {
|
|
700
|
+
expect(spec.cwd).toBe(projectRoot);
|
|
701
|
+
expect(spec.env.TOKEN).toBe('secret');
|
|
702
|
+
return client;
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
await service.upsertProjectMcp(projectRoot, 'claude', 'local', {
|
|
707
|
+
type: 'stdio',
|
|
708
|
+
command: 'node',
|
|
709
|
+
env: { TOKEN: 'secret' }
|
|
710
|
+
});
|
|
711
|
+
const result = await service.testProjectMcp(projectRoot, 'claude', 'local');
|
|
712
|
+
|
|
713
|
+
expect(result).toEqual(expect.objectContaining({ success: true }));
|
|
714
|
+
expect(JSON.stringify(result)).not.toContain('secret');
|
|
715
|
+
});
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
- [ ] **Step 2: Run the focused tests and verify they fail.**
|
|
719
|
+
|
|
720
|
+
```bash
|
|
721
|
+
npx vitest run tests/unit/services/mcp-service.test.js tests/unit/services/project-config-service.test.js
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
Expected: FAIL because codecs are private to `mcp-service.js` and project MCP methods do not exist.
|
|
725
|
+
|
|
726
|
+
- [ ] **Step 3: Extract shared conversion functions without breaking `_test`.** Create `mcp-format.js` with these exports:
|
|
727
|
+
|
|
728
|
+
```js
|
|
729
|
+
module.exports = {
|
|
730
|
+
extractServerSpec,
|
|
731
|
+
convertToCodexFormat,
|
|
732
|
+
convertFromCodexFormat,
|
|
733
|
+
convertToOpenCodeFormat,
|
|
734
|
+
convertFromOpenCodeFormat,
|
|
735
|
+
convertToOmpMcpFormat,
|
|
736
|
+
convertFromOmpMcpFormat
|
|
737
|
+
};
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
Move the existing function bodies from `mcp-service.js` into this module. Import the module from `mcp-service.js` and keep the current `_test` object delegating to the extracted functions. Existing global Claude/Gemini/Codex/OpenCode/OMP sync functions must call the same extracted implementations after the move.
|
|
741
|
+
|
|
742
|
+
- [ ] **Step 4: Implement native project MCP adapter rules.** Use the manifest format and these exact containers:
|
|
743
|
+
|
|
744
|
+
```text
|
|
745
|
+
claude-json: file root .mcp.json, servers at config.mcpServers
|
|
746
|
+
codex-toml: file root .codex/config.toml, servers at config.mcp_servers
|
|
747
|
+
stdio fields command/args/env/cwd; HTTP fields url/http_headers
|
|
748
|
+
project config is trusted-project local configuration
|
|
749
|
+
enabled_tools/disabled_tools/timeouts remain intact
|
|
750
|
+
gemini-json: file root .gemini/settings.json, servers at config.mcpServers
|
|
751
|
+
opencode-json: file root .opencode/opencode.json
|
|
752
|
+
preserve existing mcp.servers shape when present
|
|
753
|
+
otherwise preserve the legacy direct mcp shape used by this repo
|
|
754
|
+
omp-json: file root .omp/mcp.json, servers at config.mcpServers
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
Each adapter must:
|
|
758
|
+
|
|
759
|
+
- read a missing file as an empty native config;
|
|
760
|
+
- validate IDs with the existing MCP name rules where the platform has them;
|
|
761
|
+
- use `extractServerSpec` before writing generic server specs;
|
|
762
|
+
- use the platform codec before writing Codex/OpenCode/OMP specs;
|
|
763
|
+
- update only the named server key;
|
|
764
|
+
- retain `$schema`, unrelated settings, existing servers and unknown fields;
|
|
765
|
+
- write atomically through the shared adapter helper;
|
|
766
|
+
- return a project-scoped DTO with redacted secret fields.
|
|
767
|
+
|
|
768
|
+
- [ ] **Step 5: Add project MCP methods to `ProjectConfigService`.** Implement:
|
|
769
|
+
|
|
770
|
+
```js
|
|
771
|
+
listProjectMcp(projectPath, platform)
|
|
772
|
+
upsertProjectMcp(projectPath, platform, id, spec)
|
|
773
|
+
removeProjectMcp(projectPath, platform, id)
|
|
774
|
+
testProjectMcp(projectPath, platform, id)
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
`upsertProjectMcp` validates `mcp-service.validateServerSpec` before writing. It never calls `saveServer`, `toggleServerApp`, `deleteServer` or any global center-record writer. `testProjectMcp` reads the project spec, injects `cwd=projectPath` only for stdio specs that do not already define `cwd`, constructs `McpClient`, calls `connect`, optionally calls `listTools`, then closes the client in `finally`.
|
|
778
|
+
|
|
779
|
+
- [ ] **Step 6: Complete the aggregate snapshot.** `getSnapshot` must call the project MCP adapter and project Skill listing, include `inherited` user Skills without granting project delete rights, and return platform capability metadata. A missing project MCP file is a supported empty list, not a failure.
|
|
780
|
+
|
|
781
|
+
- [ ] **Step 7: Run the focused tests.**
|
|
782
|
+
|
|
783
|
+
```bash
|
|
784
|
+
npx vitest run tests/unit/services/mcp-service.test.js tests/unit/services/project-config-service.test.js
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
Expected: PASS; old global MCP tests remain green, project writes preserve unrelated data, project testing uses cwd, and serialized responses contain no raw secret.
|
|
788
|
+
|
|
789
|
+
- [ ] **Step 8: Commit.**
|
|
790
|
+
|
|
791
|
+
```bash
|
|
792
|
+
git add src/server/services/mcp-format.js src/server/services/mcp-service.js src/server/services/project-config-adapters src/server/services/project-config-service.js tests/unit/services/mcp-service.test.js tests/unit/services/project-config-service.test.js
|
|
793
|
+
git commit -m "feat: add project-scoped mcp configuration"
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
### Task 5: Expose project configuration through HTTP API
|
|
797
|
+
|
|
798
|
+
**Files:**
|
|
799
|
+
- Create: `src/server/api/project-config.js`
|
|
800
|
+
- Modify: `src/server/index.js`
|
|
801
|
+
- Create: `tests/unit/api/project-config-api.test.js`
|
|
802
|
+
|
|
803
|
+
- [ ] **Step 1: Write failing route tests.** Use the existing Express/http helper pattern from `tests/unit/api/workspaces-api.test.js`. Stub `ProjectConfigService` with these methods and assert route payloads:
|
|
804
|
+
|
|
805
|
+
```js
|
|
806
|
+
test('returns the project configuration snapshot', async () => {
|
|
807
|
+
const res = await request(buildApp()).get('/?projectPath=%2Ftmp%2Fproject&platform=codex');
|
|
808
|
+
|
|
809
|
+
expect(res.status).toBe(200);
|
|
810
|
+
expect(res.body.success).toBe(true);
|
|
811
|
+
expect(res.body.projectPath).toBe('/tmp/project');
|
|
812
|
+
expect(res.body.platform).toBe('codex');
|
|
813
|
+
expect(projectConfigService.getSnapshot).toHaveBeenCalledWith('/tmp/project', 'codex');
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
test('rejects project MCP mutation without projectPath', async () => {
|
|
817
|
+
const res = await request(buildApp()).put('/mcp/demo', {
|
|
818
|
+
platform: 'claude',
|
|
819
|
+
server: { type: 'stdio', command: 'node' }
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
expect(res.status).toBe(400);
|
|
823
|
+
expect(res.body.success).toBe(false);
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
test('returns structured unsupported instruction state', async () => {
|
|
827
|
+
projectConfigService.readInstruction.mockResolvedValue({ supported: false, path: null });
|
|
828
|
+
const res = await request(buildApp()).get('/instruction?projectPath=%2Ftmp%2Fproject&platform=omp');
|
|
829
|
+
|
|
830
|
+
expect(res.status).toBe(200);
|
|
831
|
+
expect(res.body.success).toBe(true);
|
|
832
|
+
expect(res.body.instruction.supported).toBe(false);
|
|
833
|
+
});
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
- [ ] **Step 2: Run the route test and verify it fails.**
|
|
837
|
+
|
|
838
|
+
```bash
|
|
839
|
+
npx vitest run tests/unit/api/project-config-api.test.js
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
Expected: FAIL because the route and server mount do not exist.
|
|
843
|
+
|
|
844
|
+
- [ ] **Step 3: Implement `src/server/api/project-config.js`.** Export an Express router with these routes:
|
|
845
|
+
|
|
846
|
+
```text
|
|
847
|
+
GET / -> getSnapshot(projectPath, platform)
|
|
848
|
+
GET /instruction -> readInstruction(projectPath, platform)
|
|
849
|
+
PUT /instruction -> writeInstruction(projectPath, platform, content)
|
|
850
|
+
DELETE /instruction -> deleteInstruction(projectPath, platform)
|
|
851
|
+
GET /mcp -> listProjectMcp(projectPath, platform)
|
|
852
|
+
PUT /mcp/:id -> upsertProjectMcp(projectPath, platform, id, server)
|
|
853
|
+
DELETE /mcp/:id -> removeProjectMcp(projectPath, platform, id)
|
|
854
|
+
POST /mcp/:id/test -> testProjectMcp(projectPath, platform, id)
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
The router must require non-empty `projectPath` and valid `platform` for every route, require string `content` for instruction PUT, require an object `server` for MCP PUT, and map service errors to `sendApiError`. `unsupported` remains a successful structured response; invalid path/spec is 400; path containment failure is 403.
|
|
858
|
+
|
|
859
|
+
- [ ] **Step 4: Mount the route.** Add this line near the existing resource routes in `src/server/index.js`:
|
|
860
|
+
|
|
861
|
+
```js
|
|
862
|
+
app.use('/api/project-config', require('./api/project-config'));
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
- [ ] **Step 5: Run API tests.**
|
|
866
|
+
|
|
867
|
+
```bash
|
|
868
|
+
npx vitest run tests/unit/api/project-config-api.test.js tests/unit/api/skills-api.test.js
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
Expected: PASS; all route parameters reach the service, project scope is explicit, and old Skills routes retain their behavior.
|
|
872
|
+
|
|
873
|
+
- [ ] **Step 6: Commit.**
|
|
874
|
+
|
|
875
|
+
```bash
|
|
876
|
+
git add src/server/api/project-config.js src/server/index.js tests/unit/api/project-config-api.test.js
|
|
877
|
+
git commit -m "feat: expose project config api"
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
### Task 6: Build the project configuration Web UI shell and panels
|
|
881
|
+
|
|
882
|
+
**Files:**
|
|
883
|
+
- Create: `src/web/src/api/project-config.js`
|
|
884
|
+
- Create: `src/web/src/components/ProjectConfigDrawer.vue`
|
|
885
|
+
- Create: `src/web/src/components/ProjectInstructionPanel.vue`
|
|
886
|
+
- Create: `src/web/src/components/ProjectMcpPanel.vue`
|
|
887
|
+
- Modify: `src/web/src/components/SkillsPanel.vue`
|
|
888
|
+
- Modify: `src/web/src/components/SkillCreateModal.vue`
|
|
889
|
+
- Modify: `src/web/src/components/SkillDetailDrawer.vue`
|
|
890
|
+
- Modify: `src/web/src/components/McpFormDrawer.vue`
|
|
891
|
+
- Create: `src/web/src/components/__tests__/ProjectConfigDrawer.test.js`
|
|
892
|
+
- Create: `src/web/src/components/__tests__/ProjectInstructionPanel.test.js`
|
|
893
|
+
|
|
894
|
+
- [ ] **Step 1: Write failing Web tests.** Use `@vue/test-utils` in the existing Web Vitest environment. Stub network API functions and child resource panels:
|
|
895
|
+
|
|
896
|
+
```js
|
|
897
|
+
import { mount } from '@vue/test-utils';
|
|
898
|
+
import ProjectConfigDrawer from '../ProjectConfigDrawer.vue';
|
|
899
|
+
|
|
900
|
+
vi.mock('../../api/project-config', () => ({
|
|
901
|
+
getProjectConfig: vi.fn(async () => ({
|
|
902
|
+
success: true,
|
|
903
|
+
projectPath: '/tmp/project',
|
|
904
|
+
platform: 'codex',
|
|
905
|
+
instruction: { supported: true, path: 'AGENTS.md', exists: false, content: '' },
|
|
906
|
+
skills: { supported: true, project: [], inherited: [] },
|
|
907
|
+
mcp: { supported: true, path: '.codex/config.toml', servers: [] },
|
|
908
|
+
capabilities: { instruction: true, skills: true, mcp: true }
|
|
909
|
+
}))
|
|
910
|
+
}));
|
|
911
|
+
|
|
912
|
+
test('renders project configuration tabs with the canonical path', async () => {
|
|
913
|
+
const wrapper = mount(ProjectConfigDrawer, {
|
|
914
|
+
props: { show: true, projectPath: '/tmp/project', platform: 'codex' },
|
|
915
|
+
global: { stubs: { SkillsPanel: true, ProjectMcpPanel: true } }
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
await wrapper.vm.$nextTick();
|
|
919
|
+
expect(wrapper.text()).toContain('项目指令');
|
|
920
|
+
expect(wrapper.text()).toContain('Skills');
|
|
921
|
+
expect(wrapper.text()).toContain('MCP');
|
|
922
|
+
expect(wrapper.text()).toContain('/tmp/project');
|
|
923
|
+
});
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
For `ProjectInstructionPanel.test.js`, assert `saveInstruction('/tmp/project', 'codex', '# Rules')` receives the exact path and content and that a disabled state is rendered when `supported` is false.
|
|
927
|
+
|
|
928
|
+
- [ ] **Step 2: Run the Web tests and verify they fail.**
|
|
929
|
+
|
|
930
|
+
```bash
|
|
931
|
+
cd src/web && npx vitest run src/components/__tests__/ProjectConfigDrawer.test.js src/components/__tests__/ProjectInstructionPanel.test.js
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
Expected: FAIL because the Web API module and components do not exist.
|
|
935
|
+
|
|
936
|
+
- [ ] **Step 3: Add the Web API module.** `src/web/src/api/project-config.js` must export:
|
|
937
|
+
|
|
938
|
+
```js
|
|
939
|
+
export async function getProjectConfig(projectPath, platform) {}
|
|
940
|
+
export async function getProjectInstruction(projectPath, platform) {}
|
|
941
|
+
export async function saveProjectInstruction(projectPath, platform, content) {}
|
|
942
|
+
export async function deleteProjectInstruction(projectPath, platform) {}
|
|
943
|
+
export async function getProjectMcp(projectPath, platform) {}
|
|
944
|
+
export async function saveProjectMcp(projectPath, platform, id, server) {}
|
|
945
|
+
export async function deleteProjectMcp(projectPath, platform, id) {}
|
|
946
|
+
export async function testProjectMcp(projectPath, platform, id) {}
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
Use `client` exactly as the other API modules do. Query functions send `projectPath` and `platform`; mutation bodies always include both. Do not reuse global `saveServer`, `toggleServerApp` or `deleteServer` for project MCP.
|
|
950
|
+
|
|
951
|
+
- [ ] **Step 4: Implement the drawer shell.** `ProjectConfigDrawer.vue` accepts:
|
|
952
|
+
|
|
953
|
+
```js
|
|
954
|
+
{
|
|
955
|
+
show: Boolean,
|
|
956
|
+
projectPath: String,
|
|
957
|
+
platform: String
|
|
958
|
+
}
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
When opened or when either prop changes, load `getProjectConfig`. Render `n-tabs` with `项目指令`, `Skills`, and `MCP`. Pass the current platform and canonical path to child panels. Abort or ignore stale responses with a monotonically increasing request ID so opening a second project cannot display the first project’s snapshot. Render capability-disabled content instead of save controls for unsupported resources.
|
|
962
|
+
|
|
963
|
+
- [ ] **Step 5: Implement the instruction panel.** `ProjectInstructionPanel.vue` must display the native relative path, current content, save button, explicit delete button, loading state, and an unsupported message. Save uses `saveProjectInstruction`; empty content writes an empty file, while delete calls `deleteProjectInstruction` only after confirmation. Never call the Agents API.
|
|
964
|
+
|
|
965
|
+
- [ ] **Step 6: Reuse Skills UI with an explicit project scope.** Add a `scope` prop to `SkillsPanel.vue` with default `'user'`. For project mode:
|
|
966
|
+
|
|
967
|
+
```vue
|
|
968
|
+
<SkillsPanel
|
|
969
|
+
:platform="platform"
|
|
970
|
+
:project-path="projectPath"
|
|
971
|
+
scope="project"
|
|
972
|
+
in-drawer
|
|
973
|
+
/>
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
Thread `scope` and `projectPath` into `getSkills`, install, local install, create, detail, file CRUD and uninstall calls. Add matching props to `SkillCreateModal.vue` and `SkillDetailDrawer.vue`. Keep existing global drawer calls unchanged by retaining `scope='user'` as the default.
|
|
977
|
+
|
|
978
|
+
- [ ] **Step 7: Implement the project MCP panel and form transport.** `ProjectMcpPanel.vue` lists project servers, shows the native target path and redacted values, and supports add/edit/test/remove. Extend `McpFormDrawer.vue` with `scope='user'` and `projectPath` props. In user mode it keeps calling `saveServer`; in project mode it calls `saveProjectMcp` and hides platform toggles. The project panel passes `scope='project'` and the canonical path. Removing a server calls only the project DELETE route.
|
|
979
|
+
|
|
980
|
+
- [ ] **Step 8: Run Web tests.**
|
|
981
|
+
|
|
982
|
+
```bash
|
|
983
|
+
cd src/web && npx vitest run src/components/__tests__/ProjectConfigDrawer.test.js src/components/__tests__/ProjectInstructionPanel.test.js
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
Expected: PASS; tabs use the supplied project path, unsupported resources cannot be submitted, and project Skills/MCP transports are distinct from global transports.
|
|
987
|
+
|
|
988
|
+
- [ ] **Step 9: Commit.**
|
|
989
|
+
|
|
990
|
+
```bash
|
|
991
|
+
git add src/web/src/api/project-config.js src/web/src/components/ProjectConfigDrawer.vue src/web/src/components/ProjectInstructionPanel.vue src/web/src/components/ProjectMcpPanel.vue src/web/src/components/SkillsPanel.vue src/web/src/components/SkillCreateModal.vue src/web/src/components/SkillDetailDrawer.vue src/web/src/components/McpFormDrawer.vue src/web/src/components/__tests__
|
|
992
|
+
git commit -m "feat: add project config panels"
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
### Task 7: Add the project configuration entry to the history page
|
|
996
|
+
|
|
997
|
+
**Files:**
|
|
998
|
+
- Modify: `src/web/src/views/SessionList.vue`
|
|
999
|
+
- Create: `src/web/src/views/__tests__/SessionListProjectConfig.test.js`
|
|
1000
|
+
|
|
1001
|
+
- [ ] **Step 1: Write the failing integration assertion.** Extend the Web component test with a mocked sessions store whose `currentProjectInfo` is `{ fullPath: '/tmp/project', displayName: 'project-display-name' }`, mount `SessionList` with `projectName: 'project-display-name'`, and stub `ProjectConfigDrawer`. Inspect the child component directly:
|
|
1002
|
+
|
|
1003
|
+
```js
|
|
1004
|
+
const drawer = wrapper.findComponent(ProjectConfigDrawer);
|
|
1005
|
+
expect(drawer.props('projectPath')).toBe('/tmp/project');
|
|
1006
|
+
expect(drawer.props('projectPath')).not.toBe('project-display-name');
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
- [ ] **Step 2: Run the focused Web test and verify it fails.**
|
|
1010
|
+
|
|
1011
|
+
```bash
|
|
1012
|
+
cd src/web && npx vitest run src/components/__tests__/ProjectConfigDrawer.test.js
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
Expected: FAIL because `SessionList.vue` has no project configuration state or button.
|
|
1016
|
+
|
|
1017
|
+
- [ ] **Step 3: Add the history-page entry.** In `SessionList.vue`, add `ProjectConfigDrawer` beside the existing history UI and add a button next to the current “管理” action:
|
|
1018
|
+
|
|
1019
|
+
```vue
|
|
1020
|
+
<n-button
|
|
1021
|
+
v-else
|
|
1022
|
+
size="small"
|
|
1023
|
+
type="primary"
|
|
1024
|
+
secondary
|
|
1025
|
+
:disabled="!displayProjectPath"
|
|
1026
|
+
@click="showProjectConfig = true"
|
|
1027
|
+
>
|
|
1028
|
+
项目配置
|
|
1029
|
+
</n-button>
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
Use `displayProjectPath`, which already resolves `store.currentProjectInfo.fullPath`, as the drawer prop:
|
|
1033
|
+
|
|
1034
|
+
```vue
|
|
1035
|
+
<ProjectConfigDrawer
|
|
1036
|
+
v-model:show="showProjectConfig"
|
|
1037
|
+
:project-path="displayProjectPath"
|
|
1038
|
+
:platform="currentChannel"
|
|
1039
|
+
/>
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
The project configuration button must not trigger session selection or deletion. Keep `ProjectCard` as a navigation surface until a later shortcut can reuse this same drawer.
|
|
1043
|
+
|
|
1044
|
+
- [ ] **Step 4: Handle stale project state.** Reset or close the drawer when `props.projectName` or `currentChannel` changes. Do not render a request until `ensureProjectNameResolved()` has populated `store.currentProjectInfo`; the drawer receives the full path, not `effectiveProjectName`.
|
|
1045
|
+
|
|
1046
|
+
- [ ] **Step 5: Run Web tests and build.**
|
|
1047
|
+
|
|
1048
|
+
```bash
|
|
1049
|
+
cd src/web && npx vitest run src/components/__tests__/ProjectConfigDrawer.test.js && npm run build
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
Expected: PASS and a successful Vite production build.
|
|
1053
|
+
|
|
1054
|
+
- [ ] **Step 6: Commit.**
|
|
1055
|
+
|
|
1056
|
+
```bash
|
|
1057
|
+
git add src/web/src/views/SessionList.vue src/web/src/components/__tests__/ProjectConfigDrawer.test.js
|
|
1058
|
+
git commit -m "feat: add project config entry to history"
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
### Task 8: Route templates through project semantics and fix workspace file boundaries
|
|
1062
|
+
|
|
1063
|
+
**Files:**
|
|
1064
|
+
- Modify: `src/server/services/config-templates-service.js`
|
|
1065
|
+
- Modify: `src/server/services/workspace-service.js`
|
|
1066
|
+
- Modify: `src/server/services/project-path-validation.js`
|
|
1067
|
+
- Modify: `src/server/api/config-templates.js`
|
|
1068
|
+
- Modify: `src/server/api/workspaces.js`
|
|
1069
|
+
- Modify: `src/commands/workspace.js`
|
|
1070
|
+
- Modify: `tests/unit/services/config-templates-service.test.js`
|
|
1071
|
+
- Modify: `tests/unit/services/workspace-service.test.js`
|
|
1072
|
+
- Modify: `tests/unit/api/config-templates-api.test.js`
|
|
1073
|
+
- Modify: `tests/unit/api/workspaces-api.test.js`
|
|
1074
|
+
|
|
1075
|
+
- [ ] **Step 1: Write failing template and path-boundary tests.** Add assertions that a template uses the project service and that an allowed basename outside a known root is rejected:
|
|
1076
|
+
|
|
1077
|
+
```js
|
|
1078
|
+
test('workspace file route rejects an allowed basename outside known roots', async () => {
|
|
1079
|
+
const outside = path.join(testDir, 'outside', 'AGENTS.md');
|
|
1080
|
+
fs.mkdirSync(path.dirname(outside), { recursive: true });
|
|
1081
|
+
fs.writeFileSync(outside, '# outside', 'utf8');
|
|
1082
|
+
|
|
1083
|
+
const res = await request(buildApp()).get(`/read-file?path=${encodeURIComponent(outside)}`);
|
|
1084
|
+
|
|
1085
|
+
expect(res.status).toBe(403);
|
|
1086
|
+
expect(res.body.success).toBe(false);
|
|
1087
|
+
});
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
In `config-templates-service.test.js`, mock `ProjectConfigService` and assert `writeInstruction`, `installProjectSkill` and `upsertProjectMcp` receive the target project root. In `config-templates-api.test.js`, assert the apply route awaits an asynchronous `applyTemplateToProject` result. In `workspace-service.test.js`, await `createWorkspace()` and assert the provisional workspace record exists before the mocked template call.
|
|
1091
|
+
|
|
1092
|
+
- [ ] **Step 2: Run focused tests and verify they fail.**
|
|
1093
|
+
|
|
1094
|
+
```bash
|
|
1095
|
+
npx vitest run tests/unit/services/config-templates-service.test.js tests/unit/services/workspace-service.test.js tests/unit/api/config-templates-api.test.js tests/unit/api/workspaces-api.test.js
|
|
1096
|
+
```
|
|
1097
|
+
|
|
1098
|
+
Expected: FAIL because templates still write files directly, workspace/template calls are synchronous, and `/read-file` accepts any path with an allowed basename.
|
|
1099
|
+
|
|
1100
|
+
- [ ] **Step 3: Make template application asynchronous and preserve the workspace lifecycle.** Change `applyTemplate()` and `applyTemplateToProject()` in `config-templates-service.js` to return Promises. Iterate selected project Skills sequentially so one failed install produces one skipped result without preventing the remaining items from applying. Use `SkillService.installLocalSkill(directory, { scope: 'project', cwd: targetDir })` when a template item has no repository source, and `SkillService.installSkill(directory, repo, fullDirectory, { scope: 'project', cwd: targetDir })` when it does. Keep `results.skills.items`, `results.skipped` and `.ctx-config.json`.
|
|
1101
|
+
|
|
1102
|
+
Change `workspace-service.createWorkspace()` to `async`. After all project targets are created, construct and save the workspace record before awaiting template application. Keep the generated record in a local variable. If a later fatal operation fails, remove that record from `workspaces.json` before deleting the physical workspace directory. Template errors remain non-fatal as in the current behavior.
|
|
1103
|
+
|
|
1104
|
+
Update every caller:
|
|
1105
|
+
|
|
1106
|
+
```js
|
|
1107
|
+
// src/server/api/workspaces.js
|
|
1108
|
+
router.post('/', async (req, res) => {
|
|
1109
|
+
const workspace = await workspaceService.createWorkspace({
|
|
1110
|
+
name,
|
|
1111
|
+
description,
|
|
1112
|
+
baseDir,
|
|
1113
|
+
projects,
|
|
1114
|
+
configTemplateId
|
|
1115
|
+
});
|
|
1116
|
+
res.json({ success: true, message: '工作区创建成功', data: workspace });
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
// src/server/api/config-templates.js
|
|
1120
|
+
router.post('/:id/apply', async (req, res) => {
|
|
1121
|
+
const result = await templatesService.applyTemplateToProject(targetPath, req.params.id, options);
|
|
1122
|
+
res.json({ success: true, message: '模板应用成功', data: result });
|
|
1123
|
+
});
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
`src/commands/workspace.js` is already asynchronous; add `await` to its `createWorkspace` call. Update all affected service tests to await the returned Promise. Do not add a public path-validation bypass option.
|
|
1127
|
+
|
|
1128
|
+
The controlled sequence is:
|
|
1129
|
+
|
|
1130
|
+
```text
|
|
1131
|
+
create directory and project targets
|
|
1132
|
+
-> validate project targets and symlink components
|
|
1133
|
+
-> save provisional workspace record
|
|
1134
|
+
-> await template through ProjectConfigService
|
|
1135
|
+
-> update configTemplate and save workspace record
|
|
1136
|
+
```
|
|
1137
|
+
|
|
1138
|
+
- [ ] **Step 4: Route project template writes through one service.** In `config-templates-service.js`, replace direct writes for the requested resources with the project service use cases:
|
|
1139
|
+
|
|
1140
|
+
```text
|
|
1141
|
+
AI config -> writeInstruction
|
|
1142
|
+
project Skill -> installProjectSkill
|
|
1143
|
+
project MCP -> upsertProjectMcp
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
Keep agent/command template behavior unchanged unless it is required to pass the project root through the same safe resolver. The record remains provenance only; reads still scan native files. Use a lazy `require('./project-config-service')` inside the application function to avoid module-load cycles with workspace validation.
|
|
1147
|
+
|
|
1148
|
+
- [ ] **Step 5: Add known-root file validation.** Add `isKnownProjectPath(candidate)` or an equivalent helper to `project-path-validation.js`. It must resolve the candidate’s existing parent path, reject symlink escape, and return true only when the candidate is inside a known project/workspace root. Convert `/api/workspaces/read-file` to async and require both:
|
|
1149
|
+
|
|
1150
|
+
```text
|
|
1151
|
+
basename in ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md', '.ctx-config.json']
|
|
1152
|
+
AND
|
|
1153
|
+
file path is inside a known project/workspace root
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
Do not accept arbitrary nested paths solely because the basename is allowed.
|
|
1157
|
+
|
|
1158
|
+
- [ ] **Step 6: Run focused tests.**
|
|
1159
|
+
|
|
1160
|
+
```bash
|
|
1161
|
+
npx vitest run tests/unit/services/config-templates-service.test.js tests/unit/services/workspace-service.test.js tests/unit/api/config-templates-api.test.js tests/unit/api/workspaces-api.test.js
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
Expected: PASS; workspace creation still applies templates asynchronously, fatal rollback removes provisional metadata, and outside files return 403.
|
|
1165
|
+
|
|
1166
|
+
- [ ] **Step 7: Commit.**
|
|
1167
|
+
|
|
1168
|
+
```bash
|
|
1169
|
+
git add src/server/services/config-templates-service.js src/server/services/workspace-service.js src/server/services/project-path-validation.js src/server/api/config-templates.js src/server/api/workspaces.js src/commands/workspace.js tests/unit/services/config-templates-service.test.js tests/unit/services/workspace-service.test.js tests/unit/api/config-templates-api.test.js tests/unit/api/workspaces-api.test.js
|
|
1170
|
+
git commit -m "fix: enforce project config path ownership"
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1173
|
+
### Task 9: Update documentation and perform end-to-end verification
|
|
1174
|
+
|
|
1175
|
+
**Files:**
|
|
1176
|
+
- Modify: `README.md`
|
|
1177
|
+
- Modify: `CHANGELOG.md`
|
|
1178
|
+
- No source files should be changed in this task unless a verification failure identifies a contract regression.
|
|
1179
|
+
|
|
1180
|
+
- [ ] **Step 1: Document the user flow.** Add a README subsection under configuration/workspace usage stating:
|
|
1181
|
+
|
|
1182
|
+
```text
|
|
1183
|
+
在项目历史会话页点击“项目配置”,可管理当前项目的项目指令、Skills 和 MCP。
|
|
1184
|
+
项目配置写入 CLI 原生文件;工作区只负责分组和发现项目。
|
|
1185
|
+
```
|
|
1186
|
+
|
|
1187
|
+
Include the supported native paths from the spec, the distinction between `AGENTS.md` and Agents definitions, and the warning that static MCP secrets may enter Git.
|
|
1188
|
+
In `CHANGELOG.md` under `## [Unreleased]` → `### Added`, add:
|
|
1189
|
+
|
|
1190
|
+
```text
|
|
1191
|
+
- **项目级配置控制** - 在项目历史会话页管理原生项目指令、Skills 和 MCP,配置按项目 realpath 隔离并保留用户级配置。
|
|
1192
|
+
```
|
|
1193
|
+
|
|
1194
|
+
- [ ] **Step 2: Run all changed backend unit/API tests.**
|
|
1195
|
+
|
|
1196
|
+
```bash
|
|
1197
|
+
npx vitest run \
|
|
1198
|
+
tests/unit/platforms/manifest-schema.test.js \
|
|
1199
|
+
tests/unit/platforms/registry.test.js \
|
|
1200
|
+
tests/unit/services/project-config-service.test.js \
|
|
1201
|
+
tests/unit/services/skill-service.test.js \
|
|
1202
|
+
tests/unit/services/mcp-service.test.js \
|
|
1203
|
+
tests/unit/services/config-templates-service.test.js \
|
|
1204
|
+
tests/unit/services/workspace-service.test.js \
|
|
1205
|
+
tests/unit/api/project-config-api.test.js \
|
|
1206
|
+
tests/unit/api/skills-api.test.js \
|
|
1207
|
+
tests/unit/api/config-templates-api.test.js \
|
|
1208
|
+
tests/unit/api/workspaces-api.test.js
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1211
|
+
Expected: PASS with no changes to user-level global MCP/Skills assertions.
|
|
1212
|
+
|
|
1213
|
+
- [ ] **Step 3: Run Web tests and production build.**
|
|
1214
|
+
|
|
1215
|
+
```bash
|
|
1216
|
+
cd src/web && npx vitest run src/components/__tests__/ProjectConfigDrawer.test.js src/components/__tests__/ProjectInstructionPanel.test.js src/views/__tests__/SessionListProjectConfig.test.js && npm run build
|
|
1217
|
+
```
|
|
1218
|
+
|
|
1219
|
+
Expected: PASS and a successful Vite production build in `src/web/dist`.
|
|
1220
|
+
|
|
1221
|
+
- [ ] **Step 4: Run the actual smoke scenario.** Start the application with:
|
|
1222
|
+
|
|
1223
|
+
```bash
|
|
1224
|
+
node bin/ctx.js ui
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1227
|
+
Open the Web UI, select a project with an existing history list, and verify:
|
|
1228
|
+
|
|
1229
|
+
```text
|
|
1230
|
+
项目历史页 -> 项目配置 -> 项目指令
|
|
1231
|
+
项目指令保存后,项目根目录文件内容改变
|
|
1232
|
+
项目 Skills 安装后,Skill 目录位于项目 canonical root
|
|
1233
|
+
项目 MCP 添加后,平台原生 project config 改变且全局 mcp-servers.json 不变
|
|
1234
|
+
手动修改 native file -> 刷新项目配置 -> UI 显示手动修改
|
|
1235
|
+
打开另一个项目 -> 旧项目内容不残留
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
Use temporary project directories and temporary native config paths for the smoke run. Do not use a real home directory with production credentials.
|
|
1239
|
+
|
|
1240
|
+
- [ ] **Step 5: Run the project’s existing regression suite once.**
|
|
1241
|
+
|
|
1242
|
+
```bash
|
|
1243
|
+
npm test
|
|
1244
|
+
```
|
|
1245
|
+
|
|
1246
|
+
Expected: PASS across basic, API, Codex Agents, Skills and plugin-market checks. If an existing test fails because it asserts an intentionally changed project-scope contract, update that test to the spec rather than adding a compatibility branch.
|
|
1247
|
+
|
|
1248
|
+
- [ ] **Step 6: Commit documentation and final verification changes.**
|
|
1249
|
+
|
|
1250
|
+
```bash
|
|
1251
|
+
git add README.md CHANGELOG.md
|
|
1252
|
+
git commit -m "docs: document project config control"
|
|
1253
|
+
```
|
|
1254
|
+
|
|
1255
|
+
## Plan Self-Review
|
|
1256
|
+
|
|
1257
|
+
- **Spec coverage:** Manifest metadata is Task 1; path safety and adapters are Task 2; project Skills are Task 3; MCP codecs and project operations are Task 4; HTTP API is Task 5; Web UI and SessionList entry are Tasks 6–7; template integration and workspace file security are Task 8; documentation and full verification are Task 9.
|
|
1258
|
+
- **No unresolved placeholders:** The plan contains concrete files, method names, request shapes, test assertions, commands and expected outcomes. No task depends on a later undefined symbol.
|
|
1259
|
+
- **Type consistency:** `projectPath` means canonical absolute realpath in service responses; `cwd` is the incoming project-path field used by existing Skills APIs; `scope` is exactly `user` or `project`; `ProjectConfigService` uses `listProjectMcp`, `upsertProjectMcp`, `removeProjectMcp`, and `testProjectMcp` consistently.
|
|
1260
|
+
- **Compatibility:** Existing global MCP endpoints, global Skill calls, Agents definitions and workspace JSON format remain unchanged. The only intentional behavior correction outside the new feature is rejecting workspace file reads outside known roots.
|