@cloudcli-ai/cloudcli 1.36.0 → 1.36.2

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 (217) hide show
  1. package/README.de.md +17 -18
  2. package/README.ja.md +16 -17
  3. package/README.ko.md +17 -18
  4. package/README.md +19 -20
  5. package/README.ru.md +17 -18
  6. package/README.tr.md +17 -18
  7. package/README.zh-CN.md +17 -18
  8. package/README.zh-TW.md +17 -18
  9. package/dist/api-docs.html +2 -3
  10. package/dist/assets/{index-S6cIy7hj.js → index-CDGzV0LN.js} +264 -259
  11. package/dist/assets/index-DLmhQ15L.css +32 -0
  12. package/dist/assets/{vendor-xterm-CJZjLICi.js → vendor-xterm-CS4rQ5Mr.js} +12 -12
  13. package/dist/index.html +3 -3
  14. package/dist-server/server/claude-sdk.js +37 -90
  15. package/dist-server/server/claude-sdk.js.map +1 -1
  16. package/dist-server/server/cli.js +1 -4
  17. package/dist-server/server/cli.js.map +1 -1
  18. package/dist-server/server/cursor-cli.js +12 -11
  19. package/dist-server/server/cursor-cli.js.map +1 -1
  20. package/dist-server/server/index.js +10 -141
  21. package/dist-server/server/index.js.map +1 -1
  22. package/dist-server/server/modules/assets/assets.routes.js +89 -0
  23. package/dist-server/server/modules/assets/assets.routes.js.map +1 -0
  24. package/dist-server/server/modules/assets/index.js +4 -0
  25. package/dist-server/server/modules/assets/index.js.map +1 -0
  26. package/dist-server/server/modules/assets/services/image-assets.service.js +57 -0
  27. package/dist-server/server/modules/assets/services/image-assets.service.js.map +1 -0
  28. package/dist-server/server/modules/assets/tests/image-assets.service.test.js +36 -0
  29. package/dist-server/server/modules/assets/tests/image-assets.service.test.js.map +1 -0
  30. package/dist-server/server/modules/browser-use/browser-use.service.js +6 -3
  31. package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -1
  32. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +2 -2
  33. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -1
  34. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +0 -1
  35. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -1
  36. package/dist-server/server/modules/projects/services/project-clone.service.js +2 -1
  37. package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -1
  38. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +28 -0
  39. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  40. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +54 -1
  41. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  42. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +36 -1
  43. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  44. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +3 -17
  45. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -1
  46. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +7 -1
  47. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
  48. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +49 -13
  49. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
  50. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +0 -1
  51. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -1
  52. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +8 -14
  53. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  54. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +24 -5
  55. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  56. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +10 -21
  57. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -1
  58. package/dist-server/server/modules/providers/provider.registry.js +0 -2
  59. package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
  60. package/dist-server/server/modules/providers/provider.routes.js +0 -1
  61. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  62. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +7 -14
  63. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
  64. package/dist-server/server/modules/providers/services/provider-models.service.js +1 -1
  65. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  66. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +3 -77
  67. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  68. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +0 -1
  69. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  70. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +0 -13
  71. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  72. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +96 -0
  73. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -0
  74. package/dist-server/server/modules/providers/tests/mcp.test.js +3 -29
  75. package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
  76. package/dist-server/server/modules/providers/tests/opencode-models.test.js +7 -0
  77. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  78. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +117 -0
  79. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  80. package/dist-server/server/modules/providers/tests/provider-image-history.test.js +147 -0
  81. package/dist-server/server/modules/providers/tests/provider-image-history.test.js.map +1 -0
  82. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +8 -8
  83. package/dist-server/server/modules/providers/tests/skills.test.js +3 -26
  84. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  85. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +14 -0
  86. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
  87. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +36 -2
  88. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  89. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +3 -12
  90. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  91. package/dist-server/server/modules/websocket/tests/chat-image-filter.test.js +33 -0
  92. package/dist-server/server/modules/websocket/tests/chat-image-filter.test.js.map +1 -0
  93. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +37 -4
  94. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
  95. package/dist-server/server/openai-codex.js +8 -2
  96. package/dist-server/server/openai-codex.js.map +1 -1
  97. package/dist-server/server/opencode-cli.js +43 -6
  98. package/dist-server/server/opencode-cli.js.map +1 -1
  99. package/dist-server/server/opencode-cli.test.js +103 -3
  100. package/dist-server/server/opencode-cli.test.js.map +1 -1
  101. package/dist-server/server/routes/agent.js +9 -19
  102. package/dist-server/server/routes/agent.js.map +1 -1
  103. package/dist-server/server/routes/commands.js +1 -2
  104. package/dist-server/server/routes/commands.js.map +1 -1
  105. package/dist-server/server/routes/git.js +179 -53
  106. package/dist-server/server/routes/git.js.map +1 -1
  107. package/dist-server/server/routes/git.test.js +80 -0
  108. package/dist-server/server/routes/git.test.js.map +1 -0
  109. package/dist-server/server/routes/taskmaster.js +3 -1
  110. package/dist-server/server/routes/taskmaster.js.map +1 -1
  111. package/dist-server/server/routes/user.js +2 -1
  112. package/dist-server/server/routes/user.js.map +1 -1
  113. package/dist-server/server/shared/image-attachments.js +270 -0
  114. package/dist-server/server/shared/image-attachments.js.map +1 -0
  115. package/dist-server/server/shared/tests/image-attachments.test.js +220 -0
  116. package/dist-server/server/shared/tests/image-attachments.test.js.map +1 -0
  117. package/dist-server/server/shared/utils.js +43 -0
  118. package/dist-server/server/shared/utils.js.map +1 -1
  119. package/dist-server/server/utils/gitConfig.js +2 -1
  120. package/dist-server/server/utils/gitConfig.js.map +1 -1
  121. package/dist-server/server/utils/plugin-process-manager.js +2 -1
  122. package/dist-server/server/utils/plugin-process-manager.js.map +1 -1
  123. package/electron/launcher/launcher.js +1 -1
  124. package/package.json +4 -4
  125. package/public/api-docs.html +2 -3
  126. package/server/claude-sdk.js +37 -105
  127. package/server/cli.js +1 -4
  128. package/server/cursor-cli.js +13 -12
  129. package/server/index.js +11 -159
  130. package/server/modules/assets/assets.routes.ts +103 -0
  131. package/server/modules/assets/index.ts +3 -0
  132. package/server/modules/assets/services/image-assets.service.ts +82 -0
  133. package/server/modules/assets/tests/image-assets.service.test.ts +46 -0
  134. package/server/modules/browser-use/browser-use.service.ts +7 -3
  135. package/server/modules/database/tests/sessions-provider-mapping.test.ts +2 -2
  136. package/server/modules/notifications/services/notification-orchestrator.service.js +0 -1
  137. package/server/modules/projects/services/project-clone.service.ts +3 -1
  138. package/server/modules/providers/README.md +1 -7
  139. package/server/modules/providers/list/claude/claude-sessions.provider.ts +30 -0
  140. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +60 -1
  141. package/server/modules/providers/list/codex/codex-sessions.provider.ts +41 -1
  142. package/server/modules/providers/list/cursor/cursor-models.provider.ts +3 -17
  143. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +7 -1
  144. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +56 -15
  145. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +0 -1
  146. package/server/modules/providers/list/opencode/opencode-models.provider.ts +15 -19
  147. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +25 -4
  148. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +10 -21
  149. package/server/modules/providers/provider.registry.ts +0 -2
  150. package/server/modules/providers/provider.routes.ts +0 -1
  151. package/server/modules/providers/services/provider-capabilities.service.ts +7 -14
  152. package/server/modules/providers/services/provider-models.service.ts +1 -1
  153. package/server/modules/providers/services/session-conversations-search.service.ts +3 -93
  154. package/server/modules/providers/services/session-synchronizer.service.ts +0 -1
  155. package/server/modules/providers/services/sessions-watcher.service.ts +0 -14
  156. package/server/modules/providers/tests/codex-sessions.test.ts +111 -0
  157. package/server/modules/providers/tests/mcp.test.ts +3 -34
  158. package/server/modules/providers/tests/opencode-models.test.ts +7 -0
  159. package/server/modules/providers/tests/opencode-sessions.test.ts +139 -0
  160. package/server/modules/providers/tests/provider-image-history.test.ts +180 -0
  161. package/server/modules/providers/tests/provider-models.service.test.ts +8 -8
  162. package/server/modules/providers/tests/skills.test.ts +3 -42
  163. package/server/modules/websocket/services/chat-run-registry.service.ts +16 -0
  164. package/server/modules/websocket/services/chat-websocket.service.ts +41 -2
  165. package/server/modules/websocket/services/shell-websocket.service.ts +1 -11
  166. package/server/modules/websocket/tests/chat-image-filter.test.ts +44 -0
  167. package/server/modules/websocket/tests/chat-run-registry.test.ts +42 -4
  168. package/server/openai-codex.js +9 -1
  169. package/server/opencode-cli.js +44 -6
  170. package/server/opencode-cli.test.js +112 -3
  171. package/server/routes/agent.js +9 -19
  172. package/server/routes/commands.js +1 -2
  173. package/server/routes/git.js +201 -60
  174. package/server/routes/git.test.js +106 -0
  175. package/server/routes/taskmaster.js +3 -1
  176. package/server/routes/user.js +2 -1
  177. package/server/shared/image-attachments.ts +341 -0
  178. package/server/shared/tests/image-attachments.test.ts +298 -0
  179. package/server/shared/types.ts +1 -1
  180. package/server/shared/utils.ts +45 -0
  181. package/server/utils/gitConfig.js +2 -1
  182. package/server/utils/plugin-process-manager.js +3 -1
  183. package/dist/assets/index-uLUE6Qt1.css +0 -32
  184. package/dist/icons/gemini-ai-icon.svg +0 -1
  185. package/dist-server/server/gemini-cli.js +0 -551
  186. package/dist-server/server/gemini-cli.js.map +0 -1
  187. package/dist-server/server/gemini-response-handler.js +0 -106
  188. package/dist-server/server/gemini-response-handler.js.map +0 -1
  189. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +0 -254
  190. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +0 -1
  191. package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js +0 -82
  192. package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js.map +0 -1
  193. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +0 -24
  194. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +0 -1
  195. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +0 -312
  196. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +0 -1
  197. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +0 -474
  198. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +0 -1
  199. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +0 -40
  200. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +0 -1
  201. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +0 -19
  202. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +0 -1
  203. package/dist-server/server/routes/gemini.js +0 -21
  204. package/dist-server/server/routes/gemini.js.map +0 -1
  205. package/dist-server/server/sessionManager.js +0 -194
  206. package/dist-server/server/sessionManager.js.map +0 -1
  207. package/server/gemini-cli.js +0 -638
  208. package/server/gemini-response-handler.js +0 -117
  209. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +0 -307
  210. package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +0 -110
  211. package/server/modules/providers/list/gemini/gemini-models.provider.ts +0 -39
  212. package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +0 -405
  213. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +0 -540
  214. package/server/modules/providers/list/gemini/gemini-skills.provider.ts +0 -44
  215. package/server/modules/providers/list/gemini/gemini.provider.ts +0 -27
  216. package/server/routes/gemini.js +0 -25
  217. package/server/sessionManager.js +0 -226
@@ -0,0 +1,3 @@
1
+ // Express router mounted at /api/assets by server/index.js (upload + serving
2
+ // of chat image attachments stored in the global ~/.cloudcli/assets folder).
3
+ export { default as assetsRoutes } from './assets.routes.js';
@@ -0,0 +1,82 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { getGlobalImageAssetsDir, toPosixPath } from '@/shared/image-attachments.js';
5
+
6
+ /**
7
+ * Image mime types accepted for chat attachment uploads. SVG is allowed for
8
+ * storage/preview even though some providers (Claude API) skip it at send time.
9
+ */
10
+ const ALLOWED_IMAGE_MIME_TYPES = new Set([
11
+ 'image/jpeg',
12
+ 'image/png',
13
+ 'image/gif',
14
+ 'image/webp',
15
+ 'image/svg+xml',
16
+ ]);
17
+
18
+ // Used only by this service and the assets routes via the barrel file.
19
+ type StoredImageAsset = {
20
+ /** Original upload filename, for display. */
21
+ name: string;
22
+ /** Absolute posix-normalized path inside the global assets folder. */
23
+ path: string;
24
+ size: number;
25
+ mimeType: string;
26
+ };
27
+
28
+ // Shape of one multer-stored file; kept local because only this module reads it.
29
+ type UploadedImageFile = {
30
+ originalname: string;
31
+ filename: string;
32
+ size: number;
33
+ mimetype: string;
34
+ };
35
+
36
+ /** Returns whether one uploaded mime type may be stored as a chat image asset. */
37
+ export function isAllowedImageMimeType(mimeType: string): boolean {
38
+ return ALLOWED_IMAGE_MIME_TYPES.has(mimeType);
39
+ }
40
+
41
+ /** Creates the global `~/.cloudcli/assets` folder if needed and returns it. */
42
+ export async function ensureImageAssetsDir(): Promise<string> {
43
+ const assetsDir = getGlobalImageAssetsDir();
44
+ await fs.mkdir(assetsDir, { recursive: true });
45
+ return assetsDir;
46
+ }
47
+
48
+ /**
49
+ * Maps multer-stored upload files to the attachment records returned to the
50
+ * chat composer. The absolute path is what providers receive and what session
51
+ * history carries back to the UI.
52
+ */
53
+ export function buildStoredImageRecords(files: UploadedImageFile[]): StoredImageAsset[] {
54
+ const assetsDir = getGlobalImageAssetsDir();
55
+ return files.map((file) => ({
56
+ name: file.originalname,
57
+ path: toPosixPath(path.join(assetsDir, file.filename)),
58
+ size: file.size,
59
+ mimeType: file.mimetype,
60
+ }));
61
+ }
62
+
63
+ /**
64
+ * Resolves one asset filename to its absolute path inside the global assets
65
+ * folder, or null when the name is empty, contains path separators/traversal,
66
+ * or would escape the folder. This is the only lookup the serving route uses,
67
+ * so nothing outside `~/.cloudcli/assets` can ever be read through it.
68
+ */
69
+ export function resolveImageAssetFile(filename: string): string | null {
70
+ const trimmed = typeof filename === 'string' ? filename.trim() : '';
71
+ if (!trimmed || trimmed.includes('/') || trimmed.includes('\\') || trimmed.includes('..')) {
72
+ return null;
73
+ }
74
+
75
+ const assetsDir = path.resolve(getGlobalImageAssetsDir());
76
+ const resolved = path.resolve(assetsDir, trimmed);
77
+ if (!resolved.startsWith(assetsDir + path.sep)) {
78
+ return null;
79
+ }
80
+
81
+ return resolved;
82
+ }
@@ -0,0 +1,46 @@
1
+ import assert from 'node:assert/strict';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import test from 'node:test';
5
+
6
+ import {
7
+ buildStoredImageRecords,
8
+ isAllowedImageMimeType,
9
+ resolveImageAssetFile,
10
+ } from '@/modules/assets/services/image-assets.service.js';
11
+
12
+ const ASSETS_DIR = path.join(os.homedir(), '.cloudcli', 'assets');
13
+
14
+ test('isAllowedImageMimeType accepts image formats and rejects the rest', () => {
15
+ assert.equal(isAllowedImageMimeType('image/png'), true);
16
+ assert.equal(isAllowedImageMimeType('image/svg+xml'), true);
17
+ assert.equal(isAllowedImageMimeType('application/pdf'), false);
18
+ assert.equal(isAllowedImageMimeType('text/html'), false);
19
+ });
20
+
21
+ test('buildStoredImageRecords returns absolute posix paths in the assets dir', () => {
22
+ const records = buildStoredImageRecords([
23
+ { originalname: 'shot.png', filename: '123-456-shot.png', size: 42, mimetype: 'image/png' },
24
+ ]);
25
+
26
+ assert.equal(records.length, 1);
27
+ assert.equal(records[0].name, 'shot.png');
28
+ assert.equal(records[0].size, 42);
29
+ assert.equal(records[0].mimeType, 'image/png');
30
+ assert.equal(records[0].path, `${ASSETS_DIR.replace(/\\/g, '/')}/123-456-shot.png`);
31
+ });
32
+
33
+ test('resolveImageAssetFile resolves plain filenames inside the assets dir', () => {
34
+ const resolved = resolveImageAssetFile('123-shot.png');
35
+ assert.equal(resolved, path.join(path.resolve(ASSETS_DIR), '123-shot.png'));
36
+ });
37
+
38
+ test('resolveImageAssetFile rejects traversal and separator attempts', () => {
39
+ assert.equal(resolveImageAssetFile(''), null);
40
+ assert.equal(resolveImageAssetFile(' '), null);
41
+ assert.equal(resolveImageAssetFile('../auth.db'), null);
42
+ assert.equal(resolveImageAssetFile('..'), null);
43
+ assert.equal(resolveImageAssetFile('sub/dir.png'), null);
44
+ assert.equal(resolveImageAssetFile('sub\\dir.png'), null);
45
+ assert.equal(resolveImageAssetFile('a..b/../c.png'), null);
46
+ });
@@ -1,10 +1,12 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { randomBytes, randomUUID } from 'node:crypto';
3
- import { spawn } from 'node:child_process';
4
3
  import fs from 'node:fs';
5
4
  import os from 'node:os';
6
5
  import path from 'node:path';
7
6
 
7
+ // cross-spawn: drop-in spawn with Windows .cmd/PATHEXT resolution.
8
+ import spawn from 'cross-spawn';
9
+
8
10
  import { appConfigDb } from '@/modules/database/index.js';
9
11
  import { providerMcpService } from '@/modules/providers/index.js';
10
12
  import { getModuleDir } from '@/utils/runtime-paths.js';
@@ -270,8 +272,10 @@ function runCommand(command: string, args: string[]): Promise<void> {
270
272
  }, INSTALL_COMMAND_TIMEOUT_MS);
271
273
  timer.unref?.();
272
274
 
273
- child.stdout.on('data', (chunk) => output.push(String(chunk)));
274
- child.stderr.on('data', (chunk) => output.push(String(chunk)));
275
+ // stdio config above guarantees the pipes exist; cross-spawn's types
276
+ // just don't narrow them the way node's spawn overloads do.
277
+ child.stdout?.on('data', (chunk) => output.push(String(chunk)));
278
+ child.stderr?.on('data', (chunk) => output.push(String(chunk)));
275
279
  child.on('error', (error) => finish(() => reject(error)));
276
280
  child.on('close', (code) => finish(() => {
277
281
  if (code === 0) {
@@ -100,9 +100,9 @@ test('assignProviderSessionId merges a watcher-created duplicate into the app ro
100
100
 
101
101
  test('legacy provider-keyed rows stay resolvable through both lookups', async () => {
102
102
  await withIsolatedDatabase(() => {
103
- sessionsDb.createSession('legacy-1', 'gemini', '/workspace/demo');
103
+ sessionsDb.createSession('legacy-1', 'opencode', '/workspace/demo');
104
104
 
105
- assert.equal(sessionsDb.getSessionById('legacy-1')?.provider, 'gemini');
105
+ assert.equal(sessionsDb.getSessionById('legacy-1')?.provider, 'opencode');
106
106
  assert.equal(sessionsDb.getSessionByProviderSessionId('legacy-1')?.session_id, 'legacy-1');
107
107
  });
108
108
  });
@@ -13,7 +13,6 @@ const PROVIDER_LABELS = {
13
13
  claude: 'Claude',
14
14
  cursor: 'Cursor',
15
15
  codex: 'Codex',
16
- gemini: 'Gemini',
17
16
  system: 'System'
18
17
  };
19
18
 
@@ -1,7 +1,9 @@
1
- import { spawn } from 'node:child_process';
2
1
  import { access, mkdir, rm } from 'node:fs/promises';
3
2
  import path from 'node:path';
4
3
 
4
+ // cross-spawn: drop-in spawn with Windows .cmd/PATHEXT resolution.
5
+ import spawn from 'cross-spawn';
6
+
5
7
  import { githubTokensDb } from '@/modules/database/index.js';
6
8
  import { createProject } from '@/modules/projects/services/project-management.service.js';
7
9
  import type { WorkspacePathValidationResult } from '@/shared/types.js';
@@ -36,7 +36,6 @@ Current provider ids in this repo are:
36
36
  - `claude`
37
37
  - `codex`
38
38
  - `cursor`
39
- - `gemini`
40
39
  - `opencode`
41
40
 
42
41
  Those ids are mirrored in backend unions and frontend provider constants. If
@@ -56,8 +55,7 @@ server/modules/providers/list/<provider>/
56
55
  <provider>-session-synchronizer.provider.ts
57
56
  ```
58
57
 
59
- The existing provider folders are `claude`, `codex`, `cursor`, `gemini`, and
60
- `opencode`.
58
+ The existing provider folders are `claude`, `codex`, `cursor`, and `opencode`.
61
59
 
62
60
  ## What Each Facet Does
63
61
 
@@ -123,7 +121,6 @@ Current MCP formats in this repo are:
123
121
  | Claude | `.mcp.json` in user / local / project locations | `user`, `local`, `project` | `stdio`, `http`, `sse` |
124
122
  | Codex | `.codex/config.toml` | `user`, `project` | `stdio`, `http` |
125
123
  | Cursor | `.cursor/mcp.json` | `user`, `project` | `stdio`, `http` |
126
- | Gemini | `.gemini/settings.json` | `user`, `project` | `stdio`, `http` |
127
124
  | OpenCode | `~/.config/opencode/opencode.json` or `<workspace>/opencode.json` (`.jsonc` is read when present) | `user`, `project` | `stdio`, `http` |
128
125
 
129
126
  5. Implement skills.
@@ -144,7 +141,6 @@ Current skill discovery roots are:
144
141
  | Claude | `~/.claude/skills` | `<workspace>/.claude/skills` | `/` | Also discovers Claude plugin skills from enabled plugin installs. Command skills live under `commands/`; markdown skills live under `skills/` and are scanned recursively. |
145
142
  | Codex | `~/.agents/skills`, `~/.codex/skills/.system`, `/etc/codex/skills` | `<workspace>/.agents/skills`, `path.dirname(workspacePath)/.agents/skills`, topmost git root `.agents/skills` | `$` | Overlapping roots are deduplicated before scanning. |
146
143
  | Cursor | `~/.cursor/skills` | `<workspace>/.cursor/skills`, `<workspace>/.agents/skills` | `/` | Uses slash-style commands. |
147
- | Gemini | `~/.gemini/skills`, `~/.agents/skills` | `<workspace>/.gemini/skills`, `<workspace>/.agents/skills` | `/` | Uses slash-style commands. |
148
144
  | OpenCode | `~/.config/opencode/skills`, `~/.claude/skills`, `~/.agents/skills` | Cwd-to-topmost-git-root `.opencode/skills`, `.claude/skills`, and `.agents/skills` | `/` | Reuses OpenCode, Claude, and Agents skill locations. Overlapping roots are deduplicated before scanning. |
149
145
 
150
146
  Command forms currently used by the providers are:
@@ -153,7 +149,6 @@ Command forms currently used by the providers are:
153
149
  - Claude plugin skills: `/plugin-name:skill-name`
154
150
  - Codex skills: `$skill-name`
155
151
  - Cursor skills: `/skill-name`
156
- - Gemini skills: `/skill-name`
157
152
  - OpenCode skills: `/skill-name`
158
153
 
159
154
  6. Implement sessions.
@@ -191,7 +186,6 @@ Current session sync roots are:
191
186
  | Claude | `~/.claude/projects/**/*.jsonl` | Uses `~/.claude/history.jsonl` for name lookup and the trailing `ai-title`, `last-prompt`, or `custom-title` entries for title recovery. |
192
187
  | Codex | `~/.codex/sessions/**/*.jsonl` | Uses `~/.codex/session_index.jsonl` for title lookup and the last `task_complete` message for a fallback title. |
193
188
  | Cursor | `~/.cursor/projects/**/*.jsonl` | Uses sibling `worker.log` to recover `workspacePath`, then derives the session title from the first user prompt. |
194
- | Gemini | `~/.gemini/tmp/**/*.jsonl` | Current full scans only index temp JSONL chat artifacts. Single-file sync also accepts legacy `.json` files. |
195
189
  | OpenCode | `~/.local/share/opencode/opencode.db` | Reads active sessions/messages/parts from OpenCode's shared SQLite database and stores `jsonl_path` as `null` so deleting one app session cannot remove the shared DB. |
196
190
 
197
191
  8. Register the provider.
@@ -313,6 +313,18 @@ export class ClaudeSessionsProvider implements IProviderSessions {
313
313
 
314
314
  if (raw.message?.role === 'user' && raw.message?.content && raw.isMeta !== true) {
315
315
  if (Array.isArray(raw.message.content)) {
316
+ // Image attachments sent through the SDK are persisted as base64
317
+ // `image` blocks next to the prompt text. Collect them so the UI can
318
+ // render them on the user bubble.
319
+ const imageAttachments: Array<{ data: string }> = [];
320
+ for (const part of raw.message.content) {
321
+ if (part?.type === 'image' && part.source?.type === 'base64' && typeof part.source.data === 'string') {
322
+ const mediaType = typeof part.source.media_type === 'string' ? part.source.media_type : 'image/png';
323
+ imageAttachments.push({ data: `data:${mediaType};base64,${part.source.data}` });
324
+ }
325
+ }
326
+ let imagesAttached = false;
327
+
316
328
  for (let partIndex = 0; partIndex < raw.message.content.length; partIndex++) {
317
329
  const part = raw.message.content[partIndex];
318
330
  if (part.type === 'tool_result') {
@@ -339,7 +351,9 @@ export class ClaudeSessionsProvider implements IProviderSessions {
339
351
  kind: 'text',
340
352
  role: 'user',
341
353
  content: text,
354
+ images: !imagesAttached && imageAttachments.length > 0 ? imageAttachments : undefined,
342
355
  }));
356
+ imagesAttached = true;
343
357
  }
344
358
  }
345
359
  }
@@ -359,9 +373,25 @@ export class ClaudeSessionsProvider implements IProviderSessions {
359
373
  kind: 'text',
360
374
  role: 'user',
361
375
  content: textParts,
376
+ images: imageAttachments.length > 0 ? imageAttachments : undefined,
362
377
  }));
378
+ imagesAttached = true;
363
379
  }
364
380
  }
381
+
382
+ // Image-only turns still deserve a user bubble even without text.
383
+ if (!imagesAttached && imageAttachments.length > 0) {
384
+ messages.push(createNormalizedMessage({
385
+ id: `${baseId}_images`,
386
+ sessionId,
387
+ timestamp: ts,
388
+ provider: PROVIDER,
389
+ kind: 'text',
390
+ role: 'user',
391
+ content: '',
392
+ images: imageAttachments,
393
+ }));
394
+ }
365
395
  } else if (typeof raw.message.content === 'string') {
366
396
  const text = raw.message.content;
367
397
 
@@ -133,7 +133,23 @@ export class CodexSessionSynchronizer implements IProviderSessionSynchronizer {
133
133
  };
134
134
  }
135
135
 
136
- let sessionName = nameMap.get(parsed.sessionId);
136
+ // Sessions started by sending a message from cloudcli carry a distinct
137
+ // app-allocated session_id mapped to the provider id. For these we title the
138
+ // conversation from the first user message the user typed, instead of the
139
+ // generic "Untitled Codex Session" placeholder. Sessions discovered purely
140
+ // by indexing (session_id === provider_session_id) keep the existing
141
+ // thread_name/last-agent-message setup below.
142
+ const isAppCreated =
143
+ existingSession != null &&
144
+ existingSession.provider_session_id != null &&
145
+ existingSession.session_id !== existingSession.provider_session_id;
146
+
147
+ let sessionName = isAppCreated
148
+ ? await this.extractFirstUserMessageFromStart(filePath)
149
+ : undefined;
150
+ if (!sessionName) {
151
+ sessionName = nameMap.get(parsed.sessionId);
152
+ }
137
153
  if (!sessionName) {
138
154
  sessionName = await this.extractLastAgentMessageFromEnd(filePath);
139
155
  }
@@ -144,6 +160,49 @@ export class CodexSessionSynchronizer implements IProviderSessionSynchronizer {
144
160
  };
145
161
  }
146
162
 
163
+ /**
164
+ * Returns the first user message text in a Codex transcript, used to title
165
+ * app-created sessions from the prompt the user sent from cloudcli.
166
+ *
167
+ * Reads the `event_msg`/`user_message` payload rather than the raw
168
+ * `response_item` user turn so injected `<environment_context>` boilerplate is
169
+ * never mistaken for the user's prompt.
170
+ */
171
+ private async extractFirstUserMessageFromStart(filePath: string): Promise<string | undefined> {
172
+ try {
173
+ const content = await readFile(filePath, 'utf8');
174
+ const lines = content.split(/\r?\n/);
175
+
176
+ for (const rawLine of lines) {
177
+ const line = rawLine.trim();
178
+ if (!line) {
179
+ continue;
180
+ }
181
+
182
+ let parsed: unknown;
183
+ try {
184
+ parsed = JSON.parse(line);
185
+ } catch {
186
+ continue;
187
+ }
188
+
189
+ const data = parsed as Record<string, unknown>;
190
+ const eventType = typeof data.type === 'string' ? data.type : undefined;
191
+ const payload = data.payload as Record<string, unknown> | undefined;
192
+ const payloadType = typeof payload?.type === 'string' ? payload.type : undefined;
193
+ const message = typeof payload?.message === 'string' ? payload.message : undefined;
194
+
195
+ if (eventType === 'event_msg' && payloadType === 'user_message' && message?.trim()) {
196
+ return message;
197
+ }
198
+ }
199
+ } catch {
200
+ // Ignore missing/unreadable files so sync can continue.
201
+ }
202
+
203
+ return undefined;
204
+ }
205
+
147
206
  private async extractLastAgentMessageFromEnd(filePath: string): Promise<string | undefined> {
148
207
  try {
149
208
  const content = await readFile(filePath, 'utf8');
@@ -2,6 +2,7 @@ import fsSync from 'node:fs';
2
2
  import readline from 'node:readline';
3
3
 
4
4
  import { sessionsDb } from '@/modules/database/index.js';
5
+ import { toImageAttachments } from '@/shared/image-attachments.js';
5
6
  import type { IProviderSessions } from '@/shared/interfaces.js';
6
7
  import type { AnyRecord, FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
7
8
  import { createNormalizedMessage, generateMessageId, readObjectRecord, sliceTailPage } from '@/shared/utils.js';
@@ -31,6 +32,42 @@ function isVisibleCodexUserMessage(payload: AnyRecord | null | undefined): boole
31
32
  return typeof payload.message === 'string' && payload.message.trim().length > 0;
32
33
  }
33
34
 
35
+ /**
36
+ * Reads the image attachments Codex records on `user_message` events.
37
+ * Turns sent with `local_image` input items land in `local_images` as file
38
+ * paths (verified against real rollout JSONL); the `images` array can carry
39
+ * base64 data URLs, which are passed through as inline `data` attachments so
40
+ * the UI can preview them without a file lookup.
41
+ *
42
+ * Exported for tests.
43
+ */
44
+ export function extractCodexUserImages(
45
+ payload: AnyRecord | null | undefined,
46
+ ): Array<{ path?: string; data?: string }> | undefined {
47
+ if (!payload) {
48
+ return undefined;
49
+ }
50
+
51
+ const candidates = [
52
+ ...(Array.isArray(payload.local_images) ? payload.local_images : []),
53
+ ...(Array.isArray(payload.images) ? payload.images : []),
54
+ ];
55
+
56
+ const attachments: Array<{ path?: string; data?: string }> = [];
57
+ for (const entry of candidates) {
58
+ if (typeof entry !== 'string' || !entry.trim()) {
59
+ continue;
60
+ }
61
+ if (entry.startsWith('data:')) {
62
+ attachments.push({ data: entry });
63
+ } else {
64
+ attachments.push(...toImageAttachments([entry]));
65
+ }
66
+ }
67
+
68
+ return attachments.length > 0 ? attachments : undefined;
69
+ }
70
+
34
71
  function extractCodexTextContent(content: unknown): string {
35
72
  if (!Array.isArray(content)) {
36
73
  return typeof content === 'string' ? content : '';
@@ -104,6 +141,7 @@ async function getCodexSessionMessages(
104
141
  role: 'user',
105
142
  content: entry.payload.message,
106
143
  },
144
+ images: extractCodexUserImages(entry.payload as AnyRecord),
107
145
  });
108
146
  }
109
147
 
@@ -296,7 +334,8 @@ export class CodexSessionsProvider implements IProviderSessions {
296
334
  .filter(Boolean)
297
335
  .join('\n')
298
336
  : String(raw.message.content || '');
299
- if (!content.trim()) {
337
+ const rawImages = Array.isArray(raw.images) && raw.images.length > 0 ? raw.images : undefined;
338
+ if (!content.trim() && !rawImages) {
300
339
  return [];
301
340
  }
302
341
  return [createNormalizedMessage({
@@ -307,6 +346,7 @@ export class CodexSessionsProvider implements IProviderSessions {
307
346
  kind: 'text',
308
347
  role: 'user',
309
348
  content,
349
+ images: rawImages,
310
350
  })];
311
351
  }
312
352
 
@@ -1,7 +1,6 @@
1
1
  import { access, readdir } from 'node:fs/promises';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { spawn } from 'node:child_process';
5
4
 
6
5
  import crossSpawn from 'cross-spawn';
7
6
 
@@ -446,11 +445,6 @@ export const CURSOR_FALLBACK_MODELS: ProviderModelsDefinition = {
446
445
  label: "gpt-5.2-xhigh-fast",
447
446
  description: "GPT-5.2 Extra High Fast",
448
447
  },
449
- {
450
- value: "gemini-3.1-pro",
451
- label: "gemini-3.1-pro",
452
- description: "Gemini 3.1 Pro",
453
- },
454
448
  {
455
449
  value: "gpt-5.4-mini-none",
456
450
  label: "gpt-5.4-mini-none",
@@ -531,16 +525,6 @@ export const CURSOR_FALLBACK_MODELS: ProviderModelsDefinition = {
531
525
  label: "gpt-5.1-high",
532
526
  description: "GPT-5.1 High",
533
527
  },
534
- {
535
- value: "gemini-3-flash",
536
- label: "gemini-3-flash",
537
- description: "Gemini 3 Flash",
538
- },
539
- {
540
- value: "gemini-3.5-flash",
541
- label: "gemini-3.5-flash",
542
- description: "Gemini 3.5 Flash",
543
- },
544
528
  {
545
529
  value: "gpt-5.1-codex-mini-low",
546
530
  label: "gpt-5.1-codex-mini-low",
@@ -589,7 +573,9 @@ type CursorModelRow = {
589
573
 
590
574
  const CURSOR_MODELS_TIMEOUT_MS = 10_000;
591
575
  const CURSOR_CHATS_ROOT = path.join(os.homedir(), '.cursor', 'chats');
592
- const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
576
+ // cross-spawn resolves .cmd shims/PATHEXT on Windows and delegates to
577
+ // child_process.spawn everywhere else.
578
+ const spawnFunction = crossSpawn;
593
579
  const ANSI_PATTERN = new RegExp(
594
580
  // eslint-disable-next-line no-control-regex
595
581
  '[\\u001B\\u009B][[\\]()#;?]*(?:'
@@ -141,7 +141,13 @@ export class CursorSessionSynchronizer implements IProviderSessionSynchronizer {
141
141
  }
142
142
 
143
143
  const text = typeof data.message?.content?.[0]?.text === 'string' ? data.message.content[0].text : '';
144
- const firstLine = text.replace(/<\/?user_query>/g, '').trim().split('\n')[0];
144
+ // Drop Cursor's `<timestamp>…</timestamp>` prefix and `<user_query>` tags
145
+ // so the session name comes from the actual first line the user typed.
146
+ const firstLine = text
147
+ .replace(/<timestamp>[\s\S]*?<\/timestamp>/g, '')
148
+ .replace(/<\/?user_query>/g, '')
149
+ .trim()
150
+ .split('\n')[0];
145
151
 
146
152
  return {
147
153
  sessionId,
@@ -2,6 +2,7 @@ import crypto from 'node:crypto';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
 
5
+ import { parseImagesInputTag } from '@/shared/image-attachments.js';
5
6
  import type { IProviderSessions } from '@/shared/interfaces.js';
6
7
  import type { AnyRecord, FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
7
8
  import {
@@ -24,7 +25,7 @@ type CursorJsonBlob = CursorDbBlob & {
24
25
  parsed: AnyRecord;
25
26
  };
26
27
 
27
- type CursorMessageBlob = {
28
+ export type CursorMessageBlob = {
28
29
  id: string;
29
30
  sequence: number;
30
31
  rowid: number;
@@ -59,17 +60,42 @@ function unwrapUserQueryText(value: string, role: 'user' | 'assistant'): string
59
60
  return value;
60
61
  }
61
62
 
62
- const normalized = value.trimStart();
63
+ // Cursor wraps user turns as `<timestamp>…</timestamp>\n<user_query>…</user_query>`.
64
+ // Show only the `<user_query>` content, trimmed so there are no blank lines
65
+ // at the top/bottom and the `<timestamp>` prefix is dropped entirely.
63
66
  const openTag = '<user_query>';
64
67
  const closeTag = '</user_query>';
65
- if (!normalized.startsWith(openTag)) {
66
- return value;
68
+ const openIndex = value.indexOf(openTag);
69
+ if (openIndex >= 0) {
70
+ const afterOpen = value.slice(openIndex + openTag.length);
71
+ const closeIndex = afterOpen.lastIndexOf(closeTag);
72
+ const inner = closeIndex >= 0 ? afterOpen.slice(0, closeIndex) : afterOpen;
73
+ return inner.trim();
67
74
  }
68
75
 
69
- const afterOpen = normalized.slice(openTag.length);
70
- const closeIndex = afterOpen.lastIndexOf(closeTag);
71
- const inner = closeIndex >= 0 ? afterOpen.slice(0, closeIndex) : afterOpen;
72
- return inner.trim();
76
+ // No `<user_query>` wrapper: still strip a leading `<timestamp>…</timestamp>`.
77
+ return value.replace(/^\s*<timestamp>[\s\S]*?<\/timestamp>\s*/, '').trim();
78
+ }
79
+
80
+ /**
81
+ * Unwraps one user-authored text payload and splits off the `<images_input>`
82
+ * attachment block appended by the chat composer. Assistant text passes
83
+ * through untouched.
84
+ */
85
+ function extractUserTextAndImages(
86
+ value: string,
87
+ role: 'user' | 'assistant',
88
+ ): { text: string; images?: Array<{ path: string; name?: string }> } {
89
+ const unwrapped = unwrapUserQueryText(value, role);
90
+ if (role !== 'user') {
91
+ return { text: unwrapped };
92
+ }
93
+
94
+ const { text, attachments } = parseImagesInputTag(unwrapped);
95
+ return {
96
+ text,
97
+ images: attachments.length > 0 ? attachments : undefined,
98
+ };
73
99
  }
74
100
 
75
101
  function normalizeToolId(value: unknown): string | null {
@@ -401,8 +427,11 @@ export class CursorSessionsProvider implements IProviderSessions {
401
427
  /**
402
428
  * Converts Cursor SQLite message blobs into normalized messages and attaches
403
429
  * matching tool results to their tool_use entries.
430
+ *
431
+ * Public so tests can drive history normalization with synthetic blobs
432
+ * without needing a real Cursor store.db.
404
433
  */
405
- private normalizeCursorBlobs(blobs: CursorMessageBlob[], sessionId: string | null): NormalizedMessage[] {
434
+ normalizeCursorBlobs(blobs: CursorMessageBlob[], sessionId: string | null): NormalizedMessage[] {
406
435
  const messages: NormalizedMessage[] = [];
407
436
  const toolUseMap = new Map<string, NormalizedMessage>();
408
437
  const baseTime = Date.now();
@@ -442,7 +471,16 @@ export class CursorSessionsProvider implements IProviderSessions {
442
471
  text = unwrapUserQueryText(content.message.content, role);
443
472
  }
444
473
  }
445
- if (text?.trim()) {
474
+ const { text: cleanText, images } = role === 'user'
475
+ ? (() => {
476
+ const parsed = parseImagesInputTag(text);
477
+ return {
478
+ text: parsed.text,
479
+ images: parsed.attachments.length > 0 ? parsed.attachments : undefined,
480
+ };
481
+ })()
482
+ : { text, images: undefined };
483
+ if (cleanText?.trim() || images) {
446
484
  messages.push(createNormalizedMessage({
447
485
  id: baseId,
448
486
  sessionId,
@@ -450,7 +488,8 @@ export class CursorSessionsProvider implements IProviderSessions {
450
488
  provider: PROVIDER,
451
489
  kind: 'text',
452
490
  role,
453
- content: text,
491
+ content: cleanText,
492
+ images,
454
493
  sequence: blob.sequence,
455
494
  rowid: blob.rowid,
456
495
  }));
@@ -502,8 +541,8 @@ export class CursorSessionsProvider implements IProviderSessions {
502
541
  }
503
542
 
504
543
  if (part?.type === 'text' && part?.text) {
505
- const normalizedPartText = unwrapUserQueryText(part.text, role);
506
- if (!normalizedPartText) {
544
+ const { text: normalizedPartText, images } = extractUserTextAndImages(part.text, role);
545
+ if (!normalizedPartText && !images) {
507
546
  continue;
508
547
  }
509
548
  messages.push(createNormalizedMessage({
@@ -514,6 +553,7 @@ export class CursorSessionsProvider implements IProviderSessions {
514
553
  kind: 'text',
515
554
  role,
516
555
  content: normalizedPartText,
556
+ images,
517
557
  sequence: blob.sequence,
518
558
  rowid: blob.rowid,
519
559
  }));
@@ -553,8 +593,8 @@ export class CursorSessionsProvider implements IProviderSessions {
553
593
  && content.content.trim()
554
594
  && !isInternalCursorText(content.content)
555
595
  ) {
556
- const normalizedText = unwrapUserQueryText(content.content, role);
557
- if (!normalizedText) {
596
+ const { text: normalizedText, images } = extractUserTextAndImages(content.content, role);
597
+ if (!normalizedText && !images) {
558
598
  continue;
559
599
  }
560
600
  messages.push(createNormalizedMessage({
@@ -565,6 +605,7 @@ export class CursorSessionsProvider implements IProviderSessions {
565
605
  kind: 'text',
566
606
  role,
567
607
  content: normalizedText,
608
+ images,
568
609
  sequence: blob.sequence,
569
610
  rowid: blob.rowid,
570
611
  }));
@@ -19,7 +19,6 @@ const OPENCODE_ENV_CREDENTIAL_KEYS = [
19
19
  'ANTHROPIC_API_KEY',
20
20
  'OPENAI_API_KEY',
21
21
  'GOOGLE_GENERATIVE_AI_API_KEY',
22
- 'GEMINI_API_KEY',
23
22
  'GROQ_API_KEY',
24
23
  'OPENROUTER_API_KEY',
25
24
  ];