@cloudcli-ai/cloudcli 1.30.0 → 1.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/dist/assets/{index-DI22MZMb.css → index-DF8TTVHl.css} +1 -1
  2. package/dist/assets/index-DbZAcc9z.js +1262 -0
  3. package/dist/index.html +2 -2
  4. package/dist-server/server/index.js +115 -955
  5. package/dist-server/server/index.js.map +1 -1
  6. package/dist-server/server/middleware/auth.js +1 -1
  7. package/dist-server/server/middleware/auth.js.map +1 -1
  8. package/dist-server/server/modules/database/connection.js +125 -0
  9. package/dist-server/server/modules/database/connection.js.map +1 -0
  10. package/dist-server/server/modules/database/index.js +13 -0
  11. package/dist-server/server/modules/database/index.js.map +1 -0
  12. package/dist-server/server/modules/database/init-db.js +18 -0
  13. package/dist-server/server/modules/database/init-db.js.map +1 -0
  14. package/dist-server/server/modules/database/migrations.js +370 -0
  15. package/dist-server/server/modules/database/migrations.js.map +1 -0
  16. package/dist-server/server/modules/database/repositories/api-keys.js +72 -0
  17. package/dist-server/server/modules/database/repositories/api-keys.js.map +1 -0
  18. package/dist-server/server/modules/database/repositories/app-config.js +47 -0
  19. package/dist-server/server/modules/database/repositories/app-config.js.map +1 -0
  20. package/dist-server/server/modules/database/repositories/credentials.js +68 -0
  21. package/dist-server/server/modules/database/repositories/credentials.js.map +1 -0
  22. package/dist-server/server/modules/database/repositories/github-tokens.js +54 -0
  23. package/dist-server/server/modules/database/repositories/github-tokens.js.map +1 -0
  24. package/dist-server/server/modules/database/repositories/notification-preferences.js +72 -0
  25. package/dist-server/server/modules/database/repositories/notification-preferences.js.map +1 -0
  26. package/dist-server/server/modules/database/repositories/projects.db.integration.test.js +64 -0
  27. package/dist-server/server/modules/database/repositories/projects.db.integration.test.js.map +1 -0
  28. package/dist-server/server/modules/database/repositories/projects.db.js +160 -0
  29. package/dist-server/server/modules/database/repositories/projects.db.js.map +1 -0
  30. package/dist-server/server/modules/database/repositories/push-subscriptions.js +49 -0
  31. package/dist-server/server/modules/database/repositories/push-subscriptions.js.map +1 -0
  32. package/dist-server/server/modules/database/repositories/scan-state.db.js +31 -0
  33. package/dist-server/server/modules/database/repositories/scan-state.db.js.map +1 -0
  34. package/dist-server/server/modules/database/repositories/sessions.db.js +109 -0
  35. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -0
  36. package/dist-server/server/modules/database/repositories/users.js +88 -0
  37. package/dist-server/server/modules/database/repositories/users.js.map +1 -0
  38. package/dist-server/server/modules/database/repositories/vapid-keys.js +38 -0
  39. package/dist-server/server/modules/database/repositories/vapid-keys.js.map +1 -0
  40. package/dist-server/server/modules/database/schema.js +143 -0
  41. package/dist-server/server/modules/database/schema.js.map +1 -0
  42. package/dist-server/server/modules/projects/index.js +4 -0
  43. package/dist-server/server/modules/projects/index.js.map +1 -0
  44. package/dist-server/server/modules/projects/projects.routes.js +188 -0
  45. package/dist-server/server/modules/projects/projects.routes.js.map +1 -0
  46. package/dist-server/server/modules/projects/services/project-clone.service.js +219 -0
  47. package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -0
  48. package/dist-server/server/modules/projects/services/project-delete.service.js +65 -0
  49. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -0
  50. package/dist-server/server/modules/projects/services/project-management.service.js +93 -0
  51. package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -0
  52. package/dist-server/server/modules/projects/services/project-star.service.js +60 -0
  53. package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -0
  54. package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js +167 -0
  55. package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js.map +1 -0
  56. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +169 -0
  57. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -0
  58. package/dist-server/server/modules/projects/tests/project-clone.service.test.js +126 -0
  59. package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -0
  60. package/dist-server/server/modules/projects/tests/project-management.service.test.js +85 -0
  61. package/dist-server/server/modules/projects/tests/project-management.service.test.js.map +1 -0
  62. package/dist-server/server/modules/projects/tests/project-star.service.test.js +95 -0
  63. package/dist-server/server/modules/projects/tests/project-star.service.test.js.map +1 -0
  64. package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js +87 -0
  65. package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js.map +1 -0
  66. package/dist-server/server/modules/providers/index.js +4 -0
  67. package/dist-server/server/modules/providers/index.js.map +1 -0
  68. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +63 -0
  69. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -0
  70. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +144 -7
  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.provider.js +2 -0
  73. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  74. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +71 -0
  75. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -0
  76. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +225 -13
  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.provider.js +2 -0
  79. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  80. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +127 -0
  81. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -0
  82. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +2 -0
  83. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
  84. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +296 -0
  85. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +1 -0
  86. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +277 -10
  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.provider.js +2 -0
  89. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
  90. package/dist-server/server/modules/providers/provider.routes.js +172 -0
  91. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  92. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +868 -0
  93. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -0
  94. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +56 -0
  95. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -0
  96. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +234 -0
  97. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -0
  98. package/dist-server/server/modules/providers/services/sessions.service.js +78 -3
  99. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  100. package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
  101. package/dist-server/server/modules/websocket/index.js +3 -0
  102. package/dist-server/server/modules/websocket/index.js.map +1 -0
  103. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +188 -0
  104. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -0
  105. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js +52 -0
  106. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js.map +1 -0
  107. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +334 -0
  108. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -0
  109. package/dist-server/server/modules/websocket/services/websocket-auth.service.js +32 -0
  110. package/dist-server/server/modules/websocket/services/websocket-auth.service.js.map +1 -0
  111. package/dist-server/server/modules/websocket/services/websocket-server.service.js +36 -0
  112. package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -0
  113. package/dist-server/server/modules/websocket/services/websocket-state.service.js +14 -0
  114. package/dist-server/server/modules/websocket/services/websocket-state.service.js.map +1 -0
  115. package/dist-server/server/modules/websocket/services/websocket-writer.service.js +32 -0
  116. package/dist-server/server/modules/websocket/services/websocket-writer.service.js.map +1 -0
  117. package/dist-server/server/routes/agent.js +10 -17
  118. package/dist-server/server/routes/agent.js.map +1 -1
  119. package/dist-server/server/routes/auth.js +3 -1
  120. package/dist-server/server/routes/auth.js.map +1 -1
  121. package/dist-server/server/routes/commands.js +1 -1
  122. package/dist-server/server/routes/commands.js.map +1 -1
  123. package/dist-server/server/routes/gemini.js +2 -2
  124. package/dist-server/server/routes/gemini.js.map +1 -1
  125. package/dist-server/server/routes/git.js +33 -29
  126. package/dist-server/server/routes/git.js.map +1 -1
  127. package/dist-server/server/routes/settings.js +1 -1
  128. package/dist-server/server/routes/settings.js.map +1 -1
  129. package/dist-server/server/routes/taskmaster.js +97 -110
  130. package/dist-server/server/routes/taskmaster.js.map +1 -1
  131. package/dist-server/server/routes/user.js +1 -1
  132. package/dist-server/server/routes/user.js.map +1 -1
  133. package/dist-server/server/services/notification-orchestrator.js +2 -2
  134. package/dist-server/server/services/notification-orchestrator.js.map +1 -1
  135. package/dist-server/server/services/vapid-keys.js +2 -1
  136. package/dist-server/server/services/vapid-keys.js.map +1 -1
  137. package/dist-server/server/shared/types.js +0 -1
  138. package/dist-server/server/shared/types.js.map +1 -1
  139. package/dist-server/server/shared/utils.js +408 -7
  140. package/dist-server/server/shared/utils.js.map +1 -1
  141. package/dist-server/server/utils/taskmaster-websocket.js +18 -12
  142. package/dist-server/server/utils/taskmaster-websocket.js.map +1 -1
  143. package/dist-server/shared/modelConstants.js +2 -1
  144. package/dist-server/shared/modelConstants.js.map +1 -1
  145. package/package.json +3 -1
  146. package/server/index.js +146 -1012
  147. package/server/middleware/auth.js +1 -1
  148. package/server/modules/database/connection.ts +143 -0
  149. package/server/modules/database/index.ts +12 -0
  150. package/server/modules/database/init-db.ts +17 -0
  151. package/server/modules/database/migrations.ts +442 -0
  152. package/server/modules/database/repositories/api-keys.ts +119 -0
  153. package/server/modules/database/repositories/app-config.ts +53 -0
  154. package/server/modules/database/repositories/credentials.ts +106 -0
  155. package/server/modules/database/repositories/github-tokens.ts +100 -0
  156. package/server/modules/database/repositories/notification-preferences.ts +103 -0
  157. package/server/modules/database/repositories/projects.db.integration.test.ts +72 -0
  158. package/server/modules/database/repositories/projects.db.ts +183 -0
  159. package/server/modules/database/repositories/push-subscriptions.ts +80 -0
  160. package/server/modules/database/repositories/scan-state.db.ts +42 -0
  161. package/server/modules/database/repositories/sessions.db.ts +174 -0
  162. package/server/modules/database/repositories/users.ts +140 -0
  163. package/server/modules/database/repositories/vapid-keys.ts +57 -0
  164. package/server/modules/database/schema.ts +152 -0
  165. package/server/modules/projects/index.ts +6 -0
  166. package/server/modules/projects/projects.routes.ts +247 -0
  167. package/server/modules/projects/services/project-clone.service.ts +321 -0
  168. package/server/modules/projects/services/project-delete.service.ts +75 -0
  169. package/server/modules/projects/services/project-management.service.ts +150 -0
  170. package/server/modules/projects/services/project-star.service.ts +78 -0
  171. package/server/modules/projects/services/projects-has-taskmaster.service.ts +248 -0
  172. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +285 -0
  173. package/server/modules/projects/tests/project-clone.service.test.ts +183 -0
  174. package/server/modules/projects/tests/project-management.service.test.ts +117 -0
  175. package/server/modules/projects/tests/project-star.service.test.ts +123 -0
  176. package/server/modules/projects/tests/projects-has-taskmaster.service.test.ts +105 -0
  177. package/server/modules/providers/index.ts +4 -0
  178. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +110 -0
  179. package/server/modules/providers/list/claude/claude-sessions.provider.ts +182 -13
  180. package/server/modules/providers/list/claude/claude.provider.ts +3 -1
  181. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +119 -0
  182. package/server/modules/providers/list/codex/codex-sessions.provider.ts +262 -17
  183. package/server/modules/providers/list/codex/codex.provider.ts +3 -1
  184. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +176 -0
  185. package/server/modules/providers/list/cursor/cursor.provider.ts +3 -1
  186. package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +401 -0
  187. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +327 -13
  188. package/server/modules/providers/list/gemini/gemini.provider.ts +3 -1
  189. package/server/modules/providers/provider.routes.ts +220 -12
  190. package/server/modules/providers/services/session-conversations-search.service.ts +1167 -0
  191. package/server/modules/providers/services/session-synchronizer.service.ts +74 -0
  192. package/server/modules/providers/services/sessions-watcher.service.ts +283 -0
  193. package/server/modules/providers/services/sessions.service.ts +89 -4
  194. package/server/modules/providers/shared/base/abstract.provider.ts +8 -1
  195. package/server/modules/websocket/README.md +267 -0
  196. package/server/modules/websocket/index.ts +2 -0
  197. package/server/modules/websocket/services/chat-websocket.service.ts +271 -0
  198. package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +65 -0
  199. package/server/modules/websocket/services/shell-websocket.service.ts +453 -0
  200. package/server/modules/websocket/services/websocket-auth.service.ts +54 -0
  201. package/server/modules/websocket/services/websocket-server.service.ts +58 -0
  202. package/server/modules/websocket/services/websocket-state.service.ts +16 -0
  203. package/server/modules/websocket/services/websocket-writer.service.ts +38 -0
  204. package/server/routes/agent.js +11 -16
  205. package/server/routes/auth.js +4 -2
  206. package/server/routes/commands.js +1 -1
  207. package/server/routes/gemini.js +3 -2
  208. package/server/routes/git.js +33 -28
  209. package/server/routes/settings.js +1 -1
  210. package/server/routes/taskmaster.js +108 -111
  211. package/server/routes/user.js +1 -1
  212. package/server/services/notification-orchestrator.js +3 -2
  213. package/server/services/vapid-keys.js +2 -1
  214. package/server/shared/interfaces.ts +39 -1
  215. package/server/shared/types.ts +185 -30
  216. package/server/shared/utils.ts +485 -9
  217. package/server/utils/taskmaster-websocket.js +19 -13
  218. package/shared/modelConstants.js +2 -1
  219. package/dist/assets/index-RX-W_g_Q.js +0 -1262
  220. package/dist-server/server/database/db.js +0 -535
  221. package/dist-server/server/database/db.js.map +0 -1
  222. package/dist-server/server/database/schema.js +0 -97
  223. package/dist-server/server/database/schema.js.map +0 -1
  224. package/dist-server/server/projects.js +0 -2325
  225. package/dist-server/server/projects.js.map +0 -1
  226. package/dist-server/server/routes/codex.js +0 -18
  227. package/dist-server/server/routes/codex.js.map +0 -1
  228. package/dist-server/server/routes/messages.js +0 -56
  229. package/dist-server/server/routes/messages.js.map +0 -1
  230. package/dist-server/server/routes/projects.js +0 -505
  231. package/dist-server/server/routes/projects.js.map +0 -1
  232. package/server/database/db.js +0 -593
  233. package/server/database/schema.js +0 -102
  234. package/server/projects.js +0 -2555
  235. package/server/routes/codex.js +0 -19
  236. package/server/routes/messages.js +0 -61
  237. package/server/routes/projects.js +0 -548
@@ -0,0 +1,119 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+
4
+ import { sessionsDb } from '@/modules/database/index.js';
5
+ import {
6
+ buildLookupMap,
7
+ extractFirstValidJsonlData,
8
+ findFilesRecursivelyCreatedAfter,
9
+ normalizeSessionName,
10
+ readFileTimestamps,
11
+ } from '@/shared/utils.js';
12
+ import type { IProviderSessionSynchronizer } from '@/shared/interfaces.js';
13
+
14
+ type ParsedSession = {
15
+ sessionId: string;
16
+ projectPath: string;
17
+ sessionName?: string;
18
+ };
19
+
20
+ /**
21
+ * Session indexer for Codex transcript artifacts.
22
+ */
23
+ export class CodexSessionSynchronizer implements IProviderSessionSynchronizer {
24
+ private readonly provider = 'codex' as const;
25
+ private readonly codexHome = path.join(os.homedir(), '.codex');
26
+
27
+ /**
28
+ * Scans ~/.codex/sessions and upserts discovered sessions into DB.
29
+ */
30
+ async synchronize(since?: Date): Promise<number> {
31
+ const nameMap = await buildLookupMap(path.join(this.codexHome, 'session_index.jsonl'), 'id', 'thread_name');
32
+ const files = await findFilesRecursivelyCreatedAfter(
33
+ path.join(this.codexHome, 'sessions'),
34
+ '.jsonl',
35
+ since ?? null
36
+ );
37
+
38
+ let processed = 0;
39
+ for (const filePath of files) {
40
+ const parsed = await this.processSessionFile(filePath, nameMap);
41
+ if (!parsed) {
42
+ continue;
43
+ }
44
+
45
+ const existingSession = sessionsDb.getSessionById(parsed.sessionId);
46
+ if (existingSession) {
47
+ // If session name is untitled and we now have a name, update it
48
+ if (existingSession.custom_name === 'Untitled Codex Session' && parsed.sessionName && parsed.sessionName !== 'Untitled Codex Session') {
49
+ sessionsDb.updateSessionCustomName(parsed.sessionId, parsed.sessionName);
50
+ }
51
+ }
52
+
53
+ const timestamps = await readFileTimestamps(filePath);
54
+ sessionsDb.createSession(
55
+ parsed.sessionId,
56
+ this.provider,
57
+ parsed.projectPath,
58
+ parsed.sessionName,
59
+ timestamps.createdAt,
60
+ timestamps.updatedAt,
61
+ filePath
62
+ );
63
+ processed += 1;
64
+ }
65
+
66
+ return processed;
67
+ }
68
+
69
+ /**
70
+ * Parses and upserts one Codex session JSONL file.
71
+ */
72
+ async synchronizeFile(filePath: string): Promise<string | null> {
73
+ if (!filePath.endsWith('.jsonl')) {
74
+ return null;
75
+ }
76
+
77
+ const nameMap = await buildLookupMap(path.join(this.codexHome, 'session_index.jsonl'), 'id', 'thread_name');
78
+ const parsed = await this.processSessionFile(filePath, nameMap);
79
+ if (!parsed) {
80
+ return null;
81
+ }
82
+
83
+ const timestamps = await readFileTimestamps(filePath);
84
+ return sessionsDb.createSession(
85
+ parsed.sessionId,
86
+ this.provider,
87
+ parsed.projectPath,
88
+ parsed.sessionName,
89
+ timestamps.createdAt,
90
+ timestamps.updatedAt,
91
+ filePath
92
+ );
93
+ }
94
+
95
+ /**
96
+ * Extracts session metadata from one Codex JSONL session file.
97
+ */
98
+ private async processSessionFile(
99
+ filePath: string,
100
+ nameMap: Map<string, string>
101
+ ): Promise<ParsedSession | null> {
102
+ return extractFirstValidJsonlData(filePath, (rawData) => {
103
+ const data = rawData as Record<string, unknown>;
104
+ const payload = data.payload as Record<string, unknown> | undefined;
105
+ const sessionId = typeof payload?.id === 'string' ? payload.id : undefined;
106
+ const projectPath = typeof payload?.cwd === 'string' ? payload.cwd : undefined;
107
+
108
+ if (!sessionId || !projectPath) {
109
+ return null;
110
+ }
111
+
112
+ return {
113
+ sessionId,
114
+ projectPath,
115
+ sessionName: normalizeSessionName(nameMap.get(sessionId), 'Untitled Codex Session'),
116
+ };
117
+ });
118
+ }
119
+ }
@@ -1,4 +1,7 @@
1
- import { getCodexSessionMessages } from '@/projects.js';
1
+ import fsSync from 'node:fs';
2
+ import readline from 'node:readline';
3
+
4
+ import { sessionsDb } from '@/modules/database/index.js';
2
5
  import type { IProviderSessions } from '@/shared/interfaces.js';
3
6
  import type { AnyRecord, FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
4
7
  import { createNormalizedMessage, generateMessageId, readObjectRecord } from '@/shared/utils.js';
@@ -11,14 +14,250 @@ type CodexHistoryResult =
11
14
  messages?: AnyRecord[];
12
15
  total?: number;
13
16
  hasMore?: boolean;
17
+ offset?: number;
18
+ limit?: number | null;
14
19
  tokenUsage?: unknown;
15
20
  };
16
21
 
17
- const loadCodexSessionMessages = getCodexSessionMessages as unknown as (
22
+ function isVisibleCodexUserMessage(payload: AnyRecord | null | undefined): boolean {
23
+ if (!payload || payload.type !== 'user_message') {
24
+ return false;
25
+ }
26
+
27
+ if (payload.kind && payload.kind !== 'plain') {
28
+ return false;
29
+ }
30
+
31
+ return typeof payload.message === 'string' && payload.message.trim().length > 0;
32
+ }
33
+
34
+ function extractCodexTextContent(content: unknown): string {
35
+ if (!Array.isArray(content)) {
36
+ return typeof content === 'string' ? content : '';
37
+ }
38
+
39
+ return content
40
+ .map((item) => {
41
+ if (!item || typeof item !== 'object') {
42
+ return '';
43
+ }
44
+
45
+ const record = item as AnyRecord;
46
+ if (
47
+ (record.type === 'input_text' || record.type === 'output_text' || record.type === 'text')
48
+ && typeof record.text === 'string'
49
+ ) {
50
+ return record.text;
51
+ }
52
+
53
+ return '';
54
+ })
55
+ .filter(Boolean)
56
+ .join('\n');
57
+ }
58
+
59
+ async function getCodexSessionMessages(
18
60
  sessionId: string,
19
- limit: number | null,
20
- offset: number,
21
- ) => Promise<CodexHistoryResult>;
61
+ limit: number | null = null,
62
+ offset = 0,
63
+ ): Promise<CodexHistoryResult> {
64
+ try {
65
+ const sessionFilePath = sessionsDb.getSessionById(sessionId)?.jsonl_path;
66
+
67
+ if (!sessionFilePath) {
68
+ console.warn(`Codex session file not found for session ${sessionId}`);
69
+ return { messages: [], total: 0, hasMore: false };
70
+ }
71
+
72
+ const messages: AnyRecord[] = [];
73
+ let tokenUsage: AnyRecord | null = null;
74
+ const fileStream = fsSync.createReadStream(sessionFilePath);
75
+ const rl = readline.createInterface({
76
+ input: fileStream,
77
+ crlfDelay: Infinity,
78
+ });
79
+
80
+ for await (const line of rl) {
81
+ if (!line.trim()) {
82
+ continue;
83
+ }
84
+
85
+ try {
86
+ const entry = JSON.parse(line) as AnyRecord;
87
+
88
+ if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
89
+ const info = entry.payload.info as AnyRecord;
90
+ if (info.total_token_usage) {
91
+ const usage = info.total_token_usage as AnyRecord;
92
+ tokenUsage = {
93
+ used: usage.total_tokens || 0,
94
+ total: info.model_context_window || 200000,
95
+ };
96
+ }
97
+ }
98
+
99
+ if (entry.type === 'event_msg' && isVisibleCodexUserMessage(entry.payload as AnyRecord)) {
100
+ messages.push({
101
+ type: 'user',
102
+ timestamp: entry.timestamp,
103
+ message: {
104
+ role: 'user',
105
+ content: entry.payload.message,
106
+ },
107
+ });
108
+ }
109
+
110
+ if (
111
+ entry.type === 'response_item' &&
112
+ entry.payload?.type === 'message' &&
113
+ entry.payload.role === 'assistant'
114
+ ) {
115
+ const textContent = extractCodexTextContent(entry.payload.content);
116
+ if (textContent.trim()) {
117
+ messages.push({
118
+ type: 'assistant',
119
+ timestamp: entry.timestamp,
120
+ message: {
121
+ role: 'assistant',
122
+ content: textContent,
123
+ },
124
+ });
125
+ }
126
+ }
127
+
128
+ if (entry.type === 'response_item' && entry.payload?.type === 'reasoning') {
129
+ const summaryText = Array.isArray(entry.payload.summary)
130
+ ? entry.payload.summary
131
+ .map((item: AnyRecord) => item?.text)
132
+ .filter(Boolean)
133
+ .join('\n')
134
+ : '';
135
+
136
+ if (summaryText.trim()) {
137
+ messages.push({
138
+ type: 'thinking',
139
+ timestamp: entry.timestamp,
140
+ message: {
141
+ role: 'assistant',
142
+ content: summaryText,
143
+ },
144
+ });
145
+ }
146
+ }
147
+
148
+ if (entry.type === 'response_item' && entry.payload?.type === 'function_call') {
149
+ let toolName = entry.payload.name;
150
+ let toolInput = entry.payload.arguments;
151
+
152
+ if (toolName === 'shell_command') {
153
+ toolName = 'Bash';
154
+ try {
155
+ const args = JSON.parse(entry.payload.arguments) as AnyRecord;
156
+ toolInput = JSON.stringify({ command: args.command });
157
+ } catch {
158
+ // Keep original arguments when parsing fails.
159
+ }
160
+ }
161
+
162
+ messages.push({
163
+ type: 'tool_use',
164
+ timestamp: entry.timestamp,
165
+ toolName,
166
+ toolInput,
167
+ toolCallId: entry.payload.call_id,
168
+ });
169
+ }
170
+
171
+ if (entry.type === 'response_item' && entry.payload?.type === 'function_call_output') {
172
+ messages.push({
173
+ type: 'tool_result',
174
+ timestamp: entry.timestamp,
175
+ toolCallId: entry.payload.call_id,
176
+ output: entry.payload.output,
177
+ });
178
+ }
179
+
180
+ if (entry.type === 'response_item' && entry.payload?.type === 'custom_tool_call') {
181
+ const toolName = entry.payload.name || 'custom_tool';
182
+ const input = entry.payload.input || '';
183
+
184
+ if (toolName === 'apply_patch') {
185
+ const fileMatch = String(input).match(/\*\*\* Update File: (.+)/);
186
+ const filePath = fileMatch ? fileMatch[1].trim() : 'unknown';
187
+ const lines = String(input).split('\n');
188
+ const oldLines: string[] = [];
189
+ const newLines: string[] = [];
190
+
191
+ for (const lineContent of lines) {
192
+ if (lineContent.startsWith('-') && !lineContent.startsWith('---')) {
193
+ oldLines.push(lineContent.slice(1));
194
+ } else if (lineContent.startsWith('+') && !lineContent.startsWith('+++')) {
195
+ newLines.push(lineContent.slice(1));
196
+ }
197
+ }
198
+
199
+ messages.push({
200
+ type: 'tool_use',
201
+ timestamp: entry.timestamp,
202
+ toolName: 'Edit',
203
+ toolInput: JSON.stringify({
204
+ file_path: filePath,
205
+ old_string: oldLines.join('\n'),
206
+ new_string: newLines.join('\n'),
207
+ }),
208
+ toolCallId: entry.payload.call_id,
209
+ });
210
+ } else {
211
+ messages.push({
212
+ type: 'tool_use',
213
+ timestamp: entry.timestamp,
214
+ toolName,
215
+ toolInput: input,
216
+ toolCallId: entry.payload.call_id,
217
+ });
218
+ }
219
+ }
220
+
221
+ if (entry.type === 'response_item' && entry.payload?.type === 'custom_tool_call_output') {
222
+ messages.push({
223
+ type: 'tool_result',
224
+ timestamp: entry.timestamp,
225
+ toolCallId: entry.payload.call_id,
226
+ output: entry.payload.output || '',
227
+ });
228
+ }
229
+ } catch {
230
+ // Skip malformed lines.
231
+ }
232
+ }
233
+
234
+ messages.sort(
235
+ (a, b) => new Date(a.timestamp || 0).getTime() - new Date(b.timestamp || 0).getTime(),
236
+ );
237
+ const total = messages.length;
238
+
239
+ if (limit !== null) {
240
+ const startIndex = Math.max(0, total - offset - limit);
241
+ const endIndex = total - offset;
242
+ const paginatedMessages = messages.slice(startIndex, endIndex);
243
+ const hasMore = startIndex > 0;
244
+
245
+ return {
246
+ messages: paginatedMessages,
247
+ total,
248
+ hasMore,
249
+ offset,
250
+ limit,
251
+ tokenUsage,
252
+ };
253
+ }
254
+
255
+ return { messages, tokenUsage };
256
+ } catch (error) {
257
+ console.error(`Error reading Codex session messages for ${sessionId}:`, error);
258
+ return { messages: [], total: 0, hasMore: false };
259
+ }
260
+ }
22
261
 
23
262
  export class CodexSessionsProvider implements IProviderSessions {
24
263
  /**
@@ -31,6 +270,23 @@ export class CodexSessionsProvider implements IProviderSessions {
31
270
  const ts = raw.timestamp || new Date().toISOString();
32
271
  const baseId = raw.uuid || generateMessageId('codex');
33
272
 
273
+ if (raw.type === 'thinking' || raw.isReasoning) {
274
+ const thinkingContent = typeof raw.message?.content === 'string'
275
+ ? raw.message.content
276
+ : '';
277
+ if (!thinkingContent.trim()) {
278
+ return [];
279
+ }
280
+ return [createNormalizedMessage({
281
+ id: baseId,
282
+ sessionId,
283
+ timestamp: ts,
284
+ provider: PROVIDER,
285
+ kind: 'thinking',
286
+ content: thinkingContent,
287
+ })];
288
+ }
289
+
34
290
  if (raw.message?.role === 'user') {
35
291
  const content = typeof raw.message.content === 'string'
36
292
  ? raw.message.content
@@ -77,17 +333,6 @@ export class CodexSessionsProvider implements IProviderSessions {
77
333
  })];
78
334
  }
79
335
 
80
- if (raw.type === 'thinking' || raw.isReasoning) {
81
- return [createNormalizedMessage({
82
- id: baseId,
83
- sessionId,
84
- timestamp: ts,
85
- provider: PROVIDER,
86
- kind: 'thinking',
87
- content: raw.message?.content || '',
88
- })];
89
- }
90
-
91
336
  if (raw.type === 'tool_use' || raw.toolName) {
92
337
  return [createNormalizedMessage({
93
338
  id: baseId,
@@ -275,7 +520,7 @@ export class CodexSessionsProvider implements IProviderSessions {
275
520
 
276
521
  let result: CodexHistoryResult;
277
522
  try {
278
- result = await loadCodexSessionMessages(sessionId, limit, offset);
523
+ result = await getCodexSessionMessages(sessionId, limit, offset);
279
524
  } catch (error) {
280
525
  const message = error instanceof Error ? error.message : String(error);
281
526
  console.warn(`[CodexProvider] Failed to load session ${sessionId}:`, message);
@@ -1,13 +1,15 @@
1
1
  import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js';
2
2
  import { CodexProviderAuth } from '@/modules/providers/list/codex/codex-auth.provider.js';
3
3
  import { CodexMcpProvider } from '@/modules/providers/list/codex/codex-mcp.provider.js';
4
+ import { CodexSessionSynchronizer } from '@/modules/providers/list/codex/codex-session-synchronizer.provider.js';
4
5
  import { CodexSessionsProvider } from '@/modules/providers/list/codex/codex-sessions.provider.js';
5
- import type { IProviderAuth, IProviderSessions } from '@/shared/interfaces.js';
6
+ import type { IProviderAuth, IProviderSessionSynchronizer, IProviderSessions } from '@/shared/interfaces.js';
6
7
 
7
8
  export class CodexProvider extends AbstractProvider {
8
9
  readonly mcp = new CodexMcpProvider();
9
10
  readonly auth: IProviderAuth = new CodexProviderAuth();
10
11
  readonly sessions: IProviderSessions = new CodexSessionsProvider();
12
+ readonly sessionSynchronizer: IProviderSessionSynchronizer = new CodexSessionSynchronizer();
11
13
 
12
14
  constructor() {
13
15
  super('codex');
@@ -0,0 +1,176 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import fsp from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import readline from 'node:readline';
7
+
8
+ import { sessionsDb } from '@/modules/database/index.js';
9
+ import {
10
+ extractFirstValidJsonlData,
11
+ findFilesRecursivelyCreatedAfter,
12
+ normalizeSessionName,
13
+ readFileTimestamps,
14
+ } from '@/shared/utils.js';
15
+ import type { IProviderSessionSynchronizer } from '@/shared/interfaces.js';
16
+
17
+ type ParsedSession = {
18
+ sessionId: string;
19
+ projectPath: string;
20
+ sessionName?: string;
21
+ };
22
+
23
+ /**
24
+ * Returns directory entries or an empty list when the folder is missing.
25
+ */
26
+ async function listDirectoryEntriesSafe(
27
+ directoryPath: string
28
+ ): Promise<import('node:fs').Dirent[]> {
29
+ try {
30
+ return await fsp.readdir(directoryPath, { withFileTypes: true });
31
+ } catch {
32
+ return [];
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Session indexer for Cursor transcript artifacts.
38
+ */
39
+ export class CursorSessionSynchronizer implements IProviderSessionSynchronizer {
40
+ private readonly provider = 'cursor' as const;
41
+ private readonly cursorHome = path.join(os.homedir(), '.cursor');
42
+
43
+ /**
44
+ * Scans Cursor chats and upserts discovered sessions into DB.
45
+ */
46
+ async synchronize(since?: Date): Promise<number> {
47
+ const projectsDir = path.join(this.cursorHome, 'projects');
48
+ const projectEntries = await listDirectoryEntriesSafe(projectsDir);
49
+ const seenProjectPaths = new Set<string>();
50
+
51
+ let processed = 0;
52
+ for (const entry of projectEntries) {
53
+ if (!entry.isDirectory()) {
54
+ continue;
55
+ }
56
+
57
+ const workerLogPath = path.join(projectsDir, entry.name, 'worker.log');
58
+ const projectPath = await this.extractProjectPathFromWorkerLog(workerLogPath);
59
+ if (!projectPath || seenProjectPaths.has(projectPath)) {
60
+ continue;
61
+ }
62
+
63
+ seenProjectPaths.add(projectPath);
64
+ const projectHash = this.md5(projectPath);
65
+ const chatsDir = path.join(this.cursorHome, 'chats', projectHash);
66
+ const files = await findFilesRecursivelyCreatedAfter(chatsDir, '.jsonl', since ?? null);
67
+
68
+ for (const filePath of files) {
69
+ const parsed = await this.processSessionFile(filePath);
70
+ if (!parsed) {
71
+ continue;
72
+ }
73
+
74
+ const timestamps = await readFileTimestamps(filePath);
75
+ sessionsDb.createSession(
76
+ parsed.sessionId,
77
+ this.provider,
78
+ parsed.projectPath,
79
+ parsed.sessionName,
80
+ timestamps.createdAt,
81
+ timestamps.updatedAt,
82
+ filePath
83
+ );
84
+ processed += 1;
85
+ }
86
+ }
87
+
88
+ return processed;
89
+ }
90
+
91
+ /**
92
+ * Parses and upserts one Cursor session JSONL file.
93
+ */
94
+ async synchronizeFile(filePath: string): Promise<string | null> {
95
+ if (!filePath.endsWith('.jsonl')) {
96
+ return null;
97
+ }
98
+
99
+ const parsed = await this.processSessionFile(filePath);
100
+ if (!parsed) {
101
+ return null;
102
+ }
103
+
104
+ const timestamps = await readFileTimestamps(filePath);
105
+ return sessionsDb.createSession(
106
+ parsed.sessionId,
107
+ this.provider,
108
+ parsed.projectPath,
109
+ parsed.sessionName,
110
+ timestamps.createdAt,
111
+ timestamps.updatedAt,
112
+ filePath
113
+ );
114
+ }
115
+
116
+ /**
117
+ * Produces the same project hash Cursor uses in chat directory names.
118
+ */
119
+ private md5(input: string): string {
120
+ return crypto.createHash('md5').update(input).digest('hex');
121
+ }
122
+
123
+ /**
124
+ * Extracts project path from Cursor worker.log.
125
+ */
126
+ private async extractProjectPathFromWorkerLog(filePath: string): Promise<string | null> {
127
+ try {
128
+ const fileStream = fs.createReadStream(filePath, { encoding: 'utf8' });
129
+ const lineReader = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
130
+
131
+ for await (const line of lineReader) {
132
+ const match = line.match(/workspacePath=(.*)$/);
133
+ const projectPath = match?.[1]?.trim();
134
+ if (projectPath) {
135
+ lineReader.close();
136
+ fileStream.close();
137
+ return projectPath;
138
+ }
139
+ }
140
+ } catch {
141
+ // Missing worker logs are valid for partial or incomplete session data.
142
+ }
143
+
144
+ return null;
145
+ }
146
+
147
+ /**
148
+ * Extracts session metadata from one Cursor JSONL session file.
149
+ */
150
+ private async processSessionFile(filePath: string): Promise<ParsedSession | null> {
151
+ const sessionId = path.basename(filePath, '.jsonl');
152
+ const grandparentDir = path.dirname(path.dirname(filePath));
153
+ const workerLogPath = path.join(grandparentDir, 'worker.log');
154
+ const projectPath = await this.extractProjectPathFromWorkerLog(workerLogPath);
155
+
156
+ if (!projectPath) {
157
+ return null;
158
+ }
159
+
160
+ return extractFirstValidJsonlData(filePath, (rawData) => {
161
+ const data = rawData as Record<string, any>;
162
+ if (data.role !== 'user') {
163
+ return null;
164
+ }
165
+
166
+ const text = typeof data.message?.content?.[0]?.text === 'string' ? data.message.content[0].text : '';
167
+ const firstLine = text.replace(/<\/?user_query>/g, '').trim().split('\n')[0];
168
+
169
+ return {
170
+ sessionId,
171
+ projectPath,
172
+ sessionName: normalizeSessionName(firstLine, 'Untitled Cursor Session'),
173
+ };
174
+ });
175
+ }
176
+ }
@@ -1,13 +1,15 @@
1
1
  import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js';
2
2
  import { CursorProviderAuth } from '@/modules/providers/list/cursor/cursor-auth.provider.js';
3
3
  import { CursorMcpProvider } from '@/modules/providers/list/cursor/cursor-mcp.provider.js';
4
+ import { CursorSessionSynchronizer } from '@/modules/providers/list/cursor/cursor-session-synchronizer.provider.js';
4
5
  import { CursorSessionsProvider } from '@/modules/providers/list/cursor/cursor-sessions.provider.js';
5
- import type { IProviderAuth, IProviderSessions } from '@/shared/interfaces.js';
6
+ import type { IProviderAuth, IProviderSessionSynchronizer, IProviderSessions } from '@/shared/interfaces.js';
6
7
 
7
8
  export class CursorProvider extends AbstractProvider {
8
9
  readonly mcp = new CursorMcpProvider();
9
10
  readonly auth: IProviderAuth = new CursorProviderAuth();
10
11
  readonly sessions: IProviderSessions = new CursorSessionsProvider();
12
+ readonly sessionSynchronizer: IProviderSessionSynchronizer = new CursorSessionSynchronizer();
11
13
 
12
14
  constructor() {
13
15
  super('cursor');