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
@@ -17,8 +17,12 @@ const { listPlugins, getPlugin, updatePlugin: updatePluginRegistry } = require('
17
17
  const { installPlugin: installPluginCore, uninstallPlugin: uninstallPluginCore } = require('../../plugins/plugin-installer');
18
18
  const { initializePlugins, shutdownPlugins } = require('../../plugins/plugin-manager');
19
19
  const { INSTALLED_DIR, CONFIG_DIR } = require('../../plugins/constants');
20
- const { NATIVE_PATHS, PATHS } = require('../../config/paths');
21
- const { OmpNativePluginAdapter } = require('./omp-native-plugin-adapter');
20
+ const pathsModule = require('../../config/paths');
21
+ const { NATIVE_PATHS, PATHS } = pathsModule;
22
+ const getPlatformStatePath = typeof pathsModule.getPlatformStatePath === 'function'
23
+ ? pathsModule.getPlatformStatePath
24
+ : (category, platform) => PATHS[category]?.[platform];
25
+ const { OmpNativePluginAdapter } = require('../../platforms/drivers/omp/native-plugin-adapter');
22
26
  const {
23
27
  importLegacyPluginRepos,
24
28
  migratePiStorage
@@ -30,7 +34,7 @@ const {
30
34
  normalizeSafeFileStem,
31
35
  normalizeSafeRelativePath,
32
36
  resolveInsideRoot
33
- } = require('./config-artifact-paths');
37
+ } = require('../../shared/config-artifact-paths');
34
38
 
35
39
  const CLAUDE_PLUGINS_DIR = NATIVE_PATHS.claude.plugins
36
40
  || path.join(NATIVE_PATHS.claude.dir || path.dirname(NATIVE_PATHS.claude.settings), 'plugins');
@@ -43,6 +47,8 @@ const CODEX_PLUGINS_CACHE_DIR = path.join(CODEX_PLUGINS_DIR, 'cache');
43
47
  const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
44
48
  const REPO_SOURCE_META_FILE = '.cc-tool-plugin-source.json';
45
49
  const SUPPORTED_REPO_PROVIDERS = ['github', 'gitlab', 'local'];
50
+ const PLUGIN_LIST_TTL_MS = 1000;
51
+ const MARKET_CACHE_TTL_MS = 5 * 60 * 1000;
46
52
  const DEFAULT_GITHUB_HOST = 'https://github.com';
47
53
  const DEFAULT_GITLAB_HOST = 'https://gitlab.com';
48
54
  const DEFAULT_REPOS_BY_PLATFORM = {
@@ -116,7 +122,8 @@ const PLATFORM_CAPABILITIES = {
116
122
  pluginKindLabel: 'plugins/extensions',
117
123
  repositoryMode: 'native-marketplace',
118
124
  repositoryToggle: false,
119
- repositoryAuth: false
125
+ repositoryAuth: false,
126
+ installMetadataMode: 'omp'
120
127
  }
121
128
  };
122
129
 
@@ -435,27 +442,128 @@ function stripJsonComments(input = '') {
435
442
  }
436
443
 
437
444
  class PluginsService {
438
- constructor(platform = 'claude') {
445
+ constructor(platform = 'claude', { registry } = {}) {
439
446
  this.platform = resolveManagedPlatform(platform).platform;
447
+ this.registry = registry || (() => {
448
+ try {
449
+ return require('../../platforms/runtime').getPlatformRegistry();
450
+ } catch {
451
+ return null;
452
+ }
453
+ })();
454
+ this.pathContext = null;
455
+ try {
456
+ if (typeof this.registry?.resolvePathContext === 'function') {
457
+ this.pathContext = this.registry.resolvePathContext(this.platform);
458
+ }
459
+ } catch {
460
+ this.pathContext = null;
461
+ }
462
+ const native = this.pathContext?.customized
463
+ ? (this.pathContext.native || {})
464
+ : (NATIVE_PATHS[this.platform] || {});
440
465
  this._ompMigrationWarnings = [];
441
466
  if (this.platform === 'omp') {
442
467
  const migration = migratePiStorage(PATHS);
443
468
  this._ompMigrationWarnings.push(...migration.warnings);
444
469
  }
445
470
  this.configDir = PATHS.config || path.join((PATHS.base || process.env.HOME || os.homedir()), 'config');
446
- this.ccToolConfigDir = path.dirname(PATHS.pluginRepos.claude);
447
- this.opencodePluginsDir = path.join(OPENCODE_CONFIG_DIR, 'plugins');
448
- this.opencodeLegacyPluginsDir = path.join(OPENCODE_CONFIG_DIR, 'plugin');
449
- this.codexPluginsCacheDir = CODEX_PLUGINS_CACHE_DIR;
450
- this.marketCachePath = PATHS.pluginMarketCache[this.platform] || PATHS.pluginMarketCache.claude;
471
+ this.ccToolConfigDir = path.dirname(getPlatformStatePath('pluginRepos', this.platform));
472
+ this.claudePluginsDir = this.platform === 'claude'
473
+ ? native.plugins || CLAUDE_PLUGINS_DIR
474
+ : CLAUDE_PLUGINS_DIR;
475
+ this.claudeInstalledFile = path.join(this.claudePluginsDir, 'installed_plugins.json');
476
+ this.claudeMarketplacesFile = path.join(this.claudePluginsDir, 'known_marketplaces.json');
477
+ this.claudePluginsCacheDir = path.join(this.claudePluginsDir, 'cache');
478
+ this.claudeMarketplacesDir = path.join(this.claudePluginsDir, 'marketplaces');
479
+ this.codexPluginsDir = this.platform === 'codex'
480
+ ? path.join(native.dir || path.dirname(native.config || ''), 'plugins')
481
+ : CODEX_PLUGINS_DIR;
482
+ this.codexPluginsCacheDir = path.join(this.codexPluginsDir, 'cache');
483
+ this.opencodeConfigDir = this.platform === 'opencode'
484
+ ? (native.config || OPENCODE_CONFIG_DIR)
485
+ : OPENCODE_CONFIG_DIR;
486
+ this.opencodePluginsDir = path.join(this.opencodeConfigDir, 'plugins');
487
+ this.opencodeLegacyPluginsDir = path.join(this.opencodeConfigDir, 'plugin');
488
+ this.marketCachePath = getPlatformStatePath('pluginMarketCache', this.platform);
451
489
  this._marketCache = null;
490
+ this._marketCacheByKey = new Map();
491
+ this._pluginListCache = new Map();
492
+ this._pluginListGeneration = 0;
493
+ this._marketRefreshPromise = null;
494
+ this._marketFetchedAt = 0;
495
+ this._marketRepoFingerprint = null;
496
+ this._marketLastUsedStale = false;
452
497
  this.ompNativeAdapter = this.platform === 'omp'
453
- ? new OmpNativePluginAdapter()
498
+ ? new OmpNativePluginAdapter({ pathContext: this.pathContext })
454
499
  : null;
455
500
  }
456
501
 
502
+ _pluginListKey(options = {}) {
503
+ const cwd = options.cwd ? path.resolve(options.cwd) : '';
504
+ const scope = options.scope || '';
505
+ return `${this.platform}:${cwd}:${scope}`;
506
+ }
507
+
508
+ _clone(value) {
509
+ if (value === undefined) return value;
510
+ try {
511
+ return JSON.parse(JSON.stringify(value));
512
+ } catch (_) {
513
+ return value;
514
+ }
515
+ }
516
+
517
+ _invalidatePluginList(options = null) {
518
+ this._pluginListGeneration++;
519
+ if (!options) {
520
+ this._pluginListCache.clear();
521
+ return;
522
+ }
523
+ const key = this._pluginListKey(options);
524
+ this._pluginListCache.delete(key);
525
+ }
526
+
527
+
457
528
  getCapabilities() {
458
- return { ...(PLATFORM_CAPABILITIES[this.platform] || PLATFORM_CAPABILITIES.claude) };
529
+ return {
530
+ platform: this.platform,
531
+ supportsPlugins: false,
532
+ repositories: false,
533
+ market: false,
534
+ install: false,
535
+ uninstall: false,
536
+ toggle: false,
537
+ config: false,
538
+ import: false,
539
+ syncRepos: false,
540
+ ...(PLATFORM_CAPABILITIES[this.platform] || {})
541
+ };
542
+ }
543
+ getRequestOptions(requestData = {}) {
544
+ return {
545
+ ...(requestData.cwd ? { cwd: requestData.cwd } : {}),
546
+ ...(requestData.scope ? { scope: requestData.scope } : {})
547
+ };
548
+ }
549
+
550
+ normalizeInstallMetadata(requestBody = {}, repoPayload = null) {
551
+ if (repoPayload) return repoPayload;
552
+ if (this.getCapabilities().installMetadataMode !== 'omp') return null;
553
+ const metadata = requestBody.metadata && typeof requestBody.metadata === 'object'
554
+ ? requestBody.metadata
555
+ : {};
556
+ return {
557
+ ...metadata,
558
+ name: requestBody.name || metadata.name,
559
+ pluginId: requestBody.pluginId || metadata.pluginId,
560
+ pluginKind: requestBody.pluginKind || metadata.pluginKind,
561
+ marketplace: requestBody.marketplace || metadata.marketplace,
562
+ installSource: requestBody.installSource || metadata.installSource,
563
+ version: requestBody.version || metadata.version,
564
+ description: requestBody.description || metadata.description,
565
+ resourceTypes: requestBody.resourceTypes || metadata.resourceTypes
566
+ };
459
567
  }
460
568
 
461
569
  getMigrationWarnings() {
@@ -595,6 +703,10 @@ class PluginsService {
595
703
 
596
704
  clearMarketCache({ removeFile = true } = {}) {
597
705
  this._marketCache = null;
706
+ this._marketCacheByKey.clear();
707
+ this._marketFetchedAt = 0;
708
+ this._marketRepoFingerprint = null;
709
+ this._invalidatePluginList();
598
710
  if (removeFile) {
599
711
  try {
600
712
  if (fs.existsSync(this.marketCachePath)) {
@@ -606,13 +718,19 @@ class PluginsService {
606
718
  }
607
719
  }
608
720
 
609
- loadMarketCacheFromFile() {
721
+ loadMarketCacheEnvelope() {
610
722
  try {
611
- if (fs.existsSync(this.marketCachePath)) {
612
- const data = JSON.parse(fs.readFileSync(this.marketCachePath, 'utf-8'));
613
- if (Array.isArray(data.plugins)) {
614
- return data.plugins;
615
- }
723
+ if (!fs.existsSync(this.marketCachePath)) return null;
724
+ const data = JSON.parse(fs.readFileSync(this.marketCachePath, 'utf-8'));
725
+ if (Array.isArray(data)) {
726
+ return { plugins: data, fetchedAt: 0, reposFingerprint: null };
727
+ }
728
+ if (Array.isArray(data?.plugins)) {
729
+ return {
730
+ plugins: data.plugins,
731
+ fetchedAt: Number(data.fetchedAt || data.time || 0) || 0,
732
+ reposFingerprint: typeof data.reposFingerprint === 'string' ? data.reposFingerprint : null
733
+ };
616
734
  }
617
735
  } catch (err) {
618
736
  // ignore cache read errors
@@ -620,10 +738,22 @@ class PluginsService {
620
738
  return null;
621
739
  }
622
740
 
623
- saveMarketCacheToFile(plugins) {
741
+ loadMarketCacheFromFile() {
742
+ const cached = this.loadMarketCacheEnvelope();
743
+ return cached?.fetchedAt && Date.now() - cached.fetchedAt < MARKET_CACHE_TTL_MS
744
+ ? cached.plugins
745
+ : null;
746
+ }
747
+
748
+ saveMarketCacheToFile(plugins, fetchedAt = Date.now(), reposFingerprint = null) {
624
749
  try {
625
750
  this._ensureDir(path.dirname(this.marketCachePath));
626
- fs.writeFileSync(this.marketCachePath, JSON.stringify({ plugins }), 'utf-8');
751
+ fs.writeFileSync(this.marketCachePath, JSON.stringify({
752
+ fetchedAt,
753
+ time: fetchedAt,
754
+ reposFingerprint,
755
+ plugins
756
+ }), 'utf-8');
627
757
  } catch (err) {
628
758
  // ignore cache write errors
629
759
  }
@@ -694,9 +824,9 @@ class PluginsService {
694
824
  }
695
825
 
696
826
  _getOpenCodeConfigPath() {
697
- const jsonc = path.join(OPENCODE_CONFIG_DIR, 'opencode.jsonc');
698
- const json = path.join(OPENCODE_CONFIG_DIR, 'opencode.json');
699
- const config = path.join(OPENCODE_CONFIG_DIR, 'config.json');
827
+ const jsonc = path.join(this.opencodeConfigDir, 'opencode.jsonc');
828
+ const json = path.join(this.opencodeConfigDir, 'opencode.json');
829
+ const config = path.join(this.opencodeConfigDir, 'config.json');
700
830
  if (fs.existsSync(jsonc)) return jsonc;
701
831
  if (fs.existsSync(json)) return json;
702
832
  if (fs.existsSync(config)) return config;
@@ -723,6 +853,7 @@ class PluginsService {
723
853
  _writeOpenCodeConfig(filePath, config) {
724
854
  this._ensureDir(path.dirname(filePath));
725
855
  fs.writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf8');
856
+ this._invalidatePluginList();
726
857
  }
727
858
 
728
859
  _listOpenCodeConfiguredPlugins() {
@@ -850,7 +981,7 @@ class PluginsService {
850
981
  marketplace: marketplaceEntry.name,
851
982
  version: manifest.version || selectedVersion.name,
852
983
  installPath: selectedVersion.fullPath,
853
- directory: path.relative(CODEX_PLUGINS_DIR, selectedVersion.fullPath),
984
+ directory: path.relative(this.codexPluginsDir, selectedVersion.fullPath),
854
985
  source: 'codex-cache',
855
986
  installed: true,
856
987
  enabled: this._isCodexPluginEnabled(manifest.name || pluginEntry.name, marketplaceEntry.name),
@@ -881,7 +1012,9 @@ class PluginsService {
881
1012
  }
882
1013
 
883
1014
  _readCodexConfig() {
884
- const filePath = NATIVE_PATHS.codex.config;
1015
+ const filePath = this.pathContext?.customized
1016
+ ? (this.pathContext.native?.config || NATIVE_PATHS.codex.config)
1017
+ : NATIVE_PATHS.codex.config;
885
1018
  if (!fs.existsSync(filePath)) return { filePath, config: {} };
886
1019
  try {
887
1020
  const raw = fs.readFileSync(filePath, 'utf8');
@@ -894,14 +1027,19 @@ class PluginsService {
894
1027
  }
895
1028
 
896
1029
  _writeCodexConfig(config) {
897
- const filePath = NATIVE_PATHS.codex.config;
1030
+ const filePath = this.pathContext?.customized
1031
+ ? (this.pathContext.native?.config || NATIVE_PATHS.codex.config)
1032
+ : NATIVE_PATHS.codex.config;
898
1033
  this._ensureDir(path.dirname(filePath));
899
1034
  const safeConfig = JSON.parse(JSON.stringify(config || {}));
900
1035
  fs.writeFileSync(filePath, tomlStringify(safeConfig), 'utf8');
1036
+ this._invalidatePluginList();
901
1037
  }
902
1038
 
903
1039
  _readClaudeSettings() {
904
- const filePath = NATIVE_PATHS.claude.settings;
1040
+ const filePath = this.pathContext?.customized
1041
+ ? (this.pathContext.native?.settings || NATIVE_PATHS.claude.settings)
1042
+ : NATIVE_PATHS.claude.settings;
905
1043
  if (!fs.existsSync(filePath)) return { filePath, settings: {} };
906
1044
  try {
907
1045
  const raw = fs.readFileSync(filePath, 'utf8');
@@ -913,9 +1051,12 @@ class PluginsService {
913
1051
  }
914
1052
 
915
1053
  _writeClaudeSettings(settings) {
916
- const filePath = NATIVE_PATHS.claude.settings;
1054
+ const filePath = this.pathContext?.customized
1055
+ ? (this.pathContext.native?.settings || NATIVE_PATHS.claude.settings)
1056
+ : NATIVE_PATHS.claude.settings;
917
1057
  this._ensureDir(path.dirname(filePath));
918
1058
  fs.writeFileSync(filePath, JSON.stringify(settings || {}, null, 2), 'utf8');
1059
+ this._invalidatePluginList();
919
1060
  }
920
1061
 
921
1062
  _setClaudePluginEnabled(name, marketplace, enabled) {
@@ -967,7 +1108,7 @@ class PluginsService {
967
1108
 
968
1109
  _resolveClaudePluginCachePath(marketplace, pluginName, version) {
969
1110
  return resolveInsideRoot(
970
- CLAUDE_PLUGINS_CACHE_DIR,
1111
+ this.claudePluginsCacheDir,
971
1112
  path.posix.join(
972
1113
  normalizeClaudeMarketplaceSegment(marketplace),
973
1114
  normalizePluginCacheSegment(pluginName, 'Claude plugin name', 'plugin'),
@@ -979,7 +1120,7 @@ class PluginsService {
979
1120
 
980
1121
  _resolveClaudeMarketplacePath(marketplace) {
981
1122
  return resolveInsideRoot(
982
- CLAUDE_MARKETPLACES_DIR,
1123
+ this.claudeMarketplacesDir,
983
1124
  normalizeClaudeMarketplaceSegment(marketplace),
984
1125
  'Claude marketplace path'
985
1126
  );
@@ -1000,11 +1141,11 @@ class PluginsService {
1000
1141
  };
1001
1142
  fs.writeFileSync(manifestPath, JSON.stringify(marketplaceManifest, null, 2), 'utf8');
1002
1143
 
1003
- this._ensureDir(CLAUDE_PLUGINS_DIR);
1144
+ this._ensureDir(this.claudePluginsDir);
1004
1145
  let known = {};
1005
- if (fs.existsSync(CLAUDE_MARKETPLACES_FILE)) {
1146
+ if (fs.existsSync(this.claudeMarketplacesFile)) {
1006
1147
  try {
1007
- known = JSON.parse(fs.readFileSync(CLAUDE_MARKETPLACES_FILE, 'utf8'));
1148
+ known = JSON.parse(fs.readFileSync(this.claudeMarketplacesFile, 'utf8'));
1008
1149
  } catch {
1009
1150
  known = {};
1010
1151
  }
@@ -1017,7 +1158,7 @@ class PluginsService {
1017
1158
  installLocation: marketplaceDir,
1018
1159
  lastUpdated: new Date().toISOString()
1019
1160
  };
1020
- fs.writeFileSync(CLAUDE_MARKETPLACES_FILE, JSON.stringify(known, null, 2), 'utf8');
1161
+ fs.writeFileSync(this.claudeMarketplacesFile, JSON.stringify(known, null, 2), 'utf8');
1021
1162
 
1022
1163
  const { settings } = this._readClaudeSettings();
1023
1164
  const nextSettings = (settings && typeof settings === 'object') ? { ...settings } : {};
@@ -1085,11 +1226,11 @@ class PluginsService {
1085
1226
  }
1086
1227
 
1087
1228
  _registerClaudeInstalledPlugin(name, marketplace, installPath, installData = {}) {
1088
- this._ensureDir(CLAUDE_PLUGINS_DIR);
1229
+ this._ensureDir(this.claudePluginsDir);
1089
1230
  let nativeData = { version: 2, plugins: {} };
1090
- if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
1231
+ if (fs.existsSync(this.claudeInstalledFile)) {
1091
1232
  try {
1092
- nativeData = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
1233
+ nativeData = JSON.parse(fs.readFileSync(this.claudeInstalledFile, 'utf8'));
1093
1234
  } catch {
1094
1235
  nativeData = { version: 2, plugins: {} };
1095
1236
  }
@@ -1109,7 +1250,8 @@ class PluginsService {
1109
1250
  ...(installData.source ? { source: installData.source } : {}),
1110
1251
  ...(installData.repoSourceMeta || {})
1111
1252
  }];
1112
- fs.writeFileSync(CLAUDE_INSTALLED_FILE, JSON.stringify(nativeData, null, 2), 'utf8');
1253
+ fs.writeFileSync(this.claudeInstalledFile, JSON.stringify(nativeData, null, 2), 'utf8');
1254
+ this._invalidatePluginList();
1113
1255
  this._setClaudePluginEnabled(name, marketplace, true);
1114
1256
  }
1115
1257
 
@@ -1176,12 +1318,31 @@ class PluginsService {
1176
1318
  return slugifyCodexKey(repo.name || repo.owner || 'github');
1177
1319
  }
1178
1320
 
1321
+ listPlugins(options = {}) {
1322
+ const key = this._pluginListKey(options);
1323
+ const now = Date.now();
1324
+ const cached = this._pluginListCache.get(key);
1325
+ if (!options.force && cached
1326
+ && now - cached.cachedAt < PLUGIN_LIST_TTL_MS
1327
+ && cached.generation === this._pluginListGeneration) {
1328
+ return this._clone(cached.value);
1329
+ }
1330
+
1331
+ const value = this._listPluginsUncached(options);
1332
+ this._pluginListCache.set(key, {
1333
+ value: this._clone(value),
1334
+ cachedAt: now,
1335
+ generation: this._pluginListGeneration
1336
+ });
1337
+ return this._clone(value);
1338
+ }
1339
+
1179
1340
  /**
1180
1341
  * List all installed plugins with their status
1181
1342
  * Reads from Claude Code's native installed_plugins.json
1182
1343
  * @returns {Object} { plugins: Array }
1183
1344
  */
1184
- listPlugins(options = {}) {
1345
+ _listPluginsUncached(options = {}) {
1185
1346
  if (!this._pluginsSupported()) {
1186
1347
  return { plugins: [] };
1187
1348
  }
@@ -1224,11 +1385,32 @@ class PluginsService {
1224
1385
  }
1225
1386
 
1226
1387
  const plugins = [];
1388
+ const legacyPlugins = (() => {
1389
+ try {
1390
+ return listPlugins();
1391
+ } catch {
1392
+ return [];
1393
+ }
1394
+ })();
1395
+ const legacyByName = new Map(legacyPlugins.map(plugin => [plugin.name, plugin]));
1396
+ const claudeSettings = this._readClaudeSettings().settings || {};
1397
+ const enabledPlugins = claudeSettings.enabledPlugins || {};
1398
+ const isEnabled = (name, marketplace) => {
1399
+ const exactKey = marketplace ? `${name}@${marketplace}` : name;
1400
+ if (Object.prototype.hasOwnProperty.call(enabledPlugins, exactKey)) {
1401
+ return enabledPlugins[exactKey] !== false;
1402
+ }
1403
+ for (const [key, value] of Object.entries(enabledPlugins)) {
1404
+ const parsed = splitPluginMarketplaceKey(key);
1405
+ if (parsed.name === name) return value !== false;
1406
+ }
1407
+ return true;
1408
+ };
1227
1409
 
1228
1410
  // Read Claude Code's installed_plugins.json
1229
- if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
1411
+ if (fs.existsSync(this.claudeInstalledFile)) {
1230
1412
  try {
1231
- const data = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
1413
+ const data = JSON.parse(fs.readFileSync(this.claudeInstalledFile, 'utf8'));
1232
1414
  if (data.plugins) {
1233
1415
  for (const [key, installations] of Object.entries(data.plugins)) {
1234
1416
  if (installations && installations.length > 0) {
@@ -1285,9 +1467,10 @@ class PluginsService {
1285
1467
  }
1286
1468
 
1287
1469
  // Read enabled state from CTX registry (defaults to true if not set)
1288
- const legacyInfo = getPlugin(name);
1289
- const enabledState = this._isClaudePluginEnabled(name, marketplace) &&
1290
- (legacyInfo ? legacyInfo.enabled !== false : true);
1470
+ // Project settings and legacy registry are loaded once per list call.
1471
+ const legacyInfo = legacyByName.get(name);
1472
+ const enabledState = isEnabled(name, marketplace)
1473
+ && (legacyInfo ? legacyInfo.enabled !== false : true);
1291
1474
 
1292
1475
  plugins.push({
1293
1476
  name,
@@ -1319,16 +1502,11 @@ class PluginsService {
1319
1502
  }
1320
1503
  }
1321
1504
 
1322
- // Also check legacy registry
1323
- try {
1324
- const legacyPlugins = listPlugins();
1325
- for (const plugin of legacyPlugins) {
1326
- if (!plugins.find(p => p.name === plugin.name)) {
1327
- plugins.push(plugin);
1328
- }
1505
+ // Add registry-only plugins without rereading the registry.
1506
+ for (const plugin of legacyPlugins) {
1507
+ if (!plugins.find(existing => existing.name === plugin.name)) {
1508
+ plugins.push(plugin);
1329
1509
  }
1330
- } catch (err) {
1331
- // Ignore legacy registry errors
1332
1510
  }
1333
1511
 
1334
1512
  return { plugins };
@@ -1442,7 +1620,9 @@ class PluginsService {
1442
1620
  || metadata.installSource
1443
1621
  || ''
1444
1622
  ).trim();
1445
- return this.ompNativeAdapter.installPlugin(target, metadata, options);
1623
+ const result = this.ompNativeAdapter.installPlugin(target, metadata, options);
1624
+ this._invalidatePluginList(options);
1625
+ return result;
1446
1626
  }
1447
1627
 
1448
1628
  if (this._isOpenCode()) {
@@ -1868,9 +2048,10 @@ class PluginsService {
1868
2048
  error: `${this.platform} plugin management is not supported`
1869
2049
  };
1870
2050
  }
1871
-
1872
2051
  if (this._isOmp()) {
1873
- return this.ompNativeAdapter.uninstallPlugin(name, options);
2052
+ const result = this.ompNativeAdapter.uninstallPlugin(name, options);
2053
+ this._invalidatePluginList(options);
2054
+ return result;
1874
2055
  }
1875
2056
 
1876
2057
  if (this._isOpenCode()) {
@@ -1915,9 +2096,9 @@ class PluginsService {
1915
2096
  // Claude: Remove from native installed_plugins.json and delete install directories
1916
2097
  let removed = false;
1917
2098
  const safeName = normalizePluginPathName(name, 'plugin name');
1918
- if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
2099
+ if (fs.existsSync(this.claudeInstalledFile)) {
1919
2100
  try {
1920
- const data = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
2101
+ const data = JSON.parse(fs.readFileSync(this.claudeInstalledFile, 'utf8'));
1921
2102
  if (data.plugins) {
1922
2103
  const keysToDelete = [];
1923
2104
  const baseName = safeName.split('/').pop(); // handle "plugins/pr-review-toolkit" → "pr-review-toolkit"
@@ -1932,7 +2113,7 @@ class PluginsService {
1932
2113
  try {
1933
2114
  const installPath = assertInsideAllowedRoots(
1934
2115
  install.installPath,
1935
- [INSTALLED_DIR, CLAUDE_PLUGINS_DIR],
2116
+ [INSTALLED_DIR, this.claudePluginsDir],
1936
2117
  'Claude plugin install path'
1937
2118
  );
1938
2119
  fs.rmSync(installPath, { recursive: true, force: true });
@@ -1950,7 +2131,8 @@ class PluginsService {
1950
2131
  this._removeClaudePluginEnabled(parsed.name || safeName, parsed.marketplace || '');
1951
2132
  delete data.plugins[key];
1952
2133
  }
1953
- fs.writeFileSync(CLAUDE_INSTALLED_FILE, JSON.stringify(data, null, 2), 'utf8');
2134
+ fs.writeFileSync(this.claudeInstalledFile, JSON.stringify(data, null, 2), 'utf8');
2135
+ this._invalidatePluginList();
1954
2136
  removed = true;
1955
2137
  }
1956
2138
  }
@@ -2006,7 +2188,9 @@ class PluginsService {
2006
2188
  }
2007
2189
 
2008
2190
  if (this._isOmp()) {
2009
- return this.ompNativeAdapter.togglePlugin(name, enabled, options);
2191
+ const result = this.ompNativeAdapter.togglePlugin(name, enabled, options);
2192
+ this._invalidatePluginList(options);
2193
+ return result;
2010
2194
  }
2011
2195
 
2012
2196
  if (this._isOpenCode()) {
@@ -2029,9 +2213,9 @@ class PluginsService {
2029
2213
  // First check if plugin exists in native installed_plugins.json
2030
2214
  let pluginExists = false;
2031
2215
  const baseName = name.split('/').pop();
2032
- if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
2216
+ if (fs.existsSync(this.claudeInstalledFile)) {
2033
2217
  try {
2034
- const data = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
2218
+ const data = JSON.parse(fs.readFileSync(this.claudeInstalledFile, 'utf8'));
2035
2219
  if (data.plugins) {
2036
2220
  for (const key of Object.keys(data.plugins)) {
2037
2221
  const { name: pluginName } = splitPluginMarketplaceKey(key);
@@ -2064,9 +2248,9 @@ class PluginsService {
2064
2248
  } catch (e) {
2065
2249
  console.warn('[PluginsService] Failed to update plugin registry:', e.message);
2066
2250
  }
2067
- if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
2251
+ if (fs.existsSync(this.claudeInstalledFile)) {
2068
2252
  try {
2069
- const data = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
2253
+ const data = JSON.parse(fs.readFileSync(this.claudeInstalledFile, 'utf8'));
2070
2254
  for (const key of Object.keys(data.plugins || {})) {
2071
2255
  const parsed = splitPluginMarketplaceKey(key);
2072
2256
  if (parsed.name === name || key === name || parsed.name === baseName) {
@@ -2103,11 +2287,13 @@ class PluginsService {
2103
2287
  }
2104
2288
 
2105
2289
  if (this._isOmp()) {
2106
- return this.ompNativeAdapter.updatePluginConfig(name, config, options);
2290
+ const result = this.ompNativeAdapter.updatePluginConfig(name, config, options);
2291
+ this._invalidatePluginList(options);
2292
+ return result;
2107
2293
  }
2108
2294
 
2109
2295
  if (this._isOpenCode()) {
2110
- const configDir = path.join(OPENCODE_CONFIG_DIR, 'plugins-config');
2296
+ const configDir = path.join(this.opencodeConfigDir, 'plugins-config');
2111
2297
  const configFile = resolvePluginConfigFile(configDir, name);
2112
2298
  this._ensureDir(path.dirname(configFile));
2113
2299
  fs.writeFileSync(configFile, JSON.stringify(config, null, 2), 'utf8');
@@ -2142,13 +2328,14 @@ class PluginsService {
2142
2328
  * @returns {string} Config file path
2143
2329
  */
2144
2330
  getReposConfigPath() {
2145
- const filePath = PATHS.pluginRepos[this.platform] || PATHS.pluginRepos.claude;
2331
+ const filePath = getPlatformStatePath('pluginRepos', this.platform);
2332
+ if (!filePath) throw new Error(`Plugin repository path is not configured for ${this.platform}`);
2146
2333
  this._ensureDir(path.dirname(filePath));
2147
2334
  return filePath;
2148
2335
  }
2149
2336
 
2150
2337
  _getDefaultRepos() {
2151
- return cloneRepos(DEFAULT_REPOS_BY_PLATFORM[this.platform] || DEFAULT_REPOS_BY_PLATFORM.claude);
2338
+ return cloneRepos(DEFAULT_REPOS_BY_PLATFORM[this.platform] || []);
2152
2339
  }
2153
2340
 
2154
2341
  /**
@@ -2227,9 +2414,9 @@ class PluginsService {
2227
2414
  }
2228
2415
 
2229
2416
  // 2. Load Claude Code's native marketplace config (Claude only)
2230
- if (this.platform === 'claude' && fs.existsSync(CLAUDE_MARKETPLACES_FILE)) {
2417
+ if (this.platform === 'claude' && fs.existsSync(this.claudeMarketplacesFile)) {
2231
2418
  try {
2232
- const marketplaces = JSON.parse(fs.readFileSync(CLAUDE_MARKETPLACES_FILE, 'utf8'));
2419
+ const marketplaces = JSON.parse(fs.readFileSync(this.claudeMarketplacesFile, 'utf8'));
2233
2420
  const entries = [];
2234
2421
  if (Array.isArray(marketplaces)) {
2235
2422
  entries.push(...marketplaces.map(item => ({ key: '', data: item })));
@@ -2975,45 +3162,46 @@ class PluginsService {
2975
3162
  };
2976
3163
  }
2977
3164
 
2978
- async _fetchOpenCodeMarketplacePlugins(repo, branch) {
3165
+ async _fetchOpenCodeMarketplacePlugins(repo, branch, tree = null) {
2979
3166
  if (!this._isOpenCode()) return [];
2980
3167
 
2981
- const tree = await this.fetchRepoTree(repo);
2982
- const manifestFiles = tree.filter(item =>
3168
+ const sourceTree = tree || await this.fetchRepoTree(repo);
3169
+ const manifestFiles = sourceTree.filter(item =>
2983
3170
  item.type === 'blob' &&
2984
3171
  item.path.startsWith('plugins/') &&
2985
3172
  item.path.endsWith('.plugin.json')
2986
3173
  );
2987
3174
  if (manifestFiles.length === 0) return [];
2988
3175
 
2989
- const results = await Promise.allSettled(
2990
- manifestFiles.map(async (file) => {
2991
- const manifest = await this.fetchRepoJson(repo, file.path, file);
2992
-
2993
- const author = Array.isArray(manifest.authors)
2994
- ? manifest.authors.map(item => item?.name).filter(Boolean).join(', ')
2995
- : '';
2996
- const firstCategory = Array.isArray(manifest.categories) ? manifest.categories[0] : '';
2997
- const repoUrl = manifest.links?.repository || `https://github.com/${repo.owner}/${repo.name}`;
2998
- // OpenCode supports npm package plugins via opencode.json "plugin" array.
2999
- // Use package name as install source so UI install button is enabled.
3000
- const installSource = String(manifest.name || '').trim();
3001
- const githubRepo = this._parseGitHubRepo(repoUrl);
3002
-
3003
- return this.buildMarketPluginItem(repo, {
3004
- name: manifest.name || file.name.replace(/\.plugin\.json$/, ''),
3005
- displayName: manifest.displayName || '',
3006
- description: manifest.description || '',
3007
- author: author || repo.owner,
3008
- version: manifest.version || manifest.opencode?.minimumVersion || '1.0.0',
3009
- category: firstCategory ? String(firstCategory).toLowerCase() : 'general',
3010
- directory: file.path,
3011
- installSource,
3012
- marketplaceFormat: 'opencode-plugin-json',
3013
- repoUrl
3014
- });
3015
- })
3016
- );
3176
+ const results = [];
3177
+ for (let start = 0; start < manifestFiles.length; start += 5) {
3178
+ const batch = manifestFiles.slice(start, start + 5);
3179
+ results.push(...await Promise.allSettled(
3180
+ batch.map(async (file) => {
3181
+ const manifest = await this.fetchRepoJson(repo, file.path, file);
3182
+
3183
+ const author = Array.isArray(manifest.authors)
3184
+ ? manifest.authors.map(item => item?.name).filter(Boolean).join(', ')
3185
+ : '';
3186
+ const firstCategory = Array.isArray(manifest.categories) ? manifest.categories[0] : '';
3187
+ const repoUrl = manifest.links?.repository || `https://github.com/${repo.owner}/${repo.name}`;
3188
+ const installSource = String(manifest.name || '').trim();
3189
+
3190
+ return this.buildMarketPluginItem(repo, {
3191
+ name: manifest.name || file.name.replace(/\.plugin\.json$/, ''),
3192
+ displayName: manifest.displayName || '',
3193
+ description: manifest.description || '',
3194
+ author: author || repo.owner,
3195
+ version: manifest.version || manifest.opencode?.minimumVersion || '1.0.0',
3196
+ category: firstCategory ? String(firstCategory).toLowerCase() : 'general',
3197
+ directory: file.path,
3198
+ installSource,
3199
+ marketplaceFormat: 'opencode-plugin-json',
3200
+ repoUrl
3201
+ });
3202
+ })
3203
+ ));
3204
+ }
3017
3205
 
3018
3206
  return results
3019
3207
  .filter(item => item.status === 'fulfilled' && item.value)
@@ -3219,14 +3407,85 @@ class PluginsService {
3219
3407
  return results;
3220
3408
  }
3221
3409
 
3410
+ _marketKey(options = {}) {
3411
+ return `${this.platform}:${options.cwd ? path.resolve(options.cwd) : ''}:${options.scope || ''}`;
3412
+ }
3413
+
3414
+ _marketReposFingerprint(repos) {
3415
+ return JSON.stringify((repos || []).map(repo => ({
3416
+ id: repo.id || '',
3417
+ enabled: repo.enabled !== false,
3418
+ source: repo.source || repo.repoUrl || repo.localPath || ''
3419
+ })));
3420
+ }
3421
+
3422
+ async getMarketPlugins(forceRefresh = false, options = {}) {
3423
+ if (!this.getCapabilities().market) return [];
3424
+ const key = this._marketKey(options);
3425
+ const repos = this._isOmp() ? [] : this.getRepos(options).filter(repo => repo.enabled);
3426
+ const reposFingerprint = this._marketReposFingerprint(repos);
3427
+ const now = Date.now();
3428
+ const cached = this._marketCacheByKey.get(key);
3429
+ const memoryPlugins = Array.isArray(cached?.plugins)
3430
+ ? cached.plugins
3431
+ : (Array.isArray(this._marketCache) ? this._marketCache : null);
3432
+ if (!forceRefresh && cached && now - cached.fetchedAt < MARKET_CACHE_TTL_MS
3433
+ && cached.reposFingerprint === reposFingerprint) {
3434
+ return this._clone(cached.plugins);
3435
+ }
3436
+
3437
+ const disk = this.loadMarketCacheEnvelope();
3438
+ if (!forceRefresh && Array.isArray(disk?.plugins)) {
3439
+ const diskPlugins = this.prepareMarketPlugins(disk.plugins);
3440
+ const diskIsFresh = disk.fetchedAt
3441
+ && now - disk.fetchedAt < MARKET_CACHE_TTL_MS
3442
+ && (!disk.reposFingerprint || disk.reposFingerprint === reposFingerprint);
3443
+ const diskIsMoreComplete = memoryPlugins && diskPlugins.length > memoryPlugins.length;
3444
+ if (diskIsFresh || diskIsMoreComplete) {
3445
+ this._marketCacheByKey.set(key, {
3446
+ plugins: diskPlugins,
3447
+ fetchedAt: disk.fetchedAt,
3448
+ reposFingerprint: disk.reposFingerprint || reposFingerprint
3449
+ });
3450
+ this._marketCache = diskPlugins;
3451
+ this._marketFetchedAt = disk.fetchedAt;
3452
+ this._marketRepoFingerprint = disk.reposFingerprint || reposFingerprint;
3453
+ return this._clone(diskPlugins);
3454
+ }
3455
+ }
3456
+
3457
+ if (this._marketRefreshPromise) return this._marketRefreshPromise;
3458
+ const promise = this._getMarketPluginsUncached(forceRefresh, options, { reposFingerprint })
3459
+ .then(plugins => {
3460
+ this._marketCache = Array.isArray(plugins) ? plugins : [];
3461
+ this._marketFetchedAt = this._marketLastUsedStale ? 0 : Date.now();
3462
+ this._marketRepoFingerprint = reposFingerprint;
3463
+ this._marketCacheByKey.set(key, {
3464
+ plugins: this._marketCache,
3465
+ fetchedAt: this._marketFetchedAt,
3466
+ reposFingerprint
3467
+ });
3468
+ if (!this._marketLastUsedStale) {
3469
+ this.saveMarketCacheToFile(this._marketCache, this._marketFetchedAt, reposFingerprint);
3470
+ }
3471
+ return this._clone(this._marketCache);
3472
+ })
3473
+ .finally(() => {
3474
+ if (this._marketRefreshPromise === promise) this._marketRefreshPromise = null;
3475
+ });
3476
+ this._marketRefreshPromise = promise;
3477
+ return promise;
3478
+ }
3479
+
3222
3480
  /**
3223
3481
  * Get market plugins from configured repositories
3224
3482
  * @returns {Promise<Array>} List of available market plugins
3225
3483
  */
3226
- async getMarketPlugins(forceRefresh = false, options = {}) {
3484
+ async _getMarketPluginsUncached(forceRefresh = false, options = {}, { reposFingerprint = null } = {}) {
3227
3485
  if (!this.getCapabilities().market) {
3228
3486
  return [];
3229
3487
  }
3488
+ this._marketLastUsedStale = false;
3230
3489
  if (this._isOmp()) {
3231
3490
  if (forceRefresh) {
3232
3491
  this.ompNativeAdapter.updateMarketplaces('', options);
@@ -3238,27 +3497,16 @@ class PluginsService {
3238
3497
  this.clearMarketCache({ removeFile: false });
3239
3498
  }
3240
3499
 
3241
- const fileCache = this.loadMarketCacheFromFile();
3242
-
3243
- if (!forceRefresh && Array.isArray(this._marketCache) && this._marketCache.length > 0) {
3244
- if (Array.isArray(fileCache) && fileCache.length > this._marketCache.length) {
3245
- this._marketCache = this.prepareMarketPlugins(fileCache);
3246
- return this._marketCache;
3247
- }
3248
- this._marketCache = this.prepareMarketPlugins(this._marketCache);
3249
- return this._marketCache;
3250
- }
3251
-
3252
- if (!forceRefresh && Array.isArray(fileCache) && fileCache.length > 0) {
3253
- this._marketCache = this.prepareMarketPlugins(fileCache);
3254
- return this._marketCache;
3255
- }
3500
+ const fileCache = this.loadMarketCacheEnvelope()?.plugins || null;
3256
3501
 
3257
3502
  const repos = this.getRepos().filter(r => r.enabled);
3258
3503
  const marketPlugins = [];
3259
3504
  let repoFailureCount = 0;
3260
3505
 
3261
- for (const repo of repos) {
3506
+ let nextRepoIndex = 0;
3507
+ const processRepo = async () => {
3508
+ while (nextRepoIndex < repos.length) {
3509
+ const repo = repos[nextRepoIndex++];
3262
3510
  const repoLabel = repo.label || repo.repoUrl || repo.localPath || `${repo.owner || ''}/${repo.name || ''}`;
3263
3511
  const pluginsBefore = marketPlugins.length;
3264
3512
  try {
@@ -3294,7 +3542,7 @@ class PluginsService {
3294
3542
 
3295
3543
  // OpenCode plugin marketplace format: plugins/*.plugin.json
3296
3544
  if (this._isOpenCode()) {
3297
- const openCodeMarketplacePlugins = await this._fetchOpenCodeMarketplacePlugins(repo, repo.branch || 'main');
3545
+ const openCodeMarketplacePlugins = await this._fetchOpenCodeMarketplacePlugins(repo, repo.branch || 'main', tree);
3298
3546
  if (openCodeMarketplacePlugins.length > 0) {
3299
3547
  marketPlugins.push(...openCodeMarketplacePlugins);
3300
3548
  continue;
@@ -3344,7 +3592,9 @@ class PluginsService {
3344
3592
  .filter(dir => dir && !dir.startsWith('.') && dir !== 'node_modules')
3345
3593
  ));
3346
3594
 
3347
- for (const dir of pluginDirs) {
3595
+ for (let batchStart = 0; batchStart < pluginDirs.length; batchStart += 5) {
3596
+ const batch = pluginDirs.slice(batchStart, batchStart + 5);
3597
+ await Promise.all(batch.map(async (dir) => {
3348
3598
  try {
3349
3599
  const manifest = await readJson(`${dir}/plugin.json`);
3350
3600
  if (!manifest) {
@@ -3377,6 +3627,7 @@ class PluginsService {
3377
3627
  }
3378
3628
  }
3379
3629
  }
3630
+ }));
3380
3631
  }
3381
3632
  } catch (err) {
3382
3633
  repoFailureCount++;
@@ -3386,6 +3637,8 @@ class PluginsService {
3386
3637
  const added = marketPlugins.length - pluginsBefore;
3387
3638
  console.log(`[PluginsService] ${repoLabel}: ${added} plugins loaded`);
3388
3639
  }
3640
+ };
3641
+ await Promise.all(Array.from({ length: Math.min(3, repos.length) }, () => processRepo()));
3389
3642
 
3390
3643
  const preparedPlugins = this.prepareMarketPlugins(marketPlugins);
3391
3644
  const preparedFileCache = Array.isArray(fileCache) && fileCache.length > 0
@@ -3397,12 +3650,13 @@ class PluginsService {
3397
3650
  );
3398
3651
 
3399
3652
  if (shouldUseStaleFileCache) {
3653
+ this._marketLastUsedStale = true;
3400
3654
  this._marketCache = preparedFileCache;
3401
3655
  return this._marketCache;
3402
3656
  }
3403
3657
 
3404
3658
  this._marketCache = preparedPlugins;
3405
- this.saveMarketCacheToFile(preparedPlugins);
3659
+ this.saveMarketCacheToFile(preparedPlugins, Date.now(), reposFingerprint);
3406
3660
 
3407
3661
  return preparedPlugins;
3408
3662
  }