@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
|
@@ -9,6 +9,7 @@ import Database from 'better-sqlite3';
|
|
|
9
9
|
import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
|
|
10
10
|
import { OpenCodeSessionSynchronizer } from '@/modules/providers/list/opencode/opencode-session-synchronizer.provider.js';
|
|
11
11
|
import { OpenCodeSessionsProvider } from '@/modules/providers/list/opencode/opencode-sessions.provider.js';
|
|
12
|
+
import { appendImagesInputTag } from '@/shared/image-attachments.js';
|
|
12
13
|
|
|
13
14
|
const patchHomeDir = (nextHomeDir: string) => {
|
|
14
15
|
const original = os.homedir;
|
|
@@ -321,6 +322,41 @@ test('OpenCode session synchronizer adopts the pending app session before watche
|
|
|
321
322
|
}
|
|
322
323
|
});
|
|
323
324
|
|
|
325
|
+
test('OpenCode sessions provider strips <images_input> from user turns and exposes attachments', { concurrency: false }, async () => {
|
|
326
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-session-images-'));
|
|
327
|
+
const workspacePath = path.join(tempRoot, 'workspace');
|
|
328
|
+
await mkdir(workspacePath, { recursive: true });
|
|
329
|
+
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
await createOpenCodeDatabase(tempRoot, workspacePath);
|
|
333
|
+
|
|
334
|
+
// Rewrite the user text part with the tagged prompt the runtime sends.
|
|
335
|
+
const taggedPrompt = appendImagesInputTag('Look at this screenshot.', [
|
|
336
|
+
{ path: 'C:/Users/x/.cloudcli/assets/shot.png' },
|
|
337
|
+
]);
|
|
338
|
+
const db = new Database(path.join(tempRoot, '.local', 'share', 'opencode', 'opencode.db'));
|
|
339
|
+
try {
|
|
340
|
+
db.prepare('UPDATE part SET data = ? WHERE id = ?').run(
|
|
341
|
+
JSON.stringify({ type: 'text', text: taggedPrompt }),
|
|
342
|
+
'part-user-text',
|
|
343
|
+
);
|
|
344
|
+
} finally {
|
|
345
|
+
db.close();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const provider = new OpenCodeSessionsProvider();
|
|
349
|
+
const history = await provider.fetchHistory('open-session-1');
|
|
350
|
+
const userMessage = history.messages.find((message) => message.kind === 'text' && message.role === 'user');
|
|
351
|
+
|
|
352
|
+
assert.equal(userMessage?.content, 'Look at this screenshot.');
|
|
353
|
+
assert.deepEqual(userMessage?.images, [{ path: 'C:/Users/x/.cloudcli/assets/shot.png' }]);
|
|
354
|
+
} finally {
|
|
355
|
+
restoreHomeDir();
|
|
356
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
324
360
|
test('OpenCode sessions provider normalizes quoted live text and skips user echoes', () => {
|
|
325
361
|
const provider = new OpenCodeSessionsProvider();
|
|
326
362
|
const normalized = provider.normalizeMessage({
|
|
@@ -381,3 +417,106 @@ test('OpenCode sessions provider reads sqlite history and token usage', { concur
|
|
|
381
417
|
await rm(tempRoot, { recursive: true, force: true });
|
|
382
418
|
}
|
|
383
419
|
});
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Seeds a single OpenCode session with a controllable stored title and first
|
|
423
|
+
* user message. Uses a minimal schema (only the columns the synchronizer reads)
|
|
424
|
+
* with a plain-text user part so the derived name is unambiguous.
|
|
425
|
+
*/
|
|
426
|
+
const seedOpenCodeSession = async (
|
|
427
|
+
homeDir: string,
|
|
428
|
+
workspacePath: string,
|
|
429
|
+
options: { sessionId: string; title: string | null; firstUserText: string },
|
|
430
|
+
): Promise<void> => {
|
|
431
|
+
const dataDir = path.join(homeDir, '.local', 'share', 'opencode');
|
|
432
|
+
await mkdir(dataDir, { recursive: true });
|
|
433
|
+
|
|
434
|
+
const db = new Database(path.join(dataDir, 'opencode.db'));
|
|
435
|
+
try {
|
|
436
|
+
db.exec(`
|
|
437
|
+
CREATE TABLE project (id TEXT PRIMARY KEY, worktree TEXT);
|
|
438
|
+
CREATE TABLE session (
|
|
439
|
+
id TEXT PRIMARY KEY,
|
|
440
|
+
project_id TEXT,
|
|
441
|
+
directory TEXT,
|
|
442
|
+
title TEXT,
|
|
443
|
+
time_created INTEGER,
|
|
444
|
+
time_updated INTEGER,
|
|
445
|
+
time_archived INTEGER
|
|
446
|
+
);
|
|
447
|
+
CREATE TABLE message (id TEXT PRIMARY KEY, session_id TEXT, time_created INTEGER, data TEXT);
|
|
448
|
+
CREATE TABLE part (id TEXT PRIMARY KEY, message_id TEXT, session_id TEXT, time_created INTEGER, data TEXT);
|
|
449
|
+
`);
|
|
450
|
+
|
|
451
|
+
db.prepare('INSERT INTO project (id, worktree) VALUES (?, ?)').run('project-1', workspacePath);
|
|
452
|
+
db.prepare(`
|
|
453
|
+
INSERT INTO session (id, project_id, directory, title, time_created, time_updated, time_archived)
|
|
454
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL)
|
|
455
|
+
`).run(options.sessionId, 'project-1', workspacePath, options.title, 1_700_000_000_000, 1_700_000_001_000);
|
|
456
|
+
db.prepare('INSERT INTO message (id, session_id, time_created, data) VALUES (?, ?, ?, ?)')
|
|
457
|
+
.run('message-user', options.sessionId, 1_700_000_001_000, JSON.stringify({ role: 'user' }));
|
|
458
|
+
db.prepare('INSERT INTO part (id, message_id, session_id, time_created, data) VALUES (?, ?, ?, ?, ?)')
|
|
459
|
+
.run(
|
|
460
|
+
'part-user',
|
|
461
|
+
'message-user',
|
|
462
|
+
options.sessionId,
|
|
463
|
+
1_700_000_001_000,
|
|
464
|
+
// OpenCode persists the prompt as a JSON string literal inside the text
|
|
465
|
+
// field, so double-encode it here to exercise the unwrap on read.
|
|
466
|
+
JSON.stringify({ type: 'text', text: JSON.stringify(options.firstUserText) }),
|
|
467
|
+
);
|
|
468
|
+
} finally {
|
|
469
|
+
db.close();
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
test('OpenCode synchronizer titles app-created sessions from the first user message', { concurrency: false }, async () => {
|
|
474
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-session-sync-app-'));
|
|
475
|
+
const workspacePath = path.join(tempRoot, 'workspace');
|
|
476
|
+
await mkdir(workspacePath, { recursive: true });
|
|
477
|
+
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
// Stored title differs from the first message so we can prove the first
|
|
481
|
+
// message wins for sessions started from cloudcli.
|
|
482
|
+
await seedOpenCodeSession(tempRoot, workspacePath, {
|
|
483
|
+
sessionId: 'oc-app-1',
|
|
484
|
+
title: 'OpenCode generated title',
|
|
485
|
+
firstUserText: 'Fix the checkout crash',
|
|
486
|
+
});
|
|
487
|
+
await withIsolatedDatabase(async () => {
|
|
488
|
+
sessionsDb.createAppSession('app-1', 'opencode', workspacePath);
|
|
489
|
+
sessionsDb.assignProviderSessionId('app-1', 'oc-app-1');
|
|
490
|
+
|
|
491
|
+
await new OpenCodeSessionSynchronizer().synchronize();
|
|
492
|
+
|
|
493
|
+
assert.equal(sessionsDb.getSessionById('app-1')?.custom_name, 'Fix the checkout crash');
|
|
494
|
+
});
|
|
495
|
+
} finally {
|
|
496
|
+
restoreHomeDir();
|
|
497
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test('OpenCode synchronizer keeps the stored title for indexed sessions', { concurrency: false }, async () => {
|
|
502
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-session-sync-indexed-'));
|
|
503
|
+
const workspacePath = path.join(tempRoot, 'workspace');
|
|
504
|
+
await mkdir(workspacePath, { recursive: true });
|
|
505
|
+
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
await seedOpenCodeSession(tempRoot, workspacePath, {
|
|
509
|
+
sessionId: 'oc-indexed-1',
|
|
510
|
+
title: 'OpenCode generated title',
|
|
511
|
+
firstUserText: 'This prompt should be ignored',
|
|
512
|
+
});
|
|
513
|
+
await withIsolatedDatabase(async () => {
|
|
514
|
+
await new OpenCodeSessionSynchronizer().synchronize();
|
|
515
|
+
|
|
516
|
+
assert.equal(sessionsDb.getSessionById('oc-indexed-1')?.custom_name, 'OpenCode generated title');
|
|
517
|
+
});
|
|
518
|
+
} finally {
|
|
519
|
+
restoreHomeDir();
|
|
520
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
521
|
+
}
|
|
522
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { ClaudeSessionsProvider } from '@/modules/providers/list/claude/claude-sessions.provider.js';
|
|
5
|
+
import { CodexSessionsProvider, extractCodexUserImages } from '@/modules/providers/list/codex/codex-sessions.provider.js';
|
|
6
|
+
import { CursorSessionsProvider } from '@/modules/providers/list/cursor/cursor-sessions.provider.js';
|
|
7
|
+
import { appendImagesInputTag } from '@/shared/image-attachments.js';
|
|
8
|
+
|
|
9
|
+
const SESSION_ID = 'session-1';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------- Claude
|
|
12
|
+
|
|
13
|
+
test('claude history: base64 image blocks surface as user message images', () => {
|
|
14
|
+
const provider = new ClaudeSessionsProvider();
|
|
15
|
+
const entry = {
|
|
16
|
+
uuid: 'u1',
|
|
17
|
+
timestamp: '2026-07-03T10:00:00.000Z',
|
|
18
|
+
message: {
|
|
19
|
+
role: 'user',
|
|
20
|
+
content: [
|
|
21
|
+
{ type: 'text', text: 'What is in this screenshot?' },
|
|
22
|
+
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'QUJD' } },
|
|
23
|
+
{ type: 'image', source: { type: 'base64', media_type: 'image/jpeg', data: 'REVG' } },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const messages = provider.normalizeMessage(entry, SESSION_ID);
|
|
29
|
+
|
|
30
|
+
assert.equal(messages.length, 1);
|
|
31
|
+
assert.equal(messages[0].kind, 'text');
|
|
32
|
+
assert.equal(messages[0].role, 'user');
|
|
33
|
+
assert.equal(messages[0].content, 'What is in this screenshot?');
|
|
34
|
+
assert.deepEqual(messages[0].images, [
|
|
35
|
+
{ data: 'data:image/png;base64,QUJD' },
|
|
36
|
+
{ data: 'data:image/jpeg;base64,REVG' },
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('claude history: image-only user turns still produce a bubble', () => {
|
|
41
|
+
const provider = new ClaudeSessionsProvider();
|
|
42
|
+
const entry = {
|
|
43
|
+
uuid: 'u2',
|
|
44
|
+
timestamp: '2026-07-03T10:00:00.000Z',
|
|
45
|
+
message: {
|
|
46
|
+
role: 'user',
|
|
47
|
+
content: [
|
|
48
|
+
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'QUJD' } },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const messages = provider.normalizeMessage(entry, SESSION_ID);
|
|
54
|
+
|
|
55
|
+
assert.equal(messages.length, 1);
|
|
56
|
+
assert.equal(messages[0].role, 'user');
|
|
57
|
+
assert.equal(messages[0].content, '');
|
|
58
|
+
assert.deepEqual(messages[0].images, [{ data: 'data:image/png;base64,QUJD' }]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('claude history: plain text user turns carry no images field', () => {
|
|
62
|
+
const provider = new ClaudeSessionsProvider();
|
|
63
|
+
const entry = {
|
|
64
|
+
uuid: 'u3',
|
|
65
|
+
timestamp: '2026-07-03T10:00:00.000Z',
|
|
66
|
+
message: { role: 'user', content: [{ type: 'text', text: 'hello' }] },
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const messages = provider.normalizeMessage(entry, SESSION_ID);
|
|
70
|
+
assert.equal(messages.length, 1);
|
|
71
|
+
assert.equal(messages[0].images, undefined);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------- Codex
|
|
75
|
+
|
|
76
|
+
test('codex history: user_message payload images become path attachments', () => {
|
|
77
|
+
// Real rollout shape: local_image input items land in `local_images`,
|
|
78
|
+
// while `images` stays an empty array.
|
|
79
|
+
assert.deepEqual(
|
|
80
|
+
extractCodexUserImages({
|
|
81
|
+
type: 'user_message',
|
|
82
|
+
message: 'can u see attached image?',
|
|
83
|
+
images: [],
|
|
84
|
+
local_images: ['C:\\proj\\.cloudcli\\assets\\a.png'],
|
|
85
|
+
}),
|
|
86
|
+
[{ path: 'C:/proj/.cloudcli/assets/a.png' }],
|
|
87
|
+
);
|
|
88
|
+
assert.deepEqual(
|
|
89
|
+
extractCodexUserImages({ type: 'user_message', message: 'hi', images: ['/proj/b.jpg'] }),
|
|
90
|
+
[{ path: '/proj/b.jpg' }],
|
|
91
|
+
);
|
|
92
|
+
assert.equal(extractCodexUserImages({ type: 'user_message', message: 'hi' }), undefined);
|
|
93
|
+
assert.equal(extractCodexUserImages({ type: 'user_message', message: 'hi', images: [], local_images: [] }), undefined);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('codex history: base64 data URLs pass through as inline data attachments', () => {
|
|
97
|
+
const dataUrl = 'data:image/png;base64,QUJD';
|
|
98
|
+
assert.deepEqual(
|
|
99
|
+
extractCodexUserImages({
|
|
100
|
+
type: 'user_message',
|
|
101
|
+
message: 'look',
|
|
102
|
+
images: [dataUrl],
|
|
103
|
+
local_images: ['C:\\proj\\a.png'],
|
|
104
|
+
}),
|
|
105
|
+
[{ path: 'C:/proj/a.png' }, { data: dataUrl }],
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('codex history: normalized user entries keep their images', () => {
|
|
110
|
+
const provider = new CodexSessionsProvider();
|
|
111
|
+
const messages = provider.normalizeMessage(
|
|
112
|
+
{
|
|
113
|
+
timestamp: '2026-07-03T10:00:00.000Z',
|
|
114
|
+
message: { role: 'user', content: 'Look at this' },
|
|
115
|
+
images: [{ path: '.cloudcli/assets/a.png' }],
|
|
116
|
+
},
|
|
117
|
+
SESSION_ID,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
assert.equal(messages.length, 1);
|
|
121
|
+
assert.equal(messages[0].role, 'user');
|
|
122
|
+
assert.equal(messages[0].content, 'Look at this');
|
|
123
|
+
assert.deepEqual(messages[0].images, [{ path: '.cloudcli/assets/a.png' }]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------- Cursor
|
|
127
|
+
|
|
128
|
+
test('cursor history: <images_input> inside user_query is stripped and attached', () => {
|
|
129
|
+
const provider = new CursorSessionsProvider();
|
|
130
|
+
const taggedPrompt = appendImagesInputTag('Fix the layout bug', [{ path: '.cloudcli/assets/shot.png' }]);
|
|
131
|
+
const blobs = [
|
|
132
|
+
{
|
|
133
|
+
id: 'blob1',
|
|
134
|
+
sequence: 1,
|
|
135
|
+
rowid: 1,
|
|
136
|
+
content: {
|
|
137
|
+
role: 'user',
|
|
138
|
+
content: `<timestamp>2026-07-03</timestamp>\n<user_query>${taggedPrompt}</user_query>`,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: 'blob2',
|
|
143
|
+
sequence: 2,
|
|
144
|
+
rowid: 2,
|
|
145
|
+
content: {
|
|
146
|
+
role: 'assistant',
|
|
147
|
+
content: [{ type: 'text', text: 'Done — the flex container was wrong.' }],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
const messages = provider.normalizeCursorBlobs(blobs, SESSION_ID);
|
|
153
|
+
|
|
154
|
+
assert.equal(messages.length, 2);
|
|
155
|
+
assert.equal(messages[0].role, 'user');
|
|
156
|
+
assert.equal(messages[0].content, 'Fix the layout bug');
|
|
157
|
+
assert.deepEqual(messages[0].images, [{ path: '.cloudcli/assets/shot.png' }]);
|
|
158
|
+
assert.equal(messages[1].role, 'assistant');
|
|
159
|
+
assert.equal(messages[1].images, undefined);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('cursor history: user text without a tag keeps existing behavior', () => {
|
|
163
|
+
const provider = new CursorSessionsProvider();
|
|
164
|
+
const blobs = [
|
|
165
|
+
{
|
|
166
|
+
id: 'blob1',
|
|
167
|
+
sequence: 1,
|
|
168
|
+
rowid: 1,
|
|
169
|
+
content: {
|
|
170
|
+
role: 'user',
|
|
171
|
+
content: '<timestamp>2026-07-03</timestamp>\n<user_query>plain question</user_query>',
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
const messages = provider.normalizeCursorBlobs(blobs, SESSION_ID);
|
|
177
|
+
assert.equal(messages.length, 1);
|
|
178
|
+
assert.equal(messages[0].content, 'plain question');
|
|
179
|
+
assert.equal(messages[0].images, undefined);
|
|
180
|
+
});
|
|
@@ -170,13 +170,13 @@ test('provider model cache is persisted across service instances', async () => {
|
|
|
170
170
|
cachePath,
|
|
171
171
|
resolveProvider: () => ({
|
|
172
172
|
models: {
|
|
173
|
-
getSupportedModels: async () => createModels('
|
|
174
|
-
getCurrentActiveModel: async () => createCurrentActiveModel('
|
|
175
|
-
changeActiveModel: async (input) => createSessionActiveModelChange('
|
|
173
|
+
getSupportedModels: async () => createModels('cursor-cached'),
|
|
174
|
+
getCurrentActiveModel: async () => createCurrentActiveModel('cursor-active'),
|
|
175
|
+
changeActiveModel: async (input) => createSessionActiveModelChange('cursor', input),
|
|
176
176
|
},
|
|
177
177
|
}),
|
|
178
178
|
});
|
|
179
|
-
await writer.getProviderModels('
|
|
179
|
+
await writer.getProviderModels('cursor');
|
|
180
180
|
|
|
181
181
|
const reader = createProviderModelsService({
|
|
182
182
|
cachePath,
|
|
@@ -185,13 +185,13 @@ test('provider model cache is persisted across service instances', async () => {
|
|
|
185
185
|
getSupportedModels: async () => {
|
|
186
186
|
throw new Error('loader should not be called for persisted cache hits');
|
|
187
187
|
},
|
|
188
|
-
getCurrentActiveModel: async () => createCurrentActiveModel('
|
|
189
|
-
changeActiveModel: async (input) => createSessionActiveModelChange('
|
|
188
|
+
getCurrentActiveModel: async () => createCurrentActiveModel('cursor-active'),
|
|
189
|
+
changeActiveModel: async (input) => createSessionActiveModelChange('cursor', input),
|
|
190
190
|
},
|
|
191
191
|
}),
|
|
192
192
|
});
|
|
193
|
-
const models = await reader.getProviderModels('
|
|
194
|
-
assert.equal(models.models.DEFAULT, '
|
|
193
|
+
const models = await reader.getProviderModels('cursor');
|
|
194
|
+
assert.equal(models.models.DEFAULT, 'cursor-cached');
|
|
195
195
|
assert.equal(models.cache.source, 'disk');
|
|
196
196
|
} finally {
|
|
197
197
|
await rm(tempRoot, { recursive: true, force: true });
|
|
@@ -444,34 +444,22 @@ test('providerSkillsService lists opencode project and user compatibility skills
|
|
|
444
444
|
});
|
|
445
445
|
|
|
446
446
|
/**
|
|
447
|
-
* This test covers
|
|
447
|
+
* This test covers Cursor skill directory rules, including shared
|
|
448
448
|
* `.agents/skills` project support.
|
|
449
449
|
*/
|
|
450
|
-
test('providerSkillsService lists
|
|
450
|
+
test('providerSkillsService lists cursor skills from its configured directories', { concurrency: false }, async () => {
|
|
451
451
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-gc-'));
|
|
452
452
|
const workspacePath = path.join(tempRoot, 'workspace');
|
|
453
453
|
await fs.mkdir(workspacePath, { recursive: true });
|
|
454
454
|
|
|
455
455
|
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
456
456
|
try {
|
|
457
|
-
await writeSkill(
|
|
458
|
-
path.join(tempRoot, '.gemini', 'skills'),
|
|
459
|
-
'gemini-user-dir',
|
|
460
|
-
'gemini-user',
|
|
461
|
-
'Gemini user skill',
|
|
462
|
-
);
|
|
463
457
|
await writeSkill(
|
|
464
458
|
path.join(tempRoot, '.agents', 'skills'),
|
|
465
459
|
'agents-user-dir',
|
|
466
460
|
'agents-user',
|
|
467
461
|
'Agents user skill',
|
|
468
462
|
);
|
|
469
|
-
await writeSkill(
|
|
470
|
-
path.join(workspacePath, '.gemini', 'skills'),
|
|
471
|
-
'gemini-project-dir',
|
|
472
|
-
'gemini-project',
|
|
473
|
-
'Gemini project skill',
|
|
474
|
-
);
|
|
475
463
|
await writeSkill(
|
|
476
464
|
path.join(workspacePath, '.agents', 'skills'),
|
|
477
465
|
'agents-project-dir',
|
|
@@ -491,14 +479,6 @@ test('providerSkillsService lists gemini and cursor skills from their configured
|
|
|
491
479
|
'Cursor user skill',
|
|
492
480
|
);
|
|
493
481
|
|
|
494
|
-
const geminiSkills = await providerSkillsService.listProviderSkills('gemini', { workspacePath });
|
|
495
|
-
const geminiByName = new Map(geminiSkills.map((skill) => [skill.name, skill]));
|
|
496
|
-
assert.equal(geminiByName.get('gemini-user')?.scope, 'user');
|
|
497
|
-
assert.equal(geminiByName.get('agents-user')?.scope, 'user');
|
|
498
|
-
assert.equal(geminiByName.get('gemini-project')?.scope, 'project');
|
|
499
|
-
assert.equal(geminiByName.get('agents-project')?.scope, 'project');
|
|
500
|
-
assert.equal(geminiByName.get('gemini-project')?.command, '/gemini-project');
|
|
501
|
-
|
|
502
482
|
const cursorSkills = await providerSkillsService.listProviderSkills('cursor', { workspacePath });
|
|
503
483
|
const cursorByName = new Map(cursorSkills.map((skill) => [skill.name, skill]));
|
|
504
484
|
assert.equal(cursorByName.get('agents-project')?.scope, 'project');
|
|
@@ -515,7 +495,7 @@ test('providerSkillsService lists gemini and cursor skills from their configured
|
|
|
515
495
|
* This test covers managed global skill creation for providers that own a
|
|
516
496
|
* writable user skill directory.
|
|
517
497
|
*/
|
|
518
|
-
test('providerSkillsService adds global skills for claude, codex,
|
|
498
|
+
test('providerSkillsService adds global skills for claude, codex, and cursor', { concurrency: false }, async () => {
|
|
519
499
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-create-'));
|
|
520
500
|
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
521
501
|
|
|
@@ -618,22 +598,6 @@ test('providerSkillsService adds global skills for claude, codex, gemini, and cu
|
|
|
618
598
|
);
|
|
619
599
|
await assert.rejects(fs.stat(pendingBatchSkillPath), { code: 'ENOENT' });
|
|
620
600
|
|
|
621
|
-
const createdGeminiSkills = await providerSkillsService.addProviderSkills('gemini', {
|
|
622
|
-
entries: [
|
|
623
|
-
{
|
|
624
|
-
directoryName: 'gemini-global-dir',
|
|
625
|
-
content: '---\nname: gemini-global\ndescription: Gemini global skill\n---\n\nGemini body.\n',
|
|
626
|
-
},
|
|
627
|
-
],
|
|
628
|
-
});
|
|
629
|
-
const createdGeminiSkill = createdGeminiSkills[0];
|
|
630
|
-
assert.ok(createdGeminiSkill);
|
|
631
|
-
assert.equal(createdGeminiSkill.command, '/gemini-global');
|
|
632
|
-
assert.equal(
|
|
633
|
-
createdGeminiSkill.sourcePath.endsWith(path.join('.gemini', 'skills', 'gemini-global-dir', 'SKILL.md')),
|
|
634
|
-
true,
|
|
635
|
-
);
|
|
636
|
-
|
|
637
601
|
const createdCursorSkills = await providerSkillsService.addProviderSkills('cursor', {
|
|
638
602
|
entries: [
|
|
639
603
|
{
|
|
@@ -656,9 +620,6 @@ test('providerSkillsService adds global skills for claude, codex, gemini, and cu
|
|
|
656
620
|
const listedCodexSkills = await providerSkillsService.listProviderSkills('codex');
|
|
657
621
|
assert.equal(listedCodexSkills.some((skill) => skill.name === 'replacement'), true);
|
|
658
622
|
|
|
659
|
-
const listedGeminiSkills = await providerSkillsService.listProviderSkills('gemini');
|
|
660
|
-
assert.equal(listedGeminiSkills.some((skill) => skill.name === 'gemini-global'), true);
|
|
661
|
-
|
|
662
623
|
const listedCursorSkills = await providerSkillsService.listProviderSkills('cursor');
|
|
663
624
|
assert.equal(listedCursorSkills.some((skill) => skill.name === 'cursor-global'), true);
|
|
664
625
|
|
|
@@ -318,6 +318,22 @@ export const chatRunRegistry = {
|
|
|
318
318
|
run.writer.sendComplete(opts);
|
|
319
319
|
},
|
|
320
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Safety-net variant of `completeRun` scoped to one specific run: a no-op
|
|
323
|
+
* unless `run` is still the session's current, running run. A runtime
|
|
324
|
+
* promise can resolve after its own `complete` already streamed AND a new
|
|
325
|
+
* run has replaced it in the registry (a queued message sends within
|
|
326
|
+
* milliseconds of the previous turn ending) — the session-keyed
|
|
327
|
+
* `completeRun` would terminate that newer run.
|
|
328
|
+
*/
|
|
329
|
+
completeRunIfCurrent(run: ChatRun, opts: { exitCode: number; aborted?: boolean }): void {
|
|
330
|
+
if (runs.get(run.appSessionId) !== run || run.status !== 'running') {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
run.writer.sendComplete(opts);
|
|
335
|
+
},
|
|
336
|
+
|
|
321
337
|
/**
|
|
322
338
|
* Test-only escape hatch: clears every tracked run.
|
|
323
339
|
*/
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
1
3
|
import type { WebSocket } from 'ws';
|
|
2
4
|
|
|
3
5
|
import { sessionsDb } from '@/modules/database/index.js';
|
|
4
6
|
import { chatRunRegistry } from '@/modules/websocket/services/chat-run-registry.service.js';
|
|
5
7
|
import { connectedClients, WS_OPEN_STATE } from '@/modules/websocket/services/websocket-state.service.js';
|
|
8
|
+
import { getGlobalImageAssetsDir, normalizeImageDescriptors } from '@/shared/image-attachments.js';
|
|
6
9
|
import type {
|
|
7
10
|
AnyRecord,
|
|
8
11
|
AuthenticatedWebSocketRequest,
|
|
@@ -10,6 +13,37 @@ import type {
|
|
|
10
13
|
} from '@/shared/types.js';
|
|
11
14
|
import { parseIncomingJsonObject } from '@/shared/utils.js';
|
|
12
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Trust boundary for client-supplied image attachments: chat.send options come
|
|
18
|
+
* straight from the browser, and the provider runtimes read the referenced
|
|
19
|
+
* files off disk (Claude base64-encodes them into the prompt). Only images
|
|
20
|
+
* that live directly inside the global upload store (`~/.cloudcli/assets`,
|
|
21
|
+
* where POST /api/assets/images puts them) are allowed through — anything
|
|
22
|
+
* else (absolute paths elsewhere, traversal, subdirectories) is dropped.
|
|
23
|
+
*
|
|
24
|
+
* Exported for tests; `assetsRootOverride` exists only for them.
|
|
25
|
+
*/
|
|
26
|
+
export function filterImagesToUploadStore(images: unknown, assetsRootOverride?: string): AnyRecord[] {
|
|
27
|
+
const assetsRoot = path.resolve(assetsRootOverride ?? getGlobalImageAssetsDir());
|
|
28
|
+
|
|
29
|
+
return normalizeImageDescriptors(images).filter((descriptor) => {
|
|
30
|
+
// Relative paths are anchored in the store; absolute ones must already be in it.
|
|
31
|
+
const resolved = path.resolve(assetsRoot, descriptor.path);
|
|
32
|
+
const relative = path.relative(assetsRoot, resolved);
|
|
33
|
+
const isDirectChild =
|
|
34
|
+
relative.length > 0 &&
|
|
35
|
+
!relative.startsWith('..') &&
|
|
36
|
+
!path.isAbsolute(relative) &&
|
|
37
|
+
!relative.includes(path.sep) &&
|
|
38
|
+
!relative.includes('/');
|
|
39
|
+
|
|
40
|
+
if (!isDirectChild) {
|
|
41
|
+
console.warn(`[Chat] Dropping image outside the upload store: ${descriptor.path}`);
|
|
42
|
+
}
|
|
43
|
+
return isDirectChild;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
13
47
|
/**
|
|
14
48
|
* One provider runtime entry point. All five runtimes share this signature,
|
|
15
49
|
* which lets the chat handler dispatch through a provider-keyed map instead
|
|
@@ -161,6 +195,9 @@ async function handleChatSend(
|
|
|
161
195
|
// gateway writer captures and maps back to the app session id.
|
|
162
196
|
const runtimeOptions: AnyRecord = {
|
|
163
197
|
...clientOptions,
|
|
198
|
+
// Image attachments are re-validated server-side: only files inside the
|
|
199
|
+
// global upload store may reach the provider runtimes' file reads.
|
|
200
|
+
images: filterImagesToUploadStore(clientOptions.images),
|
|
164
201
|
sessionId: session.provider_session_id ?? undefined,
|
|
165
202
|
resume: Boolean(session.provider_session_id),
|
|
166
203
|
cwd: clientOptions.cwd ?? session.project_path ?? undefined,
|
|
@@ -175,8 +212,10 @@ async function handleChatSend(
|
|
|
175
212
|
} finally {
|
|
176
213
|
// Safety net: a runtime that crashed (or resolved) without emitting its
|
|
177
214
|
// terminal `complete` would otherwise leave the session stuck in
|
|
178
|
-
// "processing" forever on every connected client.
|
|
179
|
-
|
|
215
|
+
// "processing" forever on every connected client. Scoped to THIS run —
|
|
216
|
+
// a queued message can start the session's next run before this promise
|
|
217
|
+
// settles, and the session-keyed completeRun would kill that new run.
|
|
218
|
+
chatRunRegistry.completeRunIfCurrent(run, { exitCode: 1 });
|
|
180
219
|
}
|
|
181
220
|
}
|
|
182
221
|
|
|
@@ -146,14 +146,6 @@ function buildShellCommand(
|
|
|
146
146
|
return 'codex';
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
if (provider === 'gemini') {
|
|
150
|
-
const command = initialCommand || 'gemini';
|
|
151
|
-
if (resumeSessionId) {
|
|
152
|
-
return `${command} --resume "${resumeSessionId}"`;
|
|
153
|
-
}
|
|
154
|
-
return command;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
149
|
if (provider === 'opencode') {
|
|
158
150
|
if (resumeSessionId) {
|
|
159
151
|
return `opencode --session "${resumeSessionId}"`;
|
|
@@ -477,9 +469,7 @@ export function handleShellConnection(
|
|
|
477
469
|
? 'Cursor'
|
|
478
470
|
: provider === 'codex'
|
|
479
471
|
? 'Codex'
|
|
480
|
-
: provider === '
|
|
481
|
-
? 'Gemini'
|
|
482
|
-
: provider === 'opencode'
|
|
472
|
+
: provider === 'opencode'
|
|
483
473
|
? 'OpenCode'
|
|
484
474
|
: 'Claude';
|
|
485
475
|
welcomeMsg = hasSession && resumeSessionId
|
|
@@ -0,0 +1,44 @@
|
|
|
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 { filterImagesToUploadStore } from '@/modules/websocket/services/chat-websocket.service.js';
|
|
7
|
+
|
|
8
|
+
const STORE = path.join(os.tmpdir(), 'cloudcli-assets-store');
|
|
9
|
+
|
|
10
|
+
test('images inside the upload store pass through', () => {
|
|
11
|
+
const inside = path.join(STORE, 'shot.png');
|
|
12
|
+
const result = filterImagesToUploadStore(
|
|
13
|
+
[{ path: inside, name: 'shot.png', mimeType: 'image/png' }],
|
|
14
|
+
STORE,
|
|
15
|
+
);
|
|
16
|
+
assert.equal(result.length, 1);
|
|
17
|
+
assert.equal(result[0].path, inside);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('bare filenames are anchored inside the store', () => {
|
|
21
|
+
const result = filterImagesToUploadStore(['shot.png'], STORE);
|
|
22
|
+
assert.equal(result.length, 1);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('paths outside the store, traversal, and subdirs are dropped', () => {
|
|
26
|
+
const result = filterImagesToUploadStore(
|
|
27
|
+
[
|
|
28
|
+
{ path: 'C:/Users/victim/.ssh/id_rsa' },
|
|
29
|
+
{ path: '/etc/passwd' },
|
|
30
|
+
{ path: '../outside.png' },
|
|
31
|
+
{ path: path.join(STORE, '..', 'escaped.png') },
|
|
32
|
+
{ path: path.join(STORE, 'nested', 'deep.png') },
|
|
33
|
+
{ path: STORE }, // the store folder itself is not a file
|
|
34
|
+
],
|
|
35
|
+
STORE,
|
|
36
|
+
);
|
|
37
|
+
assert.deepEqual(result, []);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('malformed payloads yield no images', () => {
|
|
41
|
+
assert.deepEqual(filterImagesToUploadStore(undefined, STORE), []);
|
|
42
|
+
assert.deepEqual(filterImagesToUploadStore('nope', STORE), []);
|
|
43
|
+
assert.deepEqual(filterImagesToUploadStore([{ name: 'no-path' }, 42], STORE), []);
|
|
44
|
+
});
|