@cloudcli-ai/cloudcli 1.35.1 → 1.36.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 (223) 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 +8 -3
  10. package/dist/assets/{index-D1IdCTT6.js → index-Bz9vXV2m.js} +265 -260
  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 +62 -116
  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-models.provider.js +70 -1
  39. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  40. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +28 -0
  41. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  42. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +52 -11
  43. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
  44. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +54 -1
  45. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  46. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +36 -1
  47. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  48. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +3 -17
  49. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -1
  50. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +7 -1
  51. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
  52. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +49 -13
  53. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
  54. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +0 -1
  55. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -1
  56. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +145 -14
  57. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  58. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +24 -5
  59. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  60. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +10 -21
  61. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -1
  62. package/dist-server/server/modules/providers/provider.registry.js +0 -2
  63. package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
  64. package/dist-server/server/modules/providers/provider.routes.js +0 -1
  65. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  66. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +11 -13
  67. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
  68. package/dist-server/server/modules/providers/services/provider-models.service.js +2 -2
  69. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  70. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +3 -77
  71. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  72. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +0 -1
  73. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  74. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +0 -13
  75. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  76. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +96 -0
  77. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -0
  78. package/dist-server/server/modules/providers/tests/mcp.test.js +3 -29
  79. package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
  80. package/dist-server/server/modules/providers/tests/opencode-models.test.js +65 -1
  81. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  82. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +117 -0
  83. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  84. package/dist-server/server/modules/providers/tests/provider-image-history.test.js +147 -0
  85. package/dist-server/server/modules/providers/tests/provider-image-history.test.js.map +1 -0
  86. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +8 -8
  87. package/dist-server/server/modules/providers/tests/skills.test.js +3 -26
  88. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  89. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +14 -0
  90. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
  91. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +36 -2
  92. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  93. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +3 -12
  94. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  95. package/dist-server/server/modules/websocket/tests/chat-image-filter.test.js +33 -0
  96. package/dist-server/server/modules/websocket/tests/chat-image-filter.test.js.map +1 -0
  97. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +37 -4
  98. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
  99. package/dist-server/server/openai-codex.js +16 -9
  100. package/dist-server/server/openai-codex.js.map +1 -1
  101. package/dist-server/server/opencode-cli.js +62 -7
  102. package/dist-server/server/opencode-cli.js.map +1 -1
  103. package/dist-server/server/opencode-cli.test.js +103 -3
  104. package/dist-server/server/opencode-cli.test.js.map +1 -1
  105. package/dist-server/server/routes/agent.js +22 -21
  106. package/dist-server/server/routes/agent.js.map +1 -1
  107. package/dist-server/server/routes/commands.js +1 -2
  108. package/dist-server/server/routes/commands.js.map +1 -1
  109. package/dist-server/server/routes/git.js +179 -53
  110. package/dist-server/server/routes/git.js.map +1 -1
  111. package/dist-server/server/routes/git.test.js +80 -0
  112. package/dist-server/server/routes/git.test.js.map +1 -0
  113. package/dist-server/server/routes/taskmaster.js +3 -1
  114. package/dist-server/server/routes/taskmaster.js.map +1 -1
  115. package/dist-server/server/routes/user.js +2 -1
  116. package/dist-server/server/routes/user.js.map +1 -1
  117. package/dist-server/server/shared/image-attachments.js +270 -0
  118. package/dist-server/server/shared/image-attachments.js.map +1 -0
  119. package/dist-server/server/shared/tests/image-attachments.test.js +220 -0
  120. package/dist-server/server/shared/tests/image-attachments.test.js.map +1 -0
  121. package/dist-server/server/shared/utils.js +43 -0
  122. package/dist-server/server/shared/utils.js.map +1 -1
  123. package/dist-server/server/utils/gitConfig.js +2 -1
  124. package/dist-server/server/utils/gitConfig.js.map +1 -1
  125. package/dist-server/server/utils/plugin-process-manager.js +2 -1
  126. package/dist-server/server/utils/plugin-process-manager.js.map +1 -1
  127. package/electron/launcher/launcher.js +1 -1
  128. package/package.json +3 -3
  129. package/public/api-docs.html +8 -3
  130. package/server/claude-sdk.js +69 -131
  131. package/server/cli.js +1 -4
  132. package/server/cursor-cli.js +13 -12
  133. package/server/index.js +11 -159
  134. package/server/modules/assets/assets.routes.ts +103 -0
  135. package/server/modules/assets/index.ts +3 -0
  136. package/server/modules/assets/services/image-assets.service.ts +82 -0
  137. package/server/modules/assets/tests/image-assets.service.test.ts +46 -0
  138. package/server/modules/browser-use/browser-use.service.ts +7 -3
  139. package/server/modules/database/tests/sessions-provider-mapping.test.ts +2 -2
  140. package/server/modules/notifications/services/notification-orchestrator.service.js +0 -1
  141. package/server/modules/projects/services/project-clone.service.ts +3 -1
  142. package/server/modules/providers/README.md +1 -7
  143. package/server/modules/providers/list/claude/claude-models.provider.ts +73 -1
  144. package/server/modules/providers/list/claude/claude-sessions.provider.ts +30 -0
  145. package/server/modules/providers/list/codex/codex-models.provider.ts +59 -11
  146. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +60 -1
  147. package/server/modules/providers/list/codex/codex-sessions.provider.ts +41 -1
  148. package/server/modules/providers/list/cursor/cursor-models.provider.ts +3 -17
  149. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +7 -1
  150. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +56 -15
  151. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +0 -1
  152. package/server/modules/providers/list/opencode/opencode-models.provider.ts +193 -18
  153. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +25 -4
  154. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +10 -21
  155. package/server/modules/providers/provider.registry.ts +0 -2
  156. package/server/modules/providers/provider.routes.ts +0 -1
  157. package/server/modules/providers/services/provider-capabilities.service.ts +13 -13
  158. package/server/modules/providers/services/provider-models.service.ts +2 -2
  159. package/server/modules/providers/services/session-conversations-search.service.ts +3 -93
  160. package/server/modules/providers/services/session-synchronizer.service.ts +0 -1
  161. package/server/modules/providers/services/sessions-watcher.service.ts +0 -14
  162. package/server/modules/providers/tests/codex-sessions.test.ts +111 -0
  163. package/server/modules/providers/tests/mcp.test.ts +3 -34
  164. package/server/modules/providers/tests/opencode-models.test.ts +69 -0
  165. package/server/modules/providers/tests/opencode-sessions.test.ts +139 -0
  166. package/server/modules/providers/tests/provider-image-history.test.ts +180 -0
  167. package/server/modules/providers/tests/provider-models.service.test.ts +8 -8
  168. package/server/modules/providers/tests/skills.test.ts +3 -42
  169. package/server/modules/websocket/services/chat-run-registry.service.ts +16 -0
  170. package/server/modules/websocket/services/chat-websocket.service.ts +41 -2
  171. package/server/modules/websocket/services/shell-websocket.service.ts +1 -11
  172. package/server/modules/websocket/tests/chat-image-filter.test.ts +44 -0
  173. package/server/modules/websocket/tests/chat-run-registry.test.ts +42 -4
  174. package/server/openai-codex.js +18 -8
  175. package/server/opencode-cli.js +64 -7
  176. package/server/opencode-cli.test.js +112 -3
  177. package/server/routes/agent.js +22 -21
  178. package/server/routes/commands.js +1 -2
  179. package/server/routes/git.js +201 -60
  180. package/server/routes/git.test.js +106 -0
  181. package/server/routes/taskmaster.js +3 -1
  182. package/server/routes/user.js +2 -1
  183. package/server/shared/image-attachments.ts +341 -0
  184. package/server/shared/tests/image-attachments.test.ts +298 -0
  185. package/server/shared/types.ts +8 -1
  186. package/server/shared/utils.ts +45 -0
  187. package/server/utils/gitConfig.js +2 -1
  188. package/server/utils/plugin-process-manager.js +3 -1
  189. package/dist/assets/index-BvX30NW5.css +0 -32
  190. package/dist/icons/gemini-ai-icon.svg +0 -1
  191. package/dist-server/server/gemini-cli.js +0 -551
  192. package/dist-server/server/gemini-cli.js.map +0 -1
  193. package/dist-server/server/gemini-response-handler.js +0 -106
  194. package/dist-server/server/gemini-response-handler.js.map +0 -1
  195. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +0 -254
  196. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +0 -1
  197. package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js +0 -82
  198. package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js.map +0 -1
  199. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +0 -24
  200. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +0 -1
  201. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +0 -312
  202. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +0 -1
  203. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +0 -474
  204. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +0 -1
  205. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +0 -40
  206. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +0 -1
  207. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +0 -19
  208. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +0 -1
  209. package/dist-server/server/routes/gemini.js +0 -21
  210. package/dist-server/server/routes/gemini.js.map +0 -1
  211. package/dist-server/server/sessionManager.js +0 -194
  212. package/dist-server/server/sessionManager.js.map +0 -1
  213. package/server/gemini-cli.js +0 -638
  214. package/server/gemini-response-handler.js +0 -117
  215. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +0 -307
  216. package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +0 -110
  217. package/server/modules/providers/list/gemini/gemini-models.provider.ts +0 -39
  218. package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +0 -405
  219. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +0 -540
  220. package/server/modules/providers/list/gemini/gemini-skills.provider.ts +0 -44
  221. package/server/modules/providers/list/gemini/gemini.provider.ts +0 -27
  222. package/server/routes/gemini.js +0 -25
  223. package/server/sessionManager.js +0 -226
package/server/index.js CHANGED
@@ -5,8 +5,10 @@ import fs, { promises as fsPromises } from 'fs';
5
5
  import path from 'path';
6
6
  import os from 'os';
7
7
  import http from 'http';
8
- import { spawn } from 'child_process';
9
8
 
9
+ // cross-spawn is a drop-in for child_process.spawn that resolves .cmd
10
+ // shims/PATHEXT on Windows and delegates to the native spawn elsewhere.
11
+ import spawn from 'cross-spawn';
10
12
  import express from 'express';
11
13
  import cors from 'cors';
12
14
  import mime from 'mime-types';
@@ -33,15 +35,10 @@ import {
33
35
  queryCodex,
34
36
  abortCodexSession,
35
37
  } from './openai-codex.js';
36
- import {
37
- spawnGemini,
38
- abortGeminiSession,
39
- } from './gemini-cli.js';
40
38
  import {
41
39
  spawnOpenCode,
42
40
  abortOpenCodeSession,
43
41
  } from './opencode-cli.js';
44
- import sessionManager from './sessionManager.js';
45
42
  import {
46
43
  stripAnsiSequences,
47
44
  normalizeDetectedUrl,
@@ -59,11 +56,11 @@ import agentRoutes from './routes/agent.js';
59
56
  import projectModuleRoutes from './modules/projects/projects.routes.js';
60
57
  import notificationRoutes from './modules/notifications/notifications.routes.js';
61
58
  import userRoutes from './routes/user.js';
62
- import geminiRoutes from './routes/gemini.js';
63
59
  import pluginsRoutes from './routes/plugins.js';
64
60
  import providerRoutes from './modules/providers/provider.routes.js';
65
61
  import voiceRoutes from './voice-proxy.js';
66
62
  import browserUseRoutes from './modules/browser-use/browser-use.routes.js';
63
+ import { assetsRoutes } from './modules/assets/index.js';
67
64
  import browserUseMcpRoutes from './modules/browser-use/browser-use-mcp.routes.js';
68
65
  import { browserUseService } from './modules/browser-use/browser-use.service.js';
69
66
  import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
@@ -116,14 +113,12 @@ const wss = createWebSocketServer(server, {
116
113
  claude: queryClaudeSDK,
117
114
  cursor: spawnCursor,
118
115
  codex: queryCodex,
119
- gemini: spawnGemini,
120
116
  opencode: spawnOpenCode,
121
117
  },
122
118
  abortFns: {
123
119
  claude: abortClaudeSDKSession,
124
120
  cursor: abortCursorSession,
125
121
  codex: abortCodexSession,
126
- gemini: abortGeminiSession,
127
122
  opencode: abortOpenCodeSession,
128
123
  },
129
124
  resolveToolApproval,
@@ -132,14 +127,11 @@ const wss = createWebSocketServer(server, {
132
127
  shell: {
133
128
  resolveProviderSessionId: (sessionId, provider) => {
134
129
  const dbSession = sessionsDb.getSessionById(sessionId);
135
- const legacyGeminiSession =
136
- provider === 'gemini' ? sessionManager.getSession(sessionId) : null;
137
-
138
130
  if (dbSession) {
139
- return dbSession.provider_session_id ?? legacyGeminiSession?.cliSessionId ?? null;
131
+ return dbSession.provider_session_id ?? null;
140
132
  }
141
133
 
142
- return legacyGeminiSession?.cliSessionId;
134
+ return null;
143
135
  },
144
136
  stripAnsiSequences,
145
137
  normalizeDetectedUrl,
@@ -185,6 +177,9 @@ app.use('/api/auth', authRoutes);
185
177
  // Projects API Routes (protected)
186
178
  app.use('/api/projects', authenticateToken, projectModuleRoutes);
187
179
 
180
+ // Chat image asset upload/serving (global ~/.cloudcli/assets store, protected)
181
+ app.use('/api/assets', authenticateToken, assetsRoutes);
182
+
188
183
  // Git API Routes (protected)
189
184
  app.use('/api/git', authenticateToken, gitRoutes);
190
185
 
@@ -208,9 +203,6 @@ app.use('/api/notifications', authenticateToken, notificationRoutes);
208
203
  // User API Routes (protected)
209
204
  app.use('/api/user', authenticateToken, userRoutes);
210
205
 
211
- // Gemini API Routes (protected)
212
- app.use('/api/gemini', authenticateToken, geminiRoutes);
213
-
214
206
  // Plugins API Routes (protected)
215
207
  app.use('/api/plugins', authenticateToken, pluginsRoutes);
216
208
 
@@ -1072,92 +1064,8 @@ const uploadFilesHandler = async (req, res) => {
1072
1064
 
1073
1065
  app.post('/api/projects/:projectId/files/upload', authenticateToken, uploadFilesHandler);
1074
1066
 
1075
- // Image upload endpoint. Accepts the DB-assigned `projectId` (not a folder name)
1076
- // but the current implementation doesn't need to touch the project directory,
1077
- // so we just leave the param rename for consistency with the rest of the API.
1078
- app.post('/api/projects/:projectId/upload-images', authenticateToken, async (req, res) => {
1079
- try {
1080
- const multer = (await import('multer')).default;
1081
- const path = (await import('path')).default;
1082
- const fs = (await import('fs')).promises;
1083
- const os = (await import('os')).default;
1084
-
1085
- // Configure multer for image uploads
1086
- const storage = multer.diskStorage({
1087
- destination: async (req, file, cb) => {
1088
- const uploadDir = path.join(os.tmpdir(), 'claude-ui-uploads', String(req.user.id));
1089
- await fs.mkdir(uploadDir, { recursive: true });
1090
- cb(null, uploadDir);
1091
- },
1092
- filename: (req, file, cb) => {
1093
- const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
1094
- const sanitizedName = file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_');
1095
- cb(null, uniqueSuffix + '-' + sanitizedName);
1096
- }
1097
- });
1098
-
1099
- const fileFilter = (req, file, cb) => {
1100
- const allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'];
1101
- if (allowedMimes.includes(file.mimetype)) {
1102
- cb(null, true);
1103
- } else {
1104
- cb(new Error('Invalid file type. Only JPEG, PNG, GIF, WebP, and SVG are allowed.'));
1105
- }
1106
- };
1107
-
1108
- const upload = multer({
1109
- storage,
1110
- fileFilter,
1111
- limits: {
1112
- fileSize: 5 * 1024 * 1024, // 5MB
1113
- files: 5
1114
- }
1115
- });
1116
-
1117
- // Handle multipart form data
1118
- upload.array('images', 5)(req, res, async (err) => {
1119
- if (err) {
1120
- return res.status(400).json({ error: err.message });
1121
- }
1122
-
1123
- if (!req.files || req.files.length === 0) {
1124
- return res.status(400).json({ error: 'No image files provided' });
1125
- }
1126
-
1127
- try {
1128
- // Process uploaded images
1129
- const processedImages = await Promise.all(
1130
- req.files.map(async (file) => {
1131
- // Read file and convert to base64
1132
- const buffer = await fs.readFile(file.path);
1133
- const base64 = buffer.toString('base64');
1134
- const mimeType = file.mimetype;
1135
-
1136
- // Clean up temp file immediately
1137
- await fs.unlink(file.path);
1138
-
1139
- return {
1140
- name: file.originalname,
1141
- data: `data:${mimeType};base64,${base64}`,
1142
- size: file.size,
1143
- mimeType: mimeType
1144
- };
1145
- })
1146
- );
1147
-
1148
- res.json({ images: processedImages });
1149
- } catch (error) {
1150
- console.error('Error processing images:', error);
1151
- // Clean up any remaining files
1152
- await Promise.all(req.files.map(f => fs.unlink(f.path).catch(() => { })));
1153
- res.status(500).json({ error: 'Failed to process images' });
1154
- }
1155
- });
1156
- } catch (error) {
1157
- console.error('Error in image upload endpoint:', error);
1158
- res.status(500).json({ error: 'Internal server error' });
1159
- }
1160
- });
1067
+ // Chat image uploads moved to POST /api/assets/images (server/modules/assets),
1068
+ // which stores them in the global ~/.cloudcli/assets folder.
1161
1069
 
1162
1070
  // Get token usage for a specific session. `projectId` is the DB primary key;
1163
1071
  // the Claude branch below resolves it to an absolute path via the DB.
@@ -1197,62 +1105,6 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
1197
1105
  });
1198
1106
  }
1199
1107
 
1200
- if (provider === 'gemini') {
1201
- const session = sessionsDb.getSessionById(safeSessionId);
1202
- const sessionFilePath = session?.jsonl_path;
1203
- if (!sessionFilePath) {
1204
- return res.json({
1205
- used: 0,
1206
- inputTokens: 0,
1207
- outputTokens: 0,
1208
- breakdown: { input: 0, output: 0 },
1209
- unsupported: true,
1210
- message: 'Token usage tracking not available for this Gemini session'
1211
- });
1212
- }
1213
-
1214
- let fileContent;
1215
- try {
1216
- fileContent = await fsPromises.readFile(sessionFilePath, 'utf8');
1217
- } catch (error) {
1218
- if (error.code === 'ENOENT') {
1219
- return res.status(404).json({ error: 'Session file not found', path: sessionFilePath });
1220
- }
1221
- throw error;
1222
- }
1223
-
1224
- const lines = fileContent.trim().split('\n');
1225
- let inputTokens = 0;
1226
- let outputTokens = 0;
1227
- let totalTokens = 0;
1228
-
1229
- for (let i = lines.length - 1; i >= 0; i--) {
1230
- try {
1231
- const entry = JSON.parse(lines[i]);
1232
- if (!entry.tokens || typeof entry.tokens !== 'object') {
1233
- continue;
1234
- }
1235
-
1236
- inputTokens = Number(entry.tokens.input || 0);
1237
- outputTokens = Number(entry.tokens.output || 0);
1238
- totalTokens = Number(entry.tokens.total || inputTokens + outputTokens || 0);
1239
- break;
1240
- } catch {
1241
- continue;
1242
- }
1243
- }
1244
-
1245
- return res.json({
1246
- used: totalTokens,
1247
- inputTokens,
1248
- outputTokens,
1249
- breakdown: {
1250
- input: inputTokens,
1251
- output: outputTokens
1252
- }
1253
- });
1254
- }
1255
-
1256
1108
  if (provider === 'opencode') {
1257
1109
  const dbPath = getOpenCodeDatabasePath();
1258
1110
  if (!fs.existsSync(dbPath)) {
@@ -0,0 +1,103 @@
1
+ import fsSync, { promises as fs } from 'node:fs';
2
+
3
+ import express from 'express';
4
+ import mime from 'mime-types';
5
+ import multer from 'multer';
6
+
7
+ import {
8
+ buildStoredImageRecords,
9
+ ensureImageAssetsDir,
10
+ isAllowedImageMimeType,
11
+ resolveImageAssetFile,
12
+ } from '@/modules/assets/services/image-assets.service.js';
13
+
14
+ const router = express.Router();
15
+
16
+ // Multer writes uploads straight into the global assets folder; the service
17
+ // owns the folder location and the response record shape.
18
+ const storage = multer.diskStorage({
19
+ destination: (req, file, cb) => {
20
+ ensureImageAssetsDir()
21
+ .then((assetsDir) => cb(null, assetsDir))
22
+ .catch((error) => cb(error as Error, ''));
23
+ },
24
+ filename: (req, file, cb) => {
25
+ const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
26
+ const sanitizedName = file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_');
27
+ cb(null, `${uniqueSuffix}-${sanitizedName}`);
28
+ },
29
+ });
30
+
31
+ const upload = multer({
32
+ storage,
33
+ fileFilter: (req, file, cb) => {
34
+ if (isAllowedImageMimeType(file.mimetype)) {
35
+ cb(null, true);
36
+ } else {
37
+ cb(new Error('Invalid file type. Only JPEG, PNG, GIF, WebP, and SVG are allowed.'));
38
+ }
39
+ },
40
+ limits: {
41
+ fileSize: 5 * 1024 * 1024, // 5MB
42
+ files: 5,
43
+ },
44
+ });
45
+
46
+ /**
47
+ * Stores chat image attachments in the global `~/.cloudcli/assets` folder and
48
+ * returns their absolute paths for use in provider prompts and chat history.
49
+ */
50
+ router.post('/images', (req, res) => {
51
+ upload.array('images', 5)(req, res, (err: unknown) => {
52
+ if (err) {
53
+ const message = err instanceof Error ? err.message : 'Upload failed';
54
+ return res.status(400).json({ error: message });
55
+ }
56
+
57
+ const files = Array.isArray(req.files) ? req.files : [];
58
+ if (files.length === 0) {
59
+ return res.status(400).json({ error: 'No image files provided' });
60
+ }
61
+
62
+ res.json({ images: buildStoredImageRecords(files) });
63
+ });
64
+ });
65
+
66
+ /**
67
+ * Serves one stored image asset by filename. Only files directly inside the
68
+ * global assets folder are reachable; traversal attempts resolve to null.
69
+ */
70
+ router.get('/images/:filename', async (req, res) => {
71
+ const resolved = resolveImageAssetFile(req.params.filename);
72
+ if (!resolved) {
73
+ return res.status(400).json({ error: 'Invalid asset filename' });
74
+ }
75
+
76
+ try {
77
+ await fs.access(resolved);
78
+ } catch {
79
+ return res.status(404).json({ error: 'Asset not found' });
80
+ }
81
+
82
+ const contentType = mime.lookup(resolved) || 'application/octet-stream';
83
+ res.setHeader('Content-Type', contentType);
84
+ // Stored-XSS hardening: never let the browser sniff a different type, and
85
+ // force SVGs (which can carry scripts when rendered as a document) to
86
+ // download instead of rendering inline. The chat UI is unaffected — it
87
+ // fetches assets as blobs and shows them through <img>, where SVG scripts
88
+ // never execute.
89
+ res.setHeader('X-Content-Type-Options', 'nosniff');
90
+ if (contentType === 'image/svg+xml') {
91
+ res.setHeader('Content-Disposition', 'attachment');
92
+ }
93
+ const fileStream = fsSync.createReadStream(resolved);
94
+ fileStream.pipe(res);
95
+ fileStream.on('error', (error) => {
96
+ console.error('Error streaming image asset:', error);
97
+ if (!res.headersSent) {
98
+ res.status(500).json({ error: 'Error reading asset' });
99
+ }
100
+ });
101
+ });
102
+
103
+ export default router;
@@ -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.