@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
@@ -1,11 +1,249 @@
1
- import sessionManager from '@/sessionManager.js';
2
- import { getGeminiCliSessionMessages } from '@/projects.js';
1
+ import fsSync from 'node:fs';
2
+ import fs from 'node:fs/promises';
3
+ import readline from 'node:readline';
4
+
5
+ import { sessionsDb } from '@/modules/database/index.js';
3
6
  import type { IProviderSessions } from '@/shared/interfaces.js';
4
7
  import type { AnyRecord, FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
5
8
  import { createNormalizedMessage, generateMessageId, readObjectRecord } from '@/shared/utils.js';
6
9
 
7
10
  const PROVIDER = 'gemini';
8
11
 
12
+ type GeminiHistoryResult = {
13
+ messages: AnyRecord[];
14
+ tokenUsage?: unknown;
15
+ };
16
+
17
+ function mapGeminiRole(value: unknown): 'user' | 'assistant' | null {
18
+ if (value === 'user') {
19
+ return 'user';
20
+ }
21
+
22
+ if (value === 'gemini' || value === 'assistant') {
23
+ return 'assistant';
24
+ }
25
+
26
+ return null;
27
+ }
28
+
29
+ function extractGeminiTextContent(content: unknown): string {
30
+ if (typeof content === 'string') {
31
+ return content;
32
+ }
33
+
34
+ if (!Array.isArray(content)) {
35
+ return '';
36
+ }
37
+
38
+ return content
39
+ .map((part) => {
40
+ if (typeof part === 'string') {
41
+ return part;
42
+ }
43
+ if (!part || typeof part !== 'object') {
44
+ return '';
45
+ }
46
+
47
+ const record = part as AnyRecord;
48
+ if (typeof record.text === 'string') {
49
+ return record.text;
50
+ }
51
+
52
+ return '';
53
+ })
54
+ .filter(Boolean)
55
+ .join('\n');
56
+ }
57
+
58
+ function extractGeminiThoughts(thoughts: unknown): string {
59
+ if (!Array.isArray(thoughts)) {
60
+ return '';
61
+ }
62
+
63
+ return thoughts
64
+ .map((item) => {
65
+ if (!item || typeof item !== 'object') {
66
+ return '';
67
+ }
68
+
69
+ const record = item as AnyRecord;
70
+ const subject = typeof record.subject === 'string' ? record.subject.trim() : '';
71
+ const description = typeof record.description === 'string' ? record.description.trim() : '';
72
+
73
+ if (subject && description) {
74
+ return `${subject}: ${description}`;
75
+ }
76
+
77
+ return description || subject;
78
+ })
79
+ .filter(Boolean)
80
+ .join('\n');
81
+ }
82
+
83
+ function buildGeminiTokenUsage(tokens: unknown): AnyRecord | undefined {
84
+ if (!tokens || typeof tokens !== 'object') {
85
+ return undefined;
86
+ }
87
+
88
+ const record = tokens as AnyRecord;
89
+ const input = Number(record.input || 0);
90
+ const output = Number(record.output || 0);
91
+ const cached = Number(record.cached || 0);
92
+ const thoughts = Number(record.thoughts || 0);
93
+ const tool = Number(record.tool || 0);
94
+
95
+ const totalFromFields = input + output + cached + thoughts + tool;
96
+ const total = Number(record.total || totalFromFields || 0);
97
+
98
+ return {
99
+ used: total,
100
+ total: total,
101
+ breakdown: {
102
+ input,
103
+ output,
104
+ cached,
105
+ thoughts,
106
+ tool,
107
+ },
108
+ };
109
+ }
110
+
111
+ async function getGeminiLegacySessionMessages(sessionFilePath: string): Promise<GeminiHistoryResult> {
112
+ try {
113
+ const data = await fs.readFile(sessionFilePath, 'utf8');
114
+ const session = JSON.parse(data) as AnyRecord;
115
+ const sourceMessages = Array.isArray(session.messages) ? session.messages : [];
116
+
117
+ const messages: AnyRecord[] = [];
118
+ for (const msg of sourceMessages) {
119
+ const role = mapGeminiRole(msg.type ?? msg.role);
120
+ if (!role) {
121
+ continue;
122
+ }
123
+
124
+ messages.push({
125
+ type: 'message',
126
+ uuid: typeof msg.id === 'string' ? msg.id : undefined,
127
+ message: { role, content: msg.content },
128
+ timestamp: msg.timestamp || null,
129
+ });
130
+ }
131
+
132
+ return { messages };
133
+ } catch {
134
+ return { messages: [] };
135
+ }
136
+ }
137
+
138
+ async function getGeminiJsonlSessionMessages(sessionFilePath: string): Promise<GeminiHistoryResult> {
139
+ const messages: AnyRecord[] = [];
140
+ let tokenUsage: AnyRecord | undefined;
141
+
142
+ try {
143
+ const fileStream = fsSync.createReadStream(sessionFilePath);
144
+ const lineReader = readline.createInterface({
145
+ input: fileStream,
146
+ crlfDelay: Infinity,
147
+ });
148
+
149
+ for await (const line of lineReader) {
150
+ const trimmed = line.trim();
151
+ if (!trimmed) {
152
+ continue;
153
+ }
154
+
155
+ let entry: AnyRecord;
156
+ try {
157
+ entry = JSON.parse(trimmed) as AnyRecord;
158
+ } catch {
159
+ continue;
160
+ }
161
+
162
+ // Metadata/update lines (e.g. {$set:{lastUpdated:...}}) do not represent chat messages.
163
+ if (entry.$set) {
164
+ continue;
165
+ }
166
+
167
+ const role = mapGeminiRole(entry.type);
168
+ if (role) {
169
+ const textContent = extractGeminiTextContent(entry.content);
170
+ if (textContent.trim()) {
171
+ messages.push({
172
+ type: 'message',
173
+ uuid: typeof entry.id === 'string' ? entry.id : undefined,
174
+ message: { role, content: textContent },
175
+ timestamp: entry.timestamp || null,
176
+ });
177
+ }
178
+
179
+ const thinkingContent = extractGeminiThoughts(entry.thoughts);
180
+ if (thinkingContent.trim()) {
181
+ messages.push({
182
+ type: 'thinking',
183
+ uuid: typeof entry.id === 'string' ? `${entry.id}_thinking` : undefined,
184
+ message: { role: 'assistant', content: thinkingContent },
185
+ timestamp: entry.timestamp || null,
186
+ isReasoning: true,
187
+ });
188
+ }
189
+
190
+ if (role === 'assistant') {
191
+ const usage = buildGeminiTokenUsage(entry.tokens);
192
+ if (usage) {
193
+ tokenUsage = usage;
194
+ }
195
+ }
196
+
197
+ continue;
198
+ }
199
+
200
+ if (entry.type === 'tool_use') {
201
+ messages.push({
202
+ type: 'tool_use',
203
+ uuid: typeof entry.id === 'string' ? entry.id : undefined,
204
+ timestamp: entry.timestamp || null,
205
+ toolName: entry.tool_name || entry.name || 'Tool',
206
+ toolInput: entry.parameters ?? entry.input ?? entry.arguments ?? '',
207
+ toolCallId: entry.tool_id || entry.toolCallId || entry.id,
208
+ });
209
+ continue;
210
+ }
211
+
212
+ if (entry.type === 'tool_result') {
213
+ messages.push({
214
+ type: 'tool_result',
215
+ uuid: typeof entry.id === 'string' ? entry.id : undefined,
216
+ timestamp: entry.timestamp || null,
217
+ toolCallId: entry.tool_id || entry.toolCallId || entry.id || '',
218
+ output: entry.output ?? entry.result ?? '',
219
+ isError: Boolean(entry.error) || entry.status === 'error',
220
+ });
221
+ }
222
+ }
223
+ } catch {
224
+ return { messages: [] };
225
+ }
226
+
227
+ messages.sort(
228
+ (a, b) => new Date(a.timestamp || 0).getTime() - new Date(b.timestamp || 0).getTime(),
229
+ );
230
+
231
+ return { messages, tokenUsage };
232
+ }
233
+
234
+ async function getGeminiCliSessionMessages(sessionId: string): Promise<GeminiHistoryResult> {
235
+ const sessionFilePath = sessionsDb.getSessionById(sessionId)?.jsonl_path;
236
+ if (!sessionFilePath) {
237
+ return { messages: [] };
238
+ }
239
+
240
+ if (sessionFilePath.endsWith('.jsonl')) {
241
+ return getGeminiJsonlSessionMessages(sessionFilePath);
242
+ }
243
+
244
+ return getGeminiLegacySessionMessages(sessionFilePath);
245
+ }
246
+
9
247
  export class GeminiSessionsProvider implements IProviderSessions {
10
248
  /**
11
249
  * Normalizes live Gemini stream-json events into the shared message shape.
@@ -108,8 +346,7 @@ export class GeminiSessionsProvider implements IProviderSessions {
108
346
  }
109
347
 
110
348
  /**
111
- * Loads Gemini history from the in-memory session manager first, then falls
112
- * back to Gemini CLI session files on disk.
349
+ * Loads Gemini history from Gemini CLI session files on disk.
113
350
  */
114
351
  async fetchHistory(
115
352
  sessionId: string,
@@ -117,28 +354,73 @@ export class GeminiSessionsProvider implements IProviderSessions {
117
354
  ): Promise<FetchHistoryResult> {
118
355
  const { limit = null, offset = 0 } = options;
119
356
 
120
- let rawMessages: AnyRecord[];
357
+ let result: GeminiHistoryResult;
121
358
  try {
122
- rawMessages = sessionManager.getSessionMessages(sessionId) as AnyRecord[];
123
-
124
- if (rawMessages.length === 0) {
125
- rawMessages = await getGeminiCliSessionMessages(sessionId) as AnyRecord[];
126
- }
359
+ result = await getGeminiCliSessionMessages(sessionId);
127
360
  } catch (error) {
128
361
  const message = error instanceof Error ? error.message : String(error);
129
362
  console.warn(`[GeminiProvider] Failed to load session ${sessionId}:`, message);
130
363
  return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
131
364
  }
132
365
 
366
+ const rawMessages = result.messages;
133
367
  const normalized: NormalizedMessage[] = [];
368
+
134
369
  for (let i = 0; i < rawMessages.length; i++) {
135
370
  const raw = rawMessages[i];
136
371
  const ts = raw.timestamp || new Date().toISOString();
137
372
  const baseId = raw.uuid || generateMessageId('gemini');
138
373
 
374
+ if (raw.type === 'thinking' || raw.isReasoning) {
375
+ const thinkingContent = typeof raw.message?.content === 'string'
376
+ ? raw.message.content
377
+ : typeof raw.content === 'string'
378
+ ? raw.content
379
+ : '';
380
+
381
+ if (thinkingContent.trim()) {
382
+ normalized.push(createNormalizedMessage({
383
+ id: baseId,
384
+ sessionId,
385
+ timestamp: ts,
386
+ provider: PROVIDER,
387
+ kind: 'thinking',
388
+ content: thinkingContent,
389
+ }));
390
+ }
391
+ continue;
392
+ }
393
+
394
+ if (raw.type === 'tool_use' || raw.toolName) {
395
+ normalized.push(createNormalizedMessage({
396
+ id: baseId,
397
+ sessionId,
398
+ timestamp: ts,
399
+ provider: PROVIDER,
400
+ kind: 'tool_use',
401
+ toolName: raw.toolName || 'Tool',
402
+ toolInput: raw.toolInput,
403
+ toolId: raw.toolCallId || baseId,
404
+ }));
405
+ continue;
406
+ }
407
+
408
+ if (raw.type === 'tool_result') {
409
+ normalized.push(createNormalizedMessage({
410
+ id: baseId,
411
+ sessionId,
412
+ timestamp: ts,
413
+ provider: PROVIDER,
414
+ kind: 'tool_result',
415
+ toolId: raw.toolCallId || '',
416
+ content: raw.output === undefined ? '' : String(raw.output),
417
+ isError: Boolean(raw.isError),
418
+ }));
419
+ continue;
420
+ }
421
+
139
422
  const role = raw.message?.role || raw.role;
140
423
  const content = raw.message?.content || raw.content;
141
-
142
424
  if (!role || !content) {
143
425
  continue;
144
426
  }
@@ -147,8 +429,26 @@ export class GeminiSessionsProvider implements IProviderSessions {
147
429
 
148
430
  if (Array.isArray(content)) {
149
431
  for (let partIdx = 0; partIdx < content.length; partIdx++) {
150
- const part = content[partIdx];
151
- if (part.type === 'text' && part.text) {
432
+ const part = content[partIdx] as AnyRecord | string;
433
+
434
+ if (typeof part === 'string' && part.trim()) {
435
+ normalized.push(createNormalizedMessage({
436
+ id: `${baseId}_${partIdx}`,
437
+ sessionId,
438
+ timestamp: ts,
439
+ provider: PROVIDER,
440
+ kind: 'text',
441
+ role: normalizedRole,
442
+ content: part,
443
+ }));
444
+ continue;
445
+ }
446
+
447
+ if (!part || typeof part !== 'object') {
448
+ continue;
449
+ }
450
+
451
+ if ((part.type === 'text' || !part.type) && typeof part.text === 'string' && part.text.trim()) {
152
452
  normalized.push(createNormalizedMessage({
153
453
  id: `${baseId}_${partIdx}`,
154
454
  sessionId,
@@ -192,6 +492,19 @@ export class GeminiSessionsProvider implements IProviderSessions {
192
492
  role: normalizedRole,
193
493
  content,
194
494
  }));
495
+ } else {
496
+ const textContent = extractGeminiTextContent(content);
497
+ if (textContent.trim()) {
498
+ normalized.push(createNormalizedMessage({
499
+ id: baseId,
500
+ sessionId,
501
+ timestamp: ts,
502
+ provider: PROVIDER,
503
+ kind: 'text',
504
+ role: normalizedRole,
505
+ content: textContent,
506
+ }));
507
+ }
195
508
  }
196
509
  }
197
510
 
@@ -222,6 +535,7 @@ export class GeminiSessionsProvider implements IProviderSessions {
222
535
  hasMore: pageLimit === null ? false : start + pageLimit < normalized.length,
223
536
  offset: start,
224
537
  limit: pageLimit,
538
+ tokenUsage: result.tokenUsage,
225
539
  };
226
540
  }
227
541
  }
@@ -1,13 +1,15 @@
1
1
  import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js';
2
2
  import { GeminiProviderAuth } from '@/modules/providers/list/gemini/gemini-auth.provider.js';
3
3
  import { GeminiMcpProvider } from '@/modules/providers/list/gemini/gemini-mcp.provider.js';
4
+ import { GeminiSessionSynchronizer } from '@/modules/providers/list/gemini/gemini-session-synchronizer.provider.js';
4
5
  import { GeminiSessionsProvider } from '@/modules/providers/list/gemini/gemini-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 GeminiProvider extends AbstractProvider {
8
9
  readonly mcp = new GeminiMcpProvider();
9
10
  readonly auth: IProviderAuth = new GeminiProviderAuth();
10
11
  readonly sessions: IProviderSessions = new GeminiSessionsProvider();
12
+ readonly sessionSynchronizer: IProviderSessionSynchronizer = new GeminiSessionSynchronizer();
11
13
 
12
14
  constructor() {
13
15
  super('gemini');
@@ -2,6 +2,8 @@ import express, { type Request, type Response } from 'express';
2
2
 
3
3
  import { providerAuthService } from '@/modules/providers/services/provider-auth.service.js';
4
4
  import { providerMcpService } from '@/modules/providers/services/mcp.service.js';
5
+ import { sessionConversationsSearchService } from '@/modules/providers/services/session-conversations-search.service.js';
6
+ import { sessionsService } from '@/modules/providers/services/sessions.service.js';
5
7
  import type { LLMProvider, McpScope, McpTransport, UpsertProviderMcpServerInput } from '@/shared/types.js';
6
8
  import { AppError, asyncHandler, createApiSuccessResponse } from '@/shared/utils.js';
7
9
 
@@ -25,6 +27,20 @@ const readPathParam = (value: unknown, name: string): string => {
25
27
  const normalizeProviderParam = (value: unknown): string =>
26
28
  readPathParam(value, 'provider').trim().toLowerCase();
27
29
 
30
+ const SESSION_ID_PATTERN = /^[a-zA-Z0-9._-]{1,120}$/;
31
+
32
+ const parseSessionId = (value: unknown): string => {
33
+ const sessionId = readPathParam(value, 'sessionId').trim();
34
+ if (!SESSION_ID_PATTERN.test(sessionId)) {
35
+ throw new AppError('Invalid sessionId.', {
36
+ code: 'INVALID_SESSION_ID',
37
+ statusCode: 400,
38
+ });
39
+ }
40
+
41
+ return sessionId;
42
+ };
43
+
28
44
  const readOptionalQueryString = (value: unknown): string | undefined => {
29
45
  if (typeof value !== 'string') {
30
46
  return undefined;
@@ -34,6 +50,29 @@ const readOptionalQueryString = (value: unknown): string | undefined => {
34
50
  return normalized.length > 0 ? normalized : undefined;
35
51
  };
36
52
 
53
+ const parseOptionalBooleanQuery = (value: unknown, name: string): boolean | undefined => {
54
+ if (value === undefined) {
55
+ return undefined;
56
+ }
57
+
58
+ const normalized = readOptionalQueryString(value);
59
+ if (!normalized) {
60
+ return undefined;
61
+ }
62
+
63
+ if (normalized === 'true') {
64
+ return true;
65
+ }
66
+ if (normalized === 'false') {
67
+ return false;
68
+ }
69
+
70
+ throw new AppError(`${name} must be "true" or "false".`, {
71
+ code: 'INVALID_QUERY_PARAMETER',
72
+ statusCode: 400,
73
+ });
74
+ };
75
+
37
76
  const parseMcpScope = (value: unknown): McpScope | undefined => {
38
77
  if (value === undefined) {
39
78
  return undefined;
@@ -103,19 +142,19 @@ const parseMcpUpsertPayload = (payload: unknown): UpsertProviderMcpServerInput =
103
142
  args: Array.isArray(body.args) ? body.args.filter((entry): entry is string => typeof entry === 'string') : undefined,
104
143
  env: typeof body.env === 'object' && body.env !== null
105
144
  ? Object.fromEntries(
106
- Object.entries(body.env as Record<string, unknown>).filter(
107
- (entry): entry is [string, string] => typeof entry[1] === 'string',
108
- ),
109
- )
145
+ Object.entries(body.env as Record<string, unknown>).filter(
146
+ (entry): entry is [string, string] => typeof entry[1] === 'string',
147
+ ),
148
+ )
110
149
  : undefined,
111
150
  cwd: readOptionalQueryString(body.cwd),
112
151
  url: readOptionalQueryString(body.url),
113
152
  headers: typeof body.headers === 'object' && body.headers !== null
114
153
  ? Object.fromEntries(
115
- Object.entries(body.headers as Record<string, unknown>).filter(
116
- (entry): entry is [string, string] => typeof entry[1] === 'string',
117
- ),
118
- )
154
+ Object.entries(body.headers as Record<string, unknown>).filter(
155
+ (entry): entry is [string, string] => typeof entry[1] === 'string',
156
+ ),
157
+ )
119
158
  : undefined,
120
159
  envVars: Array.isArray(body.envVars)
121
160
  ? body.envVars.filter((entry): entry is string => typeof entry === 'string')
@@ -123,10 +162,10 @@ const parseMcpUpsertPayload = (payload: unknown): UpsertProviderMcpServerInput =
123
162
  bearerTokenEnvVar: readOptionalQueryString(body.bearerTokenEnvVar),
124
163
  envHttpHeaders: typeof body.envHttpHeaders === 'object' && body.envHttpHeaders !== null
125
164
  ? Object.fromEntries(
126
- Object.entries(body.envHttpHeaders as Record<string, unknown>).filter(
127
- (entry): entry is [string, string] => typeof entry[1] === 'string',
128
- ),
129
- )
165
+ Object.entries(body.envHttpHeaders as Record<string, unknown>).filter(
166
+ (entry): entry is [string, string] => typeof entry[1] === 'string',
167
+ ),
168
+ )
130
169
  : undefined,
131
170
  };
132
171
  };
@@ -143,6 +182,62 @@ const parseProvider = (value: unknown): LLMProvider => {
143
182
  });
144
183
  };
145
184
 
185
+ const parseSessionRenameSummary = (payload: unknown): string => {
186
+ if (!payload || typeof payload !== 'object') {
187
+ throw new AppError('Request body must be an object.', {
188
+ code: 'INVALID_REQUEST_BODY',
189
+ statusCode: 400,
190
+ });
191
+ }
192
+
193
+ const body = payload as Record<string, unknown>;
194
+ const summary = typeof body.summary === 'string' ? body.summary.trim() : '';
195
+ if (!summary) {
196
+ throw new AppError('Summary is required.', {
197
+ code: 'INVALID_SESSION_SUMMARY',
198
+ statusCode: 400,
199
+ });
200
+ }
201
+
202
+ if (summary.length > 500) {
203
+ throw new AppError('Summary must not exceed 500 characters.', {
204
+ code: 'INVALID_SESSION_SUMMARY',
205
+ statusCode: 400,
206
+ });
207
+ }
208
+
209
+ return summary;
210
+ };
211
+
212
+ const parseSessionSearchQuery = (value: unknown): string => {
213
+ const query = readOptionalQueryString(value) ?? '';
214
+ if (query.length < 2) {
215
+ throw new AppError('Query must be at least 2 characters', {
216
+ code: 'INVALID_SEARCH_QUERY',
217
+ statusCode: 400,
218
+ });
219
+ }
220
+
221
+ return query;
222
+ };
223
+
224
+ const parseSessionSearchLimit = (value: unknown): number => {
225
+ const raw = readOptionalQueryString(value);
226
+ if (!raw) {
227
+ return 50;
228
+ }
229
+
230
+ const parsed = Number.parseInt(raw, 10);
231
+ if (Number.isNaN(parsed)) {
232
+ throw new AppError('limit must be a valid integer.', {
233
+ code: 'INVALID_QUERY_PARAMETER',
234
+ statusCode: 400,
235
+ });
236
+ }
237
+
238
+ return Math.max(1, Math.min(parsed, 100));
239
+ };
240
+
146
241
  router.get(
147
242
  '/:provider/auth/status',
148
243
  asyncHandler(async (req: Request, res: Response) => {
@@ -152,6 +247,7 @@ router.get(
152
247
  }),
153
248
  );
154
249
 
250
+ // ----------------- MCP routes -----------------
155
251
  router.get(
156
252
  '/:provider/mcp/servers',
157
253
  asyncHandler(async (req: Request, res: Response) => {
@@ -214,4 +310,116 @@ router.post(
214
310
  }),
215
311
  );
216
312
 
313
+ // ----------------- Session routes -----------------
314
+ router.delete(
315
+ '/sessions/:sessionId',
316
+ asyncHandler(async (req: Request, res: Response) => {
317
+ const sessionId = parseSessionId(req.params.sessionId);
318
+ const deletedFromDisk = parseOptionalBooleanQuery(req.query.deletedFromDisk, 'deletedFromDisk') ?? false;
319
+ const result = await sessionsService.deleteSessionById(sessionId, deletedFromDisk);
320
+ res.json(createApiSuccessResponse(result));
321
+ }),
322
+ );
323
+
324
+ router.put(
325
+ '/sessions/:sessionId',
326
+ asyncHandler(async (req: Request, res: Response) => {
327
+ const sessionId = parseSessionId(req.params.sessionId);
328
+ const summary = parseSessionRenameSummary(req.body);
329
+ const result = sessionsService.renameSessionById(sessionId, summary);
330
+ res.json(createApiSuccessResponse(result));
331
+ }),
332
+ );
333
+
334
+ router.get(
335
+ '/sessions/:sessionId/messages',
336
+ asyncHandler(async (req: Request, res: Response) => {
337
+ const sessionId = parseSessionId(req.params.sessionId);
338
+ const limitRaw = readOptionalQueryString(req.query.limit);
339
+ const offsetRaw = readOptionalQueryString(req.query.offset);
340
+
341
+ let limit: number | null = null;
342
+ if (limitRaw !== undefined) {
343
+ const parsedLimit = Number.parseInt(limitRaw, 10);
344
+ if (Number.isNaN(parsedLimit) || parsedLimit < 0) {
345
+ throw new AppError('limit must be a non-negative integer.', {
346
+ code: 'INVALID_QUERY_PARAMETER',
347
+ statusCode: 400,
348
+ });
349
+ }
350
+ limit = parsedLimit;
351
+ }
352
+
353
+ let offset = 0;
354
+ if (offsetRaw !== undefined) {
355
+ const parsedOffset = Number.parseInt(offsetRaw, 10);
356
+ if (Number.isNaN(parsedOffset) || parsedOffset < 0) {
357
+ throw new AppError('offset must be a non-negative integer.', {
358
+ code: 'INVALID_QUERY_PARAMETER',
359
+ statusCode: 400,
360
+ });
361
+ }
362
+ offset = parsedOffset;
363
+ }
364
+
365
+ const result = await sessionsService.fetchHistory(sessionId, {
366
+ limit,
367
+ offset,
368
+ });
369
+ res.json(result);
370
+ }),
371
+ );
372
+
373
+ router.get('/search/sessions', asyncHandler(async (req: Request, res: Response) => {
374
+ const query = parseSessionSearchQuery(req.query.q);
375
+ const limit = parseSessionSearchLimit(req.query.limit);
376
+
377
+ res.writeHead(200, {
378
+ 'Content-Type': 'text/event-stream',
379
+ 'Cache-Control': 'no-cache',
380
+ Connection: 'keep-alive',
381
+ 'X-Accel-Buffering': 'no',
382
+ });
383
+
384
+ let closed = false;
385
+ const abortController = new AbortController();
386
+ req.on('close', () => {
387
+ closed = true;
388
+ abortController.abort();
389
+ });
390
+
391
+ try {
392
+ await sessionConversationsSearchService.search({
393
+ query,
394
+ limit,
395
+ signal: abortController.signal,
396
+ onProgress: ({ projectResult, totalMatches, scannedProjects, totalProjects }) => {
397
+ if (closed) {
398
+ return;
399
+ }
400
+
401
+ if (projectResult) {
402
+ res.write(`event: result\ndata: ${JSON.stringify({ projectResult, totalMatches, scannedProjects, totalProjects })}\n\n`);
403
+ return;
404
+ }
405
+
406
+ res.write(`event: progress\ndata: ${JSON.stringify({ totalMatches, scannedProjects, totalProjects })}\n\n`);
407
+ },
408
+ });
409
+
410
+ if (!closed) {
411
+ res.write('event: done\ndata: {}\n\n');
412
+ }
413
+ } catch (error) {
414
+ console.error('Error searching conversations:', error);
415
+ if (!closed) {
416
+ res.write(`event: error\ndata: ${JSON.stringify({ error: 'Search failed' })}\n\n`);
417
+ }
418
+ } finally {
419
+ if (!closed) {
420
+ res.end();
421
+ }
422
+ }
423
+ }));
424
+
217
425
  export default router;