@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
@@ -19,6 +19,7 @@ import path from 'path';
19
19
 
20
20
  import { query } from '@anthropic-ai/claude-agent-sdk';
21
21
 
22
+ import { buildClaudeUserContent, normalizeImageDescriptors } from './shared/image-attachments.js';
22
23
  import { CLAUDE_FALLBACK_MODELS } from './modules/providers/list/claude/claude-models.provider.js';
23
24
  import { providerModelsService } from './modules/providers/services/provider-models.service.js';
24
25
  import { resolveClaudeCodeExecutablePath } from './shared/claude-cli-path.js';
@@ -236,16 +237,13 @@ function mapCliOptionsToSDK(options = {}) {
236
237
  * Adds a session to the active sessions map
237
238
  * @param {string} sessionId - Session identifier
238
239
  * @param {Object} queryInstance - SDK query instance
239
- * @param {Array<string>} tempImagePaths - Temp image file paths for cleanup
240
- * @param {string} tempDir - Temp directory for cleanup
240
+ * @param {Object} writer - WebSocket writer for reconnect support
241
241
  */
242
- function addSession(sessionId, queryInstance, tempImagePaths = [], tempDir = null, writer = null) {
242
+ function addSession(sessionId, queryInstance, writer = null) {
243
243
  activeSessions.set(sessionId, {
244
244
  instance: queryInstance,
245
245
  startTime: Date.now(),
246
246
  status: 'active',
247
- tempImagePaths,
248
- tempDir,
249
247
  writer
250
248
  });
251
249
  }
@@ -364,90 +362,35 @@ function extractTokenBudget(sdkMessage) {
364
362
  }
365
363
 
366
364
  /**
367
- * Handles image processing for SDK queries
368
- * Saves base64 images to temporary files and returns modified prompt with file paths
369
- * @param {string} command - Original user prompt
370
- * @param {Array} images - Array of image objects with base64 data
371
- * @param {string} cwd - Working directory for temp file creation
372
- * @returns {Promise<Object>} {modifiedCommand, tempImagePaths, tempDir}
373
- */
374
- async function handleImages(command, images, cwd) {
375
- const tempImagePaths = [];
376
- let tempDir = null;
377
-
378
- if (!images || images.length === 0) {
379
- return { modifiedCommand: command, tempImagePaths, tempDir };
380
- }
381
-
382
- try {
383
- // Create temp directory in the project directory
384
- const workingDir = cwd || process.cwd();
385
- tempDir = path.join(workingDir, '.tmp', 'images', Date.now().toString());
386
- await fs.mkdir(tempDir, { recursive: true });
387
-
388
- // Save each image to a temp file
389
- for (const [index, image] of images.entries()) {
390
- // Extract base64 data and mime type
391
- const matches = image.data.match(/^data:([^;]+);base64,(.+)$/);
392
- if (!matches) {
393
- console.error('Invalid image data format');
394
- continue;
395
- }
396
-
397
- const [, mimeType, base64Data] = matches;
398
- const extension = mimeType.split('/')[1] || 'png';
399
- const filename = `image_${index}.${extension}`;
400
- const filepath = path.join(tempDir, filename);
401
-
402
- // Write base64 data to file
403
- await fs.writeFile(filepath, Buffer.from(base64Data, 'base64'));
404
- tempImagePaths.push(filepath);
405
- }
406
-
407
- // Include the full image paths in the prompt
408
- let modifiedCommand = command;
409
- if (tempImagePaths.length > 0 && command && command.trim()) {
410
- const imageNote = `\n\n[Images provided at the following paths:]\n${tempImagePaths.map((p, i) => `${i + 1}. ${p}`).join('\n')}`;
411
- modifiedCommand = command + imageNote;
412
- }
413
-
414
- // Images processed
415
- return { modifiedCommand, tempImagePaths, tempDir };
416
- } catch (error) {
417
- console.error('Error processing images for SDK:', error);
418
- return { modifiedCommand: command, tempImagePaths, tempDir };
419
- }
420
- }
421
-
422
- /**
423
- * Cleans up temporary image files
424
- * @param {Array<string>} tempImagePaths - Array of temp file paths to delete
425
- * @param {string} tempDir - Temp directory to remove
365
+ * Builds the SDK `prompt` payload for one turn.
366
+ *
367
+ * Plain text turns pass the string through unchanged. Turns with image
368
+ * attachments use the SDK's streaming-input mode: a single SDKUserMessage
369
+ * whose content carries the prompt text plus one base64 `image` block per
370
+ * attachment (read from the global `~/.cloudcli/assets` folder).
371
+ *
372
+ * @param {string} command - User prompt
373
+ * @param {Array} images - Image descriptors ({ path, name?, mimeType? })
374
+ * @param {string} cwd - Project working directory image paths resolve against
375
+ * @returns {Promise<string|AsyncIterable>} SDK prompt payload
426
376
  */
427
- async function cleanupTempFiles(tempImagePaths, tempDir) {
428
- if (!tempImagePaths || tempImagePaths.length === 0) {
429
- return;
377
+ async function buildPromptPayload(command, images, cwd) {
378
+ if (normalizeImageDescriptors(images).length === 0) {
379
+ return command;
430
380
  }
431
381
 
432
- try {
433
- // Delete individual temp files
434
- for (const imagePath of tempImagePaths) {
435
- await fs.unlink(imagePath).catch(err =>
436
- console.error(`Failed to delete temp image ${imagePath}:`, err)
437
- );
438
- }
439
-
440
- // Delete temp directory
441
- if (tempDir) {
442
- await fs.rm(tempDir, { recursive: true, force: true }).catch(err =>
443
- console.error(`Failed to delete temp directory ${tempDir}:`, err)
444
- );
445
- }
446
-
447
- // Temp files cleaned
448
- } catch (error) {
449
- console.error('Error during temp file cleanup:', error);
450
- }
382
+ const content = await buildClaudeUserContent(command, images, cwd);
383
+ return (async function* () {
384
+ yield {
385
+ type: 'user',
386
+ message: {
387
+ role: 'user',
388
+ content
389
+ },
390
+ parent_tool_use_id: null,
391
+ timestamp: new Date().toISOString()
392
+ };
393
+ })();
451
394
  }
452
395
 
453
396
  /**
@@ -518,8 +461,6 @@ async function queryClaudeSDK(command, options = {}, ws) {
518
461
  const { sessionId, sessionSummary } = options;
519
462
  let capturedSessionId = sessionId;
520
463
  let sessionCreatedSent = false;
521
- let tempImagePaths = [];
522
- let tempDir = null;
523
464
 
524
465
  const emitNotification = (event) => {
525
466
  notifyUserIfEnabled({
@@ -553,10 +494,10 @@ async function queryClaudeSDK(command, options = {}, ws) {
553
494
  sdkOptions.mcpServers = mcpServers;
554
495
  }
555
496
 
556
- const imageResult = await handleImages(command, options.images, options.cwd);
557
- const finalCommand = imageResult.modifiedCommand;
558
- tempImagePaths = imageResult.tempImagePaths;
559
- tempDir = imageResult.tempDir;
497
+ // Turns with image attachments switch to streaming input so the images
498
+ // ride along as real content blocks. Built per query attempt because an
499
+ // async generator cannot be replayed once consumed.
500
+ const createPrompt = () => buildPromptPayload(command, options.images, options.cwd);
560
501
 
561
502
  sdkOptions.hooks = {
562
503
  Notification: [{
@@ -663,7 +604,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
663
604
  let queryInstance;
664
605
  try {
665
606
  queryInstance = query({
666
- prompt: finalCommand,
607
+ prompt: await createPrompt(),
667
608
  options: sdkOptions
668
609
  });
669
610
  } catch (hookError) {
@@ -672,7 +613,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
672
613
  console.warn('Failed to initialize Claude query with hooks, retrying without hooks:', hookError?.message || hookError);
673
614
  delete sdkOptions.hooks;
674
615
  queryInstance = query({
675
- prompt: finalCommand,
616
+ prompt: await createPrompt(),
676
617
  options: sdkOptions
677
618
  });
678
619
  }
@@ -686,7 +627,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
686
627
 
687
628
  // Track the query instance for abort capability
688
629
  if (capturedSessionId) {
689
- addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir, ws);
630
+ addSession(capturedSessionId, queryInstance, ws);
690
631
  }
691
632
 
692
633
  // Process streaming messages
@@ -696,7 +637,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
696
637
  if (message.session_id && !capturedSessionId) {
697
638
 
698
639
  capturedSessionId = message.session_id;
699
- addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir, ws);
640
+ addSession(capturedSessionId, queryInstance, ws);
700
641
 
701
642
  // Set session ID on writer
702
643
  if (ws.setSessionId && typeof ws.setSessionId === 'function') {
@@ -738,9 +679,6 @@ async function queryClaudeSDK(command, options = {}, ws) {
738
679
  removeSession(capturedSessionId);
739
680
  }
740
681
 
741
- // Clean up temporary image files
742
- await cleanupTempFiles(tempImagePaths, tempDir);
743
-
744
682
  // Send the terminal completion event — skipped for aborted runs, whose
745
683
  // terminal `complete` (aborted: true) was already sent by abort-session.
746
684
  const wasAborted = capturedSessionId ? abortedSessionIds.delete(capturedSessionId) : false;
@@ -764,9 +702,6 @@ async function queryClaudeSDK(command, options = {}, ws) {
764
702
  removeSession(capturedSessionId);
765
703
  }
766
704
 
767
- // Clean up temporary image files on error
768
- await cleanupTempFiles(tempImagePaths, tempDir);
769
-
770
705
  const wasAborted = capturedSessionId ? abortedSessionIds.delete(capturedSessionId) : false;
771
706
  if (wasAborted) {
772
707
  // The abort already produced the terminal complete; a generator throw
@@ -819,9 +754,6 @@ async function abortClaudeSDKSession(sessionId) {
819
754
  // Update session status
820
755
  session.status = 'aborted';
821
756
 
822
- // Clean up temporary image files
823
- await cleanupTempFiles(session.tempImagePaths, session.tempDir);
824
-
825
757
  // Clean up session
826
758
  removeSession(sessionId);
827
759
 
package/server/cli.js CHANGED
@@ -256,13 +256,11 @@ async function updatePackage() {
256
256
  const SANDBOX_TEMPLATES = {
257
257
  claude: 'docker.io/cloudcliai/sandbox:claude-code',
258
258
  codex: 'docker.io/cloudcliai/sandbox:codex',
259
- gemini: 'docker.io/cloudcliai/sandbox:gemini',
260
259
  };
261
260
 
262
261
  const SANDBOX_SECRETS = {
263
262
  claude: 'anthropic',
264
263
  codex: 'openai',
265
- gemini: 'google',
266
264
  };
267
265
 
268
266
  function parseSandboxArgs(args) {
@@ -338,7 +336,7 @@ Subcommands:
338
336
  ${c.bright('help')} Show this help
339
337
 
340
338
  Options:
341
- -a, --agent <agent> Agent to use: claude, codex, gemini (default: claude)
339
+ -a, --agent <agent> Agent to use: claude, codex (default: claude)
342
340
  -n, --name <name> Sandbox name (default: derived from workspace folder)
343
341
  -t, --template <image> Custom template image
344
342
  -e, --env <KEY=VALUE> Set environment variable (repeatable)
@@ -359,7 +357,6 @@ Prerequisites:
359
357
  sbx login
360
358
  sbx secret set -g anthropic # for Claude
361
359
  sbx secret set -g openai # for Codex
362
- sbx secret set -g google # for Gemini
363
360
 
364
361
  Advanced usage:
365
362
  For branch mode, multiple workspaces, memory limits, network policies,
@@ -1,13 +1,15 @@
1
- import { spawn } from 'child_process';
2
1
  import crossSpawn from 'cross-spawn';
2
+
3
+ import { appendImagesInputTag } from './shared/image-attachments.js';
3
4
  import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
4
5
  import { sessionsService } from './modules/providers/services/sessions.service.js';
5
6
  import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
6
7
  import { providerModelsService } from './modules/providers/services/provider-models.service.js';
7
- import { createCompleteMessage, createNormalizedMessage } from './shared/utils.js';
8
+ import { createCompleteMessage, createNormalizedMessage, flattenPromptForWindowsShell } from './shared/utils.js';
8
9
 
9
- // Use cross-spawn on Windows for better command execution
10
- const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
10
+ // cross-spawn resolves .cmd shims/PATHEXT on Windows and delegates to
11
+ // child_process.spawn everywhere else.
12
+ const spawnFunction = crossSpawn;
11
13
 
12
14
  let activeCursorProcesses = new Map(); // Track active processes by session ID
13
15
 
@@ -28,7 +30,7 @@ function isWorkspaceTrustPrompt(text = '') {
28
30
 
29
31
  async function spawnCursor(command, options = {}, ws) {
30
32
  return new Promise(async (resolve, reject) => {
31
- const { sessionId, projectPath, cwd, resume, toolsSettings, skipPermissions, model, sessionSummary } = options;
33
+ const { sessionId, projectPath, cwd, toolsSettings, skipPermissions, model, sessionSummary, images } = options;
32
34
  const resolvedModel = await providerModelsService.resolveResumeModel('cursor', sessionId, model);
33
35
  let capturedSessionId = sessionId; // Track session ID throughout the process
34
36
  let sessionCreatedSent = false; // Track if we've already sent session-created event
@@ -55,8 +57,12 @@ async function spawnCursor(command, options = {}, ws) {
55
57
  }
56
58
 
57
59
  if (command && command.trim()) {
58
- // Provide a prompt (works for both new and resumed sessions)
59
- baseArgs.push('-p', command);
60
+ // Provide a prompt (works for both new and resumed sessions). Image
61
+ // attachments ride along as an <images_input> path list appended to the
62
+ // prompt; the session history reader strips the tag back out for display.
63
+ // cursor-agent is a .cmd shim on Windows, so the whole argument must be
64
+ // newline-free or cmd.exe silently truncates it at the first newline.
65
+ baseArgs.push('-p', flattenPromptForWindowsShell(appendImagesInputTag(command, images)));
60
66
 
61
67
  // Model overrides are applied to both new and resumed sessions so a
62
68
  // session-scoped change request can take effect on the next turn.
@@ -71,7 +77,6 @@ async function spawnCursor(command, options = {}, ws) {
71
77
  // Add skip permissions flag if enabled
72
78
  if (skipPermissions || settings.skipPermissions) {
73
79
  baseArgs.push('-f');
74
- console.log('Using -f flag (skip permissions)');
75
80
  }
76
81
 
77
82
  // Use cwd (actual project directory) instead of projectPath
@@ -126,10 +131,6 @@ async function spawnCursor(command, options = {}, ws) {
126
131
  console.log('Retrying Cursor CLI with --trust after workspace trust prompt');
127
132
  }
128
133
 
129
- console.log('Spawning Cursor CLI:', 'cursor-agent', args.join(' '));
130
- console.log('Working directory:', workingDir);
131
- console.log('Session info - Input sessionId:', sessionId, 'Resume:', resume);
132
-
133
134
  const cursorProcess = spawnFunction('cursor-agent', args, {
134
135
  cwd: workingDir,
135
136
  stdio: ['pipe', 'pipe', 'pipe'],
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;