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
@@ -1,47 +1,59 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
- const http = require('http');
5
- const https = require('https');
6
- const crypto = require('crypto');
7
- const { HttpsProxyAgent } = require('https-proxy-agent');
8
4
  const { execSync, execFileSync } = require('child_process');
9
- const { PATHS, NATIVE_PATHS } = require('../../config/paths');
10
- const { loadUIConfig, saveUIConfig } = require('./ui-config');
11
- const codexSettingsManager = require('./codex-settings-manager');
12
- const geminiSettingsManager = require('./gemini-settings-manager');
13
-
14
- const MANAGED_HOOK_NAME = 'coding-tool-notify';
15
- const MANAGED_OPENCODE_PLUGIN_FILE = 'coding-tool-notify.js';
16
- const MANAGED_OMP_EXTENSION_FILE = 'coding-tool-notify.ts';
17
- const REMOTE_PROVIDER_TYPES = [
18
- 'wechatBot',
19
- 'qqBot',
20
- 'feishuBot',
21
- 'wecomBot',
22
- 'dingtalkBot',
23
- 'telegramBot'
24
- ];
25
-
26
- const REMOTE_PROVIDER_LABELS = {
27
- wechatBot: '微信 Bot',
28
- qqBot: 'QQ Bot',
29
- feishuBot: '飞书 Bot',
30
- wecomBot: '企业微信 Bot',
31
- dingtalkBot: '钉钉 Bot',
32
- telegramBot: 'Telegram Bot'
33
- };
5
+ const { PATHS, NATIVE_PATHS } = require('../config/paths');
6
+ const { loadUIConfig, saveUIConfig } = require('../server/services/ui-config');
7
+ const remoteProviderRegistry = require('./remote-notification-providers');
8
+ const {
9
+ getRemoteProviderTypes,
10
+ normalizeRemoteProvider,
11
+ normalizeRemoteNotificationsConfig,
12
+ validateRemoteProviderConfig,
13
+ validateRemoteNotifications,
14
+ sendRemoteProviderTest,
15
+ validateFeishuWebhookUrl
16
+ } = remoteProviderRegistry;
17
+ const { getPlatformCatalog } = require('../server/services/platform-catalog');
18
+ const notificationHooksDriver = require('./drivers/notification-hooks');
19
+ const {
20
+ buildClaudeCommand,
21
+ buildCodexNotifyCommand,
22
+ buildGeminiCommand,
23
+ buildOpenCodePluginContent,
24
+ buildOmpExtensionContent,
25
+ getOpenCodeManagedPluginPath,
26
+ getOmpManagedExtensionPath,
27
+ getClaudeHookStatus,
28
+ getCodexHookStatus,
29
+ getGeminiHookStatus,
30
+ getOpenCodeHookStatus,
31
+ getOmpHookStatus,
32
+ parseCodexNotificationStatus,
33
+ parseGeminiNotificationStatus,
34
+ parseOpenCodeNotificationStatus,
35
+ parseOmpNotificationStatus
36
+ } = notificationHooksDriver;
37
+
38
+ const MANAGED_HOOK_NAME = notificationHooksDriver.MANAGED_HOOK_NAME;
39
+
40
+ function getConfiguredNativePath(platform, name, fallback) {
41
+ try {
42
+ const { getPlatformContext } = require('../server/platform-context');
43
+ const pathContext = getPlatformContext().registry.resolvePathContext(platform);
44
+ if (pathContext?.customized && pathContext.native?.[name]) {
45
+ return pathContext.native[name];
46
+ }
47
+ } catch (_) {
48
+ // Keep legacy paths when notification helpers are used in isolation.
49
+ }
50
+ return fallback;
51
+ }
34
52
 
35
53
  function normalizeType(type) {
36
54
  return type === 'dialog' || type === 'browser' ? type : 'notification';
37
55
  }
38
56
 
39
- function createClientId(prefix = 'coding-tool') {
40
- const randomPart = typeof crypto.randomUUID === 'function'
41
- ? crypto.randomUUID().replace(/-/g, '').slice(0, 16)
42
- : crypto.randomBytes(8).toString('hex');
43
- return `${prefix}-${randomPart}`;
44
- }
45
57
 
46
58
  function ensureParentDir(filePath) {
47
59
  const dir = path.dirname(filePath);
@@ -69,96 +81,13 @@ function writeJsonFile(filePath, value) {
69
81
  }
70
82
 
71
83
  function readClaudeSettings() {
72
- return readJsonFile(NATIVE_PATHS.claude.settings);
84
+ return readJsonFile(getConfiguredNativePath('claude', 'settings', NATIVE_PATHS.claude.settings));
73
85
  }
74
86
 
75
87
  function writeClaudeSettings(settings) {
76
- writeJsonFile(NATIVE_PATHS.claude.settings, settings);
77
- }
78
-
79
- function trimString(value) {
80
- return String(value || '').trim();
81
- }
82
-
83
- function normalizeRemoteProviderType(type) {
84
- const value = trimString(type);
85
- return REMOTE_PROVIDER_TYPES.includes(value) ? value : '';
86
- }
87
-
88
- function createRemoteProviderId(type, index = 0) {
89
- return `${type || 'provider'}-${Date.now().toString(36)}-${index}`;
90
- }
91
-
92
- function normalizeProviderConfig(type, config = {}) {
93
- const source = config && typeof config === 'object' ? config : {};
94
- switch (type) {
95
- case 'wechatBot':
96
- return {
97
- tokenFile: trimString(source.tokenFile),
98
- botToken: trimString(source.botToken),
99
- targetUserId: trimString(source.targetUserId),
100
- contextToken: trimString(source.contextToken)
101
- };
102
- case 'qqBot':
103
- return {
104
- endpoint: trimString(source.endpoint),
105
- accessToken: trimString(source.accessToken),
106
- targetType: source.targetType === 'group' ? 'group' : 'private',
107
- targetId: trimString(source.targetId)
108
- };
109
- case 'feishuBot':
110
- return {
111
- webhookUrl: trimString(source.webhookUrl)
112
- };
113
- case 'wecomBot':
114
- return {
115
- webhookUrl: trimString(source.webhookUrl)
116
- };
117
- case 'dingtalkBot':
118
- return {
119
- mode: source.mode === 'app' ? 'app' : 'webhook',
120
- webhookUrl: trimString(source.webhookUrl),
121
- clientId: trimString(source.clientId),
122
- clientSecret: trimString(source.clientSecret),
123
- targetType: source.targetType === 'user' ? 'user' : 'group',
124
- targetId: trimString(source.targetId)
125
- };
126
- case 'telegramBot':
127
- return {
128
- botToken: trimString(source.botToken),
129
- chatId: trimString(source.chatId),
130
- proxy: trimString(source.proxy)
131
- };
132
- default:
133
- return {};
134
- }
135
- }
136
-
137
- function normalizeRemoteProvider(provider = {}, index = 0) {
138
- const type = normalizeRemoteProviderType(provider.type);
139
- if (!type) {
140
- return null;
141
- }
142
-
143
- return {
144
- id: trimString(provider.id) || createRemoteProviderId(type, index),
145
- type,
146
- name: trimString(provider.name) || REMOTE_PROVIDER_LABELS[type],
147
- enabled: provider.enabled === true,
148
- config: normalizeProviderConfig(type, provider.config)
149
- };
88
+ writeJsonFile(getConfiguredNativePath('claude', 'settings', NATIVE_PATHS.claude.settings), settings);
150
89
  }
151
90
 
152
- function normalizeRemoteNotificationsConfig(remoteNotifications = {}) {
153
- const source = remoteNotifications && typeof remoteNotifications === 'object' ? remoteNotifications : {};
154
- const providers = Array.isArray(source.providers)
155
- ? source.providers.map((provider, index) => normalizeRemoteProvider(provider, index)).filter(Boolean)
156
- : [];
157
-
158
- return {
159
- providers
160
- };
161
- }
162
91
 
163
92
  function getRemoteNotificationsConfig(uiConfig = loadUIConfig()) {
164
93
  return normalizeRemoteNotificationsConfig(uiConfig.remoteNotifications);
@@ -190,115 +119,7 @@ function createValidationError(message) {
190
119
  return error;
191
120
  }
192
121
 
193
- function validateFeishuWebhookUrl(webhookUrl) {
194
- const value = String(webhookUrl || '').trim();
195
- if (!value) {
196
- return null;
197
- }
198
122
 
199
- let urlObj;
200
- try {
201
- urlObj = new URL(value);
202
- } catch (error) {
203
- throw createValidationError('飞书 Webhook URL 格式不正确');
204
- }
205
-
206
- if (urlObj.protocol !== 'https:') {
207
- throw createValidationError('飞书 Webhook 必须使用 HTTPS');
208
- }
209
-
210
- if (urlObj.hostname !== 'open.feishu.cn') {
211
- throw createValidationError('仅支持 open.feishu.cn 的飞书 Webhook');
212
- }
213
-
214
- return urlObj;
215
- }
216
-
217
- function validateHttpsUrl(value, label, options = {}) {
218
- const raw = trimString(value);
219
- if (!raw) {
220
- return null;
221
- }
222
-
223
- let urlObj;
224
- try {
225
- urlObj = new URL(raw);
226
- } catch (error) {
227
- throw createValidationError(`${label} 格式不正确`);
228
- }
229
-
230
- if (options.requireHttps !== false && urlObj.protocol !== 'https:') {
231
- throw createValidationError(`${label} 必须使用 HTTPS`);
232
- }
233
-
234
- if (options.hostname && urlObj.hostname !== options.hostname) {
235
- throw createValidationError(`${label} 仅支持 ${options.hostname}`);
236
- }
237
-
238
- return urlObj;
239
- }
240
-
241
- function requireConfigValue(config, key, label) {
242
- if (!trimString(config?.[key])) {
243
- throw createValidationError(`请填写${label}`);
244
- }
245
- }
246
-
247
- function validateRemoteProviderConfig(provider = {}) {
248
- const type = normalizeRemoteProviderType(provider.type);
249
- const config = provider.config || {};
250
- if (!type) {
251
- throw createValidationError('远程通知渠道类型不正确');
252
- }
253
-
254
- switch (type) {
255
- case 'wechatBot':
256
- if (!trimString(config.botToken) && !trimString(config.tokenFile)) {
257
- throw createValidationError('请填写微信 Bot Token 或 token.json 路径');
258
- }
259
- requireConfigValue(config, 'targetUserId', '微信接收用户 ID');
260
- break;
261
- case 'qqBot':
262
- requireConfigValue(config, 'endpoint', 'QQ Bot HTTP 地址');
263
- validateHttpsUrl(config.endpoint, 'QQ Bot HTTP 地址', { requireHttps: false });
264
- requireConfigValue(config, 'targetId', 'QQ 接收对象 ID');
265
- break;
266
- case 'feishuBot':
267
- validateFeishuWebhookUrl(config.webhookUrl);
268
- requireConfigValue(config, 'webhookUrl', '飞书 Webhook URL');
269
- break;
270
- case 'wecomBot':
271
- validateHttpsUrl(config.webhookUrl, '企业微信 Webhook URL');
272
- requireConfigValue(config, 'webhookUrl', '企业微信 Webhook URL');
273
- break;
274
- case 'dingtalkBot':
275
- if (config.mode === 'app') {
276
- requireConfigValue(config, 'clientId', '钉钉 App Key');
277
- requireConfigValue(config, 'clientSecret', '钉钉 App Secret');
278
- requireConfigValue(config, 'targetId', '钉钉接收对象 ID');
279
- } else {
280
- validateHttpsUrl(config.webhookUrl, '钉钉 Webhook URL');
281
- requireConfigValue(config, 'webhookUrl', '钉钉 Webhook URL');
282
- }
283
- break;
284
- case 'telegramBot':
285
- requireConfigValue(config, 'botToken', 'Telegram Bot Token');
286
- requireConfigValue(config, 'chatId', 'Telegram Chat ID');
287
- break;
288
- default:
289
- break;
290
- }
291
- }
292
-
293
- function validateRemoteNotifications(remoteNotifications = {}) {
294
- const remote = normalizeRemoteNotificationsConfig(remoteNotifications);
295
- remote.providers.forEach((provider) => {
296
- if (provider.enabled === true) {
297
- validateRemoteProviderConfig(provider);
298
- }
299
- });
300
- return remote;
301
- }
302
123
 
303
124
  function removeNotifyScript() {
304
125
  if (fs.existsSync(PATHS.notifyHook)) {
@@ -333,148 +154,6 @@ function isManagedNotifyPath(input) {
333
154
  return normalizedInput.includes('notify-hook.js') || (normalizedPath && normalizedInput.includes(normalizedPath));
334
155
  }
335
156
 
336
- function buildManagedArgs(source, type, extra = []) {
337
- const mode = normalizeType(type);
338
- return [
339
- `--source=${source}`,
340
- `--mode=${mode}`,
341
- `--cc-notify-type=${mode}`,
342
- ...extra
343
- ];
344
- }
345
-
346
- function quoteShellArg(value) {
347
- const stringValue = String(value || '');
348
- if (/^[A-Za-z0-9_./:=+-]+$/.test(stringValue)) {
349
- return stringValue;
350
- }
351
- return `"${stringValue.replace(/"/g, '\\"')}"`;
352
- }
353
-
354
- function buildClaudeCommand(type) {
355
- const mode = normalizeType(type);
356
- const args = ['node', PATHS.notifyHook, `--source=claude`, `--mode=${mode}`, `--cc-notify-type=${mode}`];
357
- return `${quoteShellArg(args[0])} ${quoteShellArg(args[1])} ${args.slice(2).map(quoteShellArg).join(' ')}`;
358
- }
359
-
360
- function buildCodexNotifyCommand(type) {
361
- const mode = normalizeType(type);
362
- return ['node', PATHS.notifyHook, `--source=codex`, `--mode=${mode}`, `--cc-notify-type=${mode}`];
363
- }
364
-
365
- function buildGeminiCommand(type) {
366
- const mode = normalizeType(type);
367
- const args = ['node', PATHS.notifyHook, `--source=gemini`, `--mode=${mode}`, `--cc-notify-type=${mode}`];
368
- return `${quoteShellArg(args[0])} ${quoteShellArg(args[1])} ${args.slice(2).map(quoteShellArg).join(' ')}`;
369
- }
370
-
371
- function getOpenCodeManagedPluginPath() {
372
- return path.join(NATIVE_PATHS.opencode.config, 'plugins', MANAGED_OPENCODE_PLUGIN_FILE);
373
- }
374
-
375
- function getOmpManagedExtensionPath() {
376
- const ompPaths = NATIVE_PATHS.omp || {};
377
- const extensionsDir = ompPaths.extensions ||
378
- path.join(ompPaths.dir || path.dirname(ompPaths.settings || PATHS.notifyHook), 'extensions');
379
- return path.join(extensionsDir, MANAGED_OMP_EXTENSION_FILE);
380
- }
381
-
382
- function buildOpenCodePluginContent(type) {
383
- const mode = normalizeType(type);
384
- return `// Managed by Coding Tool. Do not edit manually.
385
- // mode:${mode}
386
- import { spawn } from 'node:child_process'
387
-
388
- const SCRIPT_PATH = ${JSON.stringify(PATHS.notifyHook)}
389
- const MODE = ${JSON.stringify(mode)}
390
-
391
- function fire(eventType) {
392
- try {
393
- const child = spawn('node', [
394
- SCRIPT_PATH,
395
- '--source=opencode',
396
- \`--mode=\${MODE}\`,
397
- \`--cc-notify-type=\${MODE}\`,
398
- \`--event-type=\${eventType}\`
399
- ], {
400
- detached: true,
401
- stdio: 'ignore',
402
- windowsHide: true
403
- })
404
- child.unref()
405
- } catch (error) {
406
- // Ignore notification failures.
407
- }
408
- }
409
-
410
- export const CodingToolNotifyPlugin = async () => ({
411
- event: async ({ event }) => {
412
- const eventType = event?.type
413
- if (eventType === 'session.idle' || eventType === 'session.error') {
414
- fire(eventType)
415
- }
416
- }
417
- })
418
- `;
419
- }
420
-
421
- function buildOmpExtensionContent(type) {
422
- const mode = normalizeType(type);
423
- return `// Managed by Coding Tool. Do not edit manually.
424
- // mode:${mode}
425
- import { spawn } from 'node:child_process'
426
-
427
- const SCRIPT_PATH = ${JSON.stringify(PATHS.notifyHook)}
428
- const MODE = ${JSON.stringify(mode)}
429
- let lastFireAt = 0
430
-
431
- function shouldFire() {
432
- const now = Date.now()
433
- if (now - lastFireAt < 1000) {
434
- return false
435
- }
436
- lastFireAt = now
437
- return true
438
- }
439
-
440
- function fire(eventType) {
441
- if (!shouldFire()) {
442
- return
443
- }
444
-
445
- try {
446
- const child = spawn('node', [
447
- SCRIPT_PATH,
448
- '--source=omp',
449
- \`--mode=\${MODE}\`,
450
- \`--cc-notify-type=\${MODE}\`,
451
- \`--event-type=\${eventType}\`
452
- ], {
453
- detached: true,
454
- stdio: 'ignore',
455
- windowsHide: true
456
- })
457
- child.unref()
458
- } catch (error) {
459
- // Ignore notification failures.
460
- }
461
- }
462
-
463
- export default async function CodingToolNotifyExtension(omp) {
464
- const register = (eventName) => {
465
- if (typeof omp?.on !== 'function') {
466
- return
467
- }
468
- omp.on(eventName, async () => {
469
- fire(eventName)
470
- })
471
- }
472
-
473
- register('agent_settled')
474
- register('turn_end')
475
- }
476
- `;
477
- }
478
157
 
479
158
  function escapeForAppleScript(value) {
480
159
  return String(value || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
@@ -714,6 +393,20 @@ function createHttpsProxyAgent(proxyUrl) {
714
393
  const REMOTE_PROVIDERS = ${JSON.stringify(enabledProviders)}
715
394
  const CONFIG_FILE = ${JSON.stringify(PATHS.configFile)}
716
395
 
396
+ function isCliPlatformEnabled(source) {
397
+ try {
398
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'))
399
+ const configuredPlatforms = config?.enabledCliPlatforms
400
+ if (!Array.isArray(configuredPlatforms)) return true
401
+ const normalizedSource = String(source || '').trim().toLowerCase()
402
+ return configuredPlatforms.some((platform) => (
403
+ String(platform || '').trim().toLowerCase() === normalizedSource
404
+ ))
405
+ } catch (error) {
406
+ return true
407
+ }
408
+ }
409
+
717
410
  function createClientId(prefix = 'coding-tool') {
718
411
  const randomPart = typeof crypto.randomUUID === 'function'
719
412
  ? crypto.randomUUID().replace(/-/g, '').slice(0, 16)
@@ -1147,6 +840,7 @@ async function sendRemoteNotifications(message, source, eventType) {
1147
840
 
1148
841
  (async () => {
1149
842
  const source = readArg('--source') || 'claude'
843
+ if (!isCliPlatformEnabled(source)) return
1150
844
  const mode = readArg('--mode') || readArg('--cc-notify-type') || 'notification'
1151
845
  const eventType = readArg('--event-type') || ''
1152
846
  const payload = readOptionalPayload()
@@ -1382,365 +1076,91 @@ function writeNotifyScript(remoteNotifications = {}) {
1382
1076
  fs.writeFileSync(PATHS.notifyHook, generateNotifyScript(remoteNotifications), { mode: 0o755 });
1383
1077
  }
1384
1078
 
1385
- function getClaudeHookStatus() {
1386
- const settings = readClaudeSettings();
1387
- const stopHooks = Array.isArray(settings?.hooks?.Stop) ? settings.hooks.Stop : [];
1388
- let enabled = false;
1389
- let external = false;
1390
- let type = 'notification';
1391
1079
 
1392
- stopHooks.forEach((group) => {
1393
- const hooks = Array.isArray(group?.hooks) ? group.hooks : [];
1394
- hooks.forEach((hook) => {
1395
- const command = String(hook?.command || '');
1396
- if (!command) return;
1397
-
1398
- const isManaged = isManagedNotifyPath(command);
1399
- if (isManaged) {
1400
- enabled = true;
1401
- type = parseManagedType(command) || type;
1402
- } else {
1403
- external = true;
1404
- }
1405
- });
1406
- });
1407
-
1408
- return {
1409
- enabled,
1410
- external,
1411
- type,
1412
- method: 'Stop Hook'
1413
- };
1414
- }
1415
-
1416
- function saveClaudeHook(enabled, type) {
1417
- const settings = readClaudeSettings();
1418
- const hooks = settings.hooks && typeof settings.hooks === 'object' ? { ...settings.hooks } : {};
1419
- const currentGroups = Array.isArray(hooks.Stop) ? hooks.Stop : [];
1420
-
1421
- const filteredGroups = currentGroups.map((group) => {
1422
- const groupHooks = Array.isArray(group?.hooks) ? group.hooks : [];
1423
- const nextHooks = groupHooks.filter((hook) => !isManagedNotifyPath(hook?.command));
1424
- if (nextHooks.length === 0) {
1425
- return null;
1426
- }
1427
- return { ...group, hooks: nextHooks };
1428
- }).filter(Boolean);
1429
-
1430
- if (enabled) {
1431
- filteredGroups.push({
1432
- hooks: [
1433
- {
1434
- name: MANAGED_HOOK_NAME,
1435
- type: 'command',
1436
- command: buildClaudeCommand(type)
1437
- }
1438
- ]
1439
- });
1440
- }
1441
-
1442
- if (filteredGroups.length > 0) {
1443
- hooks.Stop = filteredGroups;
1444
- } else {
1445
- delete hooks.Stop;
1446
- }
1447
-
1448
- if (Object.keys(hooks).length > 0) {
1449
- settings.hooks = hooks;
1450
- } else {
1451
- delete settings.hooks;
1452
- }
1453
-
1454
- writeClaudeSettings(settings);
1455
- }
1456
-
1457
- function safeReadCodexConfig() {
1458
- try {
1459
- if (typeof codexSettingsManager.configExists === 'function' && !codexSettingsManager.configExists()) {
1460
- return {};
1461
- }
1462
- return codexSettingsManager.readConfig();
1463
- } catch (error) {
1464
- return {};
1465
- }
1466
- }
1467
-
1468
- function isManagedCodexNotify(notify) {
1469
- return Array.isArray(notify) && notify.some((part) => isManagedNotifyPath(part));
1470
- }
1471
-
1472
- function parseCodexNotificationStatus(config = {}) {
1473
- const notify = Array.isArray(config?.notify) ? config.notify : [];
1474
-
1475
- if (notify.length === 0) {
1476
- return {
1477
- enabled: false,
1478
- external: false,
1479
- type: 'notification',
1480
- method: 'notify'
1481
- };
1482
- }
1483
-
1484
- const joined = notify.join(' ');
1485
- const managed = isManagedCodexNotify(notify);
1486
-
1487
- return {
1488
- enabled: managed,
1489
- external: !managed,
1490
- type: parseManagedType(joined) || 'notification',
1491
- method: 'notify'
1492
- };
1493
- }
1494
-
1495
- function getCodexHookStatus() {
1496
- return parseCodexNotificationStatus(safeReadCodexConfig());
1497
- }
1498
-
1499
- function saveCodexHook(enabled, type) {
1500
- const config = safeReadCodexConfig();
1501
- const nextConfig = (config && typeof config === 'object') ? { ...config } : {};
1502
- const managed = isManagedCodexNotify(nextConfig.notify);
1503
-
1504
- if (enabled) {
1505
- nextConfig.notify = buildCodexNotifyCommand(type);
1506
- } else if (managed) {
1507
- delete nextConfig.notify;
1508
- }
1509
-
1510
- ensureParentDir(NATIVE_PATHS.codex.config);
1511
- codexSettingsManager.writeConfig(nextConfig);
1512
- }
1513
-
1514
- function safeReadGeminiSettings() {
1515
- try {
1516
- if (typeof geminiSettingsManager.settingsExists === 'function' && !geminiSettingsManager.settingsExists()) {
1517
- return {};
1080
+ function getHookPlatformEntries(catalog = getPlatformCatalog()) {
1081
+ const manifests = typeof catalog?.list === 'function'
1082
+ ? catalog.list({ capability: 'hooks' })
1083
+ : [];
1084
+ const entries = [];
1085
+ for (const manifest of manifests) {
1086
+ const driver = typeof catalog.driver === 'function'
1087
+ ? catalog.driver(manifest.key, 'hooks')
1088
+ : null;
1089
+ if (!driver
1090
+ || typeof driver.getHooks !== 'function'
1091
+ || typeof driver.saveHooks !== 'function'
1092
+ || typeof driver.testHooks !== 'function'
1093
+ || typeof driver.getDefinition !== 'function') {
1094
+ continue;
1518
1095
  }
1519
- return geminiSettingsManager.readSettings();
1520
- } catch (error) {
1521
- return {};
1096
+ const definition = driver.getDefinition();
1097
+ if (!definition || typeof definition !== 'object') continue;
1098
+ entries.push({ manifest, driver, definition });
1522
1099
  }
1100
+ return entries;
1523
1101
  }
1524
1102
 
1525
- function isManagedGeminiHook(hook) {
1526
- const command = String(hook?.command || '');
1527
- const name = String(hook?.name || '');
1528
- return name === MANAGED_HOOK_NAME || isManagedNotifyPath(command);
1529
- }
1530
-
1531
- function parseGeminiNotificationStatus(settings = {}) {
1532
- const afterAgentGroups = Array.isArray(settings?.hooks?.AfterAgent) ? settings.hooks.AfterAgent : [];
1533
- let enabled = false;
1534
- let external = false;
1535
- let type = 'notification';
1536
-
1537
- afterAgentGroups.forEach((group) => {
1538
- const hooks = Array.isArray(group?.hooks) ? group.hooks : [];
1539
- hooks.forEach((hook) => {
1540
- if (isManagedGeminiHook(hook)) {
1541
- enabled = true;
1542
- type = parseManagedType(hook.command) || type;
1543
- } else {
1544
- external = true;
1545
- }
1546
- });
1547
- });
1548
-
1549
- return {
1550
- enabled,
1551
- external,
1552
- type,
1553
- method: 'AfterAgent Hook'
1554
- };
1555
- }
1556
-
1557
- function getGeminiHookStatus() {
1558
- return parseGeminiNotificationStatus(safeReadGeminiSettings());
1559
- }
1560
-
1561
- function saveGeminiHook(enabled, type) {
1562
- const settings = safeReadGeminiSettings();
1563
- const nextSettings = (settings && typeof settings === 'object') ? { ...settings } : {};
1564
- const hooks = nextSettings.hooks && typeof nextSettings.hooks === 'object' ? { ...nextSettings.hooks } : {};
1565
- const currentGroups = Array.isArray(hooks.AfterAgent) ? hooks.AfterAgent : [];
1566
-
1567
- const filteredGroups = currentGroups.map((group) => {
1568
- const groupHooks = Array.isArray(group?.hooks) ? group.hooks : [];
1569
- const nextHooks = groupHooks.filter((hook) => !isManagedGeminiHook(hook));
1570
- if (nextHooks.length === 0) {
1571
- return null;
1572
- }
1573
- return { ...group, hooks: nextHooks };
1574
- }).filter(Boolean);
1575
-
1576
- if (enabled) {
1577
- filteredGroups.push({
1578
- matcher: '*',
1579
- hooks: [
1580
- {
1581
- name: MANAGED_HOOK_NAME,
1582
- type: 'command',
1583
- command: buildGeminiCommand(type)
1584
- }
1585
- ]
1586
- });
1587
- }
1588
-
1589
- if (filteredGroups.length > 0) {
1590
- hooks.AfterAgent = filteredGroups;
1591
- } else {
1592
- delete hooks.AfterAgent;
1593
- }
1594
-
1595
- if (Object.keys(hooks).length > 0) {
1596
- nextSettings.hooks = hooks;
1597
- } else {
1598
- delete nextSettings.hooks;
1599
- }
1600
-
1601
- ensureParentDir(geminiSettingsManager.getSettingsPath());
1602
- geminiSettingsManager.writeSettings(nextSettings);
1103
+ function getChannelPlatformKeys(catalog = getPlatformCatalog()) {
1104
+ const manifests = typeof catalog?.list === 'function'
1105
+ ? catalog.list({ capability: 'channels' })
1106
+ : [];
1107
+ return new Set(manifests
1108
+ .map(manifest => String(manifest?.key || '').trim().toLowerCase())
1109
+ .filter(Boolean));
1603
1110
  }
1604
1111
 
1605
- function parseOpenCodeNotificationStatus(content = '') {
1606
- if (!content) {
1607
- return {
1608
- enabled: false,
1609
- external: false,
1610
- type: 'notification',
1611
- method: 'Plugin Events'
1612
- };
1613
- }
1112
+ function isConfiguredCliPlatformEnabled(key, uiConfig, catalog) {
1113
+ const channelPlatformKeys = getChannelPlatformKeys(catalog);
1114
+ const normalizedKey = String(key || '').trim().toLowerCase();
1115
+ if (!channelPlatformKeys.has(normalizedKey)) return true;
1614
1116
 
1615
- return {
1616
- enabled: true,
1617
- external: false,
1618
- type: parseManagedType(content) || 'notification',
1619
- method: 'Plugin Events'
1620
- };
1117
+ const configuredPlatforms = uiConfig?.enabledCliPlatforms;
1118
+ if (!Array.isArray(configuredPlatforms)) return true;
1119
+ return configuredPlatforms.some(platform => (
1120
+ String(platform || '').trim().toLowerCase() === normalizedKey
1121
+ ));
1621
1122
  }
1622
1123
 
1623
- function parseOmpNotificationStatus(content = '') {
1624
- if (!content) {
1625
- return {
1626
- enabled: false,
1627
- external: false,
1628
- type: 'notification',
1629
- method: 'Extension Events'
1630
- };
1631
- }
1632
-
1124
+ function toPublicHookDefinition({ manifest, definition }) {
1633
1125
  return {
1634
- enabled: true,
1635
- external: false,
1636
- type: parseManagedType(content) || 'notification',
1637
- method: 'Extension Events'
1126
+ key: manifest.key,
1127
+ label: manifest.label || definition.label || manifest.key,
1128
+ description: typeof definition.description === 'string' ? definition.description : '',
1129
+ implementation: typeof definition.implementation === 'string' ? definition.implementation : '',
1130
+ externalMessage: typeof definition.externalMessage === 'string' ? definition.externalMessage : '',
1131
+ hints: Array.isArray(definition.hints) ? [...definition.hints] : []
1638
1132
  };
1639
1133
  }
1640
1134
 
1641
- function getOpenCodeHookStatus() {
1642
- const pluginPath = getOpenCodeManagedPluginPath();
1643
- if (!fs.existsSync(pluginPath)) {
1644
- return parseOpenCodeNotificationStatus('');
1645
- }
1646
-
1647
- return parseOpenCodeNotificationStatus(fs.readFileSync(pluginPath, 'utf8'));
1648
- }
1649
-
1650
- function getOmpHookStatus() {
1651
- const extensionPath = getOmpManagedExtensionPath();
1652
- if (!fs.existsSync(extensionPath)) {
1653
- return parseOmpNotificationStatus('');
1654
- }
1655
-
1656
- return parseOmpNotificationStatus(fs.readFileSync(extensionPath, 'utf8'));
1657
- }
1658
-
1659
- function saveOpenCodeHook(enabled, type) {
1660
- const pluginPath = getOpenCodeManagedPluginPath();
1661
- const opencodeSettingsManager = require('./opencode-settings-manager');
1662
- const configPath = opencodeSettingsManager.selectConfigPath();
1663
-
1664
- if (!enabled) {
1665
- // Remove plugin file
1666
- if (fs.existsSync(pluginPath)) {
1667
- fs.unlinkSync(pluginPath);
1668
- }
1669
-
1670
- // Remove from opencode.json plugins array
1671
- if (fs.existsSync(configPath)) {
1672
- try {
1673
- const config = opencodeSettingsManager.readConfig(configPath);
1674
- if (Array.isArray(config.plugins)) {
1675
- config.plugins = config.plugins.filter(p => p !== './plugins/coding-tool-notify.js');
1676
- if (config.plugins.length === 0) {
1677
- delete config.plugins;
1678
- }
1679
- opencodeSettingsManager.writeConfig(configPath, config);
1680
- }
1681
- } catch (error) {
1682
- console.error('Failed to update opencode.json:', error);
1683
- }
1684
- }
1685
- return;
1686
- }
1687
-
1688
- // Create plugin file
1689
- ensureParentDir(pluginPath);
1690
- fs.writeFileSync(pluginPath, buildOpenCodePluginContent(type), 'utf8');
1691
-
1692
- // Add to opencode.json plugins array
1693
- try {
1694
- const config = fs.existsSync(configPath)
1695
- ? opencodeSettingsManager.readConfig(configPath)
1696
- : {};
1697
-
1698
- if (!Array.isArray(config.plugins)) {
1699
- config.plugins = [];
1700
- }
1701
-
1702
- const pluginRef = './plugins/coding-tool-notify.js';
1703
- if (!config.plugins.includes(pluginRef)) {
1704
- config.plugins.push(pluginRef);
1705
- }
1706
-
1707
- opencodeSettingsManager.writeConfig(configPath, config);
1708
- } catch (error) {
1709
- console.error('Failed to update opencode.json:', error);
1710
- }
1711
- }
1712
-
1713
- function saveOmpHook(enabled, type) {
1714
- const extensionPath = getOmpManagedExtensionPath();
1715
-
1716
- if (!enabled) {
1717
- if (fs.existsSync(extensionPath)) {
1718
- fs.unlinkSync(extensionPath);
1719
- }
1720
- return;
1721
- }
1722
-
1723
- ensureParentDir(extensionPath);
1724
- fs.writeFileSync(extensionPath, buildOmpExtensionContent(type), 'utf8');
1725
- }
1726
-
1727
- function getNotificationSettings() {
1135
+ function getNotificationSettings({ catalog } = {}) {
1136
+ const resolvedCatalog = catalog || getPlatformCatalog();
1137
+ const entries = getHookPlatformEntries(resolvedCatalog);
1728
1138
  const uiConfig = loadUIConfig();
1729
1139
  const remoteNotifications = getRemoteNotificationsConfig(uiConfig);
1140
+ const platforms = {};
1141
+ const platformDefinitions = [];
1142
+ for (const entry of entries) {
1143
+ const hooks = entry.driver.getHooks();
1144
+ platforms[entry.manifest.key] = isConfiguredCliPlatformEnabled(
1145
+ entry.manifest.key,
1146
+ uiConfig,
1147
+ resolvedCatalog
1148
+ )
1149
+ ? hooks
1150
+ : { ...hooks, enabled: false };
1151
+ platformDefinitions.push(toPublicHookDefinition(entry));
1152
+ }
1153
+ const claudeStatus = platforms.claude || { enabled: false, type: 'notification' };
1730
1154
  return {
1731
1155
  success: true,
1732
1156
  platform: os.platform(),
1733
1157
  remoteNotifications,
1734
- remoteProviderTypes: REMOTE_PROVIDER_TYPES.map((type) => ({
1735
- type,
1736
- label: REMOTE_PROVIDER_LABELS[type]
1737
- })),
1738
- platforms: {
1739
- claude: getClaudeHookStatus(),
1740
- codex: getCodexHookStatus(),
1741
- gemini: getGeminiHookStatus(),
1742
- opencode: getOpenCodeHookStatus(),
1743
- omp: getOmpHookStatus()
1158
+ remoteProviderTypes: getRemoteProviderTypes(),
1159
+ platformDefinitions,
1160
+ platforms,
1161
+ stopHook: {
1162
+ enabled: claudeStatus.enabled === true,
1163
+ type: normalizeType(claudeStatus.type)
1744
1164
  }
1745
1165
  };
1746
1166
  }
@@ -1800,43 +1220,49 @@ function emitBrowserNotification(input = {}) {
1800
1220
  timestamp: Date.now()
1801
1221
  };
1802
1222
 
1803
- const { broadcastBrowserNotification } = require('../websocket-server');
1223
+ const { broadcastBrowserNotification } = require('../server/websocket-server');
1804
1224
  broadcastBrowserNotification(payload);
1805
1225
  return payload;
1806
1226
  }
1807
-
1808
- function saveNotificationSettings(input = {}) {
1227
+ function saveNotificationSettings(input = {}, { catalog } = {}) {
1809
1228
  const remoteNotifications = validateRemoteNotifications(
1810
1229
  input.remoteNotifications !== undefined
1811
1230
  ? input.remoteNotifications
1812
1231
  : {}
1813
1232
  );
1814
- const platforms = {
1815
- claude: normalizePlatformInput(input?.platforms?.claude),
1816
- codex: normalizePlatformInput(input?.platforms?.codex),
1817
- gemini: normalizePlatformInput(input?.platforms?.gemini),
1818
- opencode: normalizePlatformInput(input?.platforms?.opencode),
1819
- omp: normalizePlatformInput(input?.platforms?.omp)
1820
- };
1821
-
1822
- saveNotificationUiConfig(remoteNotifications, platforms.claude.enabled);
1823
-
1824
- const hasManagedPlatform = Object.values(platforms).some((platform) => platform.enabled);
1825
- if (hasManagedPlatform) {
1826
- writeNotifyScript(remoteNotifications);
1827
- }
1828
-
1829
- saveClaudeHook(platforms.claude.enabled, platforms.claude.type);
1830
- saveCodexHook(platforms.codex.enabled, platforms.codex.type);
1831
- saveGeminiHook(platforms.gemini.enabled, platforms.gemini.type);
1832
- saveOpenCodeHook(platforms.opencode.enabled, platforms.opencode.type);
1833
- saveOmpHook(platforms.omp.enabled, platforms.omp.type);
1834
-
1835
- if (!hasManagedPlatform) {
1836
- removeNotifyScript();
1837
- }
1838
-
1839
- return getNotificationSettings();
1233
+ const resolvedCatalog = catalog || getPlatformCatalog();
1234
+ const entries = getHookPlatformEntries(resolvedCatalog);
1235
+ const uiConfig = loadUIConfig();
1236
+ const inputPlatforms = input.platforms && typeof input.platforms === 'object' && !Array.isArray(input.platforms)
1237
+ ? input.platforms
1238
+ : {};
1239
+ const platforms = Object.fromEntries(entries.map(entry => {
1240
+ const key = entry.manifest.key;
1241
+ const platformInput = Object.prototype.hasOwnProperty.call(inputPlatforms, key)
1242
+ ? inputPlatforms[key]
1243
+ : Object.prototype.hasOwnProperty.call(input, key)
1244
+ ? input[key]
1245
+ : key === 'claude'
1246
+ ? input.stopHook
1247
+ : undefined;
1248
+ const normalized = normalizePlatformInput(platformInput);
1249
+ return [key, isConfiguredCliPlatformEnabled(key, uiConfig, resolvedCatalog)
1250
+ ? normalized
1251
+ : { ...normalized, enabled: false }];
1252
+ }));
1253
+
1254
+ const claudeEntry = entries.find(entry => entry.manifest.key === 'claude');
1255
+ saveNotificationUiConfig(remoteNotifications, claudeEntry ? platforms.claude.enabled : undefined);
1256
+
1257
+ const hasManagedPlatform = Object.values(platforms).some(platform => platform.enabled);
1258
+ for (const entry of entries) {
1259
+ entry.driver.saveHooks(platforms[entry.manifest.key]);
1260
+ }
1261
+
1262
+ if (hasManagedPlatform) writeNotifyScript(remoteNotifications);
1263
+ else removeNotifyScript();
1264
+
1265
+ return getNotificationSettings({ catalog: resolvedCatalog });
1840
1266
  }
1841
1267
 
1842
1268
  function parseNotifyTypeMarker(command) {
@@ -1950,20 +1376,19 @@ function normalizeSavedPlatformStatus(platform = {}) {
1950
1376
  }
1951
1377
 
1952
1378
  function buildLegacyClaudeSaveInput(input = {}, currentSettings = getNotificationSettings()) {
1379
+ const platforms = {
1380
+ ...(currentSettings?.platforms && typeof currentSettings.platforms === 'object'
1381
+ ? currentSettings.platforms
1382
+ : {})
1383
+ };
1384
+ platforms.claude = input.stopHook !== undefined
1385
+ ? normalizePlatformInput(input.stopHook)
1386
+ : { enabled: false, type: 'notification' };
1953
1387
  return {
1954
- platforms: {
1955
- claude: input.stopHook !== undefined
1956
- ? normalizePlatformInput(input.stopHook)
1957
- : { enabled: false, type: 'notification' },
1958
- codex: normalizeSavedPlatformStatus(currentSettings?.platforms?.codex),
1959
- gemini: normalizeSavedPlatformStatus(currentSettings?.platforms?.gemini),
1960
- opencode: normalizeSavedPlatformStatus(currentSettings?.platforms?.opencode),
1961
- omp: normalizeSavedPlatformStatus(currentSettings?.platforms?.omp)
1962
- },
1388
+ platforms,
1963
1389
  remoteNotifications: currentSettings?.remoteNotifications || { providers: [] }
1964
1390
  };
1965
1391
  }
1966
-
1967
1392
  function getLegacyClaudeHookSettings() {
1968
1393
  return {
1969
1394
  success: true,
@@ -1986,21 +1411,21 @@ function initDefaultHooks() {
1986
1411
  return;
1987
1412
  }
1988
1413
 
1414
+ const entries = getHookPlatformEntries();
1415
+ const claudeEntry = entries.find(entry => entry.manifest.key === 'claude');
1416
+ if (!claudeEntry) return;
1417
+
1989
1418
  const currentClaudeSettings = readClaudeSettings();
1990
1419
  const currentStatus = parseStopHookStatus(currentClaudeSettings);
1420
+ const currentSettings = getNotificationSettings();
1421
+ const platforms = { ...currentSettings.platforms };
1991
1422
 
1992
1423
  if (currentStatus.enabled) {
1993
1424
  if (shouldRepairStopHook(currentClaudeSettings)) {
1994
- const currentSettings = getNotificationSettings();
1425
+ platforms.claude = { enabled: true, type: currentStatus.type || 'notification' };
1995
1426
  saveNotificationSettings({
1996
- platforms: {
1997
- claude: { enabled: true, type: currentStatus.type || 'notification' },
1998
- codex: normalizeSavedPlatformStatus(currentSettings?.platforms?.codex),
1999
- gemini: normalizeSavedPlatformStatus(currentSettings?.platforms?.gemini),
2000
- opencode: normalizeSavedPlatformStatus(currentSettings?.platforms?.opencode),
2001
- omp: normalizeSavedPlatformStatus(currentSettings?.platforms?.omp)
2002
- },
2003
- remoteNotifications: currentSettings?.remoteNotifications || { providers: [] }
1427
+ platforms,
1428
+ remoteNotifications: currentSettings.remoteNotifications || { providers: [] }
2004
1429
  });
2005
1430
  console.log('[Claude Hooks] 检测到旧版 Stop hook 路径,已自动修复');
2006
1431
  } else {
@@ -2009,16 +1434,10 @@ function initDefaultHooks() {
2009
1434
  return;
2010
1435
  }
2011
1436
 
2012
- const currentSettings = getNotificationSettings();
1437
+ platforms.claude = { enabled: true, type: 'notification' };
2013
1438
  saveNotificationSettings({
2014
- platforms: {
2015
- claude: { enabled: true, type: 'notification' },
2016
- codex: normalizeSavedPlatformStatus(currentSettings?.platforms?.codex),
2017
- gemini: normalizeSavedPlatformStatus(currentSettings?.platforms?.gemini),
2018
- opencode: normalizeSavedPlatformStatus(currentSettings?.platforms?.opencode),
2019
- omp: normalizeSavedPlatformStatus(currentSettings?.platforms?.omp)
2020
- },
2021
- remoteNotifications: currentSettings?.remoteNotifications || { providers: [] }
1439
+ platforms,
1440
+ remoteNotifications: currentSettings.remoteNotifications || { providers: [] }
2022
1441
  });
2023
1442
  console.log('[Claude Hooks] 已自动开启任务完成通知(右上角卡片)');
2024
1443
  } catch (error) {
@@ -2026,261 +1445,6 @@ function initDefaultHooks() {
2026
1445
  }
2027
1446
  }
2028
1447
 
2029
- function sendFeishuTest(webhookUrl) {
2030
- return new Promise((resolve, reject) => {
2031
- try {
2032
- const urlObj = validateFeishuWebhookUrl(webhookUrl);
2033
- const data = JSON.stringify({
2034
- msg_type: 'interactive',
2035
- card: {
2036
- header: {
2037
- title: { tag: 'plain_text', content: 'Coding Tool - 测试通知' },
2038
- template: 'blue'
2039
- },
2040
- elements: [
2041
- {
2042
- tag: 'div',
2043
- text: { tag: 'lark_md', content: '**状态**: 这是一条测试通知' }
2044
- },
2045
- {
2046
- tag: 'div',
2047
- text: { tag: 'lark_md', content: '**时间**: ' + new Date().toLocaleString('zh-CN') }
2048
- }
2049
- ]
2050
- }
2051
- });
2052
-
2053
- const options = {
2054
- hostname: urlObj.hostname,
2055
- port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
2056
- path: urlObj.pathname + urlObj.search,
2057
- method: 'POST',
2058
- headers: {
2059
- 'Content-Type': 'application/json',
2060
- 'Content-Length': Buffer.byteLength(data)
2061
- },
2062
- timeout: 10000
2063
- };
2064
-
2065
- const requestModule = urlObj.protocol === 'https:' ? https : http;
2066
- const request = requestModule.request(options, () => resolve());
2067
- request.on('error', reject);
2068
- request.on('timeout', () => {
2069
- request.destroy(new Error('飞书测试通知超时'));
2070
- });
2071
- request.write(data);
2072
- request.end();
2073
- } catch (error) {
2074
- reject(error);
2075
- }
2076
- });
2077
- }
2078
-
2079
- function requestJson(url, data, headers = {}, extraOptions = {}) {
2080
- return new Promise((resolve, reject) => {
2081
- try {
2082
- const urlObj = new URL(url);
2083
- const body = JSON.stringify(data);
2084
- const requestModule = urlObj.protocol === 'https:' ? https : http;
2085
- const request = requestModule.request({
2086
- hostname: urlObj.hostname,
2087
- port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
2088
- path: urlObj.pathname + urlObj.search,
2089
- method: 'POST',
2090
- timeout: 10000,
2091
- headers: {
2092
- 'Content-Type': 'application/json',
2093
- 'Content-Length': Buffer.byteLength(body),
2094
- ...headers
2095
- },
2096
- ...extraOptions
2097
- }, (response) => {
2098
- response.resume();
2099
- if (response.statusCode >= 200 && response.statusCode < 300) {
2100
- resolve();
2101
- } else {
2102
- reject(new Error(`远程通知返回 HTTP ${response.statusCode}`));
2103
- }
2104
- });
2105
- request.on('error', reject);
2106
- request.on('timeout', () => {
2107
- request.destroy(new Error('远程通知测试超时'));
2108
- });
2109
- request.write(body);
2110
- request.end();
2111
- } catch (error) {
2112
- reject(error);
2113
- }
2114
- });
2115
- }
2116
-
2117
- function requestDingTalkAccessToken(config = {}) {
2118
- if (!config.clientId || !config.clientSecret) {
2119
- return Promise.resolve('');
2120
- }
2121
- return new Promise((resolve, reject) => {
2122
- try {
2123
- const body = JSON.stringify({ appKey: config.clientId, appSecret: config.clientSecret });
2124
- const request = https.request({
2125
- hostname: 'api.dingtalk.com',
2126
- path: '/v1.0/oauth2/accessToken',
2127
- method: 'POST',
2128
- timeout: 10000,
2129
- headers: {
2130
- 'Content-Type': 'application/json',
2131
- 'Content-Length': Buffer.byteLength(body)
2132
- }
2133
- }, (response) => {
2134
- let responseBody = '';
2135
- response.setEncoding('utf8');
2136
- response.on('data', (chunk) => { responseBody += chunk; });
2137
- response.on('end', () => {
2138
- try {
2139
- const parsed = JSON.parse(responseBody);
2140
- if (response.statusCode >= 200 && response.statusCode < 300 && parsed.accessToken) {
2141
- resolve(parsed.accessToken);
2142
- return;
2143
- }
2144
- reject(new Error(`钉钉 Access Token 获取失败: ${responseBody.slice(0, 300)}`));
2145
- } catch (error) {
2146
- reject(error);
2147
- }
2148
- });
2149
- });
2150
- request.on('error', reject);
2151
- request.on('timeout', () => {
2152
- request.destroy(new Error('钉钉 Access Token 获取超时'));
2153
- });
2154
- request.write(body);
2155
- request.end();
2156
- } catch (error) {
2157
- reject(error);
2158
- }
2159
- });
2160
- }
2161
-
2162
- function buildTestNotificationContext() {
2163
- return {
2164
- title: 'Coding Tool - 测试通知',
2165
- message: '这是一条测试通知',
2166
- source: 'test',
2167
- eventType: 'test',
2168
- timestamp: new Date().toLocaleString('zh-CN'),
2169
- hostname: os.hostname()
2170
- };
2171
- }
2172
-
2173
- function formatProviderText(ctx) {
2174
- return [
2175
- ctx.title,
2176
- `来源: ${ctx.source}`,
2177
- `状态: ${ctx.message}`,
2178
- `时间: ${ctx.timestamp}`,
2179
- `设备: ${ctx.hostname}`
2180
- ].join('\n');
2181
- }
2182
-
2183
- async function sendRemoteProviderTest(providerInput = {}) {
2184
- const provider = normalizeRemoteProvider(providerInput);
2185
- if (!provider) {
2186
- throw createValidationError('远程通知渠道类型不正确');
2187
- }
2188
- validateRemoteProviderConfig({ ...provider, enabled: true });
2189
-
2190
- const config = provider.config || {};
2191
- const ctx = buildTestNotificationContext();
2192
- switch (provider.type) {
2193
- case 'feishuBot':
2194
- return sendFeishuTest(config.webhookUrl);
2195
- case 'qqBot': {
2196
- const pathSuffix = config.targetType === 'group' ? '/send_group_msg' : '/send_private_msg';
2197
- return requestJson(String(config.endpoint).replace(/\/$/, '') + pathSuffix, {
2198
- [config.targetType === 'group' ? 'group_id' : 'user_id']: config.targetId,
2199
- message: formatProviderText(ctx)
2200
- }, config.accessToken ? { Authorization: `Bearer ${config.accessToken}` } : {});
2201
- }
2202
- case 'wecomBot':
2203
- return requestJson(config.webhookUrl, {
2204
- msgtype: 'markdown',
2205
- markdown: { content: `**${ctx.title}**\n> 状态: ${ctx.message}\n> 时间: ${ctx.timestamp}` }
2206
- });
2207
- case 'dingtalkBot':
2208
- if (config.mode === 'app') {
2209
- return sendDingTalkAppProviderTest(config, ctx);
2210
- }
2211
- return requestJson(config.webhookUrl, {
2212
- msgtype: 'markdown',
2213
- markdown: { title: ctx.title, text: `### ${ctx.title}\n\n- 状态: ${ctx.message}\n- 时间: ${ctx.timestamp}` }
2214
- });
2215
- case 'telegramBot':
2216
- return requestTelegramMessage(config, ctx);
2217
- case 'wechatBot':
2218
- if (!config.botToken && config.tokenFile) {
2219
- try {
2220
- const raw = fs.readFileSync(config.tokenFile, 'utf8');
2221
- const parsed = JSON.parse(raw);
2222
- config.botToken = parsed.bot_token || parsed.token || '';
2223
- } catch (error) {
2224
- throw createValidationError('无法读取微信 token.json');
2225
- }
2226
- }
2227
- requireConfigValue(config, 'botToken', '微信 Bot Token');
2228
- return requestJson('https://ilinkai.weixin.qq.com/ilink/bot/sendmessage', {
2229
- msg: {
2230
- from_user_id: '',
2231
- to_user_id: config.targetUserId,
2232
- client_id: createClientId('coding-tool'),
2233
- message_type: 2,
2234
- message_state: 2,
2235
- item_list: [{ type: 1, text_item: { text: formatProviderText(ctx) } }],
2236
- ...(config.contextToken ? { context_token: config.contextToken } : {})
2237
- },
2238
- base_info: { channel_version: '2.1.8' }
2239
- }, {
2240
- AuthorizationType: 'ilink_bot_token',
2241
- Authorization: `Bearer ${config.botToken}`,
2242
- 'X-WECHAT-UIN': Buffer.from(String(Math.floor(Math.random() * 0xffffffff))).toString('base64')
2243
- });
2244
- default:
2245
- throw createValidationError('不支持的远程通知渠道');
2246
- }
2247
- }
2248
-
2249
- function requestTelegramMessage(config = {}, ctx) {
2250
- const url = `https://api.telegram.org/bot${config.botToken}/sendMessage`;
2251
- const payload = {
2252
- chat_id: config.chatId,
2253
- text: formatProviderText(ctx)
2254
- };
2255
- const agent = config.proxy ? new HttpsProxyAgent(config.proxy) : null;
2256
- return requestJson(url, payload, {}, agent ? { agent } : {});
2257
- }
2258
-
2259
- async function sendDingTalkAppProviderTest(config = {}, ctx) {
2260
- const token = await requestDingTalkAccessToken(config);
2261
- if (!token) {
2262
- throw createValidationError('钉钉 App Access Token 获取失败');
2263
- }
2264
- const isGroup = config.targetType !== 'user';
2265
- const payload = {
2266
- robotCode: config.clientId,
2267
- msgKey: 'sampleMarkdown',
2268
- msgParam: JSON.stringify({
2269
- title: ctx.title,
2270
- text: `### ${ctx.title}\n\n- 状态: ${ctx.message}\n- 时间: ${ctx.timestamp}\n- 设备: ${ctx.hostname}`
2271
- })
2272
- };
2273
- if (isGroup) {
2274
- payload.openConversationId = config.targetId;
2275
- } else {
2276
- payload.userIds = [config.targetId];
2277
- }
2278
- return requestJson(
2279
- 'https://api.dingtalk.com/v1.0/robot/' + (isGroup ? 'groupMessages/send' : 'oToMessages/batchSend'),
2280
- payload,
2281
- { 'x-acs-dingtalk-access-token': token }
2282
- );
2283
- }
2284
1448
 
2285
1449
  function generateSystemNotificationCommand(type, message, platformOverride = os.platform()) {
2286
1450
  const normalizedType = normalizeType(type);
@@ -2375,14 +1539,14 @@ function syncManagedNotificationAssets() {
2375
1539
  return settings;
2376
1540
  }
2377
1541
 
2378
- function testNotification({ type, provider } = {}) {
1542
+ function testNotification({ type, provider, source = 'claude' } = {}) {
2379
1543
  if (provider) {
2380
1544
  return sendRemoteProviderTest(provider);
2381
1545
  }
2382
1546
 
2383
1547
  if (normalizeType(type) === 'browser') {
2384
1548
  emitBrowserNotification({
2385
- source: 'claude',
1549
+ source,
2386
1550
  title: 'coding-tool-x',
2387
1551
  message: '这是一条浏览器测试通知'
2388
1552
  });
@@ -2412,6 +1576,9 @@ module.exports = {
2412
1576
  normalizeRemoteNotificationsConfig,
2413
1577
  validateRemoteProviderConfig,
2414
1578
  _test: {
1579
+ getHookPlatformEntries,
1580
+ isConfiguredCliPlatformEnabled,
1581
+ toPublicHookDefinition,
2415
1582
  applyClaudeDisablePreference,
2416
1583
  getManagedCommandType,
2417
1584
  parseManagedType,