coding-tool-x 3.8.4 → 3.8.5

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.
Files changed (434) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +86 -15
  3. package/dist/web/assets/AgentsDrawer-BG6P3DX-.js +1 -0
  4. package/dist/web/assets/AgentsDrawer-Pw2k-ZAa.css +1 -0
  5. package/dist/web/assets/Analytics--z_FmL5P.js +1 -0
  6. package/dist/web/assets/Analytics-CyHhR126.css +1 -0
  7. package/dist/web/assets/AssetPathField-CrJtRKHy.js +1 -0
  8. package/dist/web/assets/ChatHistoryDrawer-B2qBQs9L.css +1 -0
  9. package/dist/web/assets/ChatHistoryDrawer-BiuEryJj.js +1 -0
  10. package/dist/web/assets/CollapseItem-BAYEsL_Q.js +1 -0
  11. package/dist/web/assets/CommandsDrawer-BZJxlPvV.css +1 -0
  12. package/dist/web/assets/CommandsDrawer-YJ-U7BXN.js +1 -0
  13. package/dist/web/assets/ConfigExportDrawer-Ce_hAknW.js +1 -0
  14. package/dist/web/assets/ConfigExportDrawer-dwruRKWr.css +1 -0
  15. package/dist/web/assets/ConfigTemplates-Bvr1NN6K.js +1 -0
  16. package/dist/web/assets/ConfigTemplatesDrawer-BfDi8yO3.css +1 -0
  17. package/dist/web/assets/ConfigTemplatesDrawer-BjIeHfF-.js +1 -0
  18. package/dist/web/assets/ConfigTemplatesPanel-AV00tm_D.js +1 -0
  19. package/dist/web/assets/ConfigTemplatesPanel-CWyPtk67.css +1 -0
  20. package/dist/web/assets/Divider-D28g1gwj.js +1 -0
  21. package/dist/web/assets/DrawerContent-ChXgZ1Xm.js +1 -0
  22. package/dist/web/assets/FavoritesDrawer-CG9eI9Ay.css +1 -0
  23. package/dist/web/assets/FavoritesDrawer-Cz95o4CD.js +1 -0
  24. package/dist/web/assets/GatewayConvertDrawer-CuNlf7AC.css +1 -0
  25. package/dist/web/assets/GatewayConvertDrawer-DLlbMz27.js +1 -0
  26. package/dist/web/assets/Home-CSuFuJ9Y.css +1 -0
  27. package/dist/web/assets/Home-D7CCOFQC.js +1 -0
  28. package/dist/web/assets/MarkdownEditor-qfVgSre_.css +1 -0
  29. package/dist/web/assets/MarkdownEditor-rEZ-c72L.js +1 -0
  30. package/dist/web/assets/McpDrawer-C0dnsZhQ.css +1 -0
  31. package/dist/web/assets/McpDrawer-DfoCyxU2.js +1 -0
  32. package/dist/web/assets/McpFormDrawer-BRbDcawn.css +1 -0
  33. package/dist/web/assets/McpFormDrawer-SKu8HNUR.js +1 -0
  34. package/dist/web/assets/PluginManager-BbeK-3rV.js +1 -0
  35. package/dist/web/assets/PluginsDrawer-CWVJ7yjS.js +1 -0
  36. package/dist/web/assets/PluginsDrawer-DM3QjprE.css +1 -0
  37. package/dist/web/assets/PluginsPanel-Cm2N3StG.js +1 -0
  38. package/dist/web/assets/PluginsPanel-D6ZqITcR.css +1 -0
  39. package/dist/web/assets/ProjectList-677RLZ8O.css +1 -0
  40. package/dist/web/assets/ProjectList-CsjnsNPv.js +1 -0
  41. package/dist/web/assets/PromptsDrawer-BIgiYCEQ.css +1 -0
  42. package/dist/web/assets/PromptsDrawer-EGd3djFa.js +1 -0
  43. package/dist/web/assets/RecentSessionsDrawer-CKFsOYDM.css +1 -0
  44. package/dist/web/assets/RecentSessionsDrawer-D1vtoLAs.js +1 -0
  45. package/dist/web/assets/SessionCard-BgLg49U3.js +1 -0
  46. package/dist/web/assets/SessionCard-aM2NHu6i.css +1 -0
  47. package/dist/web/assets/SessionList-BpYRz1cX.css +1 -0
  48. package/dist/web/assets/SessionList-TRiTklWk.js +1 -0
  49. package/dist/web/assets/SettingsDrawer-d5liXjHq.css +1 -0
  50. package/dist/web/assets/SettingsDrawer-rz2WejGY.js +1 -0
  51. package/dist/web/assets/SkillManager-BTjTUbHq.js +1 -0
  52. package/dist/web/assets/SkillManager-C0GCT2Kb.css +1 -0
  53. package/dist/web/assets/SkillsDrawer-BCNme-aJ.css +1 -0
  54. package/dist/web/assets/SkillsDrawer-pN0hbkK3.js +1 -0
  55. package/dist/web/assets/SkillsPanel-B3Nm5j32.css +1 -0
  56. package/dist/web/assets/SkillsPanel-gmBWEoiF.js +1 -0
  57. package/dist/web/assets/SpeedTestDrawer-Dc0WToJ_.css +1 -0
  58. package/dist/web/assets/SpeedTestDrawer-DcNzuVJk.js +1 -0
  59. package/dist/web/assets/Upload-v5rV0qTT.js +1 -0
  60. package/dist/web/assets/WorkspaceDrawer-CGnMu9p1.css +1 -0
  61. package/dist/web/assets/WorkspaceDrawer-DxmicM4U.js +1 -0
  62. package/dist/web/assets/WorkspaceManager-CJHCjARN.js +1 -0
  63. package/dist/web/assets/WorkspacePanel-D0-zHTHP.css +1 -0
  64. package/dist/web/assets/WorkspacePanel-D8_Zq-du.js +1 -0
  65. package/dist/web/assets/_plugin-vue_export-helper-BeLZJZij.js +1 -0
  66. package/dist/web/assets/client-UDwXBP9i.js +1 -0
  67. package/dist/web/assets/clipboard-B46yyoz_.js +1 -0
  68. package/dist/web/assets/headers-cf-QapUq.js +1 -0
  69. package/dist/web/assets/icons-B2Vg-IOC.js +1 -0
  70. package/dist/web/assets/index-C0EHRym0.css +1 -0
  71. package/dist/web/assets/index-DnEocV0M.js +2 -0
  72. package/dist/web/assets/light-Cp0t7v32.js +1 -0
  73. package/dist/web/assets/markdown-u082bbtj.js +1 -0
  74. package/dist/web/assets/platforms-CBkB45JP.js +1 -0
  75. package/dist/web/assets/platforms-FTpjxAT6.js +1 -0
  76. package/dist/web/assets/project-config-B2baPzYL.js +1 -0
  77. package/dist/web/assets/sessions-Cmy31slD.js +1 -0
  78. package/dist/web/assets/sessions-yC_oVVh8.js +1 -0
  79. package/dist/web/assets/skill-repo-input-B_fkGlUK.js +1 -0
  80. package/dist/web/assets/use-lock-html-scroll-DXIqmUW4.js +1 -0
  81. package/dist/web/assets/use-message-DoLM8C1C.js +1 -0
  82. package/dist/web/assets/useResponsiveDrawer-B3uojyvV.js +1 -0
  83. package/dist/web/assets/vendors-D0c_0KLP.js +1 -0
  84. package/dist/web/assets/vue-vendor-BqZbbXHA.js +1 -0
  85. package/dist/web/assets/workspaces-mjyPVGS8.js +1 -0
  86. package/dist/web/assets/xml-CgY3RMlX.js +1 -0
  87. package/dist/web/index.html +12 -8
  88. package/docs/native-log-memory-2026-09-16.md +43 -0
  89. package/docs/superpowers/plans/2026-08-26-configuration-driven-cli-platform.md +1578 -0
  90. package/docs/superpowers/plans/2026-08-30-project-scoped-config-control.md +1260 -0
  91. package/docs/superpowers/plans/2026-09-01-omp-skill-refresh-boundary.md +213 -0
  92. package/docs/superpowers/plans/2026-09-01-realtime-logs-panel.md +125 -0
  93. package/docs/superpowers/plans/2026-09-02-config-driven-platform-boundary.md +86 -0
  94. package/docs/superpowers/plans/2026-09-02-platform-implementation-drivers.md +508 -0
  95. package/docs/superpowers/plans/2026-09-03-cli-platform-correctness-boundary.md +760 -0
  96. package/docs/superpowers/plans/2026-09-03-dependency-security-remediation.md +133 -0
  97. package/docs/superpowers/plans/2026-09-04-model-catalog-pricing-sync.md +1078 -0
  98. package/docs/superpowers/plans/2026-09-05-codex-project-scan-500.md +128 -0
  99. package/docs/superpowers/plans/2026-09-06-project-skill-path-fix.md +97 -0
  100. package/docs/superpowers/specs/2026-08-26-configuration-driven-cli-platform-design.md +408 -0
  101. package/docs/superpowers/specs/2026-08-30-project-scoped-config-control-design.md +467 -0
  102. package/docs/superpowers/specs/2026-09-01-claude-session-content-repair-design.md +111 -0
  103. package/docs/superpowers/specs/2026-09-01-omp-skill-refresh-boundary-design.md +83 -0
  104. package/docs/superpowers/specs/2026-09-01-realtime-logs-panel-design.md +27 -0
  105. package/docs/superpowers/specs/2026-09-02-config-driven-platform-boundary-design.md +64 -0
  106. package/docs/superpowers/specs/2026-09-02-platform-implementation-driver-boundary-design.md +123 -0
  107. package/docs/superpowers/specs/2026-09-03-cli-platform-correctness-boundary-design.md +249 -0
  108. package/docs/superpowers/specs/2026-09-03-dependency-security-remediation-design.md +87 -0
  109. package/docs/superpowers/specs/2026-09-04-model-catalog-pricing-sync-design.md +301 -0
  110. package/docs/superpowers/specs/2026-09-05-codex-project-scan-500-design.md +24 -0
  111. package/package.json +8 -4
  112. package/src/commands/channels.js +88 -191
  113. package/src/commands/cli-type.js +19 -11
  114. package/src/commands/daemon.js +94 -32
  115. package/src/commands/doctor.js +16 -9
  116. package/src/commands/export-config.js +23 -5
  117. package/src/commands/list.js +130 -52
  118. package/src/commands/logs.js +44 -27
  119. package/src/commands/platform-command-registry.js +78 -0
  120. package/src/commands/plugin.js +11 -5
  121. package/src/commands/port-config.js +34 -52
  122. package/src/commands/proxy-control.js +207 -145
  123. package/src/commands/proxy.js +2 -2
  124. package/src/commands/resume.js +87 -91
  125. package/src/commands/search.js +137 -67
  126. package/src/commands/stats.js +30 -22
  127. package/src/commands/switch.js +1 -1
  128. package/src/commands/toggle-proxy.js +94 -111
  129. package/src/commands/ui.js +67 -6
  130. package/src/commands/workspace.js +90 -8
  131. package/src/config/default.js +6 -53
  132. package/src/config/loader.js +61 -24
  133. package/src/config/model-metadata.js +49 -59
  134. package/src/config/model-metadata.json +4095 -99
  135. package/src/config/paths.js +181 -8
  136. package/src/index.js +134 -71
  137. package/src/platforms/access.js +196 -0
  138. package/src/{server/services → platforms}/agents-service.js +354 -197
  139. package/src/platforms/channel-auth-registry.js +162 -0
  140. package/src/platforms/channel-auth-service.js +82 -0
  141. package/src/{server/services → platforms}/commands-service.js +195 -115
  142. package/src/platforms/driver-registry.js +59 -0
  143. package/src/platforms/drivers/claude/api-operations.js +62 -0
  144. package/src/{server/services/channels.js → platforms/drivers/claude/channels-implementation.js} +131 -33
  145. package/src/platforms/drivers/claude/channels.js +39 -0
  146. package/src/{server/services/config-sync-service.js → platforms/drivers/claude/config-sync.js} +29 -5
  147. package/src/{server/services → platforms/drivers/claude}/health-check.js +4 -4
  148. package/src/platforms/drivers/claude/health.js +22 -0
  149. package/src/platforms/drivers/claude/mcp.js +9 -0
  150. package/src/{server/services/settings-manager.js → platforms/drivers/claude/native-config-implementation.js} +13 -4
  151. package/src/platforms/drivers/claude/native-config.js +46 -0
  152. package/src/platforms/drivers/claude/native-logs.js +72 -0
  153. package/src/{server/services/claude-openai-gateway.js → platforms/drivers/claude/openai-gateway.js} +8 -65
  154. package/src/platforms/drivers/claude/project-config.js +21 -0
  155. package/src/platforms/drivers/claude/projects.js +62 -0
  156. package/src/platforms/drivers/claude/prompts.js +9 -0
  157. package/src/{server/proxy-server.js → platforms/drivers/claude/proxy-implementation.js} +41 -278
  158. package/src/platforms/drivers/claude/proxy.js +16 -0
  159. package/src/platforms/drivers/claude/resource-sync.js +9 -0
  160. package/src/platforms/drivers/claude/session-history-adapter.js +313 -0
  161. package/src/{server/services/sessions.js → platforms/drivers/claude/sessions-implementation.js} +145 -35
  162. package/src/platforms/drivers/claude/sessions.js +76 -0
  163. package/src/{server/services/claude-statistics-service.js → platforms/drivers/claude/statistics-implementation.js} +2 -2
  164. package/src/platforms/drivers/claude/statistics.js +16 -0
  165. package/src/{server/services/claude-wire.js → platforms/drivers/claude/wire.js} +1 -1
  166. package/src/platforms/drivers/codex/api-operations.js +9 -0
  167. package/src/{server/services/codex-channels.js → platforms/drivers/codex/channels-implementation.js} +149 -14
  168. package/src/platforms/drivers/codex/channels.js +45 -0
  169. package/src/{server/services/codex-config.js → platforms/drivers/codex/config.js} +12 -2
  170. package/src/{server/services/codex-env-manager.js → platforms/drivers/codex/env-manager.js} +1 -1
  171. package/src/platforms/drivers/codex/mcp.js +9 -0
  172. package/src/{server/services/codex-settings-manager.js → platforms/drivers/codex/native-config-implementation.js} +15 -6
  173. package/src/platforms/drivers/codex/native-config.js +47 -0
  174. package/src/platforms/drivers/codex/native-logs.js +166 -0
  175. package/src/platforms/drivers/codex/project-config.js +21 -0
  176. package/src/platforms/drivers/codex/projects.js +26 -0
  177. package/src/platforms/drivers/codex/prompts.js +9 -0
  178. package/src/{server/codex-proxy-server.js → platforms/drivers/codex/proxy-implementation.js} +30 -233
  179. package/src/platforms/drivers/codex/proxy.js +16 -0
  180. package/src/platforms/drivers/codex/resource-sync.js +9 -0
  181. package/src/{server/services/session-history-adapters/codex.js → platforms/drivers/codex/session-history-adapter.js} +37 -23
  182. package/src/{server/services/codex-sessions.js → platforms/drivers/codex/sessions-implementation.js} +81 -75
  183. package/src/platforms/drivers/codex/sessions.js +36 -0
  184. package/src/{server/services/codex-statistics-service.js → platforms/drivers/codex/statistics-implementation.js} +2 -2
  185. package/src/platforms/drivers/codex/statistics.js +16 -0
  186. package/src/platforms/drivers/gemini/api-operations.js +9 -0
  187. package/src/{server/services/gemini-channels.js → platforms/drivers/gemini/channels-implementation.js} +202 -37
  188. package/src/platforms/drivers/gemini/channels.js +39 -0
  189. package/src/{server/services/gemini-config.js → platforms/drivers/gemini/config.js} +12 -2
  190. package/src/platforms/drivers/gemini/mcp.js +9 -0
  191. package/src/{server/services/gemini-settings-manager.js → platforms/drivers/gemini/native-config-implementation.js} +14 -5
  192. package/src/platforms/drivers/gemini/native-config.js +39 -0
  193. package/src/platforms/drivers/gemini/native-logs.js +180 -0
  194. package/src/platforms/drivers/gemini/project-config.js +21 -0
  195. package/src/platforms/drivers/gemini/projects.js +26 -0
  196. package/src/platforms/drivers/gemini/prompts.js +9 -0
  197. package/src/{server/gemini-proxy-server.js → platforms/drivers/gemini/proxy-implementation.js} +36 -267
  198. package/src/platforms/drivers/gemini/proxy.js +16 -0
  199. package/src/platforms/drivers/gemini/resource-sync.js +9 -0
  200. package/src/{server/services/session-history-adapters/gemini.js → platforms/drivers/gemini/session-history-adapter.js} +70 -18
  201. package/src/{server/services/gemini-sessions.js → platforms/drivers/gemini/sessions-implementation.js} +53 -37
  202. package/src/platforms/drivers/gemini/sessions.js +47 -0
  203. package/src/{server/services/gemini-statistics-service.js → platforms/drivers/gemini/statistics-implementation.js} +2 -2
  204. package/src/platforms/drivers/gemini/statistics.js +16 -0
  205. package/src/{server/services/gemini-wire.js → platforms/drivers/gemini/wire.js} +1 -1
  206. package/src/platforms/drivers/generic-filesystem.js +166 -0
  207. package/src/platforms/drivers/generic-jsonl.js +141 -0
  208. package/src/platforms/drivers/generic-mcp.js +44 -0
  209. package/src/platforms/drivers/generic-openai-compatible.js +45 -0
  210. package/src/platforms/drivers/generic-prompt.js +41 -0
  211. package/src/platforms/drivers/legacy.js +677 -0
  212. package/src/platforms/drivers/native-config-snapshot.js +125 -0
  213. package/src/platforms/drivers/native-log-utils.js +1292 -0
  214. package/src/platforms/drivers/notification-hooks.js +578 -0
  215. package/src/{server/api/omp-config.js → platforms/drivers/omp/api-operations.js} +52 -64
  216. package/src/{server/services/omp-auth-provider-worker.js → platforms/drivers/omp/auth-provider-worker.js} +1 -1
  217. package/src/{server/services/omp-auth-providers.js → platforms/drivers/omp/auth-providers.js} +1 -1
  218. package/src/{server/services/omp-channels.js → platforms/drivers/omp/channels-implementation.js} +166 -39
  219. package/src/platforms/drivers/omp/channels.js +58 -0
  220. package/src/{server/services/omp-config.js → platforms/drivers/omp/config.js} +53 -13
  221. package/src/{server/services/omp-gateway-routing.js → platforms/drivers/omp/gateway-routing.js} +14 -7
  222. package/src/{server/services/omp-gateway.js → platforms/drivers/omp/gateway.js} +4 -262
  223. package/src/platforms/drivers/omp/mcp.js +9 -0
  224. package/src/{server/services/omp-settings-manager.js → platforms/drivers/omp/native-config-implementation.js} +75 -10
  225. package/src/platforms/drivers/omp/native-config.js +46 -0
  226. package/src/platforms/drivers/omp/native-logs.js +52 -0
  227. package/src/{server/services/omp-native-plugin-adapter.js → platforms/drivers/omp/native-plugin-adapter.js} +91 -4
  228. package/src/platforms/drivers/omp/project-config.js +29 -0
  229. package/src/platforms/drivers/omp/projects.js +20 -0
  230. package/src/platforms/drivers/omp/prompts.js +9 -0
  231. package/src/{server/omp-proxy-server.js → platforms/drivers/omp/proxy-implementation.js} +6 -15
  232. package/src/platforms/drivers/omp/proxy.js +16 -0
  233. package/src/platforms/drivers/omp/resource-sync.js +9 -0
  234. package/src/{server/services/session-history-adapters/omp.js → platforms/drivers/omp/session-history-adapter.js} +42 -18
  235. package/src/{server/services/omp-sessions.js → platforms/drivers/omp/sessions-implementation.js} +130 -94
  236. package/src/platforms/drivers/omp/sessions.js +36 -0
  237. package/src/{server/services/omp-skill-discovery.js → platforms/drivers/omp/skill-discovery.js} +159 -32
  238. package/src/{server/services/omp-skill-settings-service.js → platforms/drivers/omp/skill-settings.js} +1 -1
  239. package/src/{server/services/omp-statistics-service.js → platforms/drivers/omp/statistics-implementation.js} +3 -3
  240. package/src/platforms/drivers/omp/statistics.js +16 -0
  241. package/src/platforms/drivers/opencode/api-operations.js +9 -0
  242. package/src/{server/services/opencode-channels.js → platforms/drivers/opencode/channels-implementation.js} +28 -9
  243. package/src/platforms/drivers/opencode/channels.js +40 -0
  244. package/src/platforms/drivers/opencode/conversion.js +41 -0
  245. package/src/{server/services/opencode-gateway-converter.js → platforms/drivers/opencode/gateway-converter.js} +1 -1
  246. package/src/platforms/drivers/opencode/mcp.js +9 -0
  247. package/src/{server/services/opencode-settings-manager.js → platforms/drivers/opencode/native-config-implementation.js} +17 -6
  248. package/src/platforms/drivers/opencode/native-config.js +26 -0
  249. package/src/platforms/drivers/opencode/native-logs.js +133 -0
  250. package/src/platforms/drivers/opencode/project-config.js +98 -0
  251. package/src/platforms/drivers/opencode/projects.js +20 -0
  252. package/src/platforms/drivers/opencode/prompts.js +9 -0
  253. package/src/{server/opencode-proxy-server.js → platforms/drivers/opencode/proxy-implementation.js} +27 -362
  254. package/src/platforms/drivers/opencode/proxy.js +16 -0
  255. package/src/platforms/drivers/opencode/resource-sync.js +9 -0
  256. package/src/{server/services/opencode-sessions.js → platforms/drivers/opencode/sessions-implementation.js} +211 -64
  257. package/src/platforms/drivers/opencode/sessions.js +48 -0
  258. package/src/{server/services/opencode-statistics-service.js → platforms/drivers/opencode/statistics-implementation.js} +2 -2
  259. package/src/platforms/drivers/opencode/statistics.js +16 -0
  260. package/src/platforms/drivers/secure-file-driver.js +272 -0
  261. package/src/platforms/drivers/unsupported.js +7 -0
  262. package/src/platforms/manifest-schema.js +398 -0
  263. package/src/platforms/manifests/claude.json +489 -0
  264. package/src/platforms/manifests/codex.json +448 -0
  265. package/src/platforms/manifests/gemini.json +447 -0
  266. package/src/platforms/manifests/omp.json +496 -0
  267. package/src/platforms/manifests/opencode.json +422 -0
  268. package/src/{server/services → platforms}/native-oauth-adapters.js +87 -417
  269. package/src/{server/services → platforms}/notification-hooks.js +140 -1023
  270. package/src/platforms/oauth-credentials-service.js +506 -0
  271. package/src/platforms/path-resolver.js +179 -0
  272. package/src/platforms/platform-path-context.js +305 -0
  273. package/src/platforms/registry.js +248 -0
  274. package/src/platforms/remote-notification-providers.js +678 -0
  275. package/src/platforms/runtime.js +259 -0
  276. package/src/{server/services/session-history-adapters/index.js → platforms/session-history-adapters.js} +8 -1
  277. package/src/plugins/plugin-loader.js +2 -2
  278. package/src/plugins/plugin-manager.js +2 -2
  279. package/src/plugins/registry.js +27 -10
  280. package/src/reset-config.js +21 -5
  281. package/src/server/api/agents.js +46 -72
  282. package/src/server/api/commands.js +16 -6
  283. package/src/server/api/config-registry.js +206 -59
  284. package/src/server/api/config-sync.js +3 -2
  285. package/src/server/api/config-templates.js +20 -4
  286. package/src/server/api/config.js +111 -102
  287. package/src/server/api/convert.js +54 -178
  288. package/src/server/api/dashboard.js +168 -102
  289. package/src/server/api/hooks.js +1 -1
  290. package/src/server/api/mcp.js +224 -84
  291. package/src/server/api/platform-api-config.js +102 -0
  292. package/src/server/api/platform-route-factory.js +215 -0
  293. package/src/server/api/platform-routes.js +26 -0
  294. package/src/server/api/platforms.js +110 -0
  295. package/src/server/api/plugins.js +57 -57
  296. package/src/server/api/project-config.js +168 -0
  297. package/src/server/api/prompts.js +58 -45
  298. package/src/server/api/settings.js +55 -8
  299. package/src/server/api/skills.js +262 -133
  300. package/src/server/api/ui-config.js +2 -2
  301. package/src/server/api/validation-errors.js +18 -4
  302. package/src/server/api/workspaces.js +47 -12
  303. package/src/server/index.js +164 -213
  304. package/src/server/platform-context.js +81 -0
  305. package/src/server/services/channel-balance.js +11 -20
  306. package/src/server/services/channel-health.js +21 -6
  307. package/src/server/services/channel-scheduler.js +29 -39
  308. package/src/server/services/channel-sync-utils.js +23 -13
  309. package/src/server/services/config-export-service.js +350 -319
  310. package/src/server/services/config-registry-service.js +280 -54
  311. package/src/server/services/config-sync-manager.js +172 -944
  312. package/src/server/services/config-templates-service.js +419 -353
  313. package/src/server/services/control-manifest-store.js +206 -0
  314. package/src/server/services/dashboard-snapshot-worker.js +656 -100
  315. package/src/server/services/effective-control-service.js +557 -0
  316. package/src/server/services/local-resource-index.js +314 -0
  317. package/src/server/services/mcp-service.js +607 -977
  318. package/src/server/services/model-detector.js +15 -10
  319. package/src/server/services/native-log-channel-resolver.js +214 -0
  320. package/src/server/services/native-log-observer.js +291 -0
  321. package/src/server/services/network-access.js +1 -1
  322. package/src/server/services/platform-catalog.js +82 -0
  323. package/src/server/services/platform-resolution.js +32 -29
  324. package/src/server/services/plugins-service.js +376 -122
  325. package/src/server/services/project-config-service.js +550 -0
  326. package/src/server/services/project-path-validation.js +31 -0
  327. package/src/server/services/prompts-service.js +267 -233
  328. package/src/server/services/proxy-runtime.js +7 -4
  329. package/src/server/services/remote-credential-cache.js +43 -0
  330. package/src/server/services/repo-scanner-base.js +182 -75
  331. package/src/server/services/request-logger.js +7 -4
  332. package/src/server/services/session-history-index.js +780 -213
  333. package/src/server/services/session-history-worker.js +228 -33
  334. package/src/server/services/session-snapshot-worker.js +124 -34
  335. package/src/server/services/session-snapshots.js +4 -2
  336. package/src/server/services/skill-artifact-store.js +355 -0
  337. package/src/server/services/skill-format-adapters.js +65 -0
  338. package/src/server/services/skill-projection-service.js +323 -0
  339. package/src/server/services/skill-refresh-task-service.js +275 -0
  340. package/src/server/services/skill-service.js +2020 -677
  341. package/src/server/services/snapshot-cache.js +30 -8
  342. package/src/server/services/speed-test.js +8 -8
  343. package/src/server/services/sqlite-connection.js +19 -3
  344. package/src/server/services/statistics-service.js +240 -57
  345. package/src/server/services/ui-config.js +107 -76
  346. package/src/server/services/usage-log-utils.js +98 -0
  347. package/src/server/services/web-build.js +1 -1
  348. package/src/server/services/workspace-service.js +59 -29
  349. package/src/server/utils/pricing.js +10 -19
  350. package/src/server/websocket-server.js +153 -28
  351. package/src/{server/services/base → shared}/base-channel-service.js +156 -28
  352. package/src/shared/capability-driver.js +104 -0
  353. package/src/shared/channel-auth-policy.js +27 -0
  354. package/src/shared/channel-driver.js +161 -0
  355. package/src/shared/driver-factories/api.js +180 -0
  356. package/src/shared/driver-factories/mcp.js +244 -0
  357. package/src/shared/driver-factories/projects.js +121 -0
  358. package/src/shared/driver-factories/prompts.js +58 -0
  359. package/src/shared/driver-factories/resource-sync.js +375 -0
  360. package/src/shared/driver-result.js +78 -0
  361. package/src/shared/mcp-format.js +189 -0
  362. package/src/shared/platforms.js +104 -167
  363. package/src/shared/project-config.js +512 -0
  364. package/src/shared/proxy-driver.js +57 -0
  365. package/src/{server/services/base → shared}/response-usage-parser.js +8 -4
  366. package/src/ui/menu.js +91 -67
  367. package/src/utils/port-helper.js +35 -19
  368. package/src/utils/session.js +1 -1
  369. package/dist/web/assets/Analytics-COEUXmd4.js +0 -25
  370. package/dist/web/assets/Analytics-RNn1BUbG.css +0 -1
  371. package/dist/web/assets/ConfigTemplates-DLLkS53H.js +0 -1
  372. package/dist/web/assets/Home-DCEyLzwX.js +0 -1
  373. package/dist/web/assets/Home-DYVy4cGZ.css +0 -1
  374. package/dist/web/assets/PluginManager-Oe1ZHoMO.js +0 -1
  375. package/dist/web/assets/ProjectList-CWjuAFof.js +0 -1
  376. package/dist/web/assets/ProjectList-VeMNXfgO.css +0 -1
  377. package/dist/web/assets/SessionList-C4chCPxK.css +0 -1
  378. package/dist/web/assets/SessionList-v3ehGg9U.js +0 -1
  379. package/dist/web/assets/SkillManager-BsX8Qpj0.css +0 -1
  380. package/dist/web/assets/SkillManager-DUF_qP5S.js +0 -1
  381. package/dist/web/assets/WorkspaceManager-CjAWBosO.js +0 -1
  382. package/dist/web/assets/icons-nGdSKQn2.js +0 -1
  383. package/dist/web/assets/index-D5hyHPj7.js +0 -2
  384. package/dist/web/assets/index-bJRG3RXH.css +0 -1
  385. package/dist/web/assets/markdown-BNJfEgCA.js +0 -1
  386. package/dist/web/assets/markdown-BfC0goYb.css +0 -10
  387. package/dist/web/assets/naive-ui-BALVpMgd.js +0 -1
  388. package/dist/web/assets/vendors-DQA4Hw9v.js +0 -7
  389. package/dist/web/assets/vue-vendor-B5RqjEjQ.js +0 -45
  390. package/src/config/model-pricing.js +0 -35
  391. package/src/server/api/channels.js +0 -439
  392. package/src/server/api/claude-hooks.js +0 -56
  393. package/src/server/api/claude-statistics.js +0 -57
  394. package/src/server/api/codex-channels.js +0 -491
  395. package/src/server/api/codex-projects.js +0 -117
  396. package/src/server/api/codex-proxy.js +0 -293
  397. package/src/server/api/codex-sessions.js +0 -652
  398. package/src/server/api/codex-statistics.js +0 -57
  399. package/src/server/api/gemini-channels.js +0 -392
  400. package/src/server/api/gemini-projects.js +0 -103
  401. package/src/server/api/gemini-proxy.js +0 -231
  402. package/src/server/api/gemini-sessions.js +0 -550
  403. package/src/server/api/gemini-statistics.js +0 -57
  404. package/src/server/api/health-check.js +0 -31
  405. package/src/server/api/oauth-credentials.js +0 -198
  406. package/src/server/api/omp-channels.js +0 -575
  407. package/src/server/api/omp-projects.js +0 -88
  408. package/src/server/api/omp-proxy.js +0 -125
  409. package/src/server/api/omp-sessions.js +0 -386
  410. package/src/server/api/omp-statistics.js +0 -40
  411. package/src/server/api/opencode-channels.js +0 -471
  412. package/src/server/api/opencode-projects.js +0 -110
  413. package/src/server/api/opencode-proxy.js +0 -354
  414. package/src/server/api/opencode-sessions.js +0 -466
  415. package/src/server/api/opencode-statistics.js +0 -57
  416. package/src/server/api/projects.js +0 -120
  417. package/src/server/api/proxy.js +0 -330
  418. package/src/server/api/sessions.js +0 -810
  419. package/src/server/services/oauth-credentials-service.js +0 -945
  420. package/src/server/services/omp-session-log-observer.js +0 -138
  421. package/src/server/services/opencode-gateway-adapters.js +0 -43
  422. package/src/server/services/project-snapshots.js +0 -59
  423. package/src/server/services/proxy-log-helper.js +0 -298
  424. package/src/server/services/session-cache.js +0 -127
  425. package/src/server/services/session-converter.js +0 -582
  426. package/src/server/services/session-history-adapters/claude.js +0 -235
  427. /package/src/{server/services/claude-token-recovery.js → platforms/drivers/claude/token-recovery.js} +0 -0
  428. /package/src/{server/services/codex-parser.js → platforms/drivers/codex/parser.js} +0 -0
  429. /package/src/{server/services/codex-speed-test-template.json → platforms/drivers/codex/speed-test-template.json} +0 -0
  430. /package/src/{server/services/codex-wire.js → platforms/drivers/codex/wire.js} +0 -0
  431. /package/src/{server/services/omp-auth-gateway-client.js → platforms/drivers/omp/auth-gateway-client.js} +0 -0
  432. /package/src/{server/services/opencode-normalization.js → platforms/drivers/opencode/normalization.js} +0 -0
  433. /package/src/{server/services → shared}/config-artifact-paths.js +0 -0
  434. /package/src/{server/services/base → shared}/proxy-utils.js +0 -0
@@ -7,34 +7,70 @@
7
7
 
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
+ const { randomUUID } = require('crypto');
10
11
  const yaml = require('js-yaml');
11
12
  const { PATHS } = require('../../config/paths');
12
- const { AgentsService } = require('./agents-service');
13
- const { CommandsService } = require('./commands-service');
13
+ const { getPlatformContext } = require('../platform-context');
14
+ const TEMPLATES_FILE = path.join(PATHS.config, 'config-templates.json');
15
+ const { AgentsService } = require('../../platforms/agents-service');
16
+ const { CommandsService } = require('../../platforms/commands-service');
14
17
  const { SkillService } = require('./skill-service');
15
18
  const { PluginsService } = require('./plugins-service');
16
19
  const { convertCommandToCodex, convertCommandToGemini } = require('./format-converter');
20
+ const { assertNoSymlinkComponents, resolveProjectTarget } = require('../../shared/project-config');
17
21
  const mcpService = require('./mcp-service');
18
22
  const promptsService = require('./prompts-service');
19
- const pluginsService = new PluginsService();
20
23
 
21
- // 配置模板文件路径
22
- const TEMPLATES_FILE = path.join(PATHS.config, 'config-templates.json');
23
- const AI_CONFIG_MAP = {
24
- claude: { fileName: 'CLAUDE.md', name: 'Claude' },
25
- codex: { fileName: 'AGENTS.md', name: 'Codex' },
26
- gemini: { fileName: 'GEMINI.md', name: 'Gemini' },
27
- opencode: { fileName: '.opencode/AGENTS.md', name: 'OpenCode' },
28
- omp: { fileName: null, name: 'OMP command templates' }
29
- };
24
+ function getAiConfigMap(registry = getPlatformContext().registry) {
25
+ if (!registry || typeof registry.list !== 'function') return {};
26
+ return Object.fromEntries(
27
+ registry.list({ enabledOnly: true })
28
+ .filter(platform => platform && platform.key)
29
+ .map(platform => [platform.key, {
30
+ fileName: platform.promptFile || null,
31
+ name: platform.promptLabel
32
+ || (platform.promptFile
33
+ ? (platform.label || platform.title || platform.key)
34
+ : `${platform.label || platform.title || platform.key} command templates`)
35
+ }])
36
+ );
37
+ }
30
38
 
31
- const CLI_DEFAULT_AI_TYPE = {
32
- claude: 'claude',
33
- codex: 'codex',
34
- gemini: 'gemini',
35
- opencode: 'opencode',
36
- omp: 'omp'
37
- };
39
+ function getTemplateRegistry(options = {}) {
40
+ return options.registry || getPlatformContext().registry;
41
+ }
42
+
43
+ function getTemplatePlatforms(registry = getPlatformContext().registry) {
44
+ if (!registry || typeof registry.list !== 'function') return [];
45
+ return registry.list({ enabledOnly: true })
46
+ .filter(platform => platform && platform.key);
47
+ }
48
+
49
+ function getProjectResourceDescriptor(platformKey, resourceType, registry = getPlatformContext().registry) {
50
+ if (!registry || typeof registry.resolve !== 'function') return null;
51
+ const platform = registry.resolve(platformKey);
52
+ if (!platform || platform.resourceTypes?.[resourceType] === false) return null;
53
+
54
+ const resource = platform.projectResources?.[resourceType];
55
+ if (!resource) return null;
56
+
57
+ const relativePath = resource.canonicalRoot || resource.path || null;
58
+ if (!relativePath) return null;
59
+
60
+ return {
61
+ platform: platform.key,
62
+ prefix: relativePath,
63
+ format: resource.format || resourceType,
64
+ sortOrder: platform.sortOrder || 0
65
+ };
66
+ }
67
+
68
+ function getProjectResourceTargets(platformKeys, resourceType, registry = getPlatformContext().registry) {
69
+ return (platformKeys || [])
70
+ .map(platformKey => getProjectResourceDescriptor(platformKey, resourceType, registry))
71
+ .filter(Boolean)
72
+ .sort((a, b) => a.sortOrder - b.sortOrder);
73
+ }
38
74
 
39
75
  /**
40
76
  * 确保目录存在
@@ -49,17 +85,19 @@ function pushSkipped(list, type, item, reason) {
49
85
  list.push({ type, item, reason });
50
86
  }
51
87
 
52
- function getTemplateDefaultAiType(template) {
53
- if (template?.cliType && CLI_DEFAULT_AI_TYPE[template.cliType]) {
54
- return CLI_DEFAULT_AI_TYPE[template.cliType];
88
+ function getTemplateDefaultAiType(template, registry = getPlatformContext().registry) {
89
+ const aiConfigMap = getAiConfigMap(registry);
90
+ if (template?.cliType && aiConfigMap[template.cliType]) {
91
+ return template.cliType;
55
92
  }
56
- return 'claude';
93
+ return Object.keys(aiConfigMap)[0] || 'claude';
57
94
  }
58
95
 
59
- function normalizeRequestedAiConfigTypes(options = {}, template = null, skipped = []) {
96
+ function normalizeRequestedAiConfigTypes(options = {}, template = null, skipped = [], registry = getPlatformContext().registry) {
97
+ const aiConfigMap = getAiConfigMap(registry);
60
98
  let aiConfigTypes = options.aiConfigTypes;
61
99
  if (!aiConfigTypes) {
62
- aiConfigTypes = options.aiConfigType ? [options.aiConfigType] : [getTemplateDefaultAiType(template)];
100
+ aiConfigTypes = options.aiConfigType ? [options.aiConfigType] : [getTemplateDefaultAiType(template, registry)];
63
101
  }
64
102
  if (!Array.isArray(aiConfigTypes)) {
65
103
  aiConfigTypes = [aiConfigTypes];
@@ -74,7 +112,7 @@ function normalizeRequestedAiConfigTypes(options = {}, template = null, skipped
74
112
  }
75
113
  const aiType = rawType.trim().toLowerCase();
76
114
  if (!aiType) continue;
77
- if (!AI_CONFIG_MAP[aiType]) {
115
+ if (!aiConfigMap[aiType]) {
78
116
  pushSkipped(skipped, 'aiConfigType', aiType, `不支持的 AI 配置类型: ${aiType}`);
79
117
  continue;
80
118
  }
@@ -85,7 +123,7 @@ function normalizeRequestedAiConfigTypes(options = {}, template = null, skipped
85
123
  }
86
124
 
87
125
  if (normalized.length === 0) {
88
- const fallbackType = getTemplateDefaultAiType(template);
126
+ const fallbackType = getTemplateDefaultAiType(template, registry);
89
127
  normalized.push(fallbackType);
90
128
  pushSkipped(skipped, 'aiConfigType', fallbackType, `未提供有效 AI 配置类型,已回退到默认类型: ${fallbackType}`);
91
129
  }
@@ -109,14 +147,14 @@ function resolveItemName(primary, fallback, defaultPrefix) {
109
147
  return `${defaultPrefix}-${Date.now()}`;
110
148
  }
111
149
 
112
- function normalizeAiConfigs(aiConfigs = {}, claudeMd = null) {
113
- const normalized = {
114
- claude: { enabled: false, content: '' },
115
- codex: { enabled: false, content: '' },
116
- gemini: { enabled: false, content: '' },
117
- opencode: { enabled: false, content: '' },
118
- omp: { enabled: false, content: '' }
119
- };
150
+ function normalizeAiConfigs(aiConfigs = {}, claudeMd = null, registry = getPlatformContext().registry) {
151
+ const keys = new Set([
152
+ ...Object.keys(getAiConfigMap(registry)),
153
+ ...Object.keys(aiConfigs || {})
154
+ ]);
155
+ const normalized = Object.fromEntries(
156
+ [...keys].map(key => [key, { enabled: false, content: '' }])
157
+ );
120
158
 
121
159
  for (const key of Object.keys(normalized)) {
122
160
  const cfg = aiConfigs?.[key];
@@ -128,7 +166,7 @@ function normalizeAiConfigs(aiConfigs = {}, claudeMd = null) {
128
166
  }
129
167
  }
130
168
 
131
- if (claudeMd?.enabled && claudeMd?.content && !normalized.claude.content) {
169
+ if (claudeMd?.enabled && claudeMd?.content && normalized.claude && !normalized.claude.content) {
132
170
  normalized.claude = {
133
171
  enabled: true,
134
172
  content: claudeMd.content
@@ -136,7 +174,7 @@ function normalizeAiConfigs(aiConfigs = {}, claudeMd = null) {
136
174
  }
137
175
 
138
176
  // OpenCode defaults to Codex profile if not explicitly configured.
139
- if (!normalized.opencode.content) {
177
+ if (normalized.opencode && !normalized.opencode.content) {
140
178
  const fallback = normalized.codex.content ? normalized.codex : normalized.claude;
141
179
  normalized.opencode = {
142
180
  enabled: !!fallback.enabled,
@@ -293,56 +331,34 @@ function deleteCustomTemplate(id) {
293
331
  throw new Error('模板不存在或不可删除');
294
332
  }
295
333
 
334
+ const projectConfigServiceCache = new Map();
335
+
336
+ function getProjectConfigService() {
337
+ if (!projectConfigServiceCache.has('default')) {
338
+ const { ProjectConfigService } = require('./project-config-service');
339
+ projectConfigServiceCache.set('default', new ProjectConfigService());
340
+ }
341
+ return projectConfigServiceCache.get('default');
342
+ }
343
+
296
344
  /**
297
345
  * 应用模板到指定目录
298
346
  * @param {string} targetDir - 目标目录
299
347
  * @param {string} templateId - 模板 ID
300
348
  */
301
- function applyTemplate(targetDir, templateId) {
302
- const template = getTemplateById(templateId);
303
-
304
- if (!template) {
305
- throw new Error('模板不存在');
306
- }
307
-
308
- // 确保目标目录存在
309
- ensureDir(targetDir);
310
-
311
- const results = {
312
- claudeMd: false,
313
- skills: 0,
314
- commands: 0,
315
- agents: 0,
316
- plugins: 0,
317
- mcpServers: 0
318
- };
319
-
320
- // 1. 应用 CLAUDE.md
321
- if (template.claudeMd && template.claudeMd.enabled && template.claudeMd.content) {
322
- const claudeMdPath = path.join(targetDir, 'CLAUDE.md');
323
- fs.writeFileSync(claudeMdPath, template.claudeMd.content, 'utf8');
324
- results.claudeMd = true;
325
- }
326
-
327
- // 2. 创建配置记录文件(记录应用了哪个模板)
328
- const configRecord = {
329
- templateId: template.id,
330
- templateName: template.name,
331
- appliedAt: new Date().toISOString(),
332
- skills: template.skills,
333
- commands: template.commands,
334
- agents: template.agents,
335
- plugins: template.plugins,
336
- mcpServers: template.mcpServers
337
- };
338
-
339
- const recordPath = path.join(targetDir, '.ctx-config.json');
340
- fs.writeFileSync(recordPath, JSON.stringify(configRecord, null, 2), 'utf8');
341
-
349
+ async function applyTemplate(targetDir, templateId) {
350
+ const result = await applyTemplateToProject(targetDir, templateId);
342
351
  return {
343
- success: true,
344
- results,
345
- template: template.name
352
+ success: result.success,
353
+ results: {
354
+ claudeMd: result.results.aiConfigs.some(config => config.key === 'claude'),
355
+ skills: result.results.skills.applied,
356
+ commands: result.results.commands.applied,
357
+ agents: result.results.agents.applied,
358
+ plugins: result.results.plugins.applied,
359
+ mcpServers: result.results.mcpServers.applied
360
+ },
361
+ template: result.template
346
362
  };
347
363
  }
348
364
 
@@ -372,76 +388,84 @@ function readCurrentConfig(targetDir) {
372
388
  * 获取所有可用配置(用于模板编辑器选择)
373
389
  * 返回用户级的 agents, commands, plugins + MCP 服务器列表
374
390
  */
375
- function getAvailableConfigs() {
376
- const agentPlatforms = ['claude', 'codex', 'gemini', 'opencode'];
377
- const commandPlatforms = ['claude', 'codex', 'gemini', 'opencode', 'omp'];
391
+ function getAvailableConfigs(options = {}) {
392
+ const registry = getTemplateRegistry(options);
393
+ const platforms = getTemplatePlatforms(registry);
394
+ const agentPlatforms = platforms
395
+ .filter(platform => getProjectResourceDescriptor(platform.key, 'agents', registry))
396
+ .map(platform => platform.key);
397
+ const commandPlatforms = platforms
398
+ .filter(platform => getProjectResourceDescriptor(platform.key, 'commands', registry))
399
+ .map(platform => platform.key);
378
400
  const agentMap = new Map();
379
401
  const commandMap = new Map();
380
402
  const agentsByPlatform = {};
381
403
  const commandsByPlatform = {};
382
404
 
383
405
  for (const platform of agentPlatforms) {
384
- const service = new AgentsService(platform);
385
- const { agents } = service.listAgents();
386
- for (const agent of agents || []) {
387
- if (agent.scope !== 'user') continue;
388
- const normalizedAgent = {
389
- fileName: agent.fileName,
390
- name: agent.name,
391
- description: agent.description,
392
- tools: agent.tools,
393
- model: agent.model,
394
- permissionMode: agent.permissionMode,
395
- skills: agent.skills,
396
- systemPrompt: agent.systemPrompt
397
- };
398
- agentsByPlatform[platform] = agentsByPlatform[platform] || [];
399
- agentsByPlatform[platform].push(normalizedAgent);
400
- const key = `${agent.fileName || agent.name}|${agent.model || ''}|${agent.description || ''}`;
401
- if (!agentMap.has(key)) {
402
- agentMap.set(key, normalizedAgent);
406
+ let service;
407
+ try {
408
+ service = new AgentsService(platform, { registry });
409
+ const { agents } = service.listAgents();
410
+ for (const agent of agents || []) {
411
+ if (agent.scope !== 'user') continue;
412
+ const detail = typeof service.getAgent === 'function' ? (service.getAgent(agent.fileName, 'user') || agent) : agent;
413
+ const normalizedAgent = { fileName: agent.fileName, name: agent.name, description: agent.description, tools: agent.tools, model: agent.model, permissionMode: agent.permissionMode, skills: agent.skills, systemPrompt: detail.systemPrompt };
414
+ agentsByPlatform[platform] = agentsByPlatform[platform] || [];
415
+ agentsByPlatform[platform].push(normalizedAgent);
416
+ const key = `${agent.fileName || agent.name}|${agent.model || ''}|${agent.description || ''}`;
417
+ if (!agentMap.has(key)) agentMap.set(key, normalizedAgent);
403
418
  }
419
+ } finally {
420
+ service?.dispose?.();
404
421
  }
405
422
  }
406
423
 
407
424
  for (const platform of commandPlatforms) {
408
- const service = new CommandsService(platform);
409
- const { commands } = service.listCommands();
410
- for (const command of commands || []) {
411
- if (command.scope !== 'user') continue;
412
- const normalizedCommand = {
413
- name: command.name,
414
- namespace: command.namespace,
415
- description: command.description,
416
- allowedTools: command.allowedTools,
417
- argumentHint: command.argumentHint,
418
- body: command.body
419
- };
420
- commandsByPlatform[platform] = commandsByPlatform[platform] || [];
421
- commandsByPlatform[platform].push(normalizedCommand);
422
- const key = command.namespace ? `${command.namespace}/${command.name}` : command.name;
423
- if (!commandMap.has(key)) {
424
- commandMap.set(key, normalizedCommand);
425
+ let service;
426
+ try {
427
+ service = new CommandsService(platform, { registry });
428
+ const { commands } = service.listCommands();
429
+ for (const command of commands || []) {
430
+ if (command.scope !== 'user') continue;
431
+ const detail = typeof service.getCommand === 'function' ? (service.getCommand(command.name, 'user', null, command.namespace) || command) : command;
432
+ const normalizedCommand = { name: command.name, namespace: command.namespace, description: command.description, allowedTools: command.allowedTools, argumentHint: command.argumentHint, body: detail.body };
433
+ commandsByPlatform[platform] = commandsByPlatform[platform] || [];
434
+ commandsByPlatform[platform].push(normalizedCommand);
435
+ const key = command.namespace ? `${command.namespace}/${command.name}` : command.name;
436
+ if (!commandMap.has(key)) commandMap.set(key, normalizedCommand);
425
437
  }
438
+ } finally {
439
+ service?.dispose?.();
426
440
  }
427
441
  }
428
-
429
442
  // 按平台分别获取 skills(每个平台有独立的安装目录)
430
443
  const skillsByPlatform = {};
431
- for (const platform of ['claude', 'codex', 'gemini', 'opencode', 'omp']) {
432
- const service = new SkillService(platform);
433
- skillsByPlatform[platform] = service.getInstalledSkills().map(skill => ({
434
- directory: skill.directory,
435
- name: skill.name || skill.directory,
436
- description: skill.description || '',
437
- repoOwner: skill.repoOwner || null,
438
- repoName: skill.repoName || null,
439
- repoBranch: skill.repoBranch || null
440
- }));
444
+ for (const platform of platforms
445
+ .filter(item => getProjectResourceDescriptor(item.key, 'skills', registry))
446
+ .map(item => item.key)) {
447
+ const service = new SkillService(platform, { registry });
448
+ skillsByPlatform[platform] = service.getInstalledSkills().map(skill => {
449
+ const normalized = {
450
+ directory: skill.directory,
451
+ name: skill.name || skill.directory,
452
+ description: skill.description || '',
453
+ repoOwner: skill.repoOwner || null,
454
+ repoName: skill.repoName || null,
455
+ repoBranch: skill.repoBranch || null
456
+ };
457
+ for (const key of ['fullDirectory', 'repoProvider', 'repoHost', 'repoId', 'repoDirectory', 'repoProjectPath', 'repoLocalPath', 'repoUrl']) {
458
+ if (skill[key] !== undefined && skill[key] !== null && skill[key] !== '') {
459
+ normalized[key] = skill[key];
460
+ }
461
+ }
462
+ return normalized;
463
+ });
441
464
  }
442
465
 
443
466
  // 获取已安装的插件和市场插件
444
- const { plugins: installedPlugins } = pluginsService.listPlugins();
467
+ const configuredPluginsService = new PluginsService({ registry });
468
+ const { plugins: installedPlugins } = configuredPluginsService.listPlugins();
445
469
 
446
470
  // 获取 MCP 服务器
447
471
  const mcpServers = mcpService.getAllServers();
@@ -469,6 +493,15 @@ function getAvailableConfigs() {
469
493
  }));
470
494
 
471
495
  return {
496
+ platforms: platforms.map(platform => ({
497
+ key: platform.key,
498
+ label: platform.label || platform.title || platform.key,
499
+ title: platform.title || platform.label || platform.key,
500
+ promptFile: platform.promptFile || null,
501
+ promptLabel: platform.promptLabel || null,
502
+ resourceTypes: platform.resourceTypes || {},
503
+ projectResources: platform.projectResources || {}
504
+ })),
472
505
  skillsByPlatform,
473
506
  agents: Array.from(agentMap.values()),
474
507
  agentsByPlatform,
@@ -519,110 +552,91 @@ function buildTemplateCommandFileName(commandName, format) {
519
552
  return `${commandName}.${format === 'gemini' ? 'toml' : 'md'}`;
520
553
  }
521
554
 
522
- function getCommandPreviewExtension(prefix) {
523
- return prefix === '.gemini/commands' ? 'toml' : 'md';
555
+ function getCommandPreviewExtension(format) {
556
+ return format === 'gemini' ? 'toml' : 'md';
524
557
  }
525
558
 
526
- function writeJsonFile(filePath, data) {
559
+ function writeAtomicFile(filePath, content, rootDir = null) {
560
+ if (rootDir) {
561
+ assertNoSymlinkComponents(path.resolve(rootDir), path.dirname(filePath), fs);
562
+ }
527
563
  ensureDir(path.dirname(filePath));
528
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
564
+ if (rootDir) {
565
+ assertNoSymlinkComponents(path.resolve(rootDir), path.dirname(filePath), fs);
566
+ }
567
+ const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}-${randomUUID()}`;
568
+ let fileDescriptor;
569
+ try {
570
+ const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW || 0);
571
+ fileDescriptor = fs.openSync(tempPath, flags, 0o600);
572
+ if (rootDir) {
573
+ const tempRealPath = fs.realpathSync(tempPath);
574
+ const rootRealPath = fs.realpathSync(path.resolve(rootDir));
575
+ const relative = path.relative(rootRealPath, tempRealPath);
576
+ if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
577
+ throw new Error('Template atomic write escaped project root');
578
+ }
579
+ }
580
+ fs.writeFileSync(fileDescriptor, content, 'utf-8');
581
+ fs.closeSync(fileDescriptor);
582
+ fileDescriptor = undefined;
583
+ if (rootDir) {
584
+ assertNoSymlinkComponents(path.resolve(rootDir), path.dirname(filePath), fs);
585
+ }
586
+ fs.renameSync(tempPath, filePath);
587
+ } catch (error) {
588
+ if (fileDescriptor !== undefined) {
589
+ try {
590
+ fs.closeSync(fileDescriptor);
591
+ } catch (_) {}
592
+ }
593
+ try {
594
+ if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
595
+ } catch (_) {}
596
+ throw error;
597
+ }
529
598
  }
530
599
 
531
- function writeYamlFile(filePath, data) {
532
- ensureDir(path.dirname(filePath));
533
- fs.writeFileSync(filePath, yaml.dump(data || {}, {
600
+ function writeJsonFile(filePath, data, rootDir = null) {
601
+ writeAtomicFile(filePath, JSON.stringify(data, null, 2), rootDir);
602
+ }
603
+
604
+ function writeYamlFile(filePath, data, rootDir = null) {
605
+ writeAtomicFile(filePath, yaml.dump(data || {}, {
534
606
  lineWidth: 120,
535
607
  noRefs: true,
536
608
  sortKeys: false
537
- }), 'utf-8');
609
+ }), rootDir);
538
610
  }
539
611
 
540
- function mergeOmpProjectPackages(targetDir, plugins = []) {
612
+ function mergeOmpProjectPackages(targetDir, plugins = [], relativePath) {
541
613
  const packages = plugins.map(plugin => plugin.name).filter(Boolean);
542
- if (packages.length === 0) {
614
+ if (packages.length === 0 || !relativePath) {
543
615
  return false;
544
616
  }
545
617
 
546
- const ompDir = path.join(targetDir, '.omp');
547
- const settingsPath = path.join(ompDir, 'config.yml');
618
+ const settingsPath = resolveProjectTarget(targetDir, relativePath, 'template OMP config');
548
619
  let settings = {};
549
620
  if (fs.existsSync(settingsPath)) {
621
+ let parsed;
550
622
  try {
551
- const parsed = yaml.load(fs.readFileSync(settingsPath, 'utf-8'));
552
- settings = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
553
- } catch (err) {
554
- settings = {};
623
+ parsed = yaml.load(fs.readFileSync(settingsPath, 'utf-8'));
624
+ } catch (error) {
625
+ throw new Error(`无法解析 OMP 项目配置: ${error.message}`);
626
+ }
627
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
628
+ settings = parsed;
629
+ } else {
630
+ throw new Error('无法解析 OMP 项目配置: 配置必须是对象');
555
631
  }
556
632
  }
557
633
 
558
634
  const existingPackages = Array.isArray(settings.packages) ? settings.packages : [];
559
635
  settings.packages = Array.from(new Set([...existingPackages, ...packages]));
560
- writeYamlFile(settingsPath, settings);
636
+ writeYamlFile(settingsPath, settings, targetDir);
561
637
  return true;
562
638
  }
563
639
 
564
- /**
565
- * 转换为 OpenCode MCP 结构(local/remote)
566
- */
567
- function convertToOpenCodeMcpSpec(spec = {}) {
568
- const type = spec.type || 'stdio';
569
-
570
- if (type === 'local' || type === 'remote') {
571
- return { ...spec };
572
- }
573
-
574
- if (type === 'stdio') {
575
- const command = [];
576
- if (spec.command) command.push(spec.command);
577
- if (Array.isArray(spec.args)) command.push(...spec.args);
578
-
579
- const result = {
580
- type: 'local',
581
- command
582
- };
583
-
584
- if (spec.env && typeof spec.env === 'object') {
585
- result.environment = spec.env;
586
- }
587
- if (spec.cwd) {
588
- result.cwd = spec.cwd;
589
- }
590
- return result;
591
- }
592
-
593
- const result = {
594
- type: 'remote',
595
- url: spec.url || ''
596
- };
597
- if (spec.headers && typeof spec.headers === 'object') {
598
- result.headers = spec.headers;
599
- }
600
- return result;
601
- }
602
-
603
- function convertToOmpMcpSpec(spec = {}) {
604
- const type = spec.type || 'stdio';
605
- const copyCommonFields = (target) => {
606
- for (const key of ['timeout', 'auth', 'oauth']) {
607
- if (spec[key] !== undefined) {
608
- target[key] = spec[key];
609
- }
610
- }
611
- return target;
612
- };
613
-
614
- if (type === 'streamable_http') {
615
- const result = {
616
- type: 'http',
617
- url: spec.url || ''
618
- };
619
- if (spec.headers && typeof spec.headers === 'object') {
620
- result.headers = spec.headers;
621
- }
622
- return copyCommonFields(result);
623
- }
624
- return copyCommonFields({ ...spec });
625
- }
626
640
 
627
641
  /**
628
642
  * 应用模板到项目目录(完整应用,写入实际文件)
@@ -632,17 +646,16 @@ function convertToOmpMcpSpec(spec = {}) {
632
646
  * @param {string|string[]} options.aiConfigTypes - 选择的 AI 配置类型数组: ['claude', 'codex', 'gemini', 'opencode', 'omp']
633
647
  * @param {string} options.aiConfigType - (兼容旧版) 单个 AI 配置类型
634
648
  */
635
- function applyTemplateToProject(targetDir, templateId, options = {}) {
649
+ async function applyTemplateToProject(targetDir, templateId, options = {}) {
636
650
  const template = getTemplateById(templateId);
637
651
  if (!template) {
638
652
  throw new Error('模板不存在');
639
653
  }
640
654
 
641
- ensureDir(targetDir);
642
-
655
+ const projectConfigService = getProjectConfigService();
643
656
  const results = {
644
657
  aiConfigs: [],
645
- skills: { applied: template.skills?.length || 0, items: template.skills?.map(s => s.directory || s.name) || [] },
658
+ skills: { applied: 0, items: [] },
646
659
  agents: { applied: 0, files: [] },
647
660
  commands: { applied: 0, files: [] },
648
661
  plugins: { applied: 0, items: [] },
@@ -650,48 +663,109 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
650
663
  skipped: []
651
664
  };
652
665
 
653
- const aiConfigTypes = normalizeRequestedAiConfigTypes(options, template, results.skipped);
666
+ const registry = getTemplateRegistry(options);
667
+ const aiConfigMap = getAiConfigMap(registry);
668
+ const aiConfigTypes = normalizeRequestedAiConfigTypes(options, template, results.skipped, registry);
654
669
 
655
670
  for (const aiConfigType of aiConfigTypes) {
656
671
  const aiConfig = resolveAiConfig(template, aiConfigType);
657
- const configInfo = AI_CONFIG_MAP[aiConfigType];
672
+ const configInfo = aiConfigMap[aiConfigType];
658
673
  if (!configInfo?.fileName) {
659
- pushSkipped(results.skipped, 'aiConfig', configInfo?.name || aiConfigType, 'OMP 项目级命令模板通过 .omp/commands 写入,未生成单独 AI 配置文件');
674
+ const reason = aiConfigType === 'omp'
675
+ ? 'OMP 项目级命令模板通过 .omp/commands 写入,未生成单独 AI 配置文件'
676
+ : `平台 ${configInfo?.name || aiConfigType} 未声明项目级配置文件,已跳过`;
677
+ pushSkipped(results.skipped, 'aiConfig', configInfo?.name || aiConfigType, reason);
660
678
  } else if (aiConfig?.enabled && aiConfig?.content) {
661
- const configPath = path.join(targetDir, configInfo.fileName);
662
- ensureDir(path.dirname(configPath));
663
- fs.writeFileSync(configPath, aiConfig.content, 'utf-8');
664
- results.aiConfigs.push({ applied: true, path: configInfo.fileName, type: configInfo.name, key: aiConfigType });
679
+ try {
680
+ const written = await projectConfigService.writeInstruction(targetDir, aiConfigType, aiConfig.content);
681
+ if (written?.supported === false) {
682
+ pushSkipped(results.skipped, 'aiConfig', configInfo.fileName, `平台 ${configInfo.name} 不支持项目级指令文件,已跳过`);
683
+ } else {
684
+ results.aiConfigs.push({
685
+ applied: true,
686
+ path: written?.path || configInfo.fileName,
687
+ type: configInfo.name,
688
+ key: aiConfigType
689
+ });
690
+ }
691
+ } catch (error) {
692
+ pushSkipped(results.skipped, 'aiConfig', configInfo.fileName, `写入项目级指令失败: ${error.message}`);
693
+ }
665
694
  } else {
666
- const fileName = AI_CONFIG_MAP[aiConfigType]?.fileName || aiConfigType;
695
+ const fileName = aiConfigMap[aiConfigType]?.fileName || aiConfigType;
667
696
  pushSkipped(results.skipped, 'aiConfig', fileName, `模板未启用 ${fileName},已跳过`);
668
697
  }
669
698
  }
670
699
 
671
- if (template.agents?.length > 0) {
672
- const agentTargets = [];
673
- if (aiConfigTypes.includes('claude')) {
674
- agentTargets.push({ baseDir: path.join(targetDir, '.claude', 'agents'), prefix: '.claude/agents' });
675
- }
676
- if (aiConfigTypes.includes('opencode')) {
677
- agentTargets.push({ baseDir: path.join(targetDir, '.opencode', 'agents'), prefix: '.opencode/agents' });
678
- }
679
- if (aiConfigTypes.includes('gemini')) {
680
- agentTargets.push({ baseDir: path.join(targetDir, '.gemini', 'agents'), prefix: '.gemini/agents' });
700
+ for (const skill of template.skills || []) {
701
+ const skillPlatform = skill?.platform || template.cliType || aiConfigTypes[0];
702
+ const skillName = typeof skill === 'string' ? skill : (skill?.directory || skill?.name);
703
+ if (!skillName) continue;
704
+ if (!skillPlatform || !aiConfigTypes.includes(skillPlatform)) {
705
+ pushSkipped(results.skipped, 'skill', skillName, `未选择 ${skillPlatform || '目标平台'},已跳过`);
706
+ continue;
681
707
  }
682
708
 
683
- for (const target of agentTargets) {
684
- ensureDir(target.baseDir);
709
+ const skillInput = typeof skill === 'string'
710
+ ? { directory: skill, name: skill }
711
+ : {
712
+ directory: skill.directory || skill.name,
713
+ name: skill.name || skill.directory,
714
+ ...(skill.fullDirectory ? { fullDirectory: skill.fullDirectory } : {}),
715
+ ...(Array.isArray(skill.files) ? { files: skill.files } : {}),
716
+ ...(skill.repo
717
+ ? { repo: skill.repo }
718
+ : ((skill.repoProvider || skill.repoId || skill.repoLocalPath || skill.repoProjectPath)
719
+ ? {
720
+ repo: {
721
+ provider: skill.repoProvider,
722
+ owner: skill.repoOwner || null,
723
+ name: skill.repoName || null,
724
+ branch: skill.repoBranch || null,
725
+ directory: skill.repoDirectory || '',
726
+ host: skill.repoHost || null,
727
+ projectPath: skill.repoProjectPath || null,
728
+ localPath: skill.repoLocalPath || null,
729
+ id: skill.repoId || null,
730
+ repoUrl: skill.repoUrl || null
731
+ }
732
+ }
733
+ : {}))
734
+ };
735
+
736
+ try {
737
+ const skillService = new SkillService(skillPlatform);
738
+ const registration = skillService.registerTemplateSkill({
739
+ ...skillInput,
740
+ scope: 'project',
741
+ cwd: targetDir
742
+ });
743
+ if (registration?.status === 'pending_refresh') {
744
+ results.skills.pending = (results.skills.pending || 0) + 1;
745
+ results.skills.pendingItems = [...(results.skills.pendingItems || []), skillName];
746
+ } else if (registration?.status === 'failed') {
747
+ pushSkipped(results.skipped, 'skill', skillName, '项目级 Skill 注册失败,已跳过');
748
+ } else {
749
+ results.skills.applied++;
750
+ }
751
+ results.skills.items.push(skillName);
752
+ } catch (error) {
753
+ pushSkipped(results.skipped, 'skill', skillName, `注册项目级 Skill 失败: ${error.message}`);
685
754
  }
755
+ }
756
+
757
+ if (template.agents?.length > 0) {
758
+ const agentTargets = getProjectResourceTargets(aiConfigTypes, 'agents', registry);
686
759
 
687
760
  for (const agent of template.agents) {
688
761
  const fileName = resolveItemName(agent.fileName, agent.name, 'agent').toLowerCase().replace(/\s+/g, '-');
689
762
  const content = generateAgentContent(agent);
690
763
  let written = false;
691
764
  for (const target of agentTargets) {
692
- const filePath = path.join(target.baseDir, `${fileName}.md`);
693
- fs.writeFileSync(filePath, content, 'utf-8');
694
- results.agents.files.push(`${target.prefix}/${fileName}.md`);
765
+ const relativePath = `${target.prefix}/${fileName}.md`;
766
+ const filePath = resolveProjectTarget(targetDir, relativePath, 'template agent file');
767
+ writeAtomicFile(filePath, content, targetDir);
768
+ results.agents.files.push(relativePath);
695
769
  written = true;
696
770
  }
697
771
  if (aiConfigTypes.includes('codex')) {
@@ -707,26 +781,7 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
707
781
  }
708
782
 
709
783
  if (template.commands?.length > 0) {
710
- const commandTargets = [];
711
- if (aiConfigTypes.includes('claude')) {
712
- commandTargets.push({ baseDir: path.join(targetDir, '.claude', 'commands'), prefix: '.claude/commands', format: 'claude' });
713
- }
714
- if (aiConfigTypes.includes('codex')) {
715
- commandTargets.push({ baseDir: path.join(targetDir, '.codex', 'prompts'), prefix: '.codex/prompts', format: 'codex' });
716
- }
717
- if (aiConfigTypes.includes('opencode')) {
718
- commandTargets.push({ baseDir: path.join(targetDir, '.opencode', 'commands'), prefix: '.opencode/commands', format: 'claude' });
719
- }
720
- if (aiConfigTypes.includes('gemini')) {
721
- commandTargets.push({ baseDir: path.join(targetDir, '.gemini', 'commands'), prefix: '.gemini/commands', format: 'gemini' });
722
- }
723
- if (aiConfigTypes.includes('omp')) {
724
- commandTargets.push({ baseDir: path.join(targetDir, '.omp', 'commands'), prefix: '.omp/commands', format: 'omp' });
725
- }
726
-
727
- for (const target of commandTargets) {
728
- ensureDir(target.baseDir);
729
- }
784
+ const commandTargets = getProjectResourceTargets(aiConfigTypes, 'commands', registry);
730
785
 
731
786
  for (const command of template.commands) {
732
787
  const commandName = resolveItemName(command.name, null, 'command');
@@ -738,15 +793,12 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
738
793
  } else if (target.format === 'gemini') {
739
794
  content = convertCommandToGemini(content).content;
740
795
  }
741
- const targetCmdDir = command.namespace
742
- ? path.join(target.baseDir, command.namespace)
743
- : target.baseDir;
744
- ensureDir(targetCmdDir);
745
- const filePath = path.join(targetCmdDir, buildTemplateCommandFileName(commandName, target.format));
746
- fs.writeFileSync(filePath, content, 'utf-8');
796
+ const commandFile = buildTemplateCommandFileName(commandName, target.format);
747
797
  const relativePath = command.namespace
748
- ? `${target.prefix}/${command.namespace}/${buildTemplateCommandFileName(commandName, target.format)}`
749
- : `${target.prefix}/${buildTemplateCommandFileName(commandName, target.format)}`;
798
+ ? `${target.prefix}/${command.namespace}/${commandFile}`
799
+ : `${target.prefix}/${commandFile}`;
800
+ const filePath = resolveProjectTarget(targetDir, relativePath, 'template command file');
801
+ writeAtomicFile(filePath, content, targetDir);
750
802
  results.commands.files.push(relativePath);
751
803
  written = true;
752
804
  }
@@ -768,66 +820,71 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
768
820
  }
769
821
 
770
822
  const hasMcp = template.mcpServers?.length > 0;
771
- const writesGenericMcpConfig = hasMcp && aiConfigTypes.some(type => type !== 'omp');
772
- const writesOmpMcpConfig = hasMcp && aiConfigTypes.includes('omp');
773
- const hasPluginsForOpenCode = aiConfigTypes.includes('opencode') && template.plugins?.length > 0;
774
- const hasPluginsForOmp = aiConfigTypes.includes('omp') && template.plugins?.length > 0;
775
- if (hasMcp || hasPluginsForOpenCode || hasPluginsForOmp) {
776
- const mcpConfig = { mcpServers: {} };
777
- const ompMcpConfig = { mcpServers: {} };
778
- const opencodeConfig = { mcp: {}, plugin: [] };
823
+ const openCodePluginTarget = getProjectResourceDescriptor('opencode', 'plugins', registry);
824
+ const ompPluginTarget = getProjectResourceDescriptor('omp', 'plugins', registry);
825
+ const hasPluginsForOpenCode = Boolean(openCodePluginTarget && aiConfigTypes.includes('opencode') && template.plugins?.length > 0);
826
+ const hasPluginsForOmp = Boolean(ompPluginTarget && aiConfigTypes.includes('omp') && template.plugins?.length > 0);
827
+ if (hasMcp) {
779
828
  const allServers = mcpService.getAllServers();
780
829
  const presets = mcpService.getPresets();
781
830
 
782
831
  for (const serverId of template.mcpServers || []) {
783
- // 先从已配置的服务器中查找
784
832
  let serverSpec = allServers[serverId]?.server;
785
- // 如果没有,从预设中查找
786
833
  if (!serverSpec) {
787
834
  const preset = presets.find(p => p.id === serverId);
788
- if (preset) {
789
- serverSpec = preset.server;
790
- }
835
+ if (preset) serverSpec = preset.server;
791
836
  }
792
- if (serverSpec) {
793
- if (writesGenericMcpConfig) {
794
- mcpConfig.mcpServers[serverId] = serverSpec;
795
- }
796
- if (writesOmpMcpConfig) {
797
- ompMcpConfig.mcpServers[serverId] = convertToOmpMcpSpec(serverSpec);
798
- }
799
- opencodeConfig.mcp[serverId] = convertToOpenCodeMcpSpec(serverSpec);
800
- if (writesGenericMcpConfig || writesOmpMcpConfig) {
801
- results.mcpServers.applied++;
802
- }
803
- } else {
837
+
838
+ if (!serverSpec) {
804
839
  pushSkipped(results.skipped, 'mcpServer', serverId, '未找到对应 MCP 服务配置,已跳过');
840
+ continue;
805
841
  }
806
- }
807
842
 
808
- if (writesGenericMcpConfig && Object.keys(mcpConfig.mcpServers).length > 0) {
809
- const mcpPath = path.join(targetDir, '.mcp.json');
810
- writeJsonFile(mcpPath, mcpConfig);
811
- }
812
-
813
- if (writesOmpMcpConfig && Object.keys(ompMcpConfig.mcpServers).length > 0) {
814
- const ompMcpPath = path.join(targetDir, '.omp', 'mcp.json');
815
- writeJsonFile(ompMcpPath, ompMcpConfig);
843
+ let applied = false;
844
+ for (const aiConfigType of aiConfigTypes) {
845
+ try {
846
+ const written = await projectConfigService.upsertProjectMcp(
847
+ targetDir,
848
+ aiConfigType,
849
+ serverId,
850
+ serverSpec
851
+ );
852
+ if (written?.success !== false) applied = true;
853
+ } catch (error) {
854
+ pushSkipped(
855
+ results.skipped,
856
+ 'mcpServer',
857
+ `${serverId} (${aiConfigType})`,
858
+ `写入项目级 MCP 失败: ${error.message}`
859
+ );
860
+ }
861
+ }
862
+ if (applied) results.mcpServers.applied++;
816
863
  }
864
+ }
817
865
 
818
- if (hasPluginsForOpenCode) {
819
- opencodeConfig.plugin = (template.plugins || []).map(p => p.name).filter(Boolean);
820
- }
821
- if (aiConfigTypes.includes('opencode') && (Object.keys(opencodeConfig.mcp).length > 0 || opencodeConfig.plugin.length > 0)) {
822
- const opencodeDir = path.join(targetDir, '.opencode');
823
- ensureDir(opencodeDir);
824
- const opencodePath = path.join(opencodeDir, 'opencode.json');
825
- writeJsonFile(opencodePath, opencodeConfig);
866
+ if (hasPluginsForOpenCode) {
867
+ const opencodePath = resolveProjectTarget(targetDir, openCodePluginTarget.prefix, 'template OpenCode config');
868
+ let opencodeConfig = {};
869
+ if (fs.existsSync(opencodePath)) {
870
+ let parsed;
871
+ try {
872
+ parsed = JSON.parse(fs.readFileSync(opencodePath, 'utf-8'));
873
+ } catch (error) {
874
+ throw new Error(`无法解析 OpenCode 项目配置: ${error.message}`);
875
+ }
876
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
877
+ opencodeConfig = parsed;
878
+ } else {
879
+ throw new Error('无法解析 OpenCode 项目配置: 配置必须是对象');
880
+ }
826
881
  }
882
+ opencodeConfig.plugin = (template.plugins || []).map(p => p.name).filter(Boolean);
883
+ writeJsonFile(opencodePath, opencodeConfig, targetDir);
884
+ }
827
885
 
828
- if (hasPluginsForOmp) {
829
- mergeOmpProjectPackages(targetDir, template.plugins || []);
830
- }
886
+ if (hasPluginsForOmp) {
887
+ mergeOmpProjectPackages(targetDir, template.plugins || [], ompPluginTarget.prefix);
831
888
  }
832
889
 
833
890
  const configRecord = {
@@ -843,8 +900,8 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
843
900
  mcpServers: template.mcpServers || [],
844
901
  skipped: results.skipped
845
902
  };
846
- const recordPath = path.join(targetDir, '.ctx-config.json');
847
- fs.writeFileSync(recordPath, JSON.stringify(configRecord, null, 2), 'utf-8');
903
+ const recordPath = resolveProjectTarget(targetDir, '.ctx-config.json', 'template provenance file');
904
+ writeAtomicFile(recordPath, JSON.stringify(configRecord, null, 2), targetDir);
848
905
 
849
906
  return {
850
907
  success: true,
@@ -882,15 +939,20 @@ function previewTemplateApplication(targetDir, templateId, options = {}) {
882
939
  }
883
940
  };
884
941
 
885
- const aiConfigTypes = normalizeRequestedAiConfigTypes(options, template, preview.skipped);
942
+ const registry = getTemplateRegistry(options);
943
+ const aiConfigMap = getAiConfigMap(registry);
944
+ const aiConfigTypes = normalizeRequestedAiConfigTypes(options, template, preview.skipped, registry);
886
945
 
887
946
  for (const aiConfigType of aiConfigTypes) {
888
947
  const aiConfig = resolveAiConfig(template, aiConfigType);
889
- const configInfo = AI_CONFIG_MAP[aiConfigType];
948
+ const configInfo = aiConfigMap[aiConfigType];
890
949
  if (!configInfo?.fileName) {
891
- pushSkipped(preview.skipped, 'aiConfig', configInfo?.name || aiConfigType, 'OMP 项目级命令模板通过 .omp/commands 写入,预览不生成单独 AI 配置文件');
950
+ const reason = aiConfigType === 'omp'
951
+ ? 'OMP 项目级命令模板通过 .omp/commands 写入,预览不生成单独 AI 配置文件'
952
+ : `平台 ${configInfo?.name || aiConfigType} 未声明项目级配置文件,预览已跳过`;
953
+ pushSkipped(preview.skipped, 'aiConfig', configInfo?.name || aiConfigType, reason);
892
954
  } else if (aiConfig?.enabled && aiConfig?.content) {
893
- const configPath = path.join(targetDir, configInfo.fileName);
955
+ const configPath = resolveProjectTarget(targetDir, configInfo.fileName, 'template instruction file');
894
956
  if (fs.existsSync(configPath)) {
895
957
  preview.willOverwrite.push(configInfo.fileName);
896
958
  } else {
@@ -898,7 +960,7 @@ function previewTemplateApplication(targetDir, templateId, options = {}) {
898
960
  }
899
961
  preview.summary.aiConfigs.push({ type: aiConfigType, fileName: configInfo.fileName, name: configInfo.name });
900
962
  } else {
901
- const fileName = AI_CONFIG_MAP[aiConfigType]?.fileName || aiConfigType;
963
+ const fileName = aiConfigMap[aiConfigType]?.fileName || aiConfigType;
902
964
  pushSkipped(preview.skipped, 'aiConfig', fileName, `模板未启用 ${fileName},已跳过`);
903
965
  }
904
966
  }
@@ -909,17 +971,14 @@ function previewTemplateApplication(targetDir, templateId, options = {}) {
909
971
  }
910
972
 
911
973
  if (template.agents?.length > 0) {
912
- const agentPrefixes = [];
913
- if (aiConfigTypes.includes('claude')) agentPrefixes.push('.claude/agents');
914
- if (aiConfigTypes.includes('opencode')) agentPrefixes.push('.opencode/agents');
915
- if (aiConfigTypes.includes('gemini')) agentPrefixes.push('.gemini/agents');
974
+ const agentTargets = getProjectResourceTargets(aiConfigTypes, 'agents', registry);
916
975
 
917
976
  for (const agent of template.agents) {
918
977
  const fileName = resolveItemName(agent.fileName, agent.name, 'agent').toLowerCase().replace(/\s+/g, '-');
919
978
  let applicable = false;
920
- for (const prefix of agentPrefixes) {
921
- const relativePath = `${prefix}/${fileName}.md`;
922
- const fullPath = path.join(targetDir, relativePath);
979
+ for (const target of agentTargets) {
980
+ const relativePath = `${target.prefix}/${fileName}.md`;
981
+ const fullPath = resolveProjectTarget(targetDir, relativePath, 'template agent file');
923
982
  if (fs.existsSync(fullPath)) {
924
983
  preview.willOverwrite.push(relativePath);
925
984
  } else {
@@ -940,22 +999,17 @@ function previewTemplateApplication(targetDir, templateId, options = {}) {
940
999
  }
941
1000
 
942
1001
  if (template.commands?.length > 0) {
943
- const commandPrefixes = [];
944
- if (aiConfigTypes.includes('claude')) commandPrefixes.push('.claude/commands');
945
- if (aiConfigTypes.includes('codex')) commandPrefixes.push('.codex/prompts');
946
- if (aiConfigTypes.includes('opencode')) commandPrefixes.push('.opencode/commands');
947
- if (aiConfigTypes.includes('gemini')) commandPrefixes.push('.gemini/commands');
948
- if (aiConfigTypes.includes('omp')) commandPrefixes.push('.omp/commands');
1002
+ const commandTargets = getProjectResourceTargets(aiConfigTypes, 'commands', registry);
949
1003
 
950
1004
  for (const command of template.commands) {
951
1005
  const commandName = resolveItemName(command.name, null, 'command');
952
1006
  let applicable = false;
953
- for (const prefix of commandPrefixes) {
954
- const extension = getCommandPreviewExtension(prefix);
1007
+ for (const target of commandTargets) {
1008
+ const extension = getCommandPreviewExtension(target.format);
955
1009
  const relativePath = command.namespace
956
- ? `${prefix}/${command.namespace}/${commandName}.${extension}`
957
- : `${prefix}/${commandName}.${extension}`;
958
- const fullPath = path.join(targetDir, relativePath);
1010
+ ? `${target.prefix}/${command.namespace}/${commandName}.${extension}`
1011
+ : `${target.prefix}/${commandName}.${extension}`;
1012
+ const fullPath = resolveProjectTarget(targetDir, relativePath, 'template command file');
959
1013
  if (fs.existsSync(fullPath)) {
960
1014
  preview.willOverwrite.push(relativePath);
961
1015
  } else {
@@ -969,8 +1023,16 @@ function previewTemplateApplication(targetDir, templateId, options = {}) {
969
1023
  }
970
1024
  }
971
1025
 
972
- const writesGenericMcpConfig = template.mcpServers?.length > 0 && aiConfigTypes.some(type => type !== 'omp');
973
- const writesOmpMcpConfig = template.mcpServers?.length > 0 && aiConfigTypes.includes('omp');
1026
+ const genericMcpTarget = getProjectResourceTargets(
1027
+ aiConfigTypes.filter(type => type !== 'omp'),
1028
+ 'mcp',
1029
+ registry
1030
+ )[0];
1031
+ const writesGenericMcpConfig = Boolean(
1032
+ template.mcpServers?.length > 0 && aiConfigTypes.some(type => type !== 'omp') && genericMcpTarget
1033
+ );
1034
+ const ompMcpTarget = getProjectResourceDescriptor('omp', 'mcp', registry);
1035
+ const writesOmpMcpConfig = Boolean(template.mcpServers?.length > 0 && aiConfigTypes.includes('omp') && ompMcpTarget);
974
1036
  const allServers = mcpService.getAllServers();
975
1037
  const presets = mcpService.getPresets();
976
1038
  let resolvableMcpCount = 0;
@@ -992,43 +1054,46 @@ function previewTemplateApplication(targetDir, templateId, options = {}) {
992
1054
  }
993
1055
 
994
1056
  if (writesGenericMcpConfig && resolvableMcpCount > 0) {
995
- const mcpPath = path.join(targetDir, '.mcp.json');
1057
+ const mcpRelativePath = genericMcpTarget.prefix;
1058
+ const mcpPath = resolveProjectTarget(targetDir, mcpRelativePath, 'template MCP file');
996
1059
  if (fs.existsSync(mcpPath)) {
997
- preview.willOverwrite.push('.mcp.json');
1060
+ preview.willOverwrite.push(mcpRelativePath);
998
1061
  } else {
999
- preview.willCreate.push('.mcp.json');
1062
+ preview.willCreate.push(mcpRelativePath);
1000
1063
  }
1001
1064
  preview.summary.mcpServers = resolvableMcpCount;
1002
1065
  }
1003
1066
 
1004
1067
  if (writesOmpMcpConfig && resolvableMcpCount > 0) {
1005
- const ompMcpPath = path.join(targetDir, '.omp/mcp.json');
1068
+ const ompMcpPath = resolveProjectTarget(targetDir, ompMcpTarget.prefix, 'template OMP MCP file');
1006
1069
  if (fs.existsSync(ompMcpPath)) {
1007
- preview.willOverwrite.push('.omp/mcp.json');
1070
+ preview.willOverwrite.push(ompMcpTarget.prefix);
1008
1071
  } else {
1009
- preview.willCreate.push('.omp/mcp.json');
1072
+ preview.willCreate.push(ompMcpTarget.prefix);
1010
1073
  }
1011
1074
  preview.summary.mcpServers = resolvableMcpCount;
1012
1075
  }
1013
1076
 
1014
- if (aiConfigTypes.includes('opencode') && (resolvableMcpCount > 0 || template.plugins?.length > 0)) {
1015
- const opencodeConfigPath = path.join(targetDir, '.opencode/opencode.json');
1077
+ const openCodePluginTarget = getProjectResourceDescriptor('opencode', 'plugins', registry);
1078
+ if (aiConfigTypes.includes('opencode') && openCodePluginTarget && (resolvableMcpCount > 0 || template.plugins?.length > 0)) {
1079
+ const opencodeConfigPath = resolveProjectTarget(targetDir, openCodePluginTarget.prefix, 'template OpenCode config');
1016
1080
  if (fs.existsSync(opencodeConfigPath)) {
1017
- preview.willOverwrite.push('.opencode/opencode.json');
1081
+ preview.willOverwrite.push(openCodePluginTarget.prefix);
1018
1082
  } else {
1019
- preview.willCreate.push('.opencode/opencode.json');
1083
+ preview.willCreate.push(openCodePluginTarget.prefix);
1020
1084
  }
1021
1085
  }
1022
1086
 
1023
1087
  if (template.plugins?.length > 0) {
1024
1088
  if (aiConfigTypes.includes('opencode') || aiConfigTypes.includes('omp')) {
1025
1089
  preview.summary.plugins = template.plugins.length;
1026
- if (aiConfigTypes.includes('omp')) {
1027
- const ompSettingsPath = path.join(targetDir, '.omp/config.yml');
1090
+ const ompPluginTarget = getProjectResourceDescriptor('omp', 'plugins', registry);
1091
+ if (aiConfigTypes.includes('omp') && ompPluginTarget) {
1092
+ const ompSettingsPath = resolveProjectTarget(targetDir, ompPluginTarget.prefix, 'template OMP config');
1028
1093
  if (fs.existsSync(ompSettingsPath)) {
1029
- preview.willOverwrite.push('.omp/config.yml');
1094
+ preview.willOverwrite.push(ompPluginTarget.prefix);
1030
1095
  } else {
1031
- preview.willCreate.push('.omp/config.yml');
1096
+ preview.willCreate.push(ompPluginTarget.prefix);
1032
1097
  }
1033
1098
  }
1034
1099
  } else {
@@ -1055,5 +1120,6 @@ module.exports = {
1055
1120
  readCurrentConfig,
1056
1121
  getAvailableConfigs,
1057
1122
  applyTemplateToProject,
1058
- previewTemplateApplication
1123
+ previewTemplateApplication,
1124
+ getAiConfigMap
1059
1125
  };