@cloudcli-ai/cloudcli 1.30.0 → 1.31.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 (238) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/index-B37ufcoi.js +1262 -0
  3. package/dist/assets/{index-DI22MZMb.css → index-DF8TTVHl.css} +1 -1
  4. package/dist/index.html +2 -2
  5. package/dist-server/server/index.js +115 -955
  6. package/dist-server/server/index.js.map +1 -1
  7. package/dist-server/server/middleware/auth.js +1 -1
  8. package/dist-server/server/middleware/auth.js.map +1 -1
  9. package/dist-server/server/modules/database/connection.js +125 -0
  10. package/dist-server/server/modules/database/connection.js.map +1 -0
  11. package/dist-server/server/modules/database/index.js +13 -0
  12. package/dist-server/server/modules/database/index.js.map +1 -0
  13. package/dist-server/server/modules/database/init-db.js +18 -0
  14. package/dist-server/server/modules/database/init-db.js.map +1 -0
  15. package/dist-server/server/modules/database/migrations.js +370 -0
  16. package/dist-server/server/modules/database/migrations.js.map +1 -0
  17. package/dist-server/server/modules/database/repositories/api-keys.js +72 -0
  18. package/dist-server/server/modules/database/repositories/api-keys.js.map +1 -0
  19. package/dist-server/server/modules/database/repositories/app-config.js +47 -0
  20. package/dist-server/server/modules/database/repositories/app-config.js.map +1 -0
  21. package/dist-server/server/modules/database/repositories/credentials.js +68 -0
  22. package/dist-server/server/modules/database/repositories/credentials.js.map +1 -0
  23. package/dist-server/server/modules/database/repositories/github-tokens.js +54 -0
  24. package/dist-server/server/modules/database/repositories/github-tokens.js.map +1 -0
  25. package/dist-server/server/modules/database/repositories/notification-preferences.js +72 -0
  26. package/dist-server/server/modules/database/repositories/notification-preferences.js.map +1 -0
  27. package/dist-server/server/modules/database/repositories/projects.db.integration.test.js +64 -0
  28. package/dist-server/server/modules/database/repositories/projects.db.integration.test.js.map +1 -0
  29. package/dist-server/server/modules/database/repositories/projects.db.js +160 -0
  30. package/dist-server/server/modules/database/repositories/projects.db.js.map +1 -0
  31. package/dist-server/server/modules/database/repositories/push-subscriptions.js +49 -0
  32. package/dist-server/server/modules/database/repositories/push-subscriptions.js.map +1 -0
  33. package/dist-server/server/modules/database/repositories/scan-state.db.js +31 -0
  34. package/dist-server/server/modules/database/repositories/scan-state.db.js.map +1 -0
  35. package/dist-server/server/modules/database/repositories/sessions.db.js +109 -0
  36. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -0
  37. package/dist-server/server/modules/database/repositories/users.js +88 -0
  38. package/dist-server/server/modules/database/repositories/users.js.map +1 -0
  39. package/dist-server/server/modules/database/repositories/vapid-keys.js +38 -0
  40. package/dist-server/server/modules/database/repositories/vapid-keys.js.map +1 -0
  41. package/dist-server/server/modules/database/schema.js +143 -0
  42. package/dist-server/server/modules/database/schema.js.map +1 -0
  43. package/dist-server/server/modules/projects/index.js +4 -0
  44. package/dist-server/server/modules/projects/index.js.map +1 -0
  45. package/dist-server/server/modules/projects/projects.routes.js +188 -0
  46. package/dist-server/server/modules/projects/projects.routes.js.map +1 -0
  47. package/dist-server/server/modules/projects/services/project-clone.service.js +219 -0
  48. package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -0
  49. package/dist-server/server/modules/projects/services/project-delete.service.js +65 -0
  50. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -0
  51. package/dist-server/server/modules/projects/services/project-management.service.js +93 -0
  52. package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -0
  53. package/dist-server/server/modules/projects/services/project-star.service.js +60 -0
  54. package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -0
  55. package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js +167 -0
  56. package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js.map +1 -0
  57. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +169 -0
  58. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -0
  59. package/dist-server/server/modules/projects/tests/project-clone.service.test.js +126 -0
  60. package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -0
  61. package/dist-server/server/modules/projects/tests/project-management.service.test.js +85 -0
  62. package/dist-server/server/modules/projects/tests/project-management.service.test.js.map +1 -0
  63. package/dist-server/server/modules/projects/tests/project-star.service.test.js +95 -0
  64. package/dist-server/server/modules/projects/tests/project-star.service.test.js.map +1 -0
  65. package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js +87 -0
  66. package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js.map +1 -0
  67. package/dist-server/server/modules/providers/index.js +4 -0
  68. package/dist-server/server/modules/providers/index.js.map +1 -0
  69. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +63 -0
  70. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -0
  71. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +144 -7
  72. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  73. package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
  74. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  75. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +71 -0
  76. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -0
  77. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +225 -13
  78. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  79. package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
  80. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  81. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +127 -0
  82. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -0
  83. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +2 -0
  84. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
  85. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +296 -0
  86. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +1 -0
  87. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +277 -10
  88. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
  89. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +2 -0
  90. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
  91. package/dist-server/server/modules/providers/provider.routes.js +172 -0
  92. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  93. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +868 -0
  94. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -0
  95. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +56 -0
  96. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -0
  97. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +234 -0
  98. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -0
  99. package/dist-server/server/modules/providers/services/sessions.service.js +78 -3
  100. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  101. package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
  102. package/dist-server/server/modules/websocket/index.js +3 -0
  103. package/dist-server/server/modules/websocket/index.js.map +1 -0
  104. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +188 -0
  105. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -0
  106. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js +52 -0
  107. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js.map +1 -0
  108. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +334 -0
  109. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -0
  110. package/dist-server/server/modules/websocket/services/websocket-auth.service.js +32 -0
  111. package/dist-server/server/modules/websocket/services/websocket-auth.service.js.map +1 -0
  112. package/dist-server/server/modules/websocket/services/websocket-server.service.js +36 -0
  113. package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -0
  114. package/dist-server/server/modules/websocket/services/websocket-state.service.js +14 -0
  115. package/dist-server/server/modules/websocket/services/websocket-state.service.js.map +1 -0
  116. package/dist-server/server/modules/websocket/services/websocket-writer.service.js +32 -0
  117. package/dist-server/server/modules/websocket/services/websocket-writer.service.js.map +1 -0
  118. package/dist-server/server/routes/agent.js +10 -17
  119. package/dist-server/server/routes/agent.js.map +1 -1
  120. package/dist-server/server/routes/auth.js +3 -1
  121. package/dist-server/server/routes/auth.js.map +1 -1
  122. package/dist-server/server/routes/commands.js +1 -1
  123. package/dist-server/server/routes/commands.js.map +1 -1
  124. package/dist-server/server/routes/gemini.js +2 -2
  125. package/dist-server/server/routes/gemini.js.map +1 -1
  126. package/dist-server/server/routes/git.js +33 -29
  127. package/dist-server/server/routes/git.js.map +1 -1
  128. package/dist-server/server/routes/settings.js +1 -1
  129. package/dist-server/server/routes/settings.js.map +1 -1
  130. package/dist-server/server/routes/taskmaster.js +97 -110
  131. package/dist-server/server/routes/taskmaster.js.map +1 -1
  132. package/dist-server/server/routes/user.js +1 -1
  133. package/dist-server/server/routes/user.js.map +1 -1
  134. package/dist-server/server/services/notification-orchestrator.js +2 -2
  135. package/dist-server/server/services/notification-orchestrator.js.map +1 -1
  136. package/dist-server/server/services/vapid-keys.js +2 -1
  137. package/dist-server/server/services/vapid-keys.js.map +1 -1
  138. package/dist-server/server/shared/types.js +0 -1
  139. package/dist-server/server/shared/types.js.map +1 -1
  140. package/dist-server/server/shared/utils.js +408 -7
  141. package/dist-server/server/shared/utils.js.map +1 -1
  142. package/dist-server/server/utils/taskmaster-websocket.js +18 -12
  143. package/dist-server/server/utils/taskmaster-websocket.js.map +1 -1
  144. package/dist-server/shared/modelConstants.js +2 -1
  145. package/dist-server/shared/modelConstants.js.map +1 -1
  146. package/package.json +3 -1
  147. package/server/index.js +146 -1012
  148. package/server/middleware/auth.js +1 -1
  149. package/server/modules/database/connection.ts +143 -0
  150. package/server/modules/database/index.ts +12 -0
  151. package/server/modules/database/init-db.ts +17 -0
  152. package/server/modules/database/migrations.ts +442 -0
  153. package/server/modules/database/repositories/api-keys.ts +119 -0
  154. package/server/modules/database/repositories/app-config.ts +53 -0
  155. package/server/modules/database/repositories/credentials.ts +106 -0
  156. package/server/modules/database/repositories/github-tokens.ts +100 -0
  157. package/server/modules/database/repositories/notification-preferences.ts +103 -0
  158. package/server/modules/database/repositories/projects.db.integration.test.ts +72 -0
  159. package/server/modules/database/repositories/projects.db.ts +183 -0
  160. package/server/modules/database/repositories/push-subscriptions.ts +80 -0
  161. package/server/modules/database/repositories/scan-state.db.ts +42 -0
  162. package/server/modules/database/repositories/sessions.db.ts +174 -0
  163. package/server/modules/database/repositories/users.ts +140 -0
  164. package/server/modules/database/repositories/vapid-keys.ts +57 -0
  165. package/server/modules/database/schema.ts +152 -0
  166. package/server/modules/projects/index.ts +6 -0
  167. package/server/modules/projects/projects.routes.ts +247 -0
  168. package/server/modules/projects/services/project-clone.service.ts +321 -0
  169. package/server/modules/projects/services/project-delete.service.ts +75 -0
  170. package/server/modules/projects/services/project-management.service.ts +150 -0
  171. package/server/modules/projects/services/project-star.service.ts +78 -0
  172. package/server/modules/projects/services/projects-has-taskmaster.service.ts +248 -0
  173. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +285 -0
  174. package/server/modules/projects/tests/project-clone.service.test.ts +183 -0
  175. package/server/modules/projects/tests/project-management.service.test.ts +117 -0
  176. package/server/modules/projects/tests/project-star.service.test.ts +123 -0
  177. package/server/modules/projects/tests/projects-has-taskmaster.service.test.ts +105 -0
  178. package/server/modules/providers/index.ts +4 -0
  179. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +110 -0
  180. package/server/modules/providers/list/claude/claude-sessions.provider.ts +182 -13
  181. package/server/modules/providers/list/claude/claude.provider.ts +3 -1
  182. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +119 -0
  183. package/server/modules/providers/list/codex/codex-sessions.provider.ts +262 -17
  184. package/server/modules/providers/list/codex/codex.provider.ts +3 -1
  185. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +176 -0
  186. package/server/modules/providers/list/cursor/cursor.provider.ts +3 -1
  187. package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +401 -0
  188. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +327 -13
  189. package/server/modules/providers/list/gemini/gemini.provider.ts +3 -1
  190. package/server/modules/providers/provider.routes.ts +220 -12
  191. package/server/modules/providers/services/session-conversations-search.service.ts +1167 -0
  192. package/server/modules/providers/services/session-synchronizer.service.ts +74 -0
  193. package/server/modules/providers/services/sessions-watcher.service.ts +283 -0
  194. package/server/modules/providers/services/sessions.service.ts +89 -4
  195. package/server/modules/providers/shared/base/abstract.provider.ts +8 -1
  196. package/server/modules/websocket/README.md +267 -0
  197. package/server/modules/websocket/index.ts +2 -0
  198. package/server/modules/websocket/services/chat-websocket.service.ts +271 -0
  199. package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +65 -0
  200. package/server/modules/websocket/services/shell-websocket.service.ts +453 -0
  201. package/server/modules/websocket/services/websocket-auth.service.ts +54 -0
  202. package/server/modules/websocket/services/websocket-server.service.ts +58 -0
  203. package/server/modules/websocket/services/websocket-state.service.ts +16 -0
  204. package/server/modules/websocket/services/websocket-writer.service.ts +38 -0
  205. package/server/routes/agent.js +11 -16
  206. package/server/routes/auth.js +4 -2
  207. package/server/routes/commands.js +1 -1
  208. package/server/routes/gemini.js +3 -2
  209. package/server/routes/git.js +33 -28
  210. package/server/routes/settings.js +1 -1
  211. package/server/routes/taskmaster.js +108 -111
  212. package/server/routes/user.js +1 -1
  213. package/server/services/notification-orchestrator.js +3 -2
  214. package/server/services/vapid-keys.js +2 -1
  215. package/server/shared/interfaces.ts +39 -1
  216. package/server/shared/types.ts +185 -30
  217. package/server/shared/utils.ts +485 -9
  218. package/server/utils/taskmaster-websocket.js +19 -13
  219. package/shared/modelConstants.js +2 -1
  220. package/dist/assets/index-RX-W_g_Q.js +0 -1262
  221. package/dist-server/server/database/db.js +0 -535
  222. package/dist-server/server/database/db.js.map +0 -1
  223. package/dist-server/server/database/schema.js +0 -97
  224. package/dist-server/server/database/schema.js.map +0 -1
  225. package/dist-server/server/projects.js +0 -2325
  226. package/dist-server/server/projects.js.map +0 -1
  227. package/dist-server/server/routes/codex.js +0 -18
  228. package/dist-server/server/routes/codex.js.map +0 -1
  229. package/dist-server/server/routes/messages.js +0 -56
  230. package/dist-server/server/routes/messages.js.map +0 -1
  231. package/dist-server/server/routes/projects.js +0 -505
  232. package/dist-server/server/routes/projects.js.map +0 -1
  233. package/server/database/db.js +0 -593
  234. package/server/database/schema.js +0 -102
  235. package/server/projects.js +0 -2555
  236. package/server/routes/codex.js +0 -19
  237. package/server/routes/messages.js +0 -61
  238. package/server/routes/projects.js +0 -548
@@ -0,0 +1,74 @@
1
+ import { scanStateDb } from '@/modules/database/index.js';
2
+ import { providerRegistry } from '@/modules/providers/provider.registry.js';
3
+ import type { LLMProvider } from '@/shared/types.js';
4
+
5
+ type SessionSynchronizeResult = {
6
+ processedByProvider: Record<LLMProvider, number>;
7
+ failures: string[];
8
+ };
9
+
10
+ /**
11
+ * Orchestrates provider-specific session indexers and indexed-session lifecycle operations.
12
+ */
13
+ export const sessionSynchronizerService = {
14
+ /**
15
+ * Runs all provider synchronizers and updates scan_state.last_scanned_at.
16
+ */
17
+ async synchronizeSessions(): Promise<SessionSynchronizeResult> {
18
+ const lastScanAt = scanStateDb.getLastScannedAt();
19
+ const scanBoundary = new Date();
20
+ const processedByProvider: Record<LLMProvider, number> = {
21
+ claude: 0,
22
+ codex: 0,
23
+ cursor: 0,
24
+ gemini: 0,
25
+ };
26
+ const failures: string[] = [];
27
+
28
+ const results = await Promise.allSettled(
29
+ providerRegistry.listProviders().map(async (provider) => ({
30
+ provider: provider.id,
31
+ processed: await provider.sessionSynchronizer.synchronize(lastScanAt ?? undefined),
32
+ }))
33
+ );
34
+
35
+ for (const result of results) {
36
+ if (result.status === 'fulfilled') {
37
+ processedByProvider[result.value.provider] = result.value.processed;
38
+ continue;
39
+ }
40
+
41
+ const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
42
+ failures.push(reason);
43
+ }
44
+
45
+ if (failures.length === 0) {
46
+ scanStateDb.updateLastScannedAt(scanBoundary);
47
+ } else {
48
+ console.warn(
49
+ `[Sessions] Skipping scan_state cursor advance because ${failures.length} provider sync(s) failed.`,
50
+ );
51
+ }
52
+
53
+ return {
54
+ processedByProvider,
55
+ failures,
56
+ };
57
+ },
58
+
59
+ /**
60
+ * Indexes one provider artifact file without running a full provider rescan.
61
+ */
62
+ async synchronizeProviderFile(
63
+ provider: LLMProvider,
64
+ filePath: string
65
+ ): Promise<{ provider: LLMProvider; indexed: boolean; sessionId: string | null }> {
66
+ const resolvedProvider = providerRegistry.resolveProvider(provider);
67
+ const sessionId = await resolvedProvider.sessionSynchronizer.synchronizeFile(filePath);
68
+ return {
69
+ provider,
70
+ indexed: Boolean(sessionId),
71
+ sessionId,
72
+ };
73
+ },
74
+ };
@@ -0,0 +1,283 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { promises as fsPromises } from 'node:fs';
4
+
5
+ import chokidar, { type FSWatcher } from 'chokidar';
6
+
7
+ import { sessionSynchronizerService } from '@/modules/providers/services/session-synchronizer.service.js';
8
+ import { WS_OPEN_STATE, connectedClients } from '@/modules/websocket/index.js';
9
+ import type { LLMProvider } from '@/shared/types.js';
10
+ import { getProjectsWithSessions } from '@/modules/projects/index.js';
11
+
12
+ type WatcherEventType = 'add' | 'change';
13
+
14
+ const PROVIDER_WATCH_PATHS: Array<{ provider: LLMProvider; rootPath: string }> = [
15
+ {
16
+ provider: 'claude',
17
+ rootPath: path.join(os.homedir(), '.claude', 'projects'),
18
+ },
19
+ {
20
+ provider: 'cursor',
21
+ rootPath: path.join(os.homedir(), '.cursor', 'chats'),
22
+ },
23
+ {
24
+ provider: 'codex',
25
+ rootPath: path.join(os.homedir(), '.codex', 'sessions'),
26
+ },
27
+ {
28
+ provider: 'gemini',
29
+ rootPath: path.join(os.homedir(), '.gemini', 'sessions'),
30
+ },
31
+ {
32
+ provider: 'gemini',
33
+ rootPath: path.join(os.homedir(), '.gemini', 'tmp'),
34
+ },
35
+ ];
36
+
37
+ const WATCHER_IGNORED_PATTERNS = [
38
+ '**/node_modules/**',
39
+ '**/.git/**',
40
+ '**/dist/**',
41
+ '**/build/**',
42
+ '**/*.tmp',
43
+ '**/*.swp',
44
+ '**/.DS_Store',
45
+ ];
46
+
47
+ const PROJECTS_UPDATE_DEBOUNCE_MS = 500;
48
+ const PROJECTS_UPDATE_MAX_WAIT_MS = 2_000;
49
+
50
+ const watchers: FSWatcher[] = [];
51
+
52
+ type PendingWatcherUpdate = {
53
+ providers: Set<LLMProvider>;
54
+ changeTypes: Set<WatcherEventType>;
55
+ updatedSessionIds: Set<string>;
56
+ };
57
+
58
+ let pendingWatcherUpdate: PendingWatcherUpdate | null = null;
59
+ let pendingWatcherUpdateStartedAt: number | null = null;
60
+ let pendingWatcherFlushTimer: ReturnType<typeof setTimeout> | null = null;
61
+ let watcherRefreshInFlight = false;
62
+ let watcherRescheduleAfterRefresh = false;
63
+
64
+ /**
65
+ * Filters watcher events to provider-specific session artifact file types.
66
+ */
67
+ function isWatcherTargetFile(provider: LLMProvider, filePath: string): boolean {
68
+ if (provider === 'gemini') {
69
+ return filePath.endsWith('.json') || filePath.endsWith('.jsonl');
70
+ }
71
+
72
+ return filePath.endsWith('.jsonl');
73
+ }
74
+
75
+ function clearPendingWatcherFlushTimer(): void {
76
+ if (pendingWatcherFlushTimer) {
77
+ clearTimeout(pendingWatcherFlushTimer);
78
+ pendingWatcherFlushTimer = null;
79
+ }
80
+ }
81
+
82
+ function schedulePendingWatcherFlush(): void {
83
+ if (!pendingWatcherUpdate) {
84
+ return;
85
+ }
86
+
87
+ const now = Date.now();
88
+ if (pendingWatcherUpdateStartedAt === null) {
89
+ pendingWatcherUpdateStartedAt = now;
90
+ }
91
+
92
+ const elapsed = now - pendingWatcherUpdateStartedAt;
93
+ const remainingMaxWait = Math.max(0, PROJECTS_UPDATE_MAX_WAIT_MS - elapsed);
94
+ const delay = Math.min(PROJECTS_UPDATE_DEBOUNCE_MS, remainingMaxWait);
95
+
96
+ clearPendingWatcherFlushTimer();
97
+ pendingWatcherFlushTimer = setTimeout(() => {
98
+ void flushPendingWatcherUpdate();
99
+ }, delay);
100
+ }
101
+
102
+ function queuePendingWatcherUpdate(
103
+ eventType: WatcherEventType,
104
+ provider: LLMProvider,
105
+ updatedSessionId: string | null
106
+ ): void {
107
+ if (!pendingWatcherUpdate) {
108
+ pendingWatcherUpdate = {
109
+ providers: new Set<LLMProvider>(),
110
+ changeTypes: new Set<WatcherEventType>(),
111
+ updatedSessionIds: new Set<string>(),
112
+ };
113
+ }
114
+
115
+ pendingWatcherUpdate.providers.add(provider);
116
+ pendingWatcherUpdate.changeTypes.add(eventType);
117
+ if (updatedSessionId) {
118
+ pendingWatcherUpdate.updatedSessionIds.add(updatedSessionId);
119
+ }
120
+
121
+ schedulePendingWatcherFlush();
122
+ }
123
+
124
+ async function flushPendingWatcherUpdate(): Promise<void> {
125
+ clearPendingWatcherFlushTimer();
126
+
127
+ if (!pendingWatcherUpdate) {
128
+ return;
129
+ }
130
+
131
+ if (watcherRefreshInFlight) {
132
+ watcherRescheduleAfterRefresh = true;
133
+ return;
134
+ }
135
+
136
+ const queuedUpdate = pendingWatcherUpdate;
137
+ pendingWatcherUpdate = null;
138
+ pendingWatcherUpdateStartedAt = null;
139
+ watcherRefreshInFlight = true;
140
+
141
+ try {
142
+ const updatedProjects = await getProjectsWithSessions({ skipSynchronization: true });
143
+ const changeTypes = Array.from(queuedUpdate.changeTypes);
144
+ const watchProviders = Array.from(queuedUpdate.providers);
145
+ const updatedSessionIds = Array.from(queuedUpdate.updatedSessionIds);
146
+
147
+ // Backward-compatible fields stay populated with the first queued values.
148
+ const updateMessage = JSON.stringify({
149
+ type: 'projects_updated',
150
+ projects: updatedProjects,
151
+ timestamp: new Date().toISOString(),
152
+ changeType: changeTypes[0] ?? 'change',
153
+ updatedSessionId: updatedSessionIds[0] ?? undefined,
154
+ watchProvider: watchProviders[0] ?? undefined,
155
+ changeTypes,
156
+ updatedSessionIds,
157
+ watchProviders,
158
+ batched: true,
159
+ });
160
+
161
+ connectedClients.forEach(client => {
162
+ if (client.readyState === WS_OPEN_STATE) {
163
+ client.send(updateMessage);
164
+ }
165
+ });
166
+ } catch (error) {
167
+ const message = error instanceof Error ? error.message : String(error);
168
+ console.error('Session watcher refresh failed while broadcasting projects_updated', { error: message });
169
+ } finally {
170
+ watcherRefreshInFlight = false;
171
+
172
+ if (pendingWatcherUpdate || watcherRescheduleAfterRefresh) {
173
+ watcherRescheduleAfterRefresh = false;
174
+ schedulePendingWatcherFlush();
175
+ }
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Handles file watcher updates and triggers provider file-level synchronization.
181
+ */
182
+ async function onUpdate(
183
+ eventType: WatcherEventType,
184
+ filePath: string,
185
+ provider: LLMProvider
186
+ ): Promise<void> {
187
+ if (!isWatcherTargetFile(provider, filePath)) {
188
+ return;
189
+ }
190
+
191
+ try {
192
+ const result = await sessionSynchronizerService.synchronizeProviderFile(provider, filePath);
193
+ if (!result.indexed) {
194
+ return;
195
+ }
196
+
197
+ console.log(`Session synchronization triggered by ${eventType} event for provider "${provider}"`, {
198
+ filePath,
199
+ sessionId: result.sessionId,
200
+ });
201
+ queuePendingWatcherUpdate(eventType, provider, result.sessionId);
202
+ } catch (error) {
203
+ const message = error instanceof Error ? error.message : String(error);
204
+ console.error(`Session watcher sync failed for provider "${provider}"`, {
205
+ eventType,
206
+ filePath,
207
+ error: message,
208
+ });
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Starts provider filesystem watchers and performs initial DB synchronization.
214
+ */
215
+ export async function initializeSessionsWatcher(): Promise<void> {
216
+ console.log('Setting up session watchers');
217
+
218
+ const initialSync = await sessionSynchronizerService.synchronizeSessions();
219
+ console.log('Initial session synchronization complete', {
220
+ processedByProvider: initialSync.processedByProvider,
221
+ failures: initialSync.failures,
222
+ });
223
+
224
+ for (const { provider, rootPath } of PROVIDER_WATCH_PATHS) {
225
+ try {
226
+ await fsPromises.mkdir(rootPath, { recursive: true });
227
+
228
+ const watcher = chokidar.watch(rootPath, {
229
+ ignored: WATCHER_IGNORED_PATTERNS,
230
+ persistent: true,
231
+ ignoreInitial: true,
232
+ followSymlinks: false,
233
+ depth: 6,
234
+ usePolling: true,
235
+ interval: 6_000,
236
+ binaryInterval: 6_000,
237
+ });
238
+
239
+ watcher
240
+ .on('add', (filePath: string) => {
241
+ void onUpdate('add', filePath, provider);
242
+ })
243
+ .on('change', (filePath: string) => {
244
+ void onUpdate('change', filePath, provider);
245
+ })
246
+ .on('error', (error: unknown) => {
247
+ const message = error instanceof Error ? error.message : String(error);
248
+ console.error(`Session watcher error for provider "${provider}"`, { error: message });
249
+ });
250
+
251
+ watchers.push(watcher);
252
+ } catch (error) {
253
+ const message = error instanceof Error ? error.message : String(error);
254
+ console.error(`Failed to initialize session watcher for provider "${provider}"`, {
255
+ rootPath,
256
+ error: message,
257
+ });
258
+ }
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Stops all active provider session watchers.
264
+ */
265
+ export async function closeSessionsWatcher(): Promise<void> {
266
+ clearPendingWatcherFlushTimer();
267
+
268
+ await Promise.all(
269
+ watchers.map(async (watcher) => {
270
+ try {
271
+ await watcher.close();
272
+ } catch (error) {
273
+ const message = error instanceof Error ? error.message : String(error);
274
+ console.error('Failed to close session watcher', { error: message });
275
+ }
276
+ })
277
+ );
278
+ watchers.length = 0;
279
+ pendingWatcherUpdate = null;
280
+ pendingWatcherUpdateStartedAt = null;
281
+ watcherRefreshInFlight = false;
282
+ watcherRescheduleAfterRefresh = false;
283
+ }
@@ -1,3 +1,6 @@
1
+ import fsp from 'node:fs/promises';
2
+
3
+ import { sessionsDb } from '@/modules/database/index.js';
1
4
  import { providerRegistry } from '@/modules/providers/provider.registry.js';
2
5
  import type {
3
6
  FetchHistoryOptions,
@@ -5,6 +8,23 @@ import type {
5
8
  LLMProvider,
6
9
  NormalizedMessage,
7
10
  } from '@/shared/types.js';
11
+ import { AppError } from '@/shared/utils.js';
12
+
13
+ /**
14
+ * Removes one file if it exists.
15
+ */
16
+ async function removeFileIfExists(filePath: string): Promise<boolean> {
17
+ try {
18
+ await fsp.unlink(filePath);
19
+ return true;
20
+ } catch (error) {
21
+ const code = (error as NodeJS.ErrnoException).code;
22
+ if (code === 'ENOENT') {
23
+ return false;
24
+ }
25
+ throw error;
26
+ }
27
+ }
8
28
 
9
29
  /**
10
30
  * Application service for provider-backed session message operations.
@@ -33,13 +53,78 @@ export const sessionsService = {
33
53
  },
34
54
 
35
55
  /**
36
- * Fetches normalized persisted session history for one provider/session pair.
56
+ * Fetches persisted history by session id.
57
+ *
58
+ * Provider and provider-specific lookup hints are resolved from the indexed
59
+ * session metadata in the database.
37
60
  */
38
61
  fetchHistory(
39
- providerName: string,
40
62
  sessionId: string,
41
- options?: FetchHistoryOptions,
63
+ options: Pick<FetchHistoryOptions, 'limit' | 'offset'> = {},
42
64
  ): Promise<FetchHistoryResult> {
43
- return providerRegistry.resolveProvider(providerName).sessions.fetchHistory(sessionId, options);
65
+ const session = sessionsDb.getSessionById(sessionId);
66
+ if (!session) {
67
+ throw new AppError(`Session "${sessionId}" was not found.`, {
68
+ code: 'SESSION_NOT_FOUND',
69
+ statusCode: 404,
70
+ });
71
+ }
72
+
73
+ const provider = session.provider as LLMProvider;
74
+ return providerRegistry.resolveProvider(provider).sessions.fetchHistory(sessionId, {
75
+ limit: options.limit ?? null,
76
+ offset: options.offset ?? 0,
77
+ projectPath: session.project_path ?? '',
78
+ });
79
+ },
80
+
81
+ /**
82
+ * Deletes one persisted session row by id.
83
+ *
84
+ * When `deletedFromDisk` is true and a session `jsonl_path` exists, the path
85
+ * is deleted from disk before the DB row is removed.
86
+ */
87
+ async deleteSessionById(
88
+ sessionId: string,
89
+ deletedFromDisk = false,
90
+ ): Promise<{ sessionId: string; deletedFromDisk: boolean }> {
91
+ const session = sessionsDb.getSessionById(sessionId);
92
+ if (!session) {
93
+ throw new AppError(`Session "${sessionId}" was not found.`, {
94
+ code: 'SESSION_NOT_FOUND',
95
+ statusCode: 404,
96
+ });
97
+ }
98
+
99
+ let removedFromDisk = false;
100
+ if (deletedFromDisk && session.jsonl_path) {
101
+ removedFromDisk = await removeFileIfExists(session.jsonl_path);
102
+ }
103
+
104
+ const deleted = sessionsDb.deleteSessionById(sessionId);
105
+ if (!deleted) {
106
+ throw new AppError(`Session "${sessionId}" was not found.`, {
107
+ code: 'SESSION_NOT_FOUND',
108
+ statusCode: 404,
109
+ });
110
+ }
111
+
112
+ return { sessionId, deletedFromDisk: removedFromDisk };
113
+ },
114
+
115
+ /**
116
+ * Renames one session by id without requiring the caller to pass provider.
117
+ */
118
+ renameSessionById(sessionId: string, summary: string): { sessionId: string; summary: string } {
119
+ const session = sessionsDb.getSessionById(sessionId);
120
+ if (!session) {
121
+ throw new AppError(`Session "${sessionId}" was not found.`, {
122
+ code: 'SESSION_NOT_FOUND',
123
+ statusCode: 404,
124
+ });
125
+ }
126
+
127
+ sessionsDb.updateSessionCustomName(sessionId, summary);
128
+ return { sessionId, summary };
44
129
  },
45
130
  };
@@ -1,4 +1,10 @@
1
- import type { IProvider, IProviderAuth, IProviderMcp, IProviderSessions } from '@/shared/interfaces.js';
1
+ import type {
2
+ IProvider,
3
+ IProviderAuth,
4
+ IProviderMcp,
5
+ IProviderSessionSynchronizer,
6
+ IProviderSessions,
7
+ } from '@/shared/interfaces.js';
2
8
  import type { LLMProvider } from '@/shared/types.js';
3
9
 
4
10
  /**
@@ -13,6 +19,7 @@ export abstract class AbstractProvider implements IProvider {
13
19
  abstract readonly mcp: IProviderMcp;
14
20
  abstract readonly auth: IProviderAuth;
15
21
  abstract readonly sessions: IProviderSessions;
22
+ abstract readonly sessionSynchronizer: IProviderSessionSynchronizer;
16
23
 
17
24
  protected constructor(id: LLMProvider) {
18
25
  this.id = id;