@cloudcli-ai/cloudcli 1.33.3 → 1.35.0

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 (329) hide show
  1. package/README.de.md +9 -1
  2. package/README.ja.md +8 -0
  3. package/README.ko.md +9 -1
  4. package/README.md +16 -3
  5. package/README.ru.md +9 -1
  6. package/README.tr.md +8 -1
  7. package/README.zh-CN.md +9 -1
  8. package/README.zh-TW.md +9 -1
  9. package/dist/api-docs.html +36 -18
  10. package/dist/assets/index-CtwxqzGG.js +1407 -0
  11. package/dist/assets/index-DlvtxFyt.css +32 -0
  12. package/dist/index.html +2 -2
  13. package/dist-server/server/browser-use-mcp.js +350 -0
  14. package/dist-server/server/browser-use-mcp.js.map +1 -0
  15. package/dist-server/server/claude-sdk.js +26 -6
  16. package/dist-server/server/claude-sdk.js.map +1 -1
  17. package/dist-server/server/cli.js +14 -6
  18. package/dist-server/server/cli.js.map +1 -1
  19. package/dist-server/server/computer-use-agent.js +242 -0
  20. package/dist-server/server/computer-use-agent.js.map +1 -0
  21. package/dist-server/server/computer-use-mcp.js +519 -0
  22. package/dist-server/server/computer-use-mcp.js.map +1 -0
  23. package/dist-server/server/cursor-cli.js +27 -11
  24. package/dist-server/server/cursor-cli.js.map +1 -1
  25. package/dist-server/server/gemini-cli.js +17 -2
  26. package/dist-server/server/gemini-cli.js.map +1 -1
  27. package/dist-server/server/index.js +149 -40
  28. package/dist-server/server/index.js.map +1 -1
  29. package/dist-server/server/load-env.js +1 -1
  30. package/dist-server/server/load-env.js.map +1 -1
  31. package/dist-server/server/modules/browser-use/browser-use-mcp.routes.js +107 -0
  32. package/dist-server/server/modules/browser-use/browser-use-mcp.routes.js.map +1 -0
  33. package/dist-server/server/modules/browser-use/browser-use.routes.js +93 -0
  34. package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -0
  35. package/dist-server/server/modules/browser-use/browser-use.service.js +688 -0
  36. package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -0
  37. package/dist-server/server/modules/browser-use/tests/browser-use.service.test.js +8 -0
  38. package/dist-server/server/modules/browser-use/tests/browser-use.service.test.js.map +1 -0
  39. package/dist-server/server/modules/computer-use/actions/raw-action-dispatcher.js +55 -0
  40. package/dist-server/server/modules/computer-use/actions/raw-action-dispatcher.js.map +1 -0
  41. package/dist-server/server/modules/computer-use/actions/raw-action-types.js +2 -0
  42. package/dist-server/server/modules/computer-use/actions/raw-action-types.js.map +1 -0
  43. package/dist-server/server/modules/computer-use/computer-executor.js +212 -0
  44. package/dist-server/server/modules/computer-use/computer-executor.js.map +1 -0
  45. package/dist-server/server/modules/computer-use/computer-semantics.service.js +422 -0
  46. package/dist-server/server/modules/computer-use/computer-semantics.service.js.map +1 -0
  47. package/dist-server/server/modules/computer-use/computer-use-mcp.routes.js +126 -0
  48. package/dist-server/server/modules/computer-use/computer-use-mcp.routes.js.map +1 -0
  49. package/dist-server/server/modules/computer-use/computer-use.routes.js +196 -0
  50. package/dist-server/server/modules/computer-use/computer-use.routes.js.map +1 -0
  51. package/dist-server/server/modules/computer-use/computer-use.service.js +759 -0
  52. package/dist-server/server/modules/computer-use/computer-use.service.js.map +1 -0
  53. package/dist-server/server/modules/computer-use/desktop-agent-relay.service.js +126 -0
  54. package/dist-server/server/modules/computer-use/desktop-agent-relay.service.js.map +1 -0
  55. package/dist-server/server/modules/computer-use/index.js +3 -0
  56. package/dist-server/server/modules/computer-use/index.js.map +1 -0
  57. package/dist-server/server/modules/computer-use/semantics/adapters/helper-semantic-adapter.js +59 -0
  58. package/dist-server/server/modules/computer-use/semantics/adapters/helper-semantic-adapter.js.map +1 -0
  59. package/dist-server/server/modules/computer-use/semantics/adapters/macos/macos-semantic-adapter.js +5 -0
  60. package/dist-server/server/modules/computer-use/semantics/adapters/macos/macos-semantic-adapter.js.map +1 -0
  61. package/dist-server/server/modules/computer-use/semantics/adapters/semantic-adapter.js +2 -0
  62. package/dist-server/server/modules/computer-use/semantics/adapters/semantic-adapter.js.map +1 -0
  63. package/dist-server/server/modules/computer-use/semantics/adapters/windows/windows-semantic-adapter.js +5 -0
  64. package/dist-server/server/modules/computer-use/semantics/adapters/windows/windows-semantic-adapter.js.map +1 -0
  65. package/dist-server/server/modules/computer-use/semantics/bin/darwin-arm64/CloudCLISemantics +0 -0
  66. package/dist-server/server/modules/computer-use/semantics/bin/darwin-x64/CloudCLISemantics +0 -0
  67. package/dist-server/server/modules/computer-use/semantics/bin/win32-arm64/CloudCLISemantics.exe +0 -0
  68. package/dist-server/server/modules/computer-use/semantics/bin/win32-x64/CloudCLISemantics.exe +0 -0
  69. package/dist-server/server/modules/computer-use/semantics/helpers/semantic-helper-process.js +106 -0
  70. package/dist-server/server/modules/computer-use/semantics/helpers/semantic-helper-process.js.map +1 -0
  71. package/dist-server/server/modules/computer-use/semantics/helpers/semantic-helper-resolver.js +78 -0
  72. package/dist-server/server/modules/computer-use/semantics/helpers/semantic-helper-resolver.js.map +1 -0
  73. package/dist-server/server/modules/computer-use/semantics/semantic-session-store.js +68 -0
  74. package/dist-server/server/modules/computer-use/semantics/semantic-session-store.js.map +1 -0
  75. package/dist-server/server/modules/computer-use/semantics/semantic-tool-dispatcher.js +16 -0
  76. package/dist-server/server/modules/computer-use/semantics/semantic-tool-dispatcher.js.map +1 -0
  77. package/dist-server/server/modules/computer-use/semantics/semantic-types.js +2 -0
  78. package/dist-server/server/modules/computer-use/semantics/semantic-types.js.map +1 -0
  79. package/dist-server/server/modules/database/index.js +1 -0
  80. package/dist-server/server/modules/database/index.js.map +1 -1
  81. package/dist-server/server/modules/database/migrations.js +23 -1
  82. package/dist-server/server/modules/database/migrations.js.map +1 -1
  83. package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js +100 -0
  84. package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js.map +1 -0
  85. package/dist-server/server/modules/database/repositories/notification-preferences.js +8 -0
  86. package/dist-server/server/modules/database/repositories/notification-preferences.js.map +1 -1
  87. package/dist-server/server/modules/database/repositories/sessions.db.js +173 -17
  88. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  89. package/dist-server/server/modules/database/schema.js +26 -0
  90. package/dist-server/server/modules/database/schema.js.map +1 -1
  91. package/dist-server/server/modules/database/tests/projects.db.integration.test.js.map +1 -0
  92. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +78 -0
  93. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -0
  94. package/dist-server/server/modules/database/{repositories → tests}/sessions.db.integration.test.js +10 -0
  95. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -0
  96. package/dist-server/server/modules/notifications/index.js +4 -0
  97. package/dist-server/server/modules/notifications/index.js.map +1 -0
  98. package/dist-server/server/modules/notifications/notifications.routes.js +115 -0
  99. package/dist-server/server/modules/notifications/notifications.routes.js.map +1 -0
  100. package/dist-server/server/modules/notifications/services/desktop-notification-clients.service.js +97 -0
  101. package/dist-server/server/modules/notifications/services/desktop-notification-clients.service.js.map +1 -0
  102. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +229 -0
  103. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -0
  104. package/dist-server/server/modules/notifications/websocket/desktop-notifications-websocket.service.js +79 -0
  105. package/dist-server/server/modules/notifications/websocket/desktop-notifications-websocket.service.js.map +1 -0
  106. package/dist-server/server/modules/projects/services/project-management.service.js +0 -4
  107. package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -1
  108. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +10 -38
  109. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
  110. package/dist-server/server/modules/providers/index.js +1 -0
  111. package/dist-server/server/modules/providers/index.js.map +1 -1
  112. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +5 -0
  113. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  114. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +24 -1
  115. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
  116. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +9 -12
  117. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  118. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +7 -0
  119. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
  120. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +7 -3
  121. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  122. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +3 -9
  123. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  124. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +7 -0
  125. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -1
  126. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +13 -22
  127. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
  128. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js +7 -0
  129. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js.map +1 -1
  130. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +5 -8
  131. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +1 -1
  132. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +6 -6
  133. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
  134. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +7 -0
  135. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +1 -1
  136. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +17 -3
  137. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  138. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +9 -10
  139. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -1
  140. package/dist-server/server/modules/providers/provider.routes.js +123 -1
  141. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  142. package/dist-server/server/modules/providers/services/mcp.service.js +23 -0
  143. package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -1
  144. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +65 -0
  145. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -0
  146. package/dist-server/server/modules/providers/services/provider-models.service.js +1 -1
  147. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  148. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +62 -23
  149. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  150. package/dist-server/server/modules/providers/services/sessions.service.js +62 -4
  151. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  152. package/dist-server/server/modules/providers/services/skills.service.js +11 -0
  153. package/dist-server/server/modules/providers/services/skills.service.js.map +1 -1
  154. package/dist-server/server/modules/providers/shared/skills/skills.provider.js +163 -1
  155. package/dist-server/server/modules/providers/shared/skills/skills.provider.js.map +1 -1
  156. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +45 -0
  157. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  158. package/dist-server/server/modules/providers/tests/skills.test.js +160 -0
  159. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  160. package/dist-server/server/modules/websocket/index.js +1 -0
  161. package/dist-server/server/modules/websocket/index.js.map +1 -1
  162. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +252 -0
  163. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -0
  164. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +105 -0
  165. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -0
  166. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +218 -156
  167. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  168. package/dist-server/server/modules/websocket/services/desktop-agent-websocket.service.js +39 -0
  169. package/dist-server/server/modules/websocket/services/desktop-agent-websocket.service.js.map +1 -0
  170. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +83 -31
  171. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  172. package/dist-server/server/modules/websocket/services/websocket-server.service.js +10 -0
  173. package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -1
  174. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +216 -0
  175. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -0
  176. package/dist-server/server/openai-codex.js +24 -14
  177. package/dist-server/server/openai-codex.js.map +1 -1
  178. package/dist-server/server/opencode-cli.js +21 -8
  179. package/dist-server/server/opencode-cli.js.map +1 -1
  180. package/dist-server/server/opencode-cli.test.js +19 -1
  181. package/dist-server/server/opencode-cli.test.js.map +1 -1
  182. package/dist-server/server/routes/agent.js +1 -1
  183. package/dist-server/server/routes/settings.js +1 -1
  184. package/dist-server/server/routes/settings.js.map +1 -1
  185. package/dist-server/server/services/notification-orchestrator.js +1 -176
  186. package/dist-server/server/services/notification-orchestrator.js.map +1 -1
  187. package/dist-server/server/services/tests/notification-orchestrator.test.js +64 -0
  188. package/dist-server/server/services/tests/notification-orchestrator.test.js.map +1 -0
  189. package/dist-server/server/shared/{claude-cli-path.test.js → tests/claude-cli-path.test.js} +1 -1
  190. package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -0
  191. package/dist-server/server/shared/tests/slice-tail-page.test.js +35 -0
  192. package/dist-server/server/shared/tests/slice-tail-page.test.js.map +1 -0
  193. package/dist-server/server/shared/utils.js +73 -1
  194. package/dist-server/server/shared/utils.js.map +1 -1
  195. package/dist-server/server/voice-proxy.js +226 -0
  196. package/dist-server/server/voice-proxy.js.map +1 -0
  197. package/electron/assets/logo-macos.icns +0 -0
  198. package/electron/assets/logo-macos.png +0 -0
  199. package/electron/assets/logo-windows.ico +0 -0
  200. package/electron/cloud.js +260 -0
  201. package/electron/computerAgent.js +290 -0
  202. package/electron/desktopNotifications.js +378 -0
  203. package/electron/desktopWindow.js +781 -0
  204. package/electron/launcher/index.html +14 -0
  205. package/electron/launcher/launcher.css +801 -0
  206. package/electron/launcher/launcher.js +829 -0
  207. package/electron/localServer.js +550 -0
  208. package/electron/main.js +1060 -0
  209. package/electron/preload.cjs +63 -0
  210. package/electron/scripts/generate-macos-icon.js +62 -0
  211. package/electron/serverInstaller.js +277 -0
  212. package/electron/tabs.js +87 -0
  213. package/electron/viewHost.js +331 -0
  214. package/package.json +86 -5
  215. package/public/api-docs.html +36 -18
  216. package/scripts/build-computer-semantics.mjs +133 -0
  217. package/scripts/copy-computer-semantics-bin.mjs +24 -0
  218. package/scripts/release/build-server-bundle.js +176 -0
  219. package/scripts/release/prepare-desktop-app.js +158 -0
  220. package/server/browser-use-mcp.ts +384 -0
  221. package/server/claude-sdk.js +28 -6
  222. package/server/cli.js +15 -6
  223. package/server/computer-use-agent.ts +279 -0
  224. package/server/computer-use-mcp.ts +574 -0
  225. package/server/cursor-cli.js +27 -11
  226. package/server/gemini-cli.js +17 -2
  227. package/server/index.js +151 -47
  228. package/server/load-env.js +1 -1
  229. package/server/modules/browser-use/browser-use-mcp.routes.ts +120 -0
  230. package/server/modules/browser-use/browser-use.routes.ts +96 -0
  231. package/server/modules/browser-use/browser-use.service.ts +836 -0
  232. package/server/modules/browser-use/tests/browser-use.service.test.ts +10 -0
  233. package/server/modules/computer-use/actions/raw-action-dispatcher.ts +67 -0
  234. package/server/modules/computer-use/actions/raw-action-types.ts +28 -0
  235. package/server/modules/computer-use/computer-executor.ts +242 -0
  236. package/server/modules/computer-use/computer-semantics.service.ts +460 -0
  237. package/server/modules/computer-use/computer-use-mcp.routes.ts +141 -0
  238. package/server/modules/computer-use/computer-use.routes.ts +211 -0
  239. package/server/modules/computer-use/computer-use.service.ts +920 -0
  240. package/server/modules/computer-use/desktop-agent-relay.service.ts +158 -0
  241. package/server/modules/computer-use/index.ts +2 -0
  242. package/server/modules/computer-use/semantics/adapters/helper-semantic-adapter.ts +82 -0
  243. package/server/modules/computer-use/semantics/adapters/macos/macos-semantic-adapter.ts +5 -0
  244. package/server/modules/computer-use/semantics/adapters/semantic-adapter.ts +23 -0
  245. package/server/modules/computer-use/semantics/adapters/windows/windows-semantic-adapter.ts +5 -0
  246. package/server/modules/computer-use/semantics/bin/darwin-arm64/CloudCLISemantics +0 -0
  247. package/server/modules/computer-use/semantics/bin/darwin-x64/CloudCLISemantics +0 -0
  248. package/server/modules/computer-use/semantics/bin/win32-arm64/CloudCLISemantics.exe +0 -0
  249. package/server/modules/computer-use/semantics/bin/win32-x64/CloudCLISemantics.exe +0 -0
  250. package/server/modules/computer-use/semantics/helpers/macos/CloudCLISemantics.swift +467 -0
  251. package/server/modules/computer-use/semantics/helpers/semantic-helper-process.ts +124 -0
  252. package/server/modules/computer-use/semantics/helpers/semantic-helper-resolver.ts +97 -0
  253. package/server/modules/computer-use/semantics/helpers/windows/CloudCLISemantics.csproj +11 -0
  254. package/server/modules/computer-use/semantics/helpers/windows/Program.cs +534 -0
  255. package/server/modules/computer-use/semantics/semantic-session-store.ts +87 -0
  256. package/server/modules/computer-use/semantics/semantic-tool-dispatcher.ts +17 -0
  257. package/server/modules/computer-use/semantics/semantic-types.ts +58 -0
  258. package/server/modules/database/index.ts +1 -0
  259. package/server/modules/database/migrations.ts +25 -0
  260. package/server/modules/database/repositories/notification-channel-endpoints.ts +153 -0
  261. package/server/modules/database/repositories/notification-preferences.ts +12 -1
  262. package/server/modules/database/repositories/sessions.db.ts +227 -23
  263. package/server/modules/database/schema.ts +27 -0
  264. package/server/modules/database/tests/sessions-provider-mapping.test.ts +108 -0
  265. package/server/modules/database/{repositories → tests}/sessions.db.integration.test.ts +12 -0
  266. package/server/modules/notifications/index.ts +13 -0
  267. package/server/modules/notifications/notifications.routes.ts +127 -0
  268. package/server/modules/notifications/services/desktop-notification-clients.service.ts +124 -0
  269. package/server/modules/notifications/services/notification-orchestrator.service.js +288 -0
  270. package/server/modules/notifications/websocket/desktop-notifications-websocket.service.ts +109 -0
  271. package/server/modules/projects/services/project-management.service.ts +0 -8
  272. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +12 -53
  273. package/server/modules/providers/README.md +1 -1
  274. package/server/modules/providers/index.ts +1 -0
  275. package/server/modules/providers/list/claude/claude-models.provider.ts +5 -0
  276. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +26 -1
  277. package/server/modules/providers/list/claude/claude-sessions.provider.ts +9 -14
  278. package/server/modules/providers/list/claude/claude-skills.provider.ts +8 -0
  279. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +7 -3
  280. package/server/modules/providers/list/codex/codex-sessions.provider.ts +3 -12
  281. package/server/modules/providers/list/codex/codex-skills.provider.ts +8 -0
  282. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +13 -22
  283. package/server/modules/providers/list/cursor/cursor-skills.provider.ts +8 -0
  284. package/server/modules/providers/list/gemini/gemini-models.provider.ts +5 -8
  285. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +6 -6
  286. package/server/modules/providers/list/gemini/gemini-skills.provider.ts +8 -0
  287. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +18 -4
  288. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +9 -12
  289. package/server/modules/providers/provider.routes.ts +167 -1
  290. package/server/modules/providers/services/mcp.service.ts +26 -0
  291. package/server/modules/providers/services/provider-capabilities.service.ts +91 -0
  292. package/server/modules/providers/services/provider-models.service.ts +1 -1
  293. package/server/modules/providers/services/sessions-watcher.service.ts +71 -25
  294. package/server/modules/providers/services/sessions.service.ts +79 -4
  295. package/server/modules/providers/services/skills.service.ts +25 -1
  296. package/server/modules/providers/shared/skills/skills.provider.ts +223 -0
  297. package/server/modules/providers/tests/opencode-sessions.test.ts +49 -0
  298. package/server/modules/providers/tests/skills.test.ts +212 -0
  299. package/server/modules/websocket/README.md +33 -27
  300. package/server/modules/websocket/index.ts +1 -0
  301. package/server/modules/websocket/services/chat-run-registry.service.ts +327 -0
  302. package/server/modules/websocket/services/chat-session-writer.service.ts +145 -0
  303. package/server/modules/websocket/services/chat-websocket.service.ts +292 -207
  304. package/server/modules/websocket/services/desktop-agent-websocket.service.ts +51 -0
  305. package/server/modules/websocket/services/shell-websocket.service.ts +108 -32
  306. package/server/modules/websocket/services/websocket-server.service.ts +12 -0
  307. package/server/modules/websocket/tests/chat-run-registry.test.ts +244 -0
  308. package/server/openai-codex.js +24 -14
  309. package/server/opencode-cli.js +21 -8
  310. package/server/opencode-cli.test.js +20 -1
  311. package/server/routes/agent.js +1 -1
  312. package/server/routes/settings.js +6 -1
  313. package/server/services/notification-orchestrator.js +3 -224
  314. package/server/services/tests/notification-orchestrator.test.js +80 -0
  315. package/server/shared/interfaces.ts +15 -0
  316. package/server/shared/tests/slice-tail-page.test.ts +42 -0
  317. package/server/shared/types.ts +84 -1
  318. package/server/shared/utils.ts +95 -1
  319. package/server/voice-proxy.js +224 -0
  320. package/dist/assets/index-73Xw3i9v.css +0 -32
  321. package/dist/assets/index-9n7A21AT.js +0 -1361
  322. package/dist/modelConstants.js +0 -848
  323. package/dist-server/server/modules/database/repositories/projects.db.integration.test.js.map +0 -1
  324. package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js.map +0 -1
  325. package/dist-server/server/shared/claude-cli-path.test.js.map +0 -1
  326. package/public/modelConstants.js +0 -848
  327. /package/dist-server/server/modules/database/{repositories → tests}/projects.db.integration.test.js +0 -0
  328. /package/server/modules/database/{repositories → tests}/projects.db.integration.test.ts +0 -0
  329. /package/server/shared/{claude-cli-path.test.ts → tests/claude-cli-path.test.ts} +0 -0
@@ -0,0 +1,108 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { closeConnection } from '@/modules/database/connection.js';
8
+ import { initializeDatabase } from '@/modules/database/init-db.js';
9
+ import { sessionsDb } from '@/modules/database/repositories/sessions.db.js';
10
+
11
+ async function withIsolatedDatabase(runTest: () => void | Promise<void>): Promise<void> {
12
+ const previousDatabasePath = process.env.DATABASE_PATH;
13
+ const tempDirectory = await mkdtemp(path.join(tmpdir(), 'sessions-mapping-'));
14
+ const databasePath = path.join(tempDirectory, 'auth.db');
15
+
16
+ closeConnection();
17
+ process.env.DATABASE_PATH = databasePath;
18
+ await initializeDatabase();
19
+
20
+ try {
21
+ await runTest();
22
+ } finally {
23
+ closeConnection();
24
+ if (previousDatabasePath === undefined) {
25
+ delete process.env.DATABASE_PATH;
26
+ } else {
27
+ process.env.DATABASE_PATH = previousDatabasePath;
28
+ }
29
+ await rm(tempDirectory, { recursive: true, force: true });
30
+ }
31
+ }
32
+
33
+ test('disk-discovered sessions are keyed by the provider id for both columns', async () => {
34
+ await withIsolatedDatabase(() => {
35
+ sessionsDb.createSession('provider-abc', 'claude', '/workspace/demo', 'From Disk');
36
+
37
+ const row = sessionsDb.getSessionById('provider-abc');
38
+ assert.equal(row?.session_id, 'provider-abc');
39
+ assert.equal(row?.provider_session_id, 'provider-abc');
40
+
41
+ const byProviderId = sessionsDb.getSessionByProviderSessionId('provider-abc');
42
+ assert.equal(byProviderId?.session_id, 'provider-abc');
43
+ });
44
+ });
45
+
46
+ test('app sessions get the provider id assigned without creating a duplicate row', async () => {
47
+ await withIsolatedDatabase(() => {
48
+ sessionsDb.createAppSession('app-id-1', 'claude', '/workspace/demo');
49
+ sessionsDb.assignProviderSessionId('app-id-1', 'provider-xyz');
50
+
51
+ // A later synchronizer pass that discovers the transcript on disk must
52
+ // update the app row in place instead of inserting a provider-keyed row.
53
+ const returnedId = sessionsDb.createSession(
54
+ 'provider-xyz',
55
+ 'claude',
56
+ '/workspace/demo',
57
+ 'Synced Name',
58
+ undefined,
59
+ undefined,
60
+ '/fake/path/provider-xyz.jsonl',
61
+ );
62
+
63
+ assert.equal(returnedId, 'app-id-1');
64
+ assert.equal(sessionsDb.getAllSessions().length, 1);
65
+
66
+ const row = sessionsDb.getSessionById('app-id-1');
67
+ assert.equal(row?.provider_session_id, 'provider-xyz');
68
+ assert.equal(row?.jsonl_path, '/fake/path/provider-xyz.jsonl');
69
+ });
70
+ });
71
+
72
+ test('assignProviderSessionId merges a watcher-created duplicate into the app row', async () => {
73
+ await withIsolatedDatabase(() => {
74
+ sessionsDb.createAppSession('app-id-2', 'codex', '/workspace/demo');
75
+
76
+ // Simulate the race: the filesystem watcher indexed the provider
77
+ // transcript before the runtime announced its session id to the gateway.
78
+ sessionsDb.createSession(
79
+ 'provider-race',
80
+ 'codex',
81
+ '/workspace/demo',
82
+ 'Watcher Name',
83
+ undefined,
84
+ undefined,
85
+ '/fake/provider-race.jsonl',
86
+ );
87
+ assert.equal(sessionsDb.getAllSessions().length, 2);
88
+
89
+ sessionsDb.assignProviderSessionId('app-id-2', 'provider-race');
90
+
91
+ const rows = sessionsDb.getAllSessions();
92
+ assert.equal(rows.length, 1);
93
+ assert.equal(rows[0]?.session_id, 'app-id-2');
94
+ assert.equal(rows[0]?.provider_session_id, 'provider-race');
95
+ // Transcript path and name from the duplicate are adopted.
96
+ assert.equal(rows[0]?.jsonl_path, '/fake/provider-race.jsonl');
97
+ assert.equal(rows[0]?.custom_name, 'Watcher Name');
98
+ });
99
+ });
100
+
101
+ test('legacy provider-keyed rows stay resolvable through both lookups', async () => {
102
+ await withIsolatedDatabase(() => {
103
+ sessionsDb.createSession('legacy-1', 'gemini', '/workspace/demo');
104
+
105
+ assert.equal(sessionsDb.getSessionById('legacy-1')?.provider, 'gemini');
106
+ assert.equal(sessionsDb.getSessionByProviderSessionId('legacy-1')?.session_id, 'legacy-1');
107
+ });
108
+ });
@@ -70,3 +70,15 @@ test('createSession reactivates archived rows when the session becomes active ag
70
70
  assert.equal(restoredSession?.isArchived, 0);
71
71
  });
72
72
  });
73
+
74
+ test('repository reads normalize SQLite UTC timestamps to ISO strings', async () => {
75
+ await withIsolatedDatabase(() => {
76
+ sessionsDb.createAppSession('session-timezone', 'claude', '/workspace/demo-project');
77
+
78
+ const row = sessionsDb.getSessionById('session-timezone');
79
+ assert.ok(row?.created_at.endsWith('Z'));
80
+ assert.ok(row?.updated_at.endsWith('Z'));
81
+ assert.match(row?.created_at ?? '', /^\d{4}-\d{2}-\d{2}T/);
82
+ assert.match(row?.updated_at ?? '', /^\d{4}-\d{2}-\d{2}T/);
83
+ });
84
+ });
@@ -0,0 +1,13 @@
1
+ export {
2
+ buildNotificationPayload,
3
+ createNotificationEvent,
4
+ notifyUserIfEnabled,
5
+ notifyRunFailed,
6
+ notifyRunStopped,
7
+ } from '@/modules/notifications/services/notification-orchestrator.service.js';
8
+ export {
9
+ registerDesktopNotificationClient,
10
+ sendDesktopNotification,
11
+ unregisterDesktopNotificationClient,
12
+ } from '@/modules/notifications/services/desktop-notification-clients.service.js';
13
+ export { handleDesktopNotificationsConnection } from '@/modules/notifications/websocket/desktop-notifications-websocket.service.js';
@@ -0,0 +1,127 @@
1
+ import express from 'express';
2
+
3
+ import { notificationChannelEndpointsDb, notificationPreferencesDb } from '@/modules/database/index.js';
4
+
5
+ const router = express.Router();
6
+
7
+ function readText(value: unknown): string {
8
+ return typeof value === 'string' ? value.trim() : '';
9
+ }
10
+
11
+ function sanitizeEndpoint(endpoint: any) {
12
+ return {
13
+ id: endpoint.id,
14
+ channel: endpoint.channel,
15
+ endpointId: endpoint.endpoint_id,
16
+ label: endpoint.label,
17
+ metadata: notificationChannelEndpointsDb.parseMetadata(endpoint.metadata_json),
18
+ enabled: Boolean(endpoint.enabled),
19
+ lastSeenAt: endpoint.last_seen_at,
20
+ createdAt: endpoint.created_at,
21
+ updatedAt: endpoint.updated_at,
22
+ };
23
+ }
24
+
25
+ function readUserId(req: express.Request): number {
26
+ const userId = Number((req as any).user?.id);
27
+ if (!Number.isInteger(userId) || userId <= 0) {
28
+ throw new Error('Authenticated user is missing');
29
+ }
30
+ return userId;
31
+ }
32
+
33
+ function updateChannelPreference(userId: number, channel: string): unknown {
34
+ const currentPrefs = notificationPreferencesDb.getPreferences(userId);
35
+ const hasEnabledEndpoint = notificationChannelEndpointsDb.getEnabledEndpoints(userId, channel).length > 0;
36
+ return notificationPreferencesDb.updatePreferences(userId, {
37
+ ...currentPrefs,
38
+ channels: { ...currentPrefs.channels, [channel]: hasEnabledEndpoint },
39
+ });
40
+ }
41
+
42
+ router.get('/endpoints', (req, res) => {
43
+ try {
44
+ const channel = readText(req.query.channel);
45
+ if (!channel) {
46
+ return res.status(400).json({ error: 'channel is required' });
47
+ }
48
+
49
+ const userId = readUserId(req);
50
+ const endpoints = notificationChannelEndpointsDb
51
+ .getEndpoints(userId, channel)
52
+ .map(sanitizeEndpoint);
53
+ return res.json({ success: true, endpoints });
54
+ } catch (error) {
55
+ console.error('Error fetching notification endpoints:', error);
56
+ return res.status(500).json({ error: 'Failed to fetch notification endpoints' });
57
+ }
58
+ });
59
+
60
+ router.post('/endpoints/current', (req, res) => {
61
+ try {
62
+ const { channel, endpointId, label, metadata = {}, enabled = true } = req.body || {};
63
+ const normalizedChannel = readText(channel);
64
+ const normalizedEndpointId = readText(endpointId);
65
+ if (!normalizedChannel || !normalizedEndpointId) {
66
+ return res.status(400).json({ error: 'channel and endpointId are required' });
67
+ }
68
+
69
+ const userId = readUserId(req);
70
+ const endpoint = notificationChannelEndpointsDb.upsertEndpoint({
71
+ userId,
72
+ channel: normalizedChannel,
73
+ endpointId: normalizedEndpointId,
74
+ label,
75
+ metadata: metadata && typeof metadata === 'object' ? metadata : {},
76
+ enabled: enabled !== false,
77
+ });
78
+
79
+ const preferences = updateChannelPreference(userId, normalizedChannel);
80
+ return res.json({ success: true, endpoint: sanitizeEndpoint(endpoint), preferences });
81
+ } catch (error) {
82
+ console.error('Error registering notification endpoint:', error);
83
+ return res.status(500).json({ error: 'Failed to register notification endpoint' });
84
+ }
85
+ });
86
+
87
+ router.patch('/endpoints/:channel/:endpointId', (req, res) => {
88
+ try {
89
+ const { channel, endpointId } = req.params;
90
+ const { enabled } = req.body || {};
91
+ if (typeof enabled !== 'boolean') {
92
+ return res.status(400).json({ error: 'enabled must be a boolean' });
93
+ }
94
+
95
+ const userId = readUserId(req);
96
+ const updated = notificationChannelEndpointsDb.setEndpointEnabled(userId, channel, endpointId, enabled);
97
+ if (!updated) {
98
+ return res.status(404).json({ error: 'Notification endpoint not found' });
99
+ }
100
+
101
+ const endpoint = notificationChannelEndpointsDb.getEndpoint(userId, channel, endpointId);
102
+ const preferences = updateChannelPreference(userId, channel);
103
+ return res.json({ success: true, endpoint: endpoint ? sanitizeEndpoint(endpoint) : null, preferences });
104
+ } catch (error) {
105
+ console.error('Error updating notification endpoint:', error);
106
+ return res.status(500).json({ error: 'Failed to update notification endpoint' });
107
+ }
108
+ });
109
+
110
+ router.delete('/endpoints/:channel/:endpointId', (req, res) => {
111
+ try {
112
+ const { channel, endpointId } = req.params;
113
+ const userId = readUserId(req);
114
+ const removed = notificationChannelEndpointsDb.removeEndpoint(userId, channel, endpointId);
115
+ if (!removed) {
116
+ return res.status(404).json({ error: 'Notification endpoint not found' });
117
+ }
118
+
119
+ const preferences = updateChannelPreference(userId, channel);
120
+ return res.json({ success: true, preferences });
121
+ } catch (error) {
122
+ console.error('Error removing notification endpoint:', error);
123
+ return res.status(500).json({ error: 'Failed to remove notification endpoint' });
124
+ }
125
+ });
126
+
127
+ export default router;
@@ -0,0 +1,124 @@
1
+ import type { WebSocket } from 'ws';
2
+
3
+ import { notificationChannelEndpointsDb } from '@/modules/database/index.js';
4
+
5
+ const DESKTOP_CHANNEL = 'desktop';
6
+
7
+ const clientsByUserId = new Map<number, Map<string, WebSocket>>();
8
+ const clientBySocket = new WeakMap<WebSocket, { userId: number; endpointId: string }>();
9
+
10
+ function normalizeUserId(userId: unknown): number | null {
11
+ const numeric = Number(userId);
12
+ return Number.isInteger(numeric) && numeric > 0 ? numeric : null;
13
+ }
14
+
15
+ function normalizeEndpointId(endpointId: unknown): string {
16
+ if (typeof endpointId !== 'string') return '';
17
+ return endpointId.trim();
18
+ }
19
+
20
+ function getUserClients(userId: unknown, create = false): Map<string, WebSocket> | null {
21
+ const normalizedUserId = normalizeUserId(userId);
22
+ if (!normalizedUserId) return null;
23
+ let clients = clientsByUserId.get(normalizedUserId);
24
+ if (!clients && create) {
25
+ clients = new Map();
26
+ clientsByUserId.set(normalizedUserId, clients);
27
+ }
28
+ return clients || null;
29
+ }
30
+
31
+ export function registerDesktopNotificationClient({
32
+ userId,
33
+ deviceId,
34
+ label = null,
35
+ platform = null,
36
+ appVersion = null,
37
+ ws,
38
+ }: {
39
+ userId: number;
40
+ deviceId: string;
41
+ label?: string | null;
42
+ platform?: string | null;
43
+ appVersion?: string | null;
44
+ ws: WebSocket;
45
+ }) {
46
+ const normalizedUserId = normalizeUserId(userId);
47
+ const endpointId = normalizeEndpointId(deviceId);
48
+ if (!normalizedUserId || !endpointId) {
49
+ return false;
50
+ }
51
+
52
+ const endpoint = notificationChannelEndpointsDb.upsertEndpoint({
53
+ userId: normalizedUserId,
54
+ channel: DESKTOP_CHANNEL,
55
+ endpointId,
56
+ label,
57
+ metadata: { platform, appVersion },
58
+ enabled: true,
59
+ });
60
+
61
+ const clients = getUserClients(normalizedUserId, true)!;
62
+ const previous = clients.get(endpointId);
63
+ if (previous && previous !== ws && previous.readyState === previous.OPEN) {
64
+ previous.close(4000, 'Device reconnected');
65
+ }
66
+
67
+ clients.set(endpointId, ws);
68
+ clientBySocket.set(ws, { userId: normalizedUserId, endpointId });
69
+ return endpoint;
70
+ }
71
+
72
+ export function unregisterDesktopNotificationClient(ws: WebSocket): void {
73
+ const registration = clientBySocket.get(ws);
74
+ if (!registration) return;
75
+
76
+ const clients = getUserClients(registration.userId);
77
+ if (clients?.get(registration.endpointId) === ws) {
78
+ clients.delete(registration.endpointId);
79
+ if (clients.size === 0) {
80
+ clientsByUserId.delete(registration.userId);
81
+ }
82
+ }
83
+ clientBySocket.delete(ws);
84
+ }
85
+
86
+ export function sendDesktopNotification(userId: unknown, payload: unknown): { attempted: number; sent: number } {
87
+ const normalizedUserId = normalizeUserId(userId);
88
+ if (!normalizedUserId) return { attempted: 0, sent: 0 };
89
+
90
+ const clients = getUserClients(normalizedUserId);
91
+ if (!clients?.size) return { attempted: 0, sent: 0 };
92
+
93
+ const enabledEndpointIds = new Set(
94
+ notificationChannelEndpointsDb
95
+ .getEnabledEndpoints(normalizedUserId, DESKTOP_CHANNEL)
96
+ .map((endpoint) => endpoint.endpoint_id)
97
+ );
98
+
99
+ const message = JSON.stringify({
100
+ type: 'notification',
101
+ id: typeof (payload as any)?.data?.tag === 'string' ? (payload as any).data.tag : `${Date.now()}`,
102
+ payload,
103
+ });
104
+
105
+ let attempted = 0;
106
+ let sent = 0;
107
+ for (const [endpointId, ws] of clients.entries()) {
108
+ if (!enabledEndpointIds.has(endpointId)) continue;
109
+ attempted += 1;
110
+ if (ws.readyState !== ws.OPEN) {
111
+ unregisterDesktopNotificationClient(ws);
112
+ continue;
113
+ }
114
+ try {
115
+ ws.send(message);
116
+ notificationChannelEndpointsDb.touchEndpoint(normalizedUserId, DESKTOP_CHANNEL, endpointId);
117
+ sent += 1;
118
+ } catch {
119
+ unregisterDesktopNotificationClient(ws);
120
+ }
121
+ }
122
+
123
+ return { attempted, sent };
124
+ }
@@ -0,0 +1,288 @@
1
+ import webPush from 'web-push';
2
+
3
+ import { notificationPreferencesDb, pushSubscriptionsDb, sessionsDb } from '@/modules/database/index.js';
4
+ import { sendDesktopNotification as sendDesktopNotificationToClients } from '@/modules/notifications/services/desktop-notification-clients.service.js';
5
+
6
+ const KIND_TO_PREF_KEY = {
7
+ action_required: 'actionRequired',
8
+ stop: 'stop',
9
+ error: 'error'
10
+ };
11
+
12
+ const PROVIDER_LABELS = {
13
+ claude: 'Claude',
14
+ cursor: 'Cursor',
15
+ codex: 'Codex',
16
+ gemini: 'Gemini',
17
+ system: 'System'
18
+ };
19
+
20
+ const recentEventKeys = new Map();
21
+ const DEDUPE_WINDOW_MS = 20000;
22
+
23
+ const cleanupOldEventKeys = () => {
24
+ const now = Date.now();
25
+ for (const [key, timestamp] of recentEventKeys.entries()) {
26
+ if (now - timestamp > DEDUPE_WINDOW_MS) {
27
+ recentEventKeys.delete(key);
28
+ }
29
+ }
30
+ };
31
+
32
+ function isNotificationEventEnabled(preferences, event) {
33
+ const prefEventKey = KIND_TO_PREF_KEY[event.kind];
34
+ const eventEnabled = prefEventKey ? Boolean(preferences?.events?.[prefEventKey]) : true;
35
+
36
+ return eventEnabled;
37
+ }
38
+
39
+ function isDuplicate(event) {
40
+ cleanupOldEventKeys();
41
+ const key = event.dedupeKey || `${event.provider}:${event.kind || 'info'}:${event.code || 'generic'}:${event.sessionId || 'none'}`;
42
+ if (recentEventKeys.has(key)) {
43
+ return true;
44
+ }
45
+ recentEventKeys.set(key, Date.now());
46
+ return false;
47
+ }
48
+
49
+ function createNotificationEvent({
50
+ provider,
51
+ sessionId = null,
52
+ kind = 'info',
53
+ code = 'generic.info',
54
+ meta = {},
55
+ severity = 'info',
56
+ dedupeKey = null,
57
+ requiresUserAction = false
58
+ }) {
59
+ return {
60
+ provider,
61
+ sessionId,
62
+ kind,
63
+ code,
64
+ meta,
65
+ severity,
66
+ requiresUserAction,
67
+ dedupeKey,
68
+ createdAt: new Date().toISOString()
69
+ };
70
+ }
71
+
72
+ function normalizeErrorMessage(error) {
73
+ if (typeof error === 'string') {
74
+ return error;
75
+ }
76
+
77
+ if (error && typeof error.message === 'string') {
78
+ return error.message;
79
+ }
80
+
81
+ if (error == null) {
82
+ return 'Unknown error';
83
+ }
84
+
85
+ return String(error);
86
+ }
87
+
88
+ function normalizeSessionName(sessionName) {
89
+ if (typeof sessionName !== 'string') {
90
+ return null;
91
+ }
92
+
93
+ const normalized = sessionName.replace(/\s+/g, ' ').trim();
94
+ if (!normalized) {
95
+ return null;
96
+ }
97
+
98
+ return normalized.length > 80 ? `${normalized.slice(0, 77)}...` : normalized;
99
+ }
100
+
101
+ function rowMatchesProvider(row, provider) {
102
+ return row && (!provider || row.provider === provider);
103
+ }
104
+
105
+ function resolveSessionRow(sessionId, provider) {
106
+ if (!sessionId) {
107
+ return null;
108
+ }
109
+
110
+ const appSessionRow = sessionsDb.getSessionById(sessionId);
111
+ if (rowMatchesProvider(appSessionRow, provider)) {
112
+ return appSessionRow;
113
+ }
114
+
115
+ const providerSessionRow = sessionsDb.getSessionByProviderSessionId(sessionId);
116
+ if (rowMatchesProvider(providerSessionRow, provider)) {
117
+ return providerSessionRow;
118
+ }
119
+
120
+ return null;
121
+ }
122
+
123
+ function normalizeNotificationSession(event) {
124
+ if (!event?.sessionId || !event.provider || event.provider === 'system') {
125
+ return event;
126
+ }
127
+
128
+ const row = resolveSessionRow(event.sessionId, event.provider);
129
+ if (!row || row.session_id === event.sessionId) {
130
+ return event;
131
+ }
132
+
133
+ return {
134
+ ...event,
135
+ sessionId: row.session_id
136
+ };
137
+ }
138
+
139
+ function resolveSessionName(event) {
140
+ const explicitSessionName = normalizeSessionName(event.meta?.sessionName);
141
+ if (explicitSessionName) {
142
+ return explicitSessionName;
143
+ }
144
+
145
+ if (!event.sessionId || !event.provider) {
146
+ return null;
147
+ }
148
+
149
+ return normalizeSessionName(sessionsDb.getSessionName(event.sessionId, event.provider));
150
+ }
151
+
152
+ function buildNotificationPayload(event) {
153
+ const normalizedEvent = normalizeNotificationSession(event);
154
+ const CODE_MAP = {
155
+ 'permission.required': normalizedEvent.meta?.toolName
156
+ ? `Action Required: Tool "${normalizedEvent.meta.toolName}" needs approval`
157
+ : 'Action Required: A tool needs your approval',
158
+ 'run.stopped': normalizedEvent.meta?.stopReason || 'Run Stopped: The run has stopped',
159
+ 'run.failed': normalizedEvent.meta?.error ? `Run Failed: ${normalizedEvent.meta.error}` : 'Run Failed: The run encountered an error',
160
+ 'agent.notification': normalizedEvent.meta?.message ? String(normalizedEvent.meta.message) : 'You have a new notification',
161
+ 'push.enabled': 'Push notifications are now enabled!'
162
+ };
163
+ const providerLabel = PROVIDER_LABELS[normalizedEvent.provider] || 'Assistant';
164
+ const sessionName = resolveSessionName(normalizedEvent);
165
+ const message = CODE_MAP[normalizedEvent.code] || 'You have a new notification';
166
+
167
+ return {
168
+ title: sessionName || 'CloudCLI',
169
+ body: `${providerLabel}: ${message}`,
170
+ data: {
171
+ sessionId: normalizedEvent.sessionId || null,
172
+ code: normalizedEvent.code,
173
+ provider: normalizedEvent.provider || null,
174
+ sessionName,
175
+ tag: `${normalizedEvent.provider || 'assistant'}:${normalizedEvent.sessionId || 'none'}:${normalizedEvent.code}`
176
+ }
177
+ };
178
+ }
179
+
180
+ function sendWebPushPayload(userId, payload) {
181
+ const subscriptions = pushSubscriptionsDb.getSubscriptions(userId);
182
+ if (!subscriptions.length) return Promise.resolve();
183
+
184
+ const serializedPayload = JSON.stringify(payload);
185
+ return Promise.allSettled(
186
+ subscriptions.map((sub) =>
187
+ webPush.sendNotification(
188
+ {
189
+ endpoint: sub.endpoint,
190
+ keys: {
191
+ p256dh: sub.keys_p256dh,
192
+ auth: sub.keys_auth
193
+ }
194
+ },
195
+ serializedPayload
196
+ )
197
+ )
198
+ ).then((results) => {
199
+ results.forEach((result, index) => {
200
+ if (result.status === 'rejected') {
201
+ const statusCode = result.reason?.statusCode;
202
+ if (statusCode === 410 || statusCode === 404) {
203
+ pushSubscriptionsDb.removeSubscription(subscriptions[index].endpoint);
204
+ }
205
+ }
206
+ });
207
+ });
208
+ }
209
+
210
+ const notificationChannels = [
211
+ {
212
+ id: 'webPush',
213
+ // TODO: Web push still uses push_subscriptions. Do not remove that table until
214
+ // browser push subscriptions are migrated into notification_channel_endpoints.
215
+ isEnabled: (preferences) => Boolean(preferences?.channels?.webPush),
216
+ send: ({ userId, payload }) => sendWebPushPayload(userId, payload)
217
+ },
218
+ {
219
+ id: 'desktop',
220
+ isEnabled: (preferences) => Boolean(preferences?.channels?.desktop),
221
+ send: ({ userId, payload }) => sendDesktopNotificationToClients(userId, payload)
222
+ }
223
+ ];
224
+
225
+ function notifyUserIfEnabled({ userId, event }) {
226
+ if (!userId || !event) {
227
+ return;
228
+ }
229
+
230
+ const normalizedEvent = normalizeNotificationSession(event);
231
+ const preferences = notificationPreferencesDb.getPreferences(userId);
232
+ if (!isNotificationEventEnabled(preferences, normalizedEvent)) {
233
+ return;
234
+ }
235
+ if (isDuplicate(normalizedEvent)) {
236
+ return;
237
+ }
238
+
239
+ const payload = buildNotificationPayload(normalizedEvent);
240
+ for (const channel of notificationChannels) {
241
+ if (!channel.isEnabled(preferences)) {
242
+ continue;
243
+ }
244
+ Promise.resolve(channel.send({ userId, event: normalizedEvent, payload })).catch((err) => {
245
+ console.error(`Notification channel "${channel.id}" send error:`, err);
246
+ });
247
+ }
248
+ }
249
+
250
+ function notifyRunStopped({ userId, provider, sessionId = null, stopReason = 'completed', sessionName = null }) {
251
+ notifyUserIfEnabled({
252
+ userId,
253
+ event: createNotificationEvent({
254
+ provider,
255
+ sessionId,
256
+ kind: 'stop',
257
+ code: 'run.stopped',
258
+ meta: { stopReason, sessionName },
259
+ severity: 'info',
260
+ dedupeKey: `${provider}:run:stop:${sessionId || 'none'}:${stopReason}`
261
+ })
262
+ });
263
+ }
264
+
265
+ function notifyRunFailed({ userId, provider, sessionId = null, error, sessionName = null }) {
266
+ const errorMessage = normalizeErrorMessage(error);
267
+
268
+ notifyUserIfEnabled({
269
+ userId,
270
+ event: createNotificationEvent({
271
+ provider,
272
+ sessionId,
273
+ kind: 'error',
274
+ code: 'run.failed',
275
+ meta: { error: errorMessage, sessionName },
276
+ severity: 'error',
277
+ dedupeKey: `${provider}:run:error:${sessionId || 'none'}:${errorMessage}`
278
+ })
279
+ });
280
+ }
281
+
282
+ export {
283
+ buildNotificationPayload,
284
+ createNotificationEvent,
285
+ notifyUserIfEnabled,
286
+ notifyRunStopped,
287
+ notifyRunFailed
288
+ };