@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
@@ -1,6 +1,11 @@
1
1
  import express from 'express';
2
2
 
3
- import { apiKeysDb, credentialsDb, notificationPreferencesDb, pushSubscriptionsDb } from '../modules/database/index.js';
3
+ import {
4
+ apiKeysDb,
5
+ credentialsDb,
6
+ notificationPreferencesDb,
7
+ pushSubscriptionsDb,
8
+ } from '../modules/database/index.js';
4
9
  import { getPublicKey } from '../services/vapid-keys.js';
5
10
  import { createNotificationEvent, notifyUserIfEnabled } from '../services/notification-orchestrator.js';
6
11
 
@@ -1,228 +1,7 @@
1
- import webPush from 'web-push';
2
-
3
- import { notificationPreferencesDb, pushSubscriptionsDb, sessionsDb } from '../modules/database/index.js';
4
-
5
- const KIND_TO_PREF_KEY = {
6
- action_required: 'actionRequired',
7
- stop: 'stop',
8
- error: 'error'
9
- };
10
-
11
- const PROVIDER_LABELS = {
12
- claude: 'Claude',
13
- cursor: 'Cursor',
14
- codex: 'Codex',
15
- gemini: 'Gemini',
16
- system: 'System'
17
- };
18
-
19
- const recentEventKeys = new Map();
20
- const DEDUPE_WINDOW_MS = 20000;
21
-
22
- const cleanupOldEventKeys = () => {
23
- const now = Date.now();
24
- for (const [key, timestamp] of recentEventKeys.entries()) {
25
- if (now - timestamp > DEDUPE_WINDOW_MS) {
26
- recentEventKeys.delete(key);
27
- }
28
- }
29
- };
30
-
31
- function shouldSendPush(preferences, event) {
32
- const webPushEnabled = Boolean(preferences?.channels?.webPush);
33
- const prefEventKey = KIND_TO_PREF_KEY[event.kind];
34
- const eventEnabled = prefEventKey ? Boolean(preferences?.events?.[prefEventKey]) : true;
35
-
36
- return webPushEnabled && 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 resolveSessionName(event) {
102
- const explicitSessionName = normalizeSessionName(event.meta?.sessionName);
103
- if (explicitSessionName) {
104
- return explicitSessionName;
105
- }
106
-
107
- if (!event.sessionId || !event.provider) {
108
- return null;
109
- }
110
-
111
- return normalizeSessionName(sessionsDb.getSessionName(event.sessionId, event.provider));
112
- }
113
-
114
- function buildPushBody(event) {
115
- const CODE_MAP = {
116
- 'permission.required': event.meta?.toolName
117
- ? `Action Required: Tool "${event.meta.toolName}" needs approval`
118
- : 'Action Required: A tool needs your approval',
119
- 'run.stopped': event.meta?.stopReason || 'Run Stopped: The run has stopped',
120
- 'run.failed': event.meta?.error ? `Run Failed: ${event.meta.error}` : 'Run Failed: The run encountered an error',
121
- 'agent.notification': event.meta?.message ? String(event.meta.message) : 'You have a new notification',
122
- 'push.enabled': 'Push notifications are now enabled!'
123
- };
124
- const providerLabel = PROVIDER_LABELS[event.provider] || 'Assistant';
125
- const sessionName = resolveSessionName(event);
126
- const message = CODE_MAP[event.code] || 'You have a new notification';
127
-
128
- return {
129
- title: sessionName || 'CloudCLI',
130
- body: `${providerLabel}: ${message}`,
131
- data: {
132
- sessionId: event.sessionId || null,
133
- code: event.code,
134
- provider: event.provider || null,
135
- sessionName,
136
- tag: `${event.provider || 'assistant'}:${event.sessionId || 'none'}:${event.code}`
137
- }
138
- };
139
- }
140
-
141
- async function sendWebPush(userId, event) {
142
- const subscriptions = pushSubscriptionsDb.getSubscriptions(userId);
143
- if (!subscriptions.length) return;
144
-
145
- const payload = JSON.stringify(buildPushBody(event));
146
-
147
- const results = await Promise.allSettled(
148
- subscriptions.map((sub) =>
149
- webPush.sendNotification(
150
- {
151
- endpoint: sub.endpoint,
152
- keys: {
153
- p256dh: sub.keys_p256dh,
154
- auth: sub.keys_auth
155
- }
156
- },
157
- payload
158
- )
159
- )
160
- );
161
-
162
- // Clean up gone subscriptions (410 Gone or 404)
163
- results.forEach((result, index) => {
164
- if (result.status === 'rejected') {
165
- const statusCode = result.reason?.statusCode;
166
- if (statusCode === 410 || statusCode === 404) {
167
- pushSubscriptionsDb.removeSubscription(subscriptions[index].endpoint);
168
- }
169
- }
170
- });
171
- }
172
-
173
- function notifyUserIfEnabled({ userId, event }) {
174
- if (!userId || !event) {
175
- return;
176
- }
177
-
178
- const preferences = notificationPreferencesDb.getPreferences(userId);
179
- if (!shouldSendPush(preferences, event)) {
180
- return;
181
- }
182
- if (isDuplicate(event)) {
183
- return;
184
- }
185
-
186
- sendWebPush(userId, event).catch((err) => {
187
- console.error('Web push send error:', err);
188
- });
189
- }
190
-
191
- function notifyRunStopped({ userId, provider, sessionId = null, stopReason = 'completed', sessionName = null }) {
192
- notifyUserIfEnabled({
193
- userId,
194
- event: createNotificationEvent({
195
- provider,
196
- sessionId,
197
- kind: 'stop',
198
- code: 'run.stopped',
199
- meta: { stopReason, sessionName },
200
- severity: 'info',
201
- dedupeKey: `${provider}:run:stop:${sessionId || 'none'}:${stopReason}`
202
- })
203
- });
204
- }
205
-
206
- function notifyRunFailed({ userId, provider, sessionId = null, error, sessionName = null }) {
207
- const errorMessage = normalizeErrorMessage(error);
208
-
209
- notifyUserIfEnabled({
210
- userId,
211
- event: createNotificationEvent({
212
- provider,
213
- sessionId,
214
- kind: 'error',
215
- code: 'run.failed',
216
- meta: { error: errorMessage, sessionName },
217
- severity: 'error',
218
- dedupeKey: `${provider}:run:error:${sessionId || 'none'}:${errorMessage}`
219
- })
220
- });
221
- }
222
-
223
1
  export {
2
+ buildNotificationPayload,
224
3
  createNotificationEvent,
225
4
  notifyUserIfEnabled,
226
5
  notifyRunStopped,
227
- notifyRunFailed
228
- };
6
+ notifyRunFailed,
7
+ } from '../modules/notifications/services/notification-orchestrator.service.js';
@@ -0,0 +1,80 @@
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 webPush from 'web-push';
8
+
9
+ import {
10
+ closeConnection,
11
+ initializeDatabase,
12
+ notificationPreferencesDb,
13
+ pushSubscriptionsDb,
14
+ sessionsDb,
15
+ userDb,
16
+ } from '../../modules/database/index.js';
17
+
18
+ import { notifyRunStopped } from '../notification-orchestrator.js';
19
+
20
+ async function withIsolatedDatabase(runTest) {
21
+ const previousDatabasePath = process.env.DATABASE_PATH;
22
+ const tempDirectory = await mkdtemp(path.join(tmpdir(), 'notification-orchestrator-'));
23
+ const databasePath = path.join(tempDirectory, 'auth.db');
24
+
25
+ closeConnection();
26
+ process.env.DATABASE_PATH = databasePath;
27
+ await initializeDatabase();
28
+
29
+ try {
30
+ await runTest();
31
+ } finally {
32
+ closeConnection();
33
+ if (previousDatabasePath === undefined) {
34
+ delete process.env.DATABASE_PATH;
35
+ } else {
36
+ process.env.DATABASE_PATH = previousDatabasePath;
37
+ }
38
+ await rm(tempDirectory, { recursive: true, force: true });
39
+ }
40
+ }
41
+
42
+ test('push payload uses the app session id when notified with a provider session id', async () => {
43
+ const originalSendNotification = webPush.sendNotification;
44
+ const sentPayloads = [];
45
+
46
+ webPush.sendNotification = async (_subscription, payload) => {
47
+ sentPayloads.push(JSON.parse(payload));
48
+ return {};
49
+ };
50
+
51
+ try {
52
+ await withIsolatedDatabase(async () => {
53
+ const user = userDb.createUser('notify-user', 'hash');
54
+ const userId = Number(user.id);
55
+
56
+ notificationPreferencesDb.updatePreferences(userId, {
57
+ channels: { webPush: true },
58
+ events: { actionRequired: true, stop: true, error: true },
59
+ });
60
+ pushSubscriptionsDb.saveSubscription(userId, 'https://example.test/push', 'p256dh', 'auth');
61
+ sessionsDb.createAppSession('app-session-1', 'claude', '/workspace/demo');
62
+ sessionsDb.assignProviderSessionId('app-session-1', 'claude-native-1');
63
+
64
+ notifyRunStopped({
65
+ userId,
66
+ provider: 'claude',
67
+ sessionId: 'claude-native-1',
68
+ stopReason: 'completed',
69
+ });
70
+
71
+ await new Promise((resolve) => setImmediate(resolve));
72
+
73
+ assert.equal(sentPayloads.length, 1);
74
+ assert.equal(sentPayloads[0]?.data?.sessionId, 'app-session-1');
75
+ assert.match(sentPayloads[0]?.data?.tag, /app-session-1/);
76
+ });
77
+ } finally {
78
+ webPush.sendNotification = originalSendNotification;
79
+ }
80
+ });
@@ -12,6 +12,8 @@ import type {
12
12
  ProviderModelsDefinition,
13
13
  ProviderMcpServer,
14
14
  ProviderSessionActiveModelChange,
15
+ ProviderSkillCreateInput,
16
+ ProviderSkillRemoveInput,
15
17
  UpsertProviderMcpServerInput,
16
18
  } from '@/shared/types.js';
17
19
 
@@ -101,6 +103,19 @@ export interface IProviderSkills {
101
103
  * Lists all skills visible to this provider for the optional workspace.
102
104
  */
103
105
  listSkills(options?: ProviderSkillListOptions): Promise<ProviderSkill[]>;
106
+
107
+ /**
108
+ * Writes one or more global user-scoped skills for this provider.
109
+ *
110
+ * Implementations should install the supplied markdown entries into the
111
+ * provider's writable user skill folder and return the normalized skill
112
+ * records that were written.
113
+ */
114
+ addSkills(input: ProviderSkillCreateInput): Promise<ProviderSkill[]>;
115
+
116
+ removeSkill(
117
+ input: ProviderSkillRemoveInput,
118
+ ): Promise<{ removed: boolean; provider: LLMProvider; directoryName: string }>;
104
119
  }
105
120
 
106
121
  // ---------------------------
@@ -0,0 +1,42 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { sliceTailPage } from '@/shared/utils.js';
5
+
6
+ const ITEMS = ['a', 'b', 'c', 'd', 'e'];
7
+
8
+ test('offset 0 returns the most recent page', () => {
9
+ const { page, hasMore } = sliceTailPage(ITEMS, 2, 0);
10
+ assert.deepEqual(page, ['d', 'e']);
11
+ assert.equal(hasMore, true);
12
+ });
13
+
14
+ test('increasing offsets walk backwards in time', () => {
15
+ const { page, hasMore } = sliceTailPage(ITEMS, 2, 2);
16
+ assert.deepEqual(page, ['b', 'c']);
17
+ assert.equal(hasMore, true);
18
+ });
19
+
20
+ test('the oldest page reports hasMore false', () => {
21
+ const { page, hasMore } = sliceTailPage(ITEMS, 2, 4);
22
+ assert.deepEqual(page, ['a']);
23
+ assert.equal(hasMore, false);
24
+ });
25
+
26
+ test('null limit returns everything', () => {
27
+ const { page, hasMore } = sliceTailPage(ITEMS, null, 0);
28
+ assert.deepEqual(page, ITEMS);
29
+ assert.equal(hasMore, false);
30
+ });
31
+
32
+ test('offsets past the start return an empty page', () => {
33
+ const { page, hasMore } = sliceTailPage(ITEMS, 3, 10);
34
+ assert.deepEqual(page, []);
35
+ assert.equal(hasMore, false);
36
+ });
37
+
38
+ test('zero limit returns an empty page but keeps hasMore accurate', () => {
39
+ const { page, hasMore } = sliceTailPage(ITEMS, 0, 0);
40
+ assert.deepEqual(page, []);
41
+ assert.equal(hasMore, true);
42
+ });
@@ -68,7 +68,7 @@ export type AuthenticatedWebSocketRequest = IncomingMessage & {
68
68
  export type LLMProvider = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode';
69
69
 
70
70
  /**
71
- * One selectable model row (matches the documentation `public/modelConstants.js` option shape).
71
+ * One selectable model row in a provider model catalog.
72
72
  */
73
73
  export type ProviderModelOption = {
74
74
  value: string;
@@ -175,6 +175,30 @@ export type MessageKind =
175
175
  | 'interactive_prompt'
176
176
  | 'task_notification';
177
177
 
178
+ /**
179
+ * Event kinds added by the chat gateway layer on top of provider message kinds.
180
+ *
181
+ * These are app-level realtime events (subscription acks, sidebar deltas,
182
+ * project loading progress, protocol failures) that are not produced by any
183
+ * provider adapter. Together with `MessageKind` they form the complete set of
184
+ * `kind` values a websocket client can receive, so the frontend only ever
185
+ * needs one kind-based switch.
186
+ */
187
+ export type GatewayEventKind =
188
+ | 'chat_subscribed'
189
+ | 'session_upserted'
190
+ | 'loading_progress'
191
+ | 'protocol_error';
192
+
193
+ /**
194
+ * Complete set of `kind` values emitted to websocket clients.
195
+ *
196
+ * Every server-to-client websocket frame carries a `kind` from this union.
197
+ * Provider runtimes emit `MessageKind` values; gateway services emit
198
+ * `GatewayEventKind` values.
199
+ */
200
+ export type ServerEventKind = MessageKind | GatewayEventKind;
201
+
178
202
  /**
179
203
  * Provider-neutral message envelope used in REST responses and realtime channels.
180
204
  *
@@ -187,6 +211,13 @@ export type NormalizedMessage = {
187
211
  timestamp: string;
188
212
  provider: LLMProvider;
189
213
  kind: MessageKind;
214
+ /**
215
+ * Monotonic per-run sequence number assigned by the chat run registry when a
216
+ * live event is forwarded to the websocket. History messages loaded over
217
+ * REST do not carry it. Clients use it with `chat.subscribe` to replay only
218
+ * the live events they missed across websocket reconnects.
219
+ */
220
+ seq?: number;
190
221
  role?: 'user' | 'assistant';
191
222
  content?: string;
192
223
  /**
@@ -237,11 +268,18 @@ export type NormalizedMessage = {
237
268
  *
238
269
  * Consumers should pass provider-specific lookup hints (`projectPath`) only
239
270
  * when the selected provider requires them.
271
+ *
272
+ * `providerSessionId` is the provider-native session id from the sessions
273
+ * index (transcript file name / provider database key). Provider adapters
274
+ * must use it — never the app-facing session id they were called with — when
275
+ * matching transcript rows on disk, because app-created sessions use an
276
+ * app-allocated id that the provider has never seen.
240
277
  */
241
278
  export type FetchHistoryOptions = {
242
279
  projectPath?: string;
243
280
  limit?: number | null;
244
281
  offset?: number;
282
+ providerSessionId?: string;
245
283
  };
246
284
 
247
285
  /**
@@ -282,6 +320,51 @@ export type ProviderSkillListOptions = {
282
320
  workspacePath?: string;
283
321
  };
284
322
 
323
+ /**
324
+ * One supporting file bundled with an uploaded provider skill.
325
+ *
326
+ * `relativePath` is resolved below the installed skill directory and must never
327
+ * be absolute or contain traversal segments. Text files may use `utf8`; binary
328
+ * scripts and assets should use `base64` so JSON transport does not corrupt
329
+ * their bytes.
330
+ */
331
+ export type ProviderSkillCreateFile = {
332
+ relativePath: string;
333
+ content: string;
334
+ encoding: 'utf8' | 'base64';
335
+ };
336
+
337
+ /**
338
+ * One skill markdown payload submitted for provider-managed installation.
339
+ *
340
+ * `content` is the raw markdown body that will be written to `SKILL.md`.
341
+ * `directoryName` lets callers control the target folder name explicitly when
342
+ * they want stable filesystem paths that differ from the markdown front matter
343
+ * `name` field. `fileName` is optional upload metadata used only as a final
344
+ * fallback when no directory name or front matter name is present. `files`
345
+ * carries scripts, references, and other files from a complete skill folder.
346
+ */
347
+ export type ProviderSkillCreateEntry = {
348
+ content: string;
349
+ directoryName?: string;
350
+ fileName?: string;
351
+ files?: ProviderSkillCreateFile[];
352
+ };
353
+
354
+ /**
355
+ * Shared input accepted by provider skill creation operations.
356
+ *
357
+ * The service layer batches multiple skill definitions in one request. Each
358
+ * entry can contain only markdown or a complete skill folder.
359
+ */
360
+ export type ProviderSkillCreateInput = {
361
+ entries: ProviderSkillCreateEntry[];
362
+ };
363
+
364
+ export type ProviderSkillRemoveInput = {
365
+ directoryName: string;
366
+ };
367
+
285
368
  /**
286
369
  * Normalized skill record returned by provider skill adapters.
287
370
  *
@@ -346,6 +346,84 @@ export function createNormalizedMessage(fields: NormalizedMessageInput): Normali
346
346
  };
347
347
  }
348
348
 
349
+ /**
350
+ * Build the unified terminal `complete` lifecycle message.
351
+ *
352
+ * Contract: every provider run ends with exactly one `complete` (the
353
+ * abort-session handler emits it on behalf of cancelled runs, so aborted runs
354
+ * must NOT emit their own). The frontend treats `complete` as the only
355
+ * terminal signal and never needs provider-specific handling:
356
+ *
357
+ * - `sessionId` — the id the client knows this run by ('' if never discovered)
358
+ * - `actualSessionId` — canonical id after the run; equals `sessionId` unless
359
+ * the provider rewrote it mid-run
360
+ * - `exitCode` — 0 on success; a missing/null code (e.g. killed process)
361
+ * is reported as failure
362
+ * - `success` — exitCode === 0 and not aborted
363
+ * - `aborted` — run was cancelled by the user
364
+ */
365
+ export function createCompleteMessage(opts: {
366
+ provider: NormalizedMessage['provider'];
367
+ sessionId?: string | null;
368
+ actualSessionId?: string | null;
369
+ exitCode?: number | null;
370
+ aborted?: boolean;
371
+ }): NormalizedMessage {
372
+ const exitCode = typeof opts.exitCode === 'number' ? opts.exitCode : 1;
373
+ const aborted = Boolean(opts.aborted);
374
+
375
+ return createNormalizedMessage({
376
+ kind: 'complete',
377
+ provider: opts.provider,
378
+ sessionId: opts.sessionId || null,
379
+ actualSessionId: opts.actualSessionId || opts.sessionId || null,
380
+ exitCode,
381
+ success: exitCode === 0 && !aborted,
382
+ aborted,
383
+ });
384
+ }
385
+
386
+ // ---------------------------
387
+ //----------------- CONVERSATION HISTORY PAGINATION UTILITIES ------------
388
+ /**
389
+ * Slices one page from the END of a chronologically ordered message list.
390
+ *
391
+ * This is the single pagination contract for conversation history across all
392
+ * providers: `offset = 0` returns the most recent `limit` items, increasing
393
+ * offsets walk backwards in time (for "scroll up to load older" UIs), and a
394
+ * `null` limit returns everything. Items must already be sorted oldest-first;
395
+ * the returned page preserves that order.
396
+ *
397
+ * Every provider history reader must use this helper instead of slicing
398
+ * manually so `offset`/`limit` query params behave identically regardless of
399
+ * which provider produced the session.
400
+ */
401
+ export function sliceTailPage<T>(
402
+ items: T[],
403
+ limit: number | null,
404
+ offset: number,
405
+ ): { page: T[]; hasMore: boolean } {
406
+ const total = items.length;
407
+ const normalizedOffset = Math.max(0, offset);
408
+
409
+ if (limit === null) {
410
+ // A null limit returns the full list; offset still trims newest entries
411
+ // so "everything before the page I already have" stays expressible.
412
+ const end = Math.max(0, total - normalizedOffset);
413
+ return {
414
+ page: items.slice(0, end),
415
+ hasMore: false,
416
+ };
417
+ }
418
+
419
+ const end = Math.max(0, total - normalizedOffset);
420
+ const start = Math.max(0, end - Math.max(0, limit));
421
+ return {
422
+ page: items.slice(start, end),
423
+ hasMore: start > 0,
424
+ };
425
+ }
426
+
349
427
  // ---------------------------
350
428
  //----------------- MCP CONFIG PARSING UTILITIES ------------
351
429
  /**
@@ -879,9 +957,25 @@ export async function readProviderSkillMarkdownDefinition(
879
957
  skillPath: string,
880
958
  ): Promise<{ name: string; description: string }> {
881
959
  const content = await readFile(skillPath, 'utf8');
960
+ return readProviderSkillMarkdownDefinitionFromContent(
961
+ content,
962
+ path.basename(path.dirname(skillPath)),
963
+ );
964
+ }
965
+
966
+ /**
967
+ * Reads the `name` and `description` fields from raw skill markdown content.
968
+ *
969
+ * This keeps filesystem discovery and newly uploaded skill creation aligned on
970
+ * the same front matter parsing rules. `fallbackName` is used when the markdown
971
+ * omits a `name` field so callers still get a stable, non-empty skill id.
972
+ */
973
+ export function readProviderSkillMarkdownDefinitionFromContent(
974
+ content: string,
975
+ fallbackName: string,
976
+ ): { name: string; description: string } {
882
977
  const parsed = parseFrontMatter(content);
883
978
  const data = readObjectRecord(parsed.data) ?? {};
884
- const fallbackName = path.basename(path.dirname(skillPath));
885
979
 
886
980
  return {
887
981
  name: readOptionalString(data.name) ?? fallbackName,