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.
Files changed (434) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +86 -15
  3. package/dist/web/assets/AgentsDrawer-Db9wAyaZ.js +1 -0
  4. package/dist/web/assets/AgentsDrawer-Pw2k-ZAa.css +1 -0
  5. package/dist/web/assets/Analytics-C4gMzv7g.js +1 -0
  6. package/dist/web/assets/Analytics-CyHhR126.css +1 -0
  7. package/dist/web/assets/AssetPathField-Bd_nMFu3.js +1 -0
  8. package/dist/web/assets/ChatHistoryDrawer-B2qBQs9L.css +1 -0
  9. package/dist/web/assets/ChatHistoryDrawer-CwzJHyNP.js +1 -0
  10. package/dist/web/assets/CollapseItem-CTCfSiue.js +1 -0
  11. package/dist/web/assets/CommandsDrawer-BZJxlPvV.css +1 -0
  12. package/dist/web/assets/CommandsDrawer-vDghB1U9.js +1 -0
  13. package/dist/web/assets/ConfigExportDrawer-C-XDvlHc.js +1 -0
  14. package/dist/web/assets/ConfigExportDrawer-dwruRKWr.css +1 -0
  15. package/dist/web/assets/ConfigTemplates-CYY8XRED.js +1 -0
  16. package/dist/web/assets/ConfigTemplatesDrawer-BfDi8yO3.css +1 -0
  17. package/dist/web/assets/ConfigTemplatesDrawer-CbsJp76X.js +1 -0
  18. package/dist/web/assets/ConfigTemplatesPanel-CWyPtk67.css +1 -0
  19. package/dist/web/assets/ConfigTemplatesPanel-liMSTh7s.js +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-D6QSiYaL.js +1 -0
  24. package/dist/web/assets/GatewayConvertDrawer-CuNlf7AC.css +1 -0
  25. package/dist/web/assets/GatewayConvertDrawer-RvET6iNG.js +1 -0
  26. package/dist/web/assets/Home-CSuFuJ9Y.css +1 -0
  27. package/dist/web/assets/Home-P8eseZcf.js +1 -0
  28. package/dist/web/assets/MarkdownEditor-CYmLGYju.js +1 -0
  29. package/dist/web/assets/MarkdownEditor-qfVgSre_.css +1 -0
  30. package/dist/web/assets/McpDrawer-C0dnsZhQ.css +1 -0
  31. package/dist/web/assets/McpDrawer-Ch3bkKIq.js +1 -0
  32. package/dist/web/assets/McpFormDrawer-BRbDcawn.css +1 -0
  33. package/dist/web/assets/McpFormDrawer-BcTlgSpN.js +1 -0
  34. package/dist/web/assets/PluginManager-Dh2o0xkZ.js +1 -0
  35. package/dist/web/assets/PluginsDrawer-DM3QjprE.css +1 -0
  36. package/dist/web/assets/PluginsDrawer-deKQ5u3I.js +1 -0
  37. package/dist/web/assets/PluginsPanel-BofsgQgj.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-DhSWJHTA.js +1 -0
  41. package/dist/web/assets/PromptsDrawer-BIgiYCEQ.css +1 -0
  42. package/dist/web/assets/PromptsDrawer-Cfb2FeeU.js +1 -0
  43. package/dist/web/assets/RecentSessionsDrawer-3eXamJhr.js +1 -0
  44. package/dist/web/assets/RecentSessionsDrawer-CKFsOYDM.css +1 -0
  45. package/dist/web/assets/SessionCard-CW0eHURI.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-DQfyDGWU.js +1 -0
  49. package/dist/web/assets/SettingsDrawer-Dw5NUgHC.css +1 -0
  50. package/dist/web/assets/SettingsDrawer-u_vwUuPS.js +1 -0
  51. package/dist/web/assets/SkillManager-C0GCT2Kb.css +1 -0
  52. package/dist/web/assets/SkillManager-DHMprJLA.js +1 -0
  53. package/dist/web/assets/SkillsDrawer-BCNme-aJ.css +1 -0
  54. package/dist/web/assets/SkillsDrawer-CkF3BpyC.js +1 -0
  55. package/dist/web/assets/SkillsPanel-B3Nm5j32.css +1 -0
  56. package/dist/web/assets/SkillsPanel-DWE4pFGx.js +1 -0
  57. package/dist/web/assets/SpeedTestDrawer-BgfG1Ked.js +1 -0
  58. package/dist/web/assets/SpeedTestDrawer-Dc0WToJ_.css +1 -0
  59. package/dist/web/assets/Upload-CNwaGOws.js +1 -0
  60. package/dist/web/assets/WorkspaceDrawer-Br5CeCzL.js +1 -0
  61. package/dist/web/assets/WorkspaceDrawer-CGnMu9p1.css +1 -0
  62. package/dist/web/assets/WorkspaceManager-B2O8-qIw.js +1 -0
  63. package/dist/web/assets/WorkspacePanel-5oDKit3c.js +1 -0
  64. package/dist/web/assets/WorkspacePanel-D0-zHTHP.css +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-BMAUPq5K.js +2 -0
  71. package/dist/web/assets/index-Bi1MQHYX.css +1 -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-jbmqZuJs.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 +104 -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 +165 -0
  140. package/src/platforms/channel-auth-service.js +107 -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 +98 -418
  269. package/src/{server/services → platforms}/notification-hooks.js +190 -1023
  270. package/src/platforms/oauth-credentials-service.js +635 -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 +133 -24
  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 +12 -2
  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 +876 -214
  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
@@ -0,0 +1,1578 @@
1
+ # Configuration-Driven CLI Platform 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:** 将平台扩展改造成 Manifest + Capability Driver 架构,让符合通用协议的 CLI 只需增加安全的 Manifest 配置,同时保持现有五个平台行为和测试 mock path。
6
+
7
+ **Architecture:** 内置和用户平台由 `Platform Registry` 统一加载、校验和暴露。项目、会话、渠道、代理、统计和资源同步分别使用能力 driver;Claude、Codex、Gemini、OpenCode、OMP 的现有模块保留为稳定 facade 和专用 driver。CLI、HTTP、Dashboard 和 Web UI 只依赖 Registry 与 application use case,不直接维护平台 `switch`。
8
+
9
+ **Tech Stack:** Node.js >= 22.13.0、CommonJS、Express、AJV、Vitest、Vue 3、Vite、Pinia、现有 `require.resolve()` + `require.cache` 测试 seam。
10
+
11
+ ---
12
+
13
+ ## Constraints and Working-Tree Safety
14
+
15
+ - 不重置、覆盖或格式化当前已有的 skill 相关未提交修改:`package-lock.json`、`scripts/test-skill-providers.js`、`src/server/api/plugins.js`、`src/server/api/skills.js`、`src/server/services/skill-service.js`、`tests/unit/services/skill-service.test.js` 和两个 SQLite WAL 文件。
16
+ - 每个提交只 `git add` 本任务明确列出的文件。
17
+ - 保留现有模块路径和导出名称,尤其是 `src/config/paths.js`、五个平台 channel service、五个平台 proxy server 和对应 API facade。
18
+ - 不在迁移中改变公开 CLI 命令、现有 API 路径、OMP managed mode 或原生配置恢复语义。
19
+ - 每个任务先写行为测试,再写实现;任务内只运行对应的 focused test,整套测试放在最终验证任务运行。
20
+
21
+ ## File Map
22
+
23
+ ### New core files
24
+
25
+ - `src/platforms/manifest-schema.js`: Manifest 的 AJV schema、校验错误格式和 driver ID allowlist 校验。
26
+ - `src/platforms/manifests/*.json`: Claude、Codex、Gemini、OpenCode、OMP 的内置声明。
27
+ - `src/platforms/path-resolver.js`: 受控环境变量、`{home}` 模板和内置特殊路径解析。
28
+ - `src/platforms/driver-registry.js`: capability driver 注册、allowlist 和 lazy factory。
29
+ - `src/platforms/registry.js`: 内置 Manifest、`~/.cc-tool/config/platforms.json`、旧 UI 配置兼容数据的合并与公开字段投影。
30
+ - `src/platforms/runtime.js`: Registry、PathResolver、DriverRegistry 和基础依赖的组合入口。
31
+ - `src/platforms/drivers/unsupported.js`: 明确返回 `unsupported` 的 driver。
32
+ - `src/platforms/drivers/generic-jsonl.js`: 声明式 JSONL 会话 inventory/parse driver。
33
+ - `src/platforms/drivers/generic-filesystem.js`: 声明式目录/文件资源同步 driver。
34
+ - `src/platforms/drivers/generic-openai-compatible.js`: 标准 OpenAI-compatible 渠道传输 driver。
35
+ - `src/platforms/drivers/legacy.js`: 通过原有模块路径懒加载现有平台实现的 driver factory。
36
+ - `src/server/api/platforms.js`: 平台目录和通用平台能力 API。
37
+ - `src/server/api/platform-route-factory.js`: 新平台使用的通用 route factory。
38
+ - `src/web/src/api/platforms.js`: 平台目录 API client。
39
+ - `src/web/src/stores/platforms.js`: 前端平台目录的加载、缓存和状态。
40
+ - `src/web/src/config/iconTokens.js`: `iconToken` 到 Vue 图标组件的唯一映射。
41
+ - `src/web/src/components/channel/commonChannelSchema.js`: 通用渠道表单 schema。
42
+
43
+ ### Existing files to modify
44
+
45
+ - `package.json`: 将 `src/platforms/` 纳入发布文件。
46
+ - `src/config/paths.js`: 增加中心平台配置文件路径,同时保留既有 `PATHS`、`NATIVE_PATHS` 结构。
47
+ - `src/shared/platforms.js`: 保留规范化函数,改为消费 Registry 投影或调用共享归一化逻辑。
48
+ - `src/server/services/session-history-adapters/index.js`: 保留路径和导出,接入 Registry 的 session driver 兼容层。
49
+ - `src/server/services/session-history-index.js`: 默认 adapter 从 runtime 获取,显式 `adapterRegistry` 仍优先。
50
+ - `src/server/services/dashboard-snapshot-worker.js`: 用 capability driver 替换平台聚合 `switch`。
51
+ - `src/server/index.js`: 挂载平台目录 API,逐步切换自动恢复和 route registration。
52
+ - `src/server/services/session-history-worker.js`: 子进程 inventory boot、结构化 IPC 错误和协议清理。
53
+ - `src/server/services/config-sync-manager.js`: 增加 `syncToPlatform`,让批量同步遍历 Registry。
54
+ - `src/server/services/config-registry-service.js`: 平台集合和支持矩阵从 Registry 派生。
55
+ - `src/server/services/config-export-service.js`: 平台 snapshot 和 native config 通过 driver 获取。
56
+ - `src/server/services/config-templates-service.js`: AI config 文件名和支持平台从 Manifest 派生。
57
+ - `src/server/services/ui-config.js`: 读取旧 `customCliPlatforms` 并迁移到 `platforms.json`。
58
+ - `src/commands/proxy-control.js`: 标签、端点和可用平台从 Registry 获取。
59
+ - `src/commands/toggle-proxy.js`: 通过 ProxyDriver 解析平台。
60
+ - `src/commands/daemon.js`: 端口和状态输出遍历 Registry。
61
+ - `src/commands/cli-type.js`: 类型选项从 Registry 派生。
62
+ - `src/index.js`: 保留兼容命令,平台路由使用 command registry。
63
+ - `src/web/src/config/platforms.js`: 删除完整内置平台副本,只保留 API 归一化、图标和排序 fallback。
64
+ - `src/web/src/main.js`、`src/web/src/stores/platforms.js`: 启动时加载公共平台目录并保留最小离线 fallback。
65
+ - `src/web/src/components/channel/BaseChannelPanel.vue`、`src/web/src/components/channel/commonChannelSchema.js`: 泛型渠道面板和 CRUD API 接入。
66
+ - `src/web/src/components/dashboard/ChannelColumn.vue`、`src/web/src/views/SkillManager.vue`、资源面板及命令抽屉:消费 reactive platform store。
67
+ - `src/web/src/api/channels.js`: 增加基于 platform key 的通用请求函数,保留旧导出。
68
+ - `src/web/src/components/channel/channelPanelFactories.js`: 提取 common schema,保留平台专用字段和 driver。
69
+ - `src/web/src/composables/useUIConfig.js`、`src/web/src/views/Home.vue`、`SettingsDrawer.vue` 及平台面板:消费 reactive platform catalog。
70
+
71
+ - `src/web/vitest.config.js`: 扩展前端测试发现范围。
72
+ ### New test files
73
+
74
+ - `tests/unit/platforms/manifest-schema.test.js`
75
+ - `tests/unit/platforms/path-resolver.test.js`
76
+ - `tests/unit/platforms/registry.test.js`
77
+ - `tests/unit/platforms/generic-drivers.test.js`
78
+ - `tests/unit/platforms/legacy-drivers.test.js`
79
+ - `tests/unit/services/dashboard-snapshot-worker.test.js`
80
+ - `tests/unit/api/platforms-api.test.js`
81
+ - `tests/unit/commands/proxy-control.test.js`
82
+ - `tests/unit/commands/platform-command-registry.test.js`
83
+ - `tests/unit/commands/cli-type.test.js`
84
+ - `src/web/src/config/__tests__/platforms.test.js`
85
+ - `src/web/src/stores/__tests__/platforms.test.js`
86
+ - `tests/unit/platforms/config-only-contract.test.js`
87
+ - `tests/unit/services/session-history-worker.test.js`
88
+ - `src/web/src/components/channel/__tests__/commonChannelSchema.test.js`
89
+
90
+ ---
91
+
92
+ ### Task 1: Add Manifest Schema and Registry Foundation
93
+
94
+ **Files:**
95
+ - Create: `src/platforms/manifest-schema.js`
96
+ - Create: `src/platforms/manifests/claude.json`
97
+ - Create: `src/platforms/manifests/codex.json`
98
+ - Create: `src/platforms/manifests/gemini.json`
99
+ - Create: `src/platforms/manifests/opencode.json`
100
+ - Create: `src/platforms/manifests/omp.json`
101
+ - Create: `src/platforms/path-resolver.js`
102
+ - Create: `src/platforms/registry.js`
103
+ - Create: `src/platforms/runtime.js`
104
+ - Modify: `src/config/paths.js`
105
+ - Modify: `package.json`
106
+ - Test: `tests/unit/platforms/manifest-schema.test.js`
107
+ - Test: `tests/unit/platforms/path-resolver.test.js`
108
+ - Test: `tests/unit/platforms/registry.test.js`
109
+
110
+ - [ ] **Step 1: Write failing Manifest validation tests**
111
+
112
+ ```js
113
+ const { validateManifest, normalizeManifestError } = require('../../../src/platforms/manifest-schema');
114
+
115
+ test('accepts a valid generic platform manifest', () => {
116
+ const result = validateManifest({
117
+ key: 'demo-cli',
118
+ label: 'Demo CLI',
119
+ command: 'demo',
120
+ iconToken: 'terminal',
121
+ paths: { home: '~/.demo', sessions: '{home}/sessions' },
122
+ capabilities: { sessions: 'generic-jsonl' }
123
+ });
124
+
125
+ expect(result.valid).toBe(true);
126
+ expect(result.errors).toEqual([]);
127
+ });
128
+
129
+ test('rejects executable module paths and unknown drivers', () => {
130
+ const result = validateManifest({
131
+ key: 'bad-cli',
132
+ label: 'Bad',
133
+ command: 'bad',
134
+ driverModule: '/tmp/driver.js',
135
+ capabilities: { sessions: 'user-code' }
136
+ });
137
+
138
+ expect(result.valid).toBe(false);
139
+ expect(normalizeManifestError(result.errors)).toContain('driver');
140
+ });
141
+
142
+ test('rejects duplicate or malformed platform keys', () => {
143
+ const result = validateManifest({
144
+ key: 'Bad Key',
145
+ label: 'Bad',
146
+ command: 'bad',
147
+ capabilities: {}
148
+ });
149
+
150
+ expect(result.valid).toBe(false);
151
+ expect(normalizeManifestError(result.errors)).toContain('key');
152
+ });
153
+ ```
154
+
155
+ - [ ] **Step 2: Run focused tests and verify the foundation is absent**
156
+
157
+ Run: `npx vitest run tests/unit/platforms/manifest-schema.test.js`
158
+ Expected: FAIL because `src/platforms/manifest-schema.js` does not exist.
159
+
160
+ - [ ] **Step 3: Implement the schema and safe validation result**
161
+
162
+ Use the existing `ajv` dependency and export exactly:
163
+
164
+ ```js
165
+ const Ajv = require('ajv');
166
+
167
+ const DRIVER_IDS = new Set([
168
+ 'unsupported',
169
+ 'generic-jsonl',
170
+ 'generic-filesystem',
171
+ 'generic-openai-compatible',
172
+ 'legacy:claude',
173
+ 'legacy:codex',
174
+ 'legacy:gemini',
175
+ 'legacy:opencode',
176
+ 'legacy:omp'
177
+ ]);
178
+
179
+ const PATH_RESOLVER_IDS = new Set([
180
+ 'declarative',
181
+ 'claude',
182
+ 'codex',
183
+ 'gemini',
184
+ 'opencode',
185
+ 'omp'
186
+ ]);
187
+
188
+ const schema = {
189
+ type: 'object',
190
+ required: ['key', 'label', 'command', 'capabilities'],
191
+ additionalProperties: false,
192
+ properties: {
193
+ key: { type: 'string', pattern: '^[a-z0-9][a-z0-9_-]*$' },
194
+ label: { type: 'string', minLength: 1 },
195
+ title: { type: 'string', minLength: 1 },
196
+ command: { type: 'string', minLength: 1 },
197
+ iconToken: { type: 'string', minLength: 1 },
198
+ color: { type: 'string' },
199
+ defaultVisible: { type: 'boolean' },
200
+ custom: { type: 'boolean' },
201
+ apiBasePath: { type: 'string', minLength: 1 },
202
+ logFile: { type: 'string', minLength: 1 },
203
+ logAliases: { type: 'array', items: { type: 'string', minLength: 1 } },
204
+ portKey: { type: 'string', minLength: 1 },
205
+ defaultPort: { type: 'integer', minimum: 1 },
206
+ statisticsPath: { type: 'string', minLength: 1 },
207
+ promptFile: { type: ['string', 'null'] },
208
+ paths: { type: 'object', additionalProperties: { type: 'string' } },
209
+ pathResolverId: { enum: [...PATH_RESOLVER_IDS] },
210
+ sessionMapping: { type: 'object', additionalProperties: { type: 'string' } },
211
+ resourceMappings: { type: 'object', additionalProperties: { type: 'string' } },
212
+ capabilities: {
213
+ type: 'object',
214
+ additionalProperties: { enum: [...DRIVER_IDS] }
215
+ }
216
+ }
217
+ };
218
+
219
+ const validate = new Ajv({ allErrors: true, strict: false }).compile(schema);
220
+
221
+ function validateManifest(manifest) {
222
+ const valid = validate(manifest);
223
+ return { valid, errors: valid ? [] : (validate.errors || []) };
224
+ }
225
+
226
+ function normalizeManifestError(errors = []) {
227
+ return errors.map(error => `${error.instancePath || '/'} ${error.message}`).join('; ');
228
+ }
229
+
230
+ module.exports = { DRIVER_IDS, PATH_RESOLVER_IDS, schema, validateManifest, normalizeManifestError };
231
+ ```
232
+
233
+ The implementation must additionally reject `capabilities` whose value is not in `DRIVER_IDS`; do not accept arbitrary `modulePath` fields.
234
+
235
+ - [ ] **Step 4: Add path resolution tests with injected environment and command runner**
236
+
237
+ ```js
238
+ const { resolveManifestPaths } = require('../../../src/platforms/path-resolver');
239
+
240
+ test('expands home and environment values without touching the real home directory', () => {
241
+ const paths = resolveManifestPaths({
242
+ key: 'demo-cli',
243
+ paths: { home: '$DEMO_HOME', sessions: '{home}/sessions' }
244
+ }, { env: { DEMO_HOME: '/tmp/demo-home' }, homeDir: '/tmp/test-home' });
245
+
246
+ expect(paths).toEqual({
247
+ home: '/tmp/demo-home',
248
+ sessions: '/tmp/demo-home/sessions'
249
+ });
250
+ });
251
+
252
+ test('uses the injected OMP command runner for special native paths', () => {
253
+ const paths = resolveManifestPaths({ key: 'omp', pathResolverId: 'omp' }, {
254
+ env: { OMP_COMMAND: 'omp' },
255
+ commandRunner: () => '/tmp/omp-agent\n'
256
+ });
257
+
258
+ expect(paths.home).toBe('/tmp/omp-agent');
259
+ });
260
+ ```
261
+
262
+ - [ ] **Step 5: Implement `PathResolver` and add the center config path**
263
+
264
+ `resolveManifestPaths(manifest, { env, homeDir, commandRunner })` must:
265
+
266
+ ```js
267
+ function resolveTemplate(value, resolved) {
268
+ return String(value)
269
+ .replace(/\$([A-Z][A-Z0-9_]*)/g, (_, name) => resolved.env[name] || '')
270
+ .replace(/\{home\}/g, resolved.home);
271
+ }
272
+ ```
273
+
274
+ It must support `~`, `$ENV_NAME`, `{home}`, path normalization, and the existing OMP command/profile path behavior. It must reject an empty required root and never resolve a path outside the configured home unless the Manifest explicitly uses an absolute path.
275
+
276
+ Add `PATHS.platforms = path.join(CONFIG_DIR, 'platforms.json')` in `src/config/paths.js`. Existing tests that provide partial `PATHS` objects must continue to work through a fallback:
277
+
278
+ ```js
279
+ const platformsFile = PATHS.platforms || path.join(PATHS.config || process.cwd(), 'platforms.json');
280
+ ```
281
+
282
+ - [ ] **Step 6: Add the five built-in Manifests**
283
+
284
+ Each JSON file must contain the current display metadata, path resolver ID when native paths are special, and capability IDs. The OMP Manifest must keep `proxy: 'legacy:omp'`; it must not be assigned the generic proxy driver.
285
+
286
+ - [ ] **Step 7: Implement Registry and Runtime with deterministic merge rules**
287
+
288
+ Export:
289
+
290
+ ```js
291
+ function createPlatformRegistry({ builtIns, userFile, legacyUiConfig, fsImpl, logger } = {}) {
292
+ return {
293
+ resolve(key),
294
+ list({ enabledOnly = false } = {}),
295
+ getCapability(key, capability),
296
+ resolvePaths(key, options),
297
+ getPublicDefinition(key),
298
+ diagnostics()
299
+ };
300
+ }
301
+
302
+ function createPlatformRuntime({ registry, driverRegistry, dependencies = {} } = {}) {
303
+ return {
304
+ registry,
305
+ getDriver(platform, capability, context = {}),
306
+ invoke(platform, capability, operation, args = [])
307
+ };
308
+ }
309
+ ```
310
+
311
+ `runtime.js` also exports lazy `getPlatformRegistry()` and `getPlatformRuntime()` singletons for production callers. Tests use `createPlatformRegistry()` and `createPlatformRuntime()` directly to avoid global state.
312
+
313
+ Merge order is built-in definitions, then valid user definitions, with built-in keys winning. Legacy `ui-config.customCliPlatforms` is metadata-only until Task 9 migration. An invalid user entry is excluded and added to `diagnostics()`; an invalid built-in entry throws.
314
+
315
+ - [ ] **Step 8: Add Registry tests and package inclusion**
316
+
317
+ ```js
318
+ const { createPlatformRegistry } = require('../../../src/platforms/registry');
319
+ test('resolves built-ins and rejects a user override', () => {
320
+ const registry = createPlatformRegistry({
321
+ builtIns: [{ key: 'claude', label: 'Claude', command: 'claude', capabilities: {} }],
322
+ userFile: {
323
+ platforms: [
324
+ { key: 'claude', label: 'Fake Claude', command: 'fake', capabilities: {} },
325
+ { key: 'demo-cli', label: 'Demo', command: 'demo', capabilities: { sessions: 'generic-jsonl' } }
326
+ ]
327
+ }
328
+ });
329
+
330
+ expect(registry.resolve('claude').label).toBe('Claude');
331
+ expect(registry.resolve('demo-cli').label).toBe('Demo');
332
+ expect(registry.diagnostics()).toEqual([]);
333
+ });
334
+ ```
335
+
336
+ Add `src/platforms/` to the `files` array in `package.json`. Run: `npx vitest run tests/unit/platforms/manifest-schema.test.js tests/unit/platforms/path-resolver.test.js tests/unit/platforms/registry.test.js`. Expected: PASS.
337
+
338
+ - [ ] **Step 9: Commit the foundation**
339
+
340
+ ```bash
341
+ git add package.json src/config/paths.js src/platforms tests/unit/platforms
342
+ git commit -m "feat: add platform manifest registry"
343
+ ```
344
+
345
+ ---
346
+
347
+ ### Task 2: Add Driver Registry and Generic Drivers
348
+
349
+ **Files:**
350
+ - Create: `src/platforms/driver-registry.js`
351
+ - Create: `src/platforms/drivers/unsupported.js`
352
+ - Create: `src/platforms/drivers/generic-jsonl.js`
353
+ - Create: `src/platforms/drivers/generic-filesystem.js`
354
+ - Create: `src/platforms/drivers/generic-openai-compatible.js`
355
+ - Create: `tests/unit/platforms/generic-drivers.test.js`
356
+
357
+ - [ ] **Step 1: Write driver contract tests first**
358
+
359
+ ```js
360
+ const { createDriverRegistry } = require('../../../src/platforms/driver-registry');
361
+ const { createGenericJsonlDriver } = require('../../../src/platforms/drivers/generic-jsonl');
362
+ const { createGenericFilesystemDriver } = require('../../../src/platforms/drivers/generic-filesystem');
363
+ const { createGenericOpenAICompatibleDriver } = require('../../../src/platforms/drivers/generic-openai-compatible');
364
+
365
+ function makeRegistry() {
366
+ return createDriverRegistry({
367
+ drivers: {
368
+ 'generic-jsonl': createGenericJsonlDriver,
369
+ 'generic-filesystem': createGenericFilesystemDriver,
370
+ 'generic-openai-compatible': createGenericOpenAICompatibleDriver,
371
+ unsupported: ({ platform, capability }) => ({
372
+ status: 'unsupported', platform, capability
373
+ })
374
+ }
375
+ });
376
+ }
377
+
378
+ test('generic JSONL driver inventories and normalizes sessions', async () => {
379
+ const fsImpl = {
380
+ readdir: async () => ['session-1.jsonl'],
381
+ stat: async () => ({ size: 20, mtimeMs: 10 }),
382
+ readFile: async () => '{"id":"m1","role":"user","content":"hello"}\n'
383
+ };
384
+ const driver = makeRegistry().create('generic-jsonl', {
385
+ platform: 'demo-cli',
386
+ manifest: {
387
+ paths: { sessions: '/tmp/demo/sessions' },
388
+ sessionMapping: { sessionId: 'id', messages: 'messages' }
389
+ },
390
+ fsImpl
391
+ });
392
+
393
+ const descriptors = await driver.inventory();
394
+ expect(descriptors[0]).toEqual(expect.objectContaining({ sessionId: 'session-1' }));
395
+ const parsed = await driver.parse(descriptors[0]);
396
+ expect(parsed.messages[0]).toEqual(expect.objectContaining({ role: 'user', content: 'hello' }));
397
+ });
398
+
399
+ test('unsupported driver never returns a successful empty value', () => {
400
+ const result = makeRegistry().create('unsupported', {
401
+ platform: 'demo-cli', capability: 'proxy'
402
+ });
403
+
404
+ expect(result.status).toBe('unsupported');
405
+ expect(result.status).not.toBe('ok');
406
+ });
407
+ ```
408
+
409
+ - [ ] **Step 2: Run the focused contract test and verify it fails**
410
+
411
+ Run: `npx vitest run tests/unit/platforms/generic-drivers.test.js`
412
+ Expected: FAIL because the driver registry and generic driver modules do not exist.
413
+
414
+ - [ ] **Step 3: Implement lazy driver registration**
415
+
416
+ ```js
417
+ function createDriverRegistry({ drivers = {} } = {}) {
418
+ const factories = new Map(Object.entries(drivers));
419
+
420
+ return {
421
+ register(id, factory) {
422
+ if (!/^[a-z0-9:_-]+$/.test(id) || typeof factory !== 'function') {
423
+ throw new Error(`Invalid capability driver: ${id}`);
424
+ }
425
+ factories.set(id, factory);
426
+ },
427
+ has(id) {
428
+ return factories.has(id);
429
+ },
430
+ create(id, context = {}) {
431
+ const factory = factories.get(id);
432
+ if (!factory) throw new Error(`Unknown capability driver: ${id}`);
433
+ return factory(context);
434
+ },
435
+ ids() {
436
+ return [...factories.keys()];
437
+ }
438
+ };
439
+ }
440
+
441
+ module.exports = { createDriverRegistry };
442
+ ```
443
+ `unsupported.js` exports a concrete factory:
444
+
445
+ ```js
446
+ function createUnsupportedDriver({ platform, capability }) {
447
+ return { status: 'unsupported', platform, capability };
448
+ }
449
+
450
+ module.exports = { createUnsupportedDriver };
451
+ ```
452
+
453
+ The Registry must call `driverRegistry.create()` only after resolving a capability. It must not load all platform services at module import time.
454
+
455
+ - [ ] **Step 4: Implement `generic-jsonl` with explicit field mappings**
456
+
457
+ The driver must accept `manifest.paths.sessions`, an optional `sessionGlob`, and mappings for `sessionId`, `projectName`, `messages`, `role`, `content`, `timestamp`, and `model`. It returns descriptors matching the existing index contract:
458
+
459
+ ```js
460
+ {
461
+ filePath,
462
+ size,
463
+ mtimeMs,
464
+ sessionId,
465
+ projectHint
466
+ }
467
+ ```
468
+
469
+ Parsing malformed lines must produce a `failed` error with `platform`, `capability: 'sessions'`, `operation: 'parse'`, and the file path. It must not silently convert malformed input into a successful empty session.
470
+
471
+ - [ ] **Step 5: Implement `generic-filesystem` with safe roots**
472
+
473
+ The driver must expose:
474
+
475
+ ```js
476
+ {
477
+ list(type),
478
+ sync(type, name, sourceRoot),
479
+ remove(type, name)
480
+ }
481
+ ```
482
+
483
+ It must normalize relative names, reject `..`, enforce the Manifest target root, preserve directory/file semantics, and return `{ status: 'ok', target }` or a typed failure. Do not add recursive deletion outside the resolved target root.
484
+
485
+ - [ ] **Step 6: Implement the generic OpenAI-compatible transport driver**
486
+
487
+ `generic-openai-compatible.js` handles endpoint normalization and request authentication only; it is not a local proxy lifecycle driver. A Manifest must use `proxy: 'unsupported'` unless a separate allowlisted proxy driver is implemented.
488
+
489
+ ```js
490
+ function createGenericOpenAICompatibleDriver({ platform, manifest, fetchImpl = globalThis.fetch } = {}) {
491
+ const baseUrl = String(manifest.paths?.baseUrl || manifest.baseUrl || '').replace(/\/+$/, '');
492
+ if (!baseUrl) throw new Error(`Missing base URL for ${platform}`);
493
+ return {
494
+ platform,
495
+ capability: 'channels',
496
+ normalizeEndpoint(pathname = '') {
497
+ return `${baseUrl}/${String(pathname).replace(/^\/+/, '')}`;
498
+ },
499
+ buildHeaders(channel = {}) {
500
+ const headers = { 'Content-Type': 'application/json' };
501
+ if (channel.apiKey) headers.Authorization = `Bearer ${channel.apiKey}`;
502
+ return headers;
503
+ },
504
+ async request(pathname, channel, init = {}) {
505
+ const response = await fetchImpl(this.normalizeEndpoint(pathname), {
506
+ ...init,
507
+ headers: { ...this.buildHeaders(channel), ...(init.headers || {}) }
508
+ });
509
+ if (!response.ok) {
510
+ throw new Error(`OpenAI-compatible request failed: ${response.status}`);
511
+ }
512
+ return response.json();
513
+ }
514
+ };
515
+ }
516
+
517
+ module.exports = { createGenericOpenAICompatibleDriver };
518
+ ```
519
+
520
+ The driver must strip duplicate trailing slashes and never log API keys.
521
+
522
+ - [ ] **Step 7: Add generic driver tests for boundary failures**
523
+
524
+ Cover missing session directories, malformed JSONL, path traversal, duplicate session IDs, unsupported proxy capabilities, and OpenAI-compatible endpoint normalization. Run: `npx vitest run tests/unit/platforms/generic-drivers.test.js`. Expected: PASS.
525
+
526
+ - [ ] **Step 8: Commit generic drivers**
527
+
528
+ ```bash
529
+ git add src/platforms/driver-registry.js src/platforms/drivers tests/unit/platforms/generic-drivers.test.js
530
+ git commit -m "feat: add generic platform capability drivers"
531
+ ```
532
+
533
+ ---
534
+ ### Task 3: Wrap Existing Implementations Without Breaking Mock Paths
535
+
536
+ **Files:**
537
+ - Create: `src/platforms/drivers/legacy.js`
538
+ - Test: `tests/unit/platforms/legacy-drivers.test.js`
539
+ - Reference unchanged: `src/config/paths.js`, `src/server/services/channels.js`, `src/server/services/codex-channels.js`, `src/server/services/gemini-channels.js`, `src/server/services/opencode-channels.js`, `src/server/services/omp-channels.js`, `src/server/omp-proxy-server.js`
540
+
541
+ - [ ] **Step 1: Write the cache-interception regression test**
542
+
543
+ ```js
544
+ const CHANNEL_SERVICE_PATH = require.resolve('../../../src/server/services/codex-channels');
545
+ const PATHS_PATH = require.resolve('../../../src/config/paths');
546
+
547
+ test('legacy driver loads the exact facade path after require.cache stubbing', () => {
548
+ const listChannels = vi.fn(() => ({ channels: [{ id: 'mock-channel' }] }));
549
+ require.cache[CHANNEL_SERVICE_PATH] = {
550
+ id: CHANNEL_SERVICE_PATH,
551
+ filename: CHANNEL_SERVICE_PATH,
552
+ loaded: true,
553
+ exports: { getChannels: listChannels }
554
+ };
555
+ require.cache[PATHS_PATH] = {
556
+ id: PATHS_PATH,
557
+ filename: PATHS_PATH,
558
+ loaded: true,
559
+ exports: { PATHS: { activeChannel: { codex: '/tmp/mock-active.json' } } }
560
+ };
561
+
562
+ const { createLegacyDriver } = require('../../../src/platforms/drivers/legacy');
563
+ const driver = createLegacyDriver({ platform: 'codex', capability: 'channels' });
564
+ expect(driver.list().channels).toEqual([{ id: 'mock-channel' }]);
565
+ expect(listChannels).toHaveBeenCalledTimes(1);
566
+
567
+ delete require.cache[CHANNEL_SERVICE_PATH];
568
+ delete require.cache[PATHS_PATH];
569
+ });
570
+ ```
571
+
572
+ - [ ] **Step 2: Run the focused test and verify the new factory is absent**
573
+
574
+ Run: `npx vitest run tests/unit/platforms/legacy-drivers.test.js`
575
+ Expected: FAIL because `src/platforms/drivers/legacy.js` does not exist.
576
+
577
+ - [ ] **Step 3: Implement lazy legacy module resolution**
578
+
579
+ Use an explicit allowlisted table and lazy `require` calls:
580
+
581
+ ```js
582
+ const MODULE_PATHS = {
583
+ claude: {
584
+ sessions: '../../server/services/sessions',
585
+ channels: '../../server/services/channels',
586
+ proxy: '../../server/proxy-server',
587
+ statistics: '../../server/services/claude-statistics-service'
588
+ },
589
+ codex: {
590
+ sessions: '../../server/services/codex-sessions',
591
+ channels: '../../server/services/codex-channels',
592
+ proxy: '../../server/codex-proxy-server',
593
+ statistics: '../../server/services/codex-statistics-service'
594
+ },
595
+ gemini: {
596
+ sessions: '../../server/services/gemini-sessions',
597
+ channels: '../../server/services/gemini-channels',
598
+ proxy: '../../server/gemini-proxy-server',
599
+ statistics: '../../server/services/gemini-statistics-service'
600
+ },
601
+ opencode: {
602
+ sessions: '../../server/services/opencode-sessions',
603
+ channels: '../../server/services/opencode-channels',
604
+ proxy: '../../server/opencode-proxy-server',
605
+ statistics: '../../server/services/opencode-statistics-service'
606
+ },
607
+ omp: {
608
+ sessions: '../../server/services/omp-sessions',
609
+ channels: '../../server/services/omp-channels',
610
+ proxy: '../../server/omp-proxy-server',
611
+ statistics: '../../server/services/omp-statistics-service'
612
+ }
613
+ };
614
+
615
+ function createLegacyDriver({ platform, capability, requireImpl = require } = {}) {
616
+ const modulePath = MODULE_PATHS[platform]?.[capability];
617
+ if (!modulePath) {
618
+ return { status: 'unsupported', platform, capability };
619
+ }
620
+ const moduleExports = requireImpl(modulePath);
621
+ return adaptLegacyModule(platform, capability, moduleExports);
622
+ }
623
+ ```
624
+
625
+ `adaptLegacyModule` normalizes `getAllChannels()` versus `getChannels().channels`, `getProjects()` signatures, proxy method names, and statistics method names. It must not move or rename the legacy modules.
626
+
627
+ - [ ] **Step 4: Add adapters for current method differences**
628
+
629
+ The adapter table must explicitly map:
630
+
631
+ ```text
632
+ claude channels: getAllChannels / createChannel / updateChannel / deleteChannel
633
+ codex channels: getChannels().channels / createChannel / updateChannel / deleteChannel
634
+ Gemini channels: getChannels().channels / createChannel / updateChannel / deleteChannel
635
+ OpenCode channels: getChannels().channels / createChannel / updateChannel / deleteChannel
636
+ OMP channels: getChannels().channels / createChannel / updateChannel / deleteChannel
637
+ ```
638
+
639
+ For proxy drivers preserve OMP `preserveManagedMode`, `forceAfterMs`, drain and restoration options. Do not normalize away platform-specific options.
640
+
641
+ - [ ] **Step 5: Extend tests for every existing mock seam**
642
+
643
+ Add tests that stub `src/config/paths.js`, `src/server/services/omp-channels.js`, `src/server/omp-proxy-server.js`, and one non-OMP proxy module by `require.resolve()`. Assert the legacy driver invokes the stub rather than the real implementation. Run: `npx vitest run tests/unit/platforms/legacy-drivers.test.js tests/unit/index.test.js tests/unit/services/omp-proxy-server.test.js`. Expected: PASS.
644
+
645
+ - [ ] **Step 6: Commit the compatibility layer**
646
+
647
+ ```bash
648
+ git add src/platforms/drivers/legacy.js tests/unit/platforms/legacy-drivers.test.js
649
+ git commit -m "feat: adapt legacy platform modules to capability drivers"
650
+ ```
651
+
652
+ ---
653
+
654
+ ### Task 4: Migrate Session Indexing and Dashboard Read Aggregation
655
+
656
+ **Files:**
657
+ - Modify: `src/server/services/session-history-adapters/index.js`
658
+ - Modify: `src/server/services/session-history-index.js`
659
+ - Modify: `src/server/services/dashboard-snapshot-worker.js`
660
+ - Test: `tests/unit/services/session-history-index.test.js`
661
+ - Create: `tests/unit/services/dashboard-snapshot-worker.test.js`
662
+
663
+ - [ ] **Step 1: Add a Dashboard runtime-injection test**
664
+
665
+ ```js
666
+ const fs = require('fs');
667
+ const os = require('os');
668
+ const path = require('path');
669
+ const { buildPayload } = require('../../../src/server/services/dashboard-snapshot-worker');
670
+ const { createSessionHistoryIndex } = require('../../../src/server/services/session-history-index');
671
+
672
+ const tempDbPath = path.join(
673
+ fs.mkdtempSync(path.join(os.tmpdir(), 'platform-index-test-')),
674
+ 'history.sqlite'
675
+ );
676
+
677
+ test('builds a project payload through the injected platform driver', async () => {
678
+ const listProjects = vi.fn(async () => [{ name: 'demo-project', lastUsed: 10 }]);
679
+ const runtime = {
680
+ getDriver: vi.fn((platform, capability) => {
681
+ expect(platform).toBe('demo-cli');
682
+ expect(capability).toBe('projects');
683
+ return { listProjects };
684
+ })
685
+ };
686
+
687
+ const result = await buildPayload({
688
+ kind: 'projects',
689
+ source: 'demo-cli',
690
+ config: {},
691
+ options: {},
692
+ runtime
693
+ });
694
+
695
+ expect(result.projects).toEqual([{ name: 'demo-project', lastUsed: 10 }]);
696
+ expect(listProjects).toHaveBeenCalledWith(expect.objectContaining({ force: false, config: {} }));
697
+ });
698
+
699
+ test('explicit adapterRegistry remains authoritative for tests and workers', async () => {
700
+ const inventory = vi.fn(async () => []);
701
+ const parse = vi.fn();
702
+ const index = createSessionHistoryIndex({
703
+ dbPath: tempDbPath,
704
+ adapterRegistry: { demo: { inventory, parse } },
705
+ workerRunner: async () => {}
706
+ });
707
+
708
+ await index.ensureSourceIndexed('demo', { consistency: 'complete' });
709
+ expect(inventory).toHaveBeenCalledTimes(1);
710
+ expect(parse).not.toHaveBeenCalled();
711
+ });
712
+ ```
713
+
714
+ - [ ] **Step 2: Refactor `session-history-index.js` default adapter selection**
715
+
716
+ Keep this precedence:
717
+
718
+ ```js
719
+ const adapters = opts.adapterRegistry || getDefaultSessionAdapters();
720
+ ```
721
+
722
+ `getDefaultSessionAdapters()` obtains the session drivers from `runtime`; explicit test adapters never load the Registry or native paths. Keep the existing `inventory`, `parse`, stale handling, retry and SQLite schema unchanged.
723
+
724
+ - [ ] **Step 3: Replace Dashboard `switch` with capability dispatch**
725
+
726
+ Implement one dispatcher per capability:
727
+
728
+ ```js
729
+ async function buildProjectsPayload(source, config = {}, options = {}, runtime = getPlatformRuntime()) {
730
+ const driver = runtime.getDriver(source, 'projects');
731
+ const projects = await driver.listProjects({ force: options.force === true, config });
732
+ return {
733
+ projects,
734
+ currentProject: projects[0] ? projects[0].name : null
735
+ };
736
+ }
737
+ ```
738
+
739
+ Apply the same pattern to counts, today statistics and channels. Existing legacy adapters provide the current five-platform behavior; generic platforms use their configured drivers. Production child-process IPC must continue to send only serializable `kind`, `source`, `config` and `options`; test-only runtime injection is used only when `NODE_ENV === 'test'`.
740
+
741
+ - [ ] **Step 4: Keep `session-history-adapters/index.js` as a stable facade**
742
+
743
+ It must continue exporting `claude`, `codex`, `gemini`, and `omp` objects with `inventory` and `parse`. OpenCode remains excluded from this index until its native database adapter is represented by a driver. Existing tests that stub this module path must still intercept default loading.
744
+
745
+ - [ ] **Step 5: Run focused read-side tests**
746
+
747
+ Run: `npx vitest run tests/unit/services/session-history-index.test.js tests/unit/services/dashboard-snapshot-worker.test.js tests/unit/services/{sessions,codex-sessions,gemini-sessions,opencode-sessions,omp-sessions}.test.js`
748
+ Expected: PASS with the current session normalization and snapshot behavior.
749
+
750
+ - [ ] **Step 6: Commit the read-side migration**
751
+
752
+ ```bash
753
+ git add src/server/services/session-history-adapters/index.js src/server/services/session-history-index.js src/server/services/dashboard-snapshot-worker.js tests/unit/services/session-history-index.test.js tests/unit/services/dashboard-snapshot-worker.test.js
754
+ git commit -m "refactor: route read aggregation through platform drivers"
755
+ ```
756
+
757
+ ---
758
+
759
+ ### Task 5: Add the Public Platform Catalog and Generic Route Factory
760
+
761
+ **Files:**
762
+ - Create: `src/server/api/platforms.js`
763
+ - Create: `src/server/api/platform-route-factory.js`
764
+ - Modify: `src/server/index.js`
765
+ - Create: `tests/unit/api/platforms-api.test.js`
766
+
767
+ - [ ] **Step 1: Write catalog and unsupported-capability API tests**
768
+
769
+ ```js
770
+ const express = require('express');
771
+ const { once } = require('events');
772
+ const createPlatformRouter = require('../../../src/server/api/platforms');
773
+
774
+ async function requestJson(app, route) {
775
+ const server = app.listen(0);
776
+ await once(server, 'listening');
777
+ const { port } = server.address();
778
+ const response = await fetch(`http://127.0.0.1:${port}${route}`);
779
+ const body = await response.json();
780
+ await new Promise(resolve => server.close(resolve));
781
+ return { status: response.status, body };
782
+ }
783
+
784
+ test('catalog exposes only safe public platform fields', async () => {
785
+ const app = express();
786
+ app.use('/api/platforms', createPlatformRouter({
787
+ registry: {
788
+ list: () => [{
789
+ key: 'demo-cli', label: 'Demo', command: 'demo', iconToken: 'terminal',
790
+ color: '#000', capabilities: { sessions: 'generic-jsonl' },
791
+ paths: { home: '/private/home' }, driverIds: { sessions: 'generic-jsonl' }
792
+ }]
793
+ }
794
+ }));
795
+
796
+ const response = await requestJson(app, '/api/platforms');
797
+ expect(response.status).toBe(200);
798
+ expect(response.body.platforms[0]).toEqual(expect.objectContaining({ key: 'demo-cli', label: 'Demo' }));
799
+ expect(response.body.platforms[0]).not.toHaveProperty('paths');
800
+ expect(response.body.platforms[0]).not.toHaveProperty('driverIds');
801
+ });
802
+
803
+ test('unsupported capability is explicit', async () => {
804
+ const app = express();
805
+ app.use('/api/platforms', createPlatformRouter({
806
+ registry: { list: () => [], resolve: () => ({ key: 'demo-cli' }) },
807
+ runtime: { getDriver: () => ({ status: 'unsupported', platform: 'demo-cli', capability: 'proxy' }) }
808
+ }));
809
+
810
+ const response = await requestJson(app, '/api/platforms/demo-cli/proxy/status');
811
+ expect(response.status).toBe(404);
812
+ expect(response.body.error.code).toBe('unsupported');
813
+ });
814
+ ```
815
+
816
+ - [ ] **Step 2: Run the API test and verify the route factory is absent**
817
+
818
+ Run: `npx vitest run tests/unit/api/platforms-api.test.js`
819
+ Expected: FAIL because the catalog router does not exist.
820
+
821
+ - [ ] **Step 3: Implement public projection and generic routes**
822
+
823
+ `src/server/api/platforms.js` must export the factory both as the CommonJS default and as a named property so old-style route loading and focused tests use the same implementation:
824
+
825
+ ```js
826
+ module.exports = createPlatformRouter;
827
+ module.exports.createPlatformRouter = createPlatformRouter;
828
+ ```
829
+
830
+ `src/server/api/platform-route-factory.js` must contain the shared handler construction; `platforms.js` only supplies the catalog route and binds the factory to the Registry/runtime.
831
+
832
+ The router must expose:
833
+
834
+ ```text
835
+ GET /api/platforms
836
+ GET /api/platforms/:platform/projects
837
+ GET /api/platforms/:platform/sessions/:projectName
838
+ GET /api/platforms/:platform/channels
839
+ GET /api/platforms/:platform/proxy/status
840
+ ```
841
+
842
+ Route handlers resolve the platform, call the capability driver, and map driver states as follows:
843
+
844
+ ```text
845
+ ok -> 200
846
+ unsupported -> 404 { error: { code: 'unsupported', ... } }
847
+ unavailable -> 503
848
+ invalid -> 400
849
+ failed -> 500
850
+ ```
851
+
852
+ Use `createPlatformRouter({ registry, runtime })` so tests can inject fakes. Do not import all legacy services at module scope.
853
+
854
+ - [ ] **Step 4: Mount the router without changing old endpoints**
855
+
856
+ In `src/server/index.js`, add the catalog route before static files:
857
+
858
+ ```js
859
+ const { getPlatformRegistry, getPlatformRuntime } = require('../platforms/runtime');
860
+
861
+ app.use('/api/platforms', require('./api/platforms')({
862
+ registry: getPlatformRegistry(),
863
+ runtime: getPlatformRuntime()
864
+ }));
865
+ ```
866
+
867
+ Keep existing `/api/channels`, `/api/codex/channels`, `/api/gemini/channels`, `/api/opencode/channels` and `/api/omp/channels` mounts unchanged in this task. The new route is the extension path for Manifest-driven platforms.
868
+
869
+ - [ ] **Step 5: Run focused API tests**
870
+
871
+ Run: `npx vitest run tests/unit/api/platforms-api.test.js tests/unit/api/{channels,codex-channels-api,gemini-channels-api,opencode-channels-api,omp-channels-api}.test.js`
872
+ Expected: PASS; old APIs remain available and the new catalog only exposes safe fields.
873
+
874
+ - [ ] **Step 6: Commit the catalog and route factory**
875
+
876
+ ```bash
877
+ git add src/server/api/platforms.js src/server/api/platform-route-factory.js src/server/index.js tests/unit/api/platforms-api.test.js
878
+ git commit -m "feat: expose registry-backed platform routes"
879
+ ```
880
+
881
+ ---
882
+
883
+
884
+ ### Task 6: Move Channel, Proxy, and Daemon Aggregation to the Registry
885
+
886
+ **Files:**
887
+ - Modify: `src/commands/proxy-control.js`
888
+ - Modify: `src/commands/toggle-proxy.js`
889
+ - Modify: `src/commands/daemon.js`
890
+ - Modify: `src/server/index.js`
891
+ - Modify: `src/server/services/channel-scheduler.js`
892
+ - Create: `tests/unit/commands/proxy-control.test.js`
893
+ - Test: `tests/unit/commands/daemon.test.js`
894
+ - Test: `tests/unit/services/channel-scheduler.test.js`
895
+
896
+ - [ ] **Step 1: Add a Registry-derived proxy control test**
897
+
898
+ ```js
899
+ const { createProxyControl } = require('../../../src/commands/proxy-control');
900
+
901
+ test('proxy labels and endpoint are derived from the resolved Manifest', async () => {
902
+ const httpRequest = vi.fn(async () => ({ data: { success: true, port: 23100 }, status: 200 }));
903
+ const registry = {
904
+ resolve: () => ({
905
+ key: 'demo-cli', label: 'Demo CLI', apiBasePath: '/api/platforms/demo-cli',
906
+ capabilities: { proxy: 'legacy:codex' }
907
+ }),
908
+ list: () => [{ key: 'demo-cli', label: 'Demo CLI' }]
909
+ };
910
+
911
+ const control = createProxyControl({ registry, httpRequest, loadConfig: () => ({ ports: { webUI: 19999 } }) });
912
+ await control.start('demo-cli');
913
+ expect(httpRequest).toHaveBeenCalledWith('POST', '/api/platforms/demo-cli/proxy/start');
914
+ });
915
+ ```
916
+
917
+ `proxy-control.js` must export `createProxyControl` in addition to the four existing `handleProxy*` functions. The factory receives `{ registry, httpRequest, loadConfig }` and contains no direct terminal output in its pure methods; the existing handlers remain the CLI output adapter.
918
+
919
+ - [ ] **Step 2: Refactor `proxy-control.js` to use Registry metadata**
920
+
921
+ Remove the fixed five-entry `CHANNEL_CONFIG` as the source of truth. Keep OMP-specific labels in its Manifest or legacy driver metadata:
922
+
923
+ ```js
924
+ const platform = registry.resolve(normalizePlatformKey(channel));
925
+ if (!platform) return failUnknownPlatform(channel);
926
+ const basePath = platform.apiBasePath || `/api/platforms/${platform.key}`;
927
+ ```
928
+
929
+ Preserve HTTP timeout, error text, status formatting, and `process.exit` behavior for existing commands. Existing tests may inject `require.cache` stubs for the underlying API modules; do not replace those seams with direct network calls.
930
+
931
+ - [ ] **Step 3: Refactor toggle-proxy service lookup**
932
+
933
+ Replace the hard-coded `SETTINGS_MANAGERS` lookup with a driver lookup for `nativeConfig` or `proxy`. Keep these existing service modules as allowlisted legacy drivers:
934
+
935
+ ```text
936
+ src/server/services/settings-manager.js
937
+ src/server/services/codex-settings-manager.js
938
+ src/server/services/gemini-settings-manager.js
939
+ src/server/services/opencode-settings-manager.js
940
+ ```
941
+
942
+ OMP remains handled by `omp-proxy-server.js` and `omp-settings-manager.js` with its managed-mode semantics.
943
+
944
+ - [ ] **Step 4: Refactor daemon status and automatic recovery**
945
+
946
+ Replace five repeated status blocks in `src/commands/daemon.js` with:
947
+
948
+ ```js
949
+ for (const platform of registry.list({ enabledOnly: true })) {
950
+ const proxy = runtime.getDriver(platform.key, 'proxy');
951
+ const status = await Promise.resolve(proxy.status());
952
+ printProxyStatus(platform, status, config);
953
+ }
954
+ ```
955
+
956
+ Replace `autoRestoreProxies()` with the same driver iteration. A driver may expose `restoreOnBoot()` for Codex config sync, OpenCode model sync, or OMP managed recovery. Generic drivers without that method are skipped with no error. Failures remain visible with platform and capability context.
957
+
958
+ - [ ] **Step 5: Preserve scheduler source keys**
959
+
960
+ `channel-scheduler.js` must accept any Registry key but retain existing persisted source names. Add a test for `demo-cli` and keep current `claude`, `codex`, `gemini`, `opencode`, and `omp` state behavior. Do not rename storage files in this task.
961
+
962
+ - [ ] **Step 6: Run focused command and scheduler tests**
963
+
964
+ Run: `npx vitest run tests/unit/commands/proxy-control.test.js tests/unit/commands/daemon.test.js tests/unit/commands/toggle-proxy.test.js tests/unit/services/channel-scheduler.test.js tests/unit/index.test.js`
965
+ Expected: PASS, including OMP shutdown preservation and existing CLI proxy output.
966
+
967
+ - [ ] **Step 7: Commit aggregation migration**
968
+
969
+ ```bash
970
+ git add src/commands/proxy-control.js src/commands/toggle-proxy.js src/commands/daemon.js src/server/index.js src/server/services/channel-scheduler.js tests/unit/commands/proxy-control.test.js tests/unit/commands/daemon.test.js tests/unit/commands/toggle-proxy.test.js tests/unit/services/channel-scheduler.test.js
971
+ git commit -m "refactor: derive proxy aggregation from platform registry"
972
+ ```
973
+
974
+ ---
975
+
976
+ ### Task 7: Make Config Sync, Registry, Export, and Templates Data-Driven
977
+
978
+ **Files:**
979
+ - Modify: `src/server/services/config-sync-manager.js`
980
+ - Modify: `src/server/services/config-registry-service.js`
981
+ - Modify: `src/server/services/config-export-service.js`
982
+ - Modify: `src/server/services/config-templates-service.js`
983
+ - Test: `tests/unit/services/config-sync-manager.test.js`
984
+ - Test: `tests/unit/services/config-registry-service.test.js`
985
+ - Test: `tests/unit/services/config-export-service.test.js`
986
+ - Test: `tests/unit/services/config-templates-service.test.js`
987
+
988
+ - [ ] **Step 1: Write a generic platform sync test**
989
+
990
+ ```js
991
+ const { ConfigSyncManager } = require('../../../src/server/services/config-sync-manager');
992
+
993
+ test('syncAll discovers a generic platform from the Registry', () => {
994
+ const sync = vi.fn(() => ({ status: 'ok', target: '/tmp/demo/skills/review' }));
995
+ const remove = vi.fn(() => ({ status: 'ok' }));
996
+ const manager = new ConfigSyncManager({
997
+ registry: {
998
+ list: () => [{ key: 'demo-cli', capabilities: { resourceSync: 'generic-filesystem' } }]
999
+ },
1000
+ runtime: {
1001
+ getDriver: () => ({ sync, remove })
1002
+ }
1003
+ });
1004
+
1005
+ const result = manager.syncAll('skills', {
1006
+ review: { enabled: true, platforms: { 'demo-cli': true } }
1007
+ });
1008
+
1009
+ expect(sync).toHaveBeenCalledWith('skills', 'review');
1010
+ expect(result.synced).toContainEqual({ type: 'skills', name: 'review', platform: 'demo-cli' });
1011
+ });
1012
+ ```
1013
+
1014
+ - [ ] **Step 2: Add dynamic platform assertions to registry and export tests**
1015
+
1016
+ Use a fake Registry containing `demo-cli` and the new pure export helper:
1017
+
1018
+ ```js
1019
+ const { getSupportedPlatforms } = require('../../../src/server/services/config-registry-service');
1020
+ const { exportPlatformSnapshots } = require('../../../src/server/services/config-export-service');
1021
+
1022
+ const registry = {
1023
+ list: () => [{ key: 'demo-cli', capabilities: { resourceSync: 'generic-filesystem' } }]
1024
+ };
1025
+
1026
+ expect(getSupportedPlatforms(registry)).toContain('demo-cli');
1027
+ expect(exportPlatformSnapshots({
1028
+ registry,
1029
+ exportByPlatform: () => ({})
1030
+ })).toHaveProperty('demo-cli');
1031
+ ```
1032
+
1033
+ Also assert that a platform with `agents: 'unsupported'` is reported as skipped rather than exported as an empty successful config.
1034
+
1035
+
1036
+ - [ ] **Step 3: Add `syncToPlatform` and retain old method wrappers**
1037
+
1038
+ Implement these methods on `ConfigSyncManager`; its constructor accepts `{ registry, runtime }` for tests and defaults both from `src/platforms/runtime.js`:
1039
+
1040
+ ```js
1041
+ constructor({ registry = getPlatformRegistry(), runtime = getPlatformRuntime() } = {}) {
1042
+ this.registry = registry;
1043
+ this.runtime = runtime;
1044
+ }
1045
+
1046
+ syncToPlatform(platform, type, name) {
1047
+ const driver = this.runtime.getDriver(platform, 'resourceSync');
1048
+ return driver.sync(type, name);
1049
+ }
1050
+
1051
+ removeFromPlatform(platform, type, name) {
1052
+ const driver = this.runtime.getDriver(platform, 'resourceSync');
1053
+ return driver.remove(type, name);
1054
+ }
1055
+ ```
1056
+
1057
+ Keep `syncToClaude`, `syncToCodex`, `syncToGemini`, `syncToOpenCode`, `syncToOmp` and matching remove methods as wrappers that call the same runtime. This preserves current service exports and tests while replacing `syncAll` platform enumeration with `registry.list()`.
1058
+
1059
+ - [ ] **Step 4: Derive config registry support from Manifest capabilities**
1060
+
1061
+ Import `getPlatformRegistry` from `src/platforms/runtime.js` and add:
1062
+
1063
+ ```js
1064
+ function getSupportedPlatforms(registry = getPlatformRegistry()) {
1065
+ return registry.list({ enabledOnly: true }).map(platform => platform.key);
1066
+ }
1067
+ ```
1068
+
1069
+ Replace the static `SUPPORTED_PLATFORMS` source with `getSupportedPlatforms(registry)` while retaining the exported `SUPPORTED_PLATFORMS` name for existing tests. Build `PLATFORM_SUPPORT` from `resourceSync` capability and resource type declarations. Existing OMP false support for agents must remain false, and export `getSupportedPlatforms`.
1070
+
1071
+ - [ ] **Step 5: Refactor export and template maps**
1072
+
1073
+ Add and export this pure helper in `config-export-service.js` so the platform map can be tested without reading the real home directory:
1074
+
1075
+ ```js
1076
+ function exportPlatformSnapshots({ registry, exportByPlatform }) {
1077
+ return Object.fromEntries(
1078
+ registry.list({ enabledOnly: true }).map(platform => [
1079
+ platform.key,
1080
+ exportByPlatform(platform.key)
1081
+ ])
1082
+ );
1083
+ }
1084
+ ```
1085
+
1086
+ `config-export-service.js` must use `nativeConfig` drivers for native snapshots and `resourceSync` drivers for managed resources. Keep existing special handling for Claude hooks, Codex TOML, OpenCode package configuration and OMP package/extension records.
1087
+
1088
+ `config-templates-service.js` must derive AI config file names from Manifest fields such as `promptFile` and use an explicit `unsupported` result when no file mapping exists. Preserve current fallback from Codex to OpenCode and OMP command-template semantics.
1089
+
1090
+ - [ ] **Step 6: Run focused service tests**
1091
+
1092
+ Run: `npx vitest run tests/unit/services/config-sync-manager.test.js tests/unit/services/config-registry-service.test.js tests/unit/services/config-export-service.test.js tests/unit/services/config-templates-service.test.js`
1093
+ Expected: PASS, including old platform snapshots, OMP resource skips, and new `demo-cli` discovery.
1094
+
1095
+ - [ ] **Step 7: Commit data-driven config services**
1096
+
1097
+ ```bash
1098
+ git add src/server/services/config-sync-manager.js src/server/services/config-registry-service.js src/server/services/config-export-service.js src/server/services/config-templates-service.js tests/unit/services/config-sync-manager.test.js tests/unit/services/config-registry-service.test.js tests/unit/services/config-export-service.test.js tests/unit/services/config-templates-service.test.js
1099
+ git commit -m "refactor: drive config services from platform capabilities"
1100
+ ```
1101
+
1102
+ ---
1103
+
1104
+ ### Task 8: Refactor CLI Routing and Platform Type Selection
1105
+
1106
+ **Files:**
1107
+ - Create: `src/commands/platform-command-registry.js`
1108
+ - Modify: `src/index.js`
1109
+ - Modify: `src/commands/cli-type.js`
1110
+ - Modify: `src/commands/logs.js`
1111
+ - Modify: `src/commands/stats.js`
1112
+ - Modify: `src/commands/port-config.js`
1113
+ - Create: `tests/unit/commands/platform-command-registry.test.js`
1114
+ - Create: `tests/unit/commands/cli-type.test.js`
1115
+ - Test: `tests/unit/index.test.js`
1116
+ - Test: `tests/unit/commands/logs.test.js`
1117
+ - Test: `tests/unit/commands/stats.test.js`
1118
+ - Test: `tests/unit/commands/port-config.test.js`
1119
+
1120
+ - [ ] **Step 1: Write command registry tests**
1121
+
1122
+ ```js
1123
+ const { createPlatformCommandRegistry } = require('../../../src/commands/platform-command-registry');
1124
+
1125
+ test('adds a configured platform to proxy, logs, stats, and port command options', () => {
1126
+ const registry = createPlatformCommandRegistry({
1127
+ platforms: [{
1128
+ key: 'demo-cli', label: 'Demo CLI', command: 'demo',
1129
+ logFile: 'demo-proxy.log', portKey: 'demoProxy', capabilities: {
1130
+ proxy: 'unsupported', statistics: 'unsupported'
1131
+ }
1132
+ }]
1133
+ });
1134
+
1135
+ expect(registry.resolve('demo-cli').label).toBe('Demo CLI');
1136
+ expect(registry.logTypes()).toContain('demo-cli');
1137
+ expect(registry.portKeys()).toContain('demoProxy');
1138
+ });
1139
+ ```
1140
+
1141
+ - [ ] **Step 2: Implement a pure command registry**
1142
+
1143
+ The registry exposes `resolve`, `platformKeys`, `logTypes`, `portKeys`, and `helpEntries`. It receives the Registry as a dependency and returns no terminal output. Existing aliases such as `omp` remain exact keys.
1144
+
1145
+ - [ ] **Step 3: Replace platform arrays in CLI commands**
1146
+
1147
+ Use `registry.list()` in `logs.js`, `stats.js`, `port-config.js`, and `cli-type.js`. Preserve:
1148
+
1149
+ - `omp` log alias to UI/server logs;
1150
+ - `omp` dynamic gateway labels;
1151
+ - existing default ports;
1152
+ - existing stats endpoint paths for five built-ins;
1153
+ - existing CLI type colors and prompts.
1154
+
1155
+ An unsupported capability is excluded from the relevant option list; an unknown explicit key still produces the current error path.
1156
+
1157
+ - [ ] **Step 4: Extract the platform branch from `src/index.js`**
1158
+
1159
+ Add a command table for non-platform commands and a Registry-derived platform route:
1160
+
1161
+ ```js
1162
+ const commandHandlers = {
1163
+ start: () => handleStart(),
1164
+ stop: () => handleStop(),
1165
+ restart: () => handleRestart(),
1166
+ status: () => handleStatus()
1167
+ };
1168
+
1169
+ const platform = registry.resolve(normalizePlatformKey(args[0]));
1170
+ if (platform) {
1171
+ return dispatchPlatformCommand(platform, args.slice(1), runtime);
1172
+ }
1173
+ ```
1174
+
1175
+ Do not remove compatibility branches for `daemon`, `proxy`, `security`, `plugin`, `ui`, or `reset`. Preserve `finishCli`, OMP exit cleanup, uncaught exception handling, and `_test.stopOwnedOmpGatewayBeforeExit`.
1176
+
1177
+ - [ ] **Step 5: Make help output Registry-derived**
1178
+
1179
+ Keep static descriptions for global commands, but render platform commands from `helpEntries()`. The output for existing five platforms must remain semantically identical; adding `demo-cli` must not require editing `showHelp()`.
1180
+
1181
+ - [ ] **Step 6: Run focused CLI tests and smoke help**
1182
+
1183
+ Run: `npx vitest run tests/unit/index.test.js tests/unit/commands/cli-type.test.js tests/unit/commands/logs.test.js tests/unit/commands/stats.test.js tests/unit/commands/port-config.test.js`
1184
+ Expected: PASS.
1185
+
1186
+ Run: `node bin/ctx.js --help`
1187
+ Expected: existing global command sections and all enabled Registry platform entries are printed, followed by exit code 0.
1188
+
1189
+ - [ ] **Step 7: Commit CLI routing**
1190
+
1191
+ ```bash
1192
+ git add src/index.js src/commands/platform-command-registry.js src/commands/cli-type.js src/commands/logs.js src/commands/stats.js src/commands/port-config.js tests/unit/index.test.js tests/unit/commands/platform-command-registry.test.js tests/unit/commands/cli-type.test.js tests/unit/commands/logs.test.js tests/unit/commands/stats.test.js tests/unit/commands/port-config.test.js
1193
+ git commit -m "refactor: derive CLI platform commands from registry"
1194
+ ```
1195
+
1196
+ ---
1197
+
1198
+ ### Task 9: Migrate User Platform Config and Preserve Legacy UI Config
1199
+
1200
+ **Files:**
1201
+ - Modify: `src/server/services/ui-config.js`
1202
+ - Modify: `src/shared/platforms.js`
1203
+ - Modify: `src/platforms/registry.js`
1204
+ - Test: `tests/unit/services/ui-config.test.js`
1205
+ - Test: `tests/unit/shared/platforms.test.js`
1206
+ - Test: `tests/unit/platforms/registry.test.js`
1207
+
1208
+ - [ ] **Step 1: Write migration compatibility tests**
1209
+
1210
+ ```js
1211
+ const { createPlatformRegistry } = require('../../../src/platforms/registry');
1212
+ test('legacy customCliPlatforms remain readable and become Registry metadata', () => {
1213
+ const registry = createPlatformRegistry({
1214
+ builtIns: [{ key: 'claude', label: 'Claude', command: 'claude', capabilities: {} }],
1215
+ legacyUiConfig: {
1216
+ customCliPlatforms: [{ key: 'demo-cli', name: 'Demo', command: 'demo', enabled: true }]
1217
+ },
1218
+ userFile: { platforms: [] }
1219
+ });
1220
+
1221
+ expect(registry.resolve('demo-cli')).toEqual(expect.objectContaining({
1222
+ key: 'demo-cli', label: 'Demo', custom: true
1223
+ }));
1224
+ });
1225
+
1226
+ test('user definitions cannot override built-in behavior or driver IDs', () => {
1227
+ const registry = createPlatformRegistry({
1228
+ builtIns: [{ key: 'claude', label: 'Claude', command: 'claude', capabilities: {} }],
1229
+ userFile: {
1230
+ platforms: [{ key: 'claude', label: 'Fake', command: 'fake', capabilities: { proxy: 'user-code' } }]
1231
+ }
1232
+ });
1233
+
1234
+ expect(registry.resolve('claude').label).toBe('Claude');
1235
+ expect(registry.diagnostics()[0].reason).toMatch(/built-in|driver/i);
1236
+ });
1237
+ ```
1238
+
1239
+ - [ ] **Step 2: Add `platforms.json` loading without changing current UI file shape**
1240
+
1241
+ `ui-config.js` continues returning `customCliPlatforms` for old frontend callers during migration. The Registry reads `PATHS.platforms` first, then imports valid legacy custom metadata. Saving UI config must not erase legacy fields before the frontend migration completes.
1242
+
1243
+ - [ ] **Step 3: Normalize custom metadata once**
1244
+
1245
+ Move key normalization, duplicate rejection, built-in collision rejection, and enabled filtering to one shared normalizer. Keep `normalizePlatformKey`, `normalizeCustomCliPlatform`, `normalizeCustomCliPlatforms`, and `normalizeHomeCliColumns` exports from `src/shared/platforms.js`; change their data source, not their observable behavior.
1246
+
1247
+ - [ ] **Step 4: Add safe user Manifest fields**
1248
+
1249
+ Allow user config to set only:
1250
+
1251
+ ```text
1252
+ key, label, title, command, iconToken, color, defaultVisible,
1253
+ paths, pathResolverId from allowlist, capabilities from allowlist,
1254
+ resource mappings and session field mappings
1255
+ ```
1256
+
1257
+ Reject `modulePath`, `require`, function-valued fields, shell commands, and driver IDs not in `DRIVER_IDS`. Validate all filesystem roots before Registry insertion.
1258
+
1259
+ - [ ] **Step 5: Run focused migration tests**
1260
+
1261
+ Run: `npx vitest run tests/unit/services/ui-config.test.js tests/unit/shared/platforms.test.js tests/unit/platforms/registry.test.js`
1262
+ Expected: PASS, including existing OMP key normalization and custom home column behavior.
1263
+
1264
+ - [ ] **Step 6: Commit configuration migration**
1265
+
1266
+ ```bash
1267
+ git add src/server/services/ui-config.js src/shared/platforms.js src/platforms/registry.js tests/unit/services/ui-config.test.js tests/unit/shared/platforms.test.js tests/unit/platforms/registry.test.js
1268
+ git commit -m "feat: load user CLI platform manifests safely"
1269
+ ```
1270
+
1271
+ ---
1272
+
1273
+ ### Task 10: Remove Frontend Platform Duplication and Add Generic Channel UI
1274
+
1275
+ **Files:**
1276
+ - Create: `src/web/src/api/platforms.js`
1277
+ - Create: `src/web/src/stores/platforms.js`
1278
+ - Create: `src/web/src/config/iconTokens.js`
1279
+ - Create: `src/web/src/components/channel/commonChannelSchema.js`
1280
+ - Modify: `src/web/vitest.config.js`
1281
+ - Modify: `src/web/src/config/platforms.js`
1282
+ - Modify: `src/web/src/api/channels.js`
1283
+ - Modify: `src/web/src/composables/useUIConfig.js`
1284
+ - Modify: `src/web/src/views/Home.vue`
1285
+ - Modify: `src/web/src/components/SettingsDrawer.vue`
1286
+ - Modify: `src/web/src/components/channel/channelPanelFactories.js`
1287
+ - Modify: `src/web/src/components/AgentsPanel.vue`
1288
+ - Modify: `src/web/src/components/CommandsPanel.vue`
1289
+ - Modify: `src/web/src/components/PluginsPanel.vue`
1290
+ - Modify: `src/web/src/components/SkillsPanel.vue`
1291
+ - Create: `src/web/src/config/__tests__/platforms.test.js`
1292
+ - Create: `src/web/src/components/channel/__tests__/commonChannelSchema.test.js`
1293
+
1294
+ - [ ] **Step 1: Add frontend API and store tests**
1295
+
1296
+ ```js
1297
+ import { beforeEach, expect, test } from 'vitest'
1298
+ import { setActivePinia, createPinia } from 'pinia'
1299
+ import { usePlatformStore } from '../../stores/platforms'
1300
+
1301
+ beforeEach(() => setActivePinia(createPinia()))
1302
+
1303
+ test('loads public platform metadata from the server', async () => {
1304
+ const store = usePlatformStore()
1305
+ store.fetchPlatforms = async () => {
1306
+ store.platforms = [{ key: 'demo-cli', label: 'Demo CLI', iconToken: 'terminal', capabilities: {} }]
1307
+ }
1308
+
1309
+ await store.load()
1310
+ expect(store.get('demo-cli').label).toBe('Demo CLI')
1311
+ })
1312
+ ```
1313
+
1314
+ - [ ] **Step 2: Implement `api/platforms.js` and `stores/platforms.js`**
1315
+
1316
+ The client calls `GET /platforms`, stores only public fields, and exposes `get(key)`, `all`, `enabled`, and `hasCapability(key, capability)`. Loading must be idempotent and retain the last successful catalog on a transient failure.
1317
+
1318
+ - [ ] **Step 3: Reduce `config/platforms.js` to normalization and icon mapping**
1319
+
1320
+ Delete the duplicated complete built-in platform array. Retain:
1321
+
1322
+ ```js
1323
+ export function normalizePublicPlatform(platform) {
1324
+ return {
1325
+ key: String(platform?.key || '').trim().toLowerCase(),
1326
+ label: String(platform?.label || platform?.title || platform?.key || '').trim(),
1327
+ title: String(platform?.title || platform?.label || platform?.key || '').trim(),
1328
+ command: String(platform?.command || platform?.key || '').trim(),
1329
+ iconToken: String(platform?.iconToken || 'terminal').trim(),
1330
+ color: String(platform?.color || '#64748b').trim(),
1331
+ defaultVisible: platform?.defaultVisible !== false,
1332
+ capabilities: { ...(platform?.capabilities || {}) }
1333
+ }
1334
+ }
1335
+
1336
+ export function normalizePublicPlatforms(platforms = []) {
1337
+ const seen = new Set()
1338
+ return platforms.map(normalizePublicPlatform).filter(platform => {
1339
+ if (!platform.key || seen.has(platform.key)) return false
1340
+ seen.add(platform.key)
1341
+ return true
1342
+ })
1343
+ }
1344
+
1345
+ export function getPlatformConfig(key, platforms = []) {
1346
+ return normalizePublicPlatforms(platforms).find(platform => platform.key === key) || null
1347
+ }
1348
+
1349
+ export function normalizeHomeCliColumns(input = [], platforms = []) {
1350
+ const allowed = new Set(normalizePublicPlatforms(platforms).map(platform => platform.key))
1351
+ const result = []
1352
+ for (const value of input) {
1353
+ const key = String(value || '').trim().toLowerCase()
1354
+ if (key && allowed.has(key) && !result.includes(key)) result.push(key)
1355
+ }
1356
+ return result.slice(0, 4)
1357
+ }
1358
+
1359
+ export function buildCliPlatformOptions(platforms = []) {
1360
+ return normalizePublicPlatforms(platforms).map(platform => ({
1361
+ label: platform.label || platform.title || platform.key,
1362
+ value: platform.key
1363
+ }))
1364
+ }
1365
+ ```
1366
+
1367
+ `iconTokens.js` contains the only static map from strings to imported Ionicons. Unknown tokens use `TerminalOutline`.
1368
+
1369
+ - [ ] **Step 4: Extract common channel schema**
1370
+
1371
+ Move schedule, enable/disable, health, provider URL and generic auth fields out of `channelPanelFactories.js`:
1372
+
1373
+ ```js
1374
+ export const commonChannelSchema = {
1375
+ endpoint: [
1376
+ { key: 'baseUrl', label: 'Base URL', type: 'text', required: true },
1377
+ { key: 'websiteUrl', label: '官网链接', type: 'text', required: false }
1378
+ ],
1379
+ auth: [
1380
+ { key: 'authMode', label: '认证方式', type: 'select', options: ['api_key', 'none'] },
1381
+ { key: 'apiKey', label: 'API Key', type: 'password', required: false }
1382
+ ],
1383
+ schedule: [
1384
+ { key: 'maxConcurrency', label: '最大并发', type: 'number', required: false },
1385
+ { key: 'weight', label: '调度权重', type: 'number', min: 1, max: 100 },
1386
+ { key: 'enabled', label: '渠道状态', type: 'switch', default: true }
1387
+ ]
1388
+ }
1389
+
1390
+ export function createGenericChannelPanel(manifest, api) {
1391
+ return {
1392
+ type: manifest.key,
1393
+ displayName: manifest.label,
1394
+ formSections: [
1395
+ { title: '基本信息', fields: commonChannelSchema.endpoint },
1396
+ { title: '认证', fields: commonChannelSchema.auth },
1397
+ { title: '调度配置', fields: commonChannelSchema.schedule }
1398
+ ],
1399
+ api
1400
+ }
1401
+ }
1402
+ ```
1403
+
1404
+ Keep Claude/Codex/Gemini/OpenCode/OMP-specific field definitions and serializers in their existing factories. Generic platforms use the generic factory only when their channel capability is a generic driver.
1405
+
1406
+ - [ ] **Step 5: Add generic channel API functions while retaining old exports**
1407
+
1408
+ Add:
1409
+
1410
+ ```js
1411
+ export function getPlatformChannels(platform) {
1412
+ return client.get(`/platforms/${platform}/channels`).then(response => response.data)
1413
+ }
1414
+
1415
+ export function createPlatformChannel(platform, payload) {
1416
+ return client.post(`/platforms/${platform}/channels`, payload).then(response => response.data)
1417
+ }
1418
+ ```
1419
+
1420
+ Keep `getCodexChannels`, `createCodexChannel`, and all existing named functions as wrappers. Existing specialized panels must not be forced through generic serialization.
1421
+
1422
+ - [ ] **Step 6: Migrate Home, Settings, and resource panels to the store**
1423
+
1424
+ Replace module-scope `BUILT_IN_CLI_PLATFORMS` filtering with `platformStore.enabled` and `hasCapability`. Preserve current `customCliPlatforms` loading behavior during the backend migration, then use the Registry catalog as the source for display and ordering.
1425
+
1426
+ - [ ] **Step 7: Add frontend tests and build**
1427
+
1428
+ Test generic option generation, unknown icon fallback, capability filtering, duplicate home columns, and common schema validation. Update `src/web/vitest.config.js` to include `src/**/__tests__/**/*.test.js`, then run:
1429
+
1430
+ ```bash
1431
+ cd src/web && npm run test:unit -- src/config/__tests__/platforms.test.js src/components/channel/__tests__/commonChannelSchema.test.js
1432
+ npm run build
1433
+ ```
1434
+
1435
+ Expected: PASS and a successful Vite production build.
1436
+
1437
+ - [ ] **Step 8: Commit frontend migration**
1438
+
1439
+ ```bash
1440
+ git add src/web/vitest.config.js src/web/src/api/platforms.js src/web/src/stores/platforms.js src/web/src/config src/web/src/api/channels.js src/web/src/composables/useUIConfig.js src/web/src/views/Home.vue src/web/src/components/SettingsDrawer.vue src/web/src/components/channel src/web/src/components/AgentsPanel.vue src/web/src/components/CommandsPanel.vue src/web/src/components/PluginsPanel.vue src/web/src/components/SkillsPanel.vue
1441
+ git commit -m "refactor: consume registry-backed platform metadata in web UI"
1442
+ ```
1443
+
1444
+ ---
1445
+
1446
+ ### Task 11: Complete Contract Tests, Documentation, and Regression Verification
1447
+
1448
+ **Files:**
1449
+ - Modify: `README.md`
1450
+ - Modify: `CHANGELOG.md`
1451
+ - Test: all affected unit/API tests
1452
+ - Smoke: `node bin/ctx.js --help`, service status, and Web UI build
1453
+
1454
+ - [ ] **Step 1: Add the config-only `demo-cli` contract test**
1455
+
1456
+ Register this platform only through a test Manifest and existing generic drivers:
1457
+
1458
+ ```js
1459
+ const fs = require('fs');
1460
+ const os = require('os');
1461
+ const path = require('path');
1462
+ const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'platform-contract-home-'));
1463
+ const demoManifest = {
1464
+ key: 'demo-cli',
1465
+ label: 'Demo CLI',
1466
+ command: 'demo',
1467
+ paths: { home: tempHome, sessions: '{home}/sessions' },
1468
+ capabilities: {
1469
+ sessions: 'generic-jsonl',
1470
+ resourceSync: 'generic-filesystem',
1471
+ proxy: 'unsupported',
1472
+ statistics: 'unsupported'
1473
+ }
1474
+ };
1475
+ ```
1476
+
1477
+ Assert that Registry listing, path resolution, generic route discovery, session indexing and resource sync work without editing Dashboard, CLI router, or platform-specific source files.
1478
+
1479
+ - [ ] **Step 2: Add mock path compatibility assertions to existing suites**
1480
+
1481
+ Run and retain tests covering:
1482
+
1483
+ ```text
1484
+ tests/unit/index.test.js
1485
+ tests/unit/api/channels.test.js
1486
+ tests/unit/api/codex-proxy-api.test.js
1487
+ tests/unit/api/gemini-proxy-api.test.js
1488
+ tests/unit/api/opencode-proxy-api.test.js
1489
+ tests/unit/api/proxy-api.test.js
1490
+ tests/unit/services/session-history-index.test.js
1491
+ tests/unit/services/omp-proxy-server.test.js
1492
+ ```
1493
+
1494
+ At least one test for every preserved path must install a `require.cache` stub before loading the module under test. Do not replace these with source-text assertions.
1495
+
1496
+ - [ ] **Step 3: Add explicit error-state tests**
1497
+
1498
+ Cover:
1499
+
1500
+ ```text
1501
+ unknown explicit platform -> invalid / 400 or existing CLI error
1502
+ unsupported capability -> unsupported / 404
1503
+ missing executable/path -> unavailable / 503 or diagnostic output
1504
+ malformed Manifest -> invalid with field path
1505
+ runtime driver exception -> failed with platform/capability/operation
1506
+ ```
1507
+
1508
+ Assert that none of these cases returns `{ success: true }` with an empty value.
1509
+
1510
+ - [ ] **Step 4: Update README and CHANGELOG**
1511
+
1512
+ Document:
1513
+
1514
+ - generic CLI support requires a valid Manifest and an allowlisted generic driver;
1515
+ - special CLI protocols require a capability driver;
1516
+ - user Manifest location `~/.cc-tool/config/platforms.json`;
1517
+ - arbitrary Node module loading is prohibited;
1518
+ - existing CLI/API compatibility and mock path behavior are intentional.
1519
+
1520
+ Do not claim that every arbitrary CLI gains full parity from configuration alone.
1521
+
1522
+ - [ ] **Step 5: Run the focused suites**
1523
+
1524
+ Run:
1525
+
1526
+ ```bash
1527
+ npx vitest run tests/unit/platforms tests/unit/services/session-history-index.test.js tests/unit/services/dashboard-snapshot-worker.test.js tests/unit/api/platforms-api.test.js
1528
+ npm run test:unit
1529
+ npm run test:basic
1530
+ npm run test:api
1531
+ npm run test:codex-agents
1532
+ npm run test:skills
1533
+ npm run test:plugins-market
1534
+ npm run test:windows
1535
+ ```
1536
+
1537
+ Expected: all commands exit 0. If a pre-existing user-modified skill test fails, record the exact failure and do not revert those files.
1538
+
1539
+ - [ ] **Step 6: Run runtime smoke checks**
1540
+
1541
+ Run:
1542
+
1543
+ ```bash
1544
+ node bin/ctx.js --version
1545
+ node bin/ctx.js --help
1546
+ npm run build:web
1547
+ ```
1548
+
1549
+ Expected: version output, Registry-derived help output, and a successful Web UI build. If a local service is available, start it through the existing CLI and verify `GET /api/platforms` returns the five built-ins plus any configured user platform; stop it using the existing command.
1550
+
1551
+ - [ ] **Step 7: Inspect final diff and commit only owned files**
1552
+
1553
+ Run: `git diff --check`
1554
+
1555
+ Then inspect `git status --short` and verify unrelated skill changes remain untouched. Commit only the documentation and contract tests:
1556
+
1557
+ ```bash
1558
+ git add README.md CHANGELOG.md tests/unit/platforms tests/unit/services/dashboard-snapshot-worker.test.js tests/unit/api/platforms-api.test.js
1559
+ git commit -m "docs: document configurable CLI platform extensions"
1560
+ ```
1561
+
1562
+ - [ ] **Step 8: Final acceptance review**
1563
+
1564
+ Confirm each approved-spec requirement has evidence:
1565
+
1566
+ ```text
1567
+ Manifest + Registry present
1568
+ capability drivers isolated
1569
+ config-only generic demo works
1570
+ legacy platform facades remain loadable
1571
+ require.resolve + require.cache mocks still intercept
1572
+ old CLI/API paths unchanged
1573
+ OMP managed mode and exit recovery unchanged
1574
+ unsupported and failed states are distinguishable
1575
+ frontend has no second complete built-in platform catalog
1576
+ ```
1577
+
1578
+ Only after these checks pass is the refactor complete.