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