@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.
- package/README.de.md +17 -18
- package/README.ja.md +16 -17
- package/README.ko.md +17 -18
- package/README.md +19 -20
- package/README.ru.md +17 -18
- package/README.tr.md +17 -18
- package/README.zh-CN.md +17 -18
- package/README.zh-TW.md +17 -18
- package/dist/api-docs.html +2 -3
- package/dist/assets/{index-S6cIy7hj.js → index-CDGzV0LN.js} +264 -259
- package/dist/assets/index-DLmhQ15L.css +32 -0
- package/dist/assets/{vendor-xterm-CJZjLICi.js → vendor-xterm-CS4rQ5Mr.js} +12 -12
- package/dist/index.html +3 -3
- package/dist-server/server/claude-sdk.js +37 -90
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/cli.js +1 -4
- package/dist-server/server/cli.js.map +1 -1
- package/dist-server/server/cursor-cli.js +12 -11
- package/dist-server/server/cursor-cli.js.map +1 -1
- package/dist-server/server/index.js +10 -141
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/assets/assets.routes.js +89 -0
- package/dist-server/server/modules/assets/assets.routes.js.map +1 -0
- package/dist-server/server/modules/assets/index.js +4 -0
- package/dist-server/server/modules/assets/index.js.map +1 -0
- package/dist-server/server/modules/assets/services/image-assets.service.js +57 -0
- package/dist-server/server/modules/assets/services/image-assets.service.js.map +1 -0
- package/dist-server/server/modules/assets/tests/image-assets.service.test.js +36 -0
- package/dist-server/server/modules/assets/tests/image-assets.service.test.js.map +1 -0
- package/dist-server/server/modules/browser-use/browser-use.service.js +6 -3
- package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -1
- package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +2 -2
- package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -1
- package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +0 -1
- package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-clone.service.js +2 -1
- package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +28 -0
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +54 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +36 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +3 -17
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +7 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +49 -13
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +0 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +8 -14
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +24 -5
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +10 -21
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.registry.js +0 -2
- package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +0 -1
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js +7 -14
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js +3 -77
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js +0 -1
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +0 -13
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js +96 -0
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/mcp.test.js +3 -29
- package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-models.test.js +7 -0
- package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +117 -0
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-image-history.test.js +147 -0
- package/dist-server/server/modules/providers/tests/provider-image-history.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js +8 -8
- package/dist-server/server/modules/providers/tests/skills.test.js +3 -26
- package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +14 -0
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +36 -2
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +3 -12
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/tests/chat-image-filter.test.js +33 -0
- package/dist-server/server/modules/websocket/tests/chat-image-filter.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +37 -4
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
- package/dist-server/server/openai-codex.js +8 -2
- package/dist-server/server/openai-codex.js.map +1 -1
- package/dist-server/server/opencode-cli.js +43 -6
- package/dist-server/server/opencode-cli.js.map +1 -1
- package/dist-server/server/opencode-cli.test.js +103 -3
- package/dist-server/server/opencode-cli.test.js.map +1 -1
- package/dist-server/server/routes/agent.js +9 -19
- package/dist-server/server/routes/agent.js.map +1 -1
- package/dist-server/server/routes/commands.js +1 -2
- package/dist-server/server/routes/commands.js.map +1 -1
- package/dist-server/server/routes/git.js +179 -53
- package/dist-server/server/routes/git.js.map +1 -1
- package/dist-server/server/routes/git.test.js +80 -0
- package/dist-server/server/routes/git.test.js.map +1 -0
- package/dist-server/server/routes/taskmaster.js +3 -1
- package/dist-server/server/routes/taskmaster.js.map +1 -1
- package/dist-server/server/routes/user.js +2 -1
- package/dist-server/server/routes/user.js.map +1 -1
- package/dist-server/server/shared/image-attachments.js +270 -0
- package/dist-server/server/shared/image-attachments.js.map +1 -0
- package/dist-server/server/shared/tests/image-attachments.test.js +220 -0
- package/dist-server/server/shared/tests/image-attachments.test.js.map +1 -0
- package/dist-server/server/shared/utils.js +43 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/dist-server/server/utils/gitConfig.js +2 -1
- package/dist-server/server/utils/gitConfig.js.map +1 -1
- package/dist-server/server/utils/plugin-process-manager.js +2 -1
- package/dist-server/server/utils/plugin-process-manager.js.map +1 -1
- package/electron/launcher/launcher.js +1 -1
- package/package.json +4 -4
- package/public/api-docs.html +2 -3
- package/server/claude-sdk.js +37 -105
- package/server/cli.js +1 -4
- package/server/cursor-cli.js +13 -12
- package/server/index.js +11 -159
- package/server/modules/assets/assets.routes.ts +103 -0
- package/server/modules/assets/index.ts +3 -0
- package/server/modules/assets/services/image-assets.service.ts +82 -0
- package/server/modules/assets/tests/image-assets.service.test.ts +46 -0
- package/server/modules/browser-use/browser-use.service.ts +7 -3
- package/server/modules/database/tests/sessions-provider-mapping.test.ts +2 -2
- package/server/modules/notifications/services/notification-orchestrator.service.js +0 -1
- package/server/modules/projects/services/project-clone.service.ts +3 -1
- package/server/modules/providers/README.md +1 -7
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +30 -0
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +60 -1
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +41 -1
- package/server/modules/providers/list/cursor/cursor-models.provider.ts +3 -17
- package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +7 -1
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +56 -15
- package/server/modules/providers/list/opencode/opencode-auth.provider.ts +0 -1
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +15 -19
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +25 -4
- package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +10 -21
- package/server/modules/providers/provider.registry.ts +0 -2
- package/server/modules/providers/provider.routes.ts +0 -1
- package/server/modules/providers/services/provider-capabilities.service.ts +7 -14
- package/server/modules/providers/services/provider-models.service.ts +1 -1
- package/server/modules/providers/services/session-conversations-search.service.ts +3 -93
- package/server/modules/providers/services/session-synchronizer.service.ts +0 -1
- package/server/modules/providers/services/sessions-watcher.service.ts +0 -14
- package/server/modules/providers/tests/codex-sessions.test.ts +111 -0
- package/server/modules/providers/tests/mcp.test.ts +3 -34
- package/server/modules/providers/tests/opencode-models.test.ts +7 -0
- package/server/modules/providers/tests/opencode-sessions.test.ts +139 -0
- package/server/modules/providers/tests/provider-image-history.test.ts +180 -0
- package/server/modules/providers/tests/provider-models.service.test.ts +8 -8
- package/server/modules/providers/tests/skills.test.ts +3 -42
- package/server/modules/websocket/services/chat-run-registry.service.ts +16 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +41 -2
- package/server/modules/websocket/services/shell-websocket.service.ts +1 -11
- package/server/modules/websocket/tests/chat-image-filter.test.ts +44 -0
- package/server/modules/websocket/tests/chat-run-registry.test.ts +42 -4
- package/server/openai-codex.js +9 -1
- package/server/opencode-cli.js +44 -6
- package/server/opencode-cli.test.js +112 -3
- package/server/routes/agent.js +9 -19
- package/server/routes/commands.js +1 -2
- package/server/routes/git.js +201 -60
- package/server/routes/git.test.js +106 -0
- package/server/routes/taskmaster.js +3 -1
- package/server/routes/user.js +2 -1
- package/server/shared/image-attachments.ts +341 -0
- package/server/shared/tests/image-attachments.test.ts +298 -0
- package/server/shared/types.ts +1 -1
- package/server/shared/utils.ts +45 -0
- package/server/utils/gitConfig.js +2 -1
- package/server/utils/plugin-process-manager.js +3 -1
- package/dist/assets/index-uLUE6Qt1.css +0 -32
- package/dist/icons/gemini-ai-icon.svg +0 -1
- package/dist-server/server/gemini-cli.js +0 -551
- package/dist-server/server/gemini-cli.js.map +0 -1
- package/dist-server/server/gemini-response-handler.js +0 -106
- package/dist-server/server/gemini-response-handler.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +0 -254
- package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js +0 -82
- package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +0 -24
- package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +0 -312
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +0 -474
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +0 -40
- package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +0 -19
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +0 -1
- package/dist-server/server/routes/gemini.js +0 -21
- package/dist-server/server/routes/gemini.js.map +0 -1
- package/dist-server/server/sessionManager.js +0 -194
- package/dist-server/server/sessionManager.js.map +0 -1
- package/server/gemini-cli.js +0 -638
- package/server/gemini-response-handler.js +0 -117
- package/server/modules/providers/list/gemini/gemini-auth.provider.ts +0 -307
- package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +0 -110
- package/server/modules/providers/list/gemini/gemini-models.provider.ts +0 -39
- package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +0 -405
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +0 -540
- package/server/modules/providers/list/gemini/gemini-skills.provider.ts +0 -44
- package/server/modules/providers/list/gemini/gemini.provider.ts +0 -27
- package/server/routes/gemini.js +0 -25
- package/server/sessionManager.js +0 -226
package/server/claude-sdk.js
CHANGED
|
@@ -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 {
|
|
240
|
-
* @param {string} tempDir - Temp directory for cleanup
|
|
240
|
+
* @param {Object} writer - WebSocket writer for reconnect support
|
|
241
241
|
*/
|
|
242
|
-
function addSession(sessionId, queryInstance,
|
|
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
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
|
428
|
-
if (
|
|
429
|
-
return;
|
|
377
|
+
async function buildPromptPayload(command, images, cwd) {
|
|
378
|
+
if (normalizeImageDescriptors(images).length === 0) {
|
|
379
|
+
return command;
|
|
430
380
|
}
|
|
431
381
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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:
|
|
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:
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
package/server/cursor-cli.js
CHANGED
|
@@ -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
|
-
//
|
|
10
|
-
|
|
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,
|
|
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
|
-
|
|
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 ??
|
|
131
|
+
return dbSession.provider_session_id ?? null;
|
|
140
132
|
}
|
|
141
133
|
|
|
142
|
-
return
|
|
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
|
-
//
|
|
1076
|
-
//
|
|
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;
|