@cloudcli-ai/cloudcli 1.34.0 → 1.35.1

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 (247) 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 +23 -5
  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-BvX30NW5.css +32 -0
  11. package/dist/assets/index-D1IdCTT6.js +1418 -0
  12. package/dist/index.html +12 -4
  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 +25 -5
  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/cursor-cli.js +27 -11
  20. package/dist-server/server/cursor-cli.js.map +1 -1
  21. package/dist-server/server/gemini-cli.js +17 -2
  22. package/dist-server/server/gemini-cli.js.map +1 -1
  23. package/dist-server/server/index.js +136 -40
  24. package/dist-server/server/index.js.map +1 -1
  25. package/dist-server/server/load-env.js +1 -1
  26. package/dist-server/server/load-env.js.map +1 -1
  27. package/dist-server/server/modules/browser-use/browser-use-mcp.routes.js +107 -0
  28. package/dist-server/server/modules/browser-use/browser-use-mcp.routes.js.map +1 -0
  29. package/dist-server/server/modules/browser-use/browser-use.routes.js +93 -0
  30. package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -0
  31. package/dist-server/server/modules/browser-use/browser-use.service.js +688 -0
  32. package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -0
  33. package/dist-server/server/modules/browser-use/tests/browser-use.service.test.js +8 -0
  34. package/dist-server/server/modules/browser-use/tests/browser-use.service.test.js.map +1 -0
  35. package/dist-server/server/modules/database/index.js +1 -0
  36. package/dist-server/server/modules/database/index.js.map +1 -1
  37. package/dist-server/server/modules/database/migrations.js +23 -1
  38. package/dist-server/server/modules/database/migrations.js.map +1 -1
  39. package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js +100 -0
  40. package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js.map +1 -0
  41. package/dist-server/server/modules/database/repositories/notification-preferences.js +8 -0
  42. package/dist-server/server/modules/database/repositories/notification-preferences.js.map +1 -1
  43. package/dist-server/server/modules/database/repositories/sessions.db.js +173 -17
  44. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  45. package/dist-server/server/modules/database/schema.js +26 -0
  46. package/dist-server/server/modules/database/schema.js.map +1 -1
  47. package/dist-server/server/modules/database/tests/projects.db.integration.test.js.map +1 -0
  48. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +78 -0
  49. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -0
  50. package/dist-server/server/modules/database/{repositories → tests}/sessions.db.integration.test.js +10 -0
  51. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -0
  52. package/dist-server/server/modules/notifications/index.js +4 -0
  53. package/dist-server/server/modules/notifications/index.js.map +1 -0
  54. package/dist-server/server/modules/notifications/notifications.routes.js +115 -0
  55. package/dist-server/server/modules/notifications/notifications.routes.js.map +1 -0
  56. package/dist-server/server/modules/notifications/services/desktop-notification-clients.service.js +97 -0
  57. package/dist-server/server/modules/notifications/services/desktop-notification-clients.service.js.map +1 -0
  58. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +229 -0
  59. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -0
  60. package/dist-server/server/modules/notifications/websocket/desktop-notifications-websocket.service.js +79 -0
  61. package/dist-server/server/modules/notifications/websocket/desktop-notifications-websocket.service.js.map +1 -0
  62. package/dist-server/server/modules/projects/services/project-management.service.js +0 -4
  63. package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -1
  64. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +10 -38
  65. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
  66. package/dist-server/server/modules/providers/index.js +1 -0
  67. package/dist-server/server/modules/providers/index.js.map +1 -1
  68. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +24 -1
  69. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
  70. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +9 -12
  71. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  72. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +7 -0
  73. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
  74. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +7 -3
  75. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  76. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +3 -9
  77. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  78. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +7 -0
  79. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -1
  80. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +13 -22
  81. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
  82. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js +7 -0
  83. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js.map +1 -1
  84. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +5 -8
  85. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +1 -1
  86. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +6 -6
  87. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
  88. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +7 -0
  89. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +1 -1
  90. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +17 -3
  91. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  92. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +9 -10
  93. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -1
  94. package/dist-server/server/modules/providers/provider.routes.js +123 -1
  95. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  96. package/dist-server/server/modules/providers/services/mcp.service.js +23 -0
  97. package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -1
  98. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +65 -0
  99. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -0
  100. package/dist-server/server/modules/providers/services/provider-models.service.js +1 -1
  101. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  102. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +62 -23
  103. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  104. package/dist-server/server/modules/providers/services/sessions.service.js +62 -4
  105. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  106. package/dist-server/server/modules/providers/services/skills.service.js +11 -0
  107. package/dist-server/server/modules/providers/services/skills.service.js.map +1 -1
  108. package/dist-server/server/modules/providers/shared/skills/skills.provider.js +163 -1
  109. package/dist-server/server/modules/providers/shared/skills/skills.provider.js.map +1 -1
  110. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +45 -0
  111. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  112. package/dist-server/server/modules/providers/tests/skills.test.js +160 -0
  113. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  114. package/dist-server/server/modules/websocket/index.js +1 -0
  115. package/dist-server/server/modules/websocket/index.js.map +1 -1
  116. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +252 -0
  117. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -0
  118. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +105 -0
  119. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -0
  120. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +218 -156
  121. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  122. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +83 -31
  123. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  124. package/dist-server/server/modules/websocket/services/websocket-server.service.js +5 -0
  125. package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -1
  126. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +216 -0
  127. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -0
  128. package/dist-server/server/openai-codex.js +24 -14
  129. package/dist-server/server/openai-codex.js.map +1 -1
  130. package/dist-server/server/opencode-cli.js +21 -8
  131. package/dist-server/server/opencode-cli.js.map +1 -1
  132. package/dist-server/server/opencode-cli.test.js +19 -1
  133. package/dist-server/server/opencode-cli.test.js.map +1 -1
  134. package/dist-server/server/routes/settings.js +1 -1
  135. package/dist-server/server/routes/settings.js.map +1 -1
  136. package/dist-server/server/services/notification-orchestrator.js +1 -176
  137. package/dist-server/server/services/notification-orchestrator.js.map +1 -1
  138. package/dist-server/server/services/tests/notification-orchestrator.test.js +64 -0
  139. package/dist-server/server/services/tests/notification-orchestrator.test.js.map +1 -0
  140. package/dist-server/server/shared/{claude-cli-path.test.js → tests/claude-cli-path.test.js} +1 -1
  141. package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -0
  142. package/dist-server/server/shared/tests/slice-tail-page.test.js +35 -0
  143. package/dist-server/server/shared/tests/slice-tail-page.test.js.map +1 -0
  144. package/dist-server/server/shared/utils.js +73 -1
  145. package/dist-server/server/shared/utils.js.map +1 -1
  146. package/dist-server/server/voice-proxy.js +226 -0
  147. package/dist-server/server/voice-proxy.js.map +1 -0
  148. package/electron/assets/logo-macos.icns +0 -0
  149. package/electron/assets/logo-macos.png +0 -0
  150. package/electron/assets/logo-windows.ico +0 -0
  151. package/electron/cloud.js +260 -0
  152. package/electron/desktopNotifications.js +378 -0
  153. package/electron/desktopWindow.js +766 -0
  154. package/electron/launcher/index.html +14 -0
  155. package/electron/launcher/launcher.css +801 -0
  156. package/electron/launcher/launcher.js +687 -0
  157. package/electron/localServer.js +550 -0
  158. package/electron/main.js +944 -0
  159. package/electron/preload.cjs +60 -0
  160. package/electron/scripts/generate-macos-icon.js +62 -0
  161. package/electron/serverInstaller.js +277 -0
  162. package/electron/tabs.js +87 -0
  163. package/electron/viewHost.js +331 -0
  164. package/package.json +79 -3
  165. package/public/api-docs.html +36 -18
  166. package/scripts/release/build-server-bundle.js +176 -0
  167. package/scripts/release/prepare-desktop-app.js +152 -0
  168. package/server/browser-use-mcp.ts +384 -0
  169. package/server/claude-sdk.js +27 -5
  170. package/server/cli.js +15 -6
  171. package/server/cursor-cli.js +27 -11
  172. package/server/gemini-cli.js +17 -2
  173. package/server/index.js +137 -47
  174. package/server/load-env.js +1 -1
  175. package/server/modules/browser-use/browser-use-mcp.routes.ts +120 -0
  176. package/server/modules/browser-use/browser-use.routes.ts +96 -0
  177. package/server/modules/browser-use/browser-use.service.ts +836 -0
  178. package/server/modules/browser-use/tests/browser-use.service.test.ts +10 -0
  179. package/server/modules/database/index.ts +1 -0
  180. package/server/modules/database/migrations.ts +25 -0
  181. package/server/modules/database/repositories/notification-channel-endpoints.ts +153 -0
  182. package/server/modules/database/repositories/notification-preferences.ts +12 -1
  183. package/server/modules/database/repositories/sessions.db.ts +227 -23
  184. package/server/modules/database/schema.ts +27 -0
  185. package/server/modules/database/tests/sessions-provider-mapping.test.ts +108 -0
  186. package/server/modules/database/{repositories → tests}/sessions.db.integration.test.ts +12 -0
  187. package/server/modules/notifications/index.ts +13 -0
  188. package/server/modules/notifications/notifications.routes.ts +127 -0
  189. package/server/modules/notifications/services/desktop-notification-clients.service.ts +124 -0
  190. package/server/modules/notifications/services/notification-orchestrator.service.js +288 -0
  191. package/server/modules/notifications/websocket/desktop-notifications-websocket.service.ts +109 -0
  192. package/server/modules/projects/services/project-management.service.ts +0 -8
  193. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +12 -53
  194. package/server/modules/providers/README.md +1 -1
  195. package/server/modules/providers/index.ts +1 -0
  196. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +26 -1
  197. package/server/modules/providers/list/claude/claude-sessions.provider.ts +9 -14
  198. package/server/modules/providers/list/claude/claude-skills.provider.ts +8 -0
  199. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +7 -3
  200. package/server/modules/providers/list/codex/codex-sessions.provider.ts +3 -12
  201. package/server/modules/providers/list/codex/codex-skills.provider.ts +8 -0
  202. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +13 -22
  203. package/server/modules/providers/list/cursor/cursor-skills.provider.ts +8 -0
  204. package/server/modules/providers/list/gemini/gemini-models.provider.ts +5 -8
  205. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +6 -6
  206. package/server/modules/providers/list/gemini/gemini-skills.provider.ts +8 -0
  207. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +18 -4
  208. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +9 -12
  209. package/server/modules/providers/provider.routes.ts +167 -1
  210. package/server/modules/providers/services/mcp.service.ts +26 -0
  211. package/server/modules/providers/services/provider-capabilities.service.ts +91 -0
  212. package/server/modules/providers/services/provider-models.service.ts +1 -1
  213. package/server/modules/providers/services/sessions-watcher.service.ts +71 -25
  214. package/server/modules/providers/services/sessions.service.ts +79 -4
  215. package/server/modules/providers/services/skills.service.ts +25 -1
  216. package/server/modules/providers/shared/skills/skills.provider.ts +223 -0
  217. package/server/modules/providers/tests/opencode-sessions.test.ts +49 -0
  218. package/server/modules/providers/tests/skills.test.ts +212 -0
  219. package/server/modules/websocket/README.md +33 -27
  220. package/server/modules/websocket/index.ts +1 -0
  221. package/server/modules/websocket/services/chat-run-registry.service.ts +327 -0
  222. package/server/modules/websocket/services/chat-session-writer.service.ts +145 -0
  223. package/server/modules/websocket/services/chat-websocket.service.ts +292 -207
  224. package/server/modules/websocket/services/shell-websocket.service.ts +108 -32
  225. package/server/modules/websocket/services/websocket-server.service.ts +6 -0
  226. package/server/modules/websocket/tests/chat-run-registry.test.ts +244 -0
  227. package/server/openai-codex.js +24 -14
  228. package/server/opencode-cli.js +21 -8
  229. package/server/opencode-cli.test.js +20 -1
  230. package/server/routes/settings.js +6 -1
  231. package/server/services/notification-orchestrator.js +3 -224
  232. package/server/services/tests/notification-orchestrator.test.js +80 -0
  233. package/server/shared/interfaces.ts +15 -0
  234. package/server/shared/tests/slice-tail-page.test.ts +42 -0
  235. package/server/shared/types.ts +84 -1
  236. package/server/shared/utils.ts +95 -1
  237. package/server/voice-proxy.js +224 -0
  238. package/dist/assets/index-73Xw3i9v.css +0 -32
  239. package/dist/assets/index-BIHaviaA.js +0 -1361
  240. package/dist/modelConstants.js +0 -854
  241. package/dist-server/server/modules/database/repositories/projects.db.integration.test.js.map +0 -1
  242. package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js.map +0 -1
  243. package/dist-server/server/shared/claude-cli-path.test.js.map +0 -1
  244. package/public/modelConstants.js +0 -854
  245. /package/dist-server/server/modules/database/{repositories → tests}/projects.db.integration.test.js +0 -0
  246. /package/server/modules/database/{repositories → tests}/projects.db.integration.test.ts +0 -0
  247. /package/server/shared/{claude-cli-path.test.ts → tests/claude-cli-path.test.ts} +0 -0
@@ -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
+ };
@@ -0,0 +1,109 @@
1
+ import type { WebSocket } from 'ws';
2
+
3
+ import {
4
+ registerDesktopNotificationClient,
5
+ unregisterDesktopNotificationClient,
6
+ } from '@/modules/notifications/services/desktop-notification-clients.service.js';
7
+ import type { AuthenticatedWebSocketRequest } from '@/shared/types.js';
8
+ import { parseIncomingJsonObject } from '@/shared/utils.js';
9
+
10
+ type DesktopNotificationRegisterMessage = {
11
+ type?: unknown;
12
+ kind?: unknown;
13
+ deviceId?: unknown;
14
+ label?: unknown;
15
+ platform?: unknown;
16
+ appVersion?: unknown;
17
+ };
18
+
19
+ function readRequestUserId(request: AuthenticatedWebSocketRequest): number | null {
20
+ const user = request.user;
21
+ const rawUserId = typeof user?.id === 'number' || typeof user?.id === 'string'
22
+ ? user.id
23
+ : typeof user?.userId === 'number' || typeof user?.userId === 'string'
24
+ ? user.userId
25
+ : null;
26
+ const numericUserId = Number(rawUserId);
27
+ return Number.isInteger(numericUserId) && numericUserId > 0 ? numericUserId : null;
28
+ }
29
+
30
+ function readOptionalString(value: unknown): string | null {
31
+ if (typeof value !== 'string') return null;
32
+ const normalized = value.trim();
33
+ return normalized || null;
34
+ }
35
+
36
+ function sendJson(ws: WebSocket, payload: unknown): void {
37
+ if (ws.readyState === ws.OPEN) {
38
+ ws.send(JSON.stringify(payload));
39
+ }
40
+ }
41
+
42
+ export function handleDesktopNotificationsConnection(
43
+ ws: WebSocket,
44
+ request: AuthenticatedWebSocketRequest
45
+ ): void {
46
+ const userId = readRequestUserId(request);
47
+ if (!userId) {
48
+ ws.close(1008, 'Missing authenticated user');
49
+ return;
50
+ }
51
+
52
+ let registered = false;
53
+
54
+ ws.on('message', (rawMessage) => {
55
+ const data = parseIncomingJsonObject(rawMessage) as DesktopNotificationRegisterMessage | null;
56
+ if (!data) {
57
+ return;
58
+ }
59
+
60
+ const type = typeof data.type === 'string' ? data.type : typeof data.kind === 'string' ? data.kind : '';
61
+ if (type === 'notification_ack') {
62
+ return;
63
+ }
64
+
65
+ if (type !== 'register' || registered) {
66
+ return;
67
+ }
68
+
69
+ const deviceId = readOptionalString(data.deviceId);
70
+ if (!deviceId) {
71
+ sendJson(ws, {
72
+ type: 'error',
73
+ code: 'DEVICE_ID_REQUIRED',
74
+ message: 'Desktop notification registration requires deviceId.',
75
+ });
76
+ ws.close(1008, 'Missing deviceId');
77
+ return;
78
+ }
79
+
80
+ const device = registerDesktopNotificationClient({
81
+ userId,
82
+ deviceId,
83
+ label: readOptionalString(data.label),
84
+ platform: readOptionalString(data.platform),
85
+ appVersion: readOptionalString(data.appVersion),
86
+ ws,
87
+ });
88
+
89
+ if (!device) {
90
+ ws.close(1011, 'Registration failed');
91
+ return;
92
+ }
93
+
94
+ registered = true;
95
+ sendJson(ws, {
96
+ type: 'registered',
97
+ deviceId: device.endpoint_id,
98
+ enabled: Boolean(device.enabled),
99
+ });
100
+ });
101
+
102
+ ws.on('close', () => {
103
+ unregisterDesktopNotificationClient(ws);
104
+ });
105
+
106
+ ws.on('error', () => {
107
+ unregisterDesktopNotificationClient(ws);
108
+ });
109
+ }
@@ -30,10 +30,6 @@ type ProjectApiView = {
30
30
  isArchived: boolean;
31
31
  isStarred: boolean;
32
32
  sessions: [];
33
- cursorSessions: [];
34
- codexSessions: [];
35
- geminiSessions: [];
36
- opencodeSessions: [];
37
33
  sessionMeta: {
38
34
  hasMore: false;
39
35
  total: 0;
@@ -82,10 +78,6 @@ function mapProjectRowToApiView(projectRow: ProjectRepositoryRow): ProjectApiVie
82
78
  isArchived: Boolean(projectRow.isArchived),
83
79
  isStarred: Boolean(projectRow.isStarred),
84
80
  sessions: [],
85
- cursorSessions: [],
86
- codexSessions: [],
87
- geminiSessions: [],
88
- opencodeSessions: [],
89
81
  sessionMeta: {
90
82
  hasMore: false,
91
83
  total: 0,
@@ -9,13 +9,12 @@ import { AppError } from '@/shared/utils.js';
9
9
 
10
10
  type SessionSummary = {
11
11
  id: string;
12
+ provider: string;
12
13
  summary: string;
13
14
  messageCount: number;
14
15
  lastActivity: string;
15
16
  };
16
17
 
17
- type SessionsByProvider = Record<'claude' | 'cursor' | 'codex' | 'gemini' | 'opencode', SessionSummary[]>;
18
-
19
18
  type SessionRepositoryRow = {
20
19
  provider: string;
21
20
  session_id: string;
@@ -31,10 +30,6 @@ export type ProjectListItem = {
31
30
  fullPath: string;
32
31
  isStarred: boolean;
33
32
  sessions: SessionSummary[];
34
- cursorSessions: SessionSummary[];
35
- codexSessions: SessionSummary[];
36
- geminiSessions: SessionSummary[];
37
- opencodeSessions: SessionSummary[];
38
33
  sessionMeta: {
39
34
  hasMore: boolean;
40
35
  total: number;
@@ -64,7 +59,7 @@ type SessionPaginationOptions = {
64
59
  };
65
60
 
66
61
  type ProjectSessionsPageResult = {
67
- sessionsByProvider: SessionsByProvider;
62
+ sessions: SessionSummary[];
68
63
  total: number;
69
64
  hasMore: boolean;
70
65
  };
@@ -72,10 +67,6 @@ type ProjectSessionsPageResult = {
72
67
  export type ProjectSessionsPageApiView = {
73
68
  projectId: string;
74
69
  sessions: SessionSummary[];
75
- cursorSessions: SessionSummary[];
76
- codexSessions: SessionSummary[];
77
- geminiSessions: SessionSummary[];
78
- opencodeSessions: SessionSummary[];
79
70
  sessionMeta: {
80
71
  hasMore: boolean;
81
72
  total: number;
@@ -129,39 +120,18 @@ function normalizeSessionPagination(options: SessionPaginationOptions = {}): { l
129
120
  function mapSessionRowToSummary(row: SessionRepositoryRow): SessionSummary {
130
121
  return {
131
122
  id: row.session_id,
123
+ provider: row.provider,
132
124
  summary: row.custom_name || '',
133
125
  messageCount: 0,
134
126
  lastActivity: row.updated_at ?? row.created_at ?? new Date().toISOString(),
135
127
  };
136
128
  }
137
129
 
138
- function bucketSessionRowsByProvider(rows: SessionRepositoryRow[]): SessionsByProvider {
139
- const byProvider: SessionsByProvider = {
140
- claude: [],
141
- cursor: [],
142
- codex: [],
143
- gemini: [],
144
- opencode: [],
145
- };
146
-
147
- for (const row of rows) {
148
- const provider = row.provider as keyof SessionsByProvider;
149
- const bucket = byProvider[provider];
150
- if (!bucket) {
151
- continue;
152
- }
153
-
154
- bucket.push(mapSessionRowToSummary(row));
155
- }
156
-
157
- return byProvider;
158
- }
159
-
160
130
  function readProjectSessionsIncludingArchived(projectPath: string): ProjectSessionsPageResult {
161
131
  const rows = sessionsDb.getSessionsByProjectPathIncludingArchived(projectPath) as SessionRepositoryRow[];
162
132
 
163
133
  return {
164
- sessionsByProvider: bucketSessionRowsByProvider(rows),
134
+ sessions: rows.map(mapSessionRowToSummary),
165
135
  total: rows.length,
166
136
  hasMore: false,
167
137
  };
@@ -183,16 +153,17 @@ function readProjectSessionsPageByPath(
183
153
  const total = sessionsDb.countSessionsByProjectPath(projectPath);
184
154
 
185
155
  return {
186
- sessionsByProvider: bucketSessionRowsByProvider(rows),
156
+ sessions: rows.map(mapSessionRowToSummary),
187
157
  total,
188
158
  hasMore: pagination.offset + rows.length < total,
189
159
  };
190
160
  }
191
161
 
192
- // Broadcast progress to all connected WebSocket clients
162
+ // Broadcast progress to all connected WebSocket clients.
163
+ // Uses the unified `kind` envelope like every other websocket frame.
193
164
  function broadcastProgress(progress: ProgressUpdate) {
194
165
  const message = JSON.stringify({
195
- type: 'loading_progress',
166
+ kind: 'loading_progress',
196
167
  ...progress,
197
168
  });
198
169
 
@@ -204,7 +175,7 @@ function broadcastProgress(progress: ProgressUpdate) {
204
175
  }
205
176
 
206
177
  /**
207
- * Reads all projects from DB and returns provider-bucketed session summaries.
178
+ * Reads all projects from DB and returns normalized session summaries.
208
179
  */
209
180
  export async function getProjectsWithSessions(
210
181
  options: GetProjectsWithSessionsOptions = {}
@@ -252,11 +223,7 @@ export async function getProjectsWithSessions(
252
223
  displayName,
253
224
  fullPath: projectPath,
254
225
  isStarred: Boolean(row.isStarred),
255
- sessions: sessionsPage.sessionsByProvider.claude,
256
- cursorSessions: sessionsPage.sessionsByProvider.cursor,
257
- codexSessions: sessionsPage.sessionsByProvider.codex,
258
- geminiSessions: sessionsPage.sessionsByProvider.gemini,
259
- opencodeSessions: sessionsPage.sessionsByProvider.opencode,
226
+ sessions: sessionsPage.sessions,
260
227
  sessionMeta: {
261
228
  hasMore: sessionsPage.hasMore,
262
229
  total: sessionsPage.total,
@@ -309,11 +276,7 @@ export async function getArchivedProjectsWithSessions(
309
276
  fullPath: row.project_path,
310
277
  isStarred: Boolean(row.isStarred),
311
278
  isArchived: true,
312
- sessions: sessionsPage.sessionsByProvider.claude,
313
- cursorSessions: sessionsPage.sessionsByProvider.cursor,
314
- codexSessions: sessionsPage.sessionsByProvider.codex,
315
- geminiSessions: sessionsPage.sessionsByProvider.gemini,
316
- opencodeSessions: sessionsPage.sessionsByProvider.opencode,
279
+ sessions: sessionsPage.sessions,
317
280
  sessionMeta: {
318
281
  hasMore: sessionsPage.hasMore,
319
282
  total: sessionsPage.total,
@@ -342,11 +305,7 @@ export async function getProjectSessionsPage(
342
305
  const sessionsPage = readProjectSessionsPageByPath(projectRow.project_path, options);
343
306
  return {
344
307
  projectId: projectRow.project_id,
345
- sessions: sessionsPage.sessionsByProvider.claude,
346
- cursorSessions: sessionsPage.sessionsByProvider.cursor,
347
- codexSessions: sessionsPage.sessionsByProvider.codex,
348
- geminiSessions: sessionsPage.sessionsByProvider.gemini,
349
- opencodeSessions: sessionsPage.sessionsByProvider.opencode,
308
+ sessions: sessionsPage.sessions,
350
309
  sessionMeta: {
351
310
  hasMore: sessionsPage.hasMore,
352
311
  total: sessionsPage.total,
@@ -83,7 +83,7 @@ The existing provider folders are `claude`, `codex`, `cursor`, `gemini`, and
83
83
  - Update `server/modules/providers/provider.routes.ts`.
84
84
  - Update `server/routes/agent.js` if the provider is launchable from the agent runtime.
85
85
  - Update `server/index.js` if the provider needs runtime boot or shutdown wiring.
86
- - Update `public/modelConstants.js` if the provider appears in README or public API docs.
86
+ - Update the `PROVIDER_ORDER` list in `public/api-docs.html` if the provider should appear in the public API docs.
87
87
  - Update `src/components/chat/hooks/useChatProviderState.ts` and
88
88
  `src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx` if
89
89
  the provider should be selectable in chat.
@@ -1,5 +1,6 @@
1
1
  export { sessionSynchronizerService } from './services/session-synchronizer.service.js';
2
2
  export { providerSkillsService } from './services/skills.service.js';
3
+ export { providerMcpService } from './services/mcp.service.js';
3
4
 
4
5
  export { initializeSessionsWatcher } from './services/sessions-watcher.service.js';
5
6
  export { closeSessionsWatcher } from './services/sessions-watcher.service.js';
@@ -25,6 +25,21 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
25
25
  private readonly provider = 'claude' as const;
26
26
  private readonly claudeHome = path.join(os.homedir(), '.claude');
27
27
 
28
+ /**
29
+ * Returns true when a JSONL file is a subagent transcript rather than a
30
+ * top-level session.
31
+ *
32
+ * Claude stores subagent transcripts under a `subagents/` directory, e.g.
33
+ * `~/.claude/projects/<encoded-cwd>/<session-id>/subagents/agent-<id>.jsonl`.
34
+ * Those files repeat the parent session's `sessionId`, so indexing them as
35
+ * standalone sessions overwrites the parent row's `jsonl_path` and corrupts
36
+ * the main session record. The recursive scan in `synchronize()` reaches
37
+ * them, so both entry points must skip them.
38
+ */
39
+ private isSubagentTranscript(filePath: string): boolean {
40
+ return path.normalize(filePath).split(path.sep).includes('subagents');
41
+ }
42
+
28
43
  /**
29
44
  * Scans ~/.claude/projects and upserts discovered sessions into DB.
30
45
  */
@@ -38,6 +53,10 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
38
53
 
39
54
  let processed = 0;
40
55
  for (const filePath of files) {
56
+ if (this.isSubagentTranscript(filePath)) {
57
+ continue;
58
+ }
59
+
41
60
  const parsed = await this.processSessionFile(filePath, nameMap);
42
61
  if (!parsed) {
43
62
  continue;
@@ -66,6 +85,9 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
66
85
  if (!filePath.endsWith('.jsonl')) {
67
86
  return null;
68
87
  }
88
+ if (this.isSubagentTranscript(filePath)) {
89
+ return null;
90
+ }
69
91
 
70
92
  const nameMap = await buildLookupMap(path.join(this.claudeHome, 'history.jsonl'), 'sessionId', 'display');
71
93
  const parsed = await this.processSessionFile(filePath, nameMap);
@@ -111,7 +133,10 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
111
133
  return null;
112
134
  }
113
135
 
114
- const existingSession = sessionsDb.getSessionById(parsed.sessionId);
136
+ // App-created sessions are keyed by an app id, so disk-discovered provider
137
+ // ids must be resolved through the provider-id mapping first.
138
+ const existingSession = sessionsDb.getSessionByProviderSessionId(parsed.sessionId)
139
+ ?? sessionsDb.getSessionById(parsed.sessionId);
115
140
  const existingSessionName = existingSession?.custom_name;
116
141
  if (existingSessionName && existingSessionName !== 'Untitled Claude Session') {
117
142
  return {