coding-tool-x 3.8.0 → 3.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -6
- package/package.json +2 -2
- package/src/config/paths.js +1 -0
- package/src/server/api/codex-sessions.js +14 -14
- package/src/server/api/gemini-sessions.js +52 -14
- package/src/server/api/omp-projects.js +2 -2
- package/src/server/api/omp-sessions.js +61 -23
- package/src/server/api/sessions.js +44 -2
- package/src/server/services/codex-sessions.js +41 -233
- package/src/server/services/dashboard-snapshot-worker.js +5 -5
- package/src/server/services/gemini-sessions.js +67 -157
- package/src/server/services/omp-sessions.js +102 -105
- package/src/server/services/opencode-sessions.js +339 -546
- package/src/server/services/session-converter.js +4 -4
- package/src/server/services/session-history-adapters/claude.js +235 -0
- package/src/server/services/session-history-adapters/codex.js +158 -0
- package/src/server/services/session-history-adapters/gemini.js +292 -0
- package/src/server/services/session-history-adapters/index.js +20 -0
- package/src/server/services/session-history-adapters/omp.js +256 -0
- package/src/server/services/session-history-index.js +930 -0
- package/src/server/services/session-history-worker.js +98 -0
- package/src/server/services/session-snapshot-worker.js +7 -7
- package/src/server/services/sessions.js +167 -775
- package/src/server/services/sqlite-connection.js +78 -0
- package/src/server/services/workspace-service.js +3 -3
|
@@ -470,13 +470,13 @@ async function convertSession(sourceType, targetType, sessionId, options = {}) {
|
|
|
470
470
|
}
|
|
471
471
|
sourceFilePath = sourceSession.filePath;
|
|
472
472
|
} else if (sourceType === 'codex') {
|
|
473
|
-
sourceSession = getCodexSessionById(sessionId);
|
|
473
|
+
sourceSession = await getCodexSessionById(sessionId);
|
|
474
474
|
if (!sourceSession) {
|
|
475
475
|
throw new Error('Source session not found');
|
|
476
476
|
}
|
|
477
477
|
sourceFilePath = sourceSession.filePath;
|
|
478
478
|
} else if (sourceType === 'gemini') {
|
|
479
|
-
sourceSession = getGeminiSessionById(sessionId);
|
|
479
|
+
sourceSession = await getGeminiSessionById(sessionId);
|
|
480
480
|
if (!sourceSession) {
|
|
481
481
|
throw new Error('Source session not found');
|
|
482
482
|
}
|
|
@@ -536,13 +536,13 @@ async function previewConversion(sourceType, sessionId) {
|
|
|
536
536
|
}
|
|
537
537
|
sourceFilePath = sourceSession.filePath;
|
|
538
538
|
} else if (sourceType === 'codex') {
|
|
539
|
-
const sourceSession = getCodexSessionById(sessionId);
|
|
539
|
+
const sourceSession = await getCodexSessionById(sessionId);
|
|
540
540
|
if (!sourceSession) {
|
|
541
541
|
throw new Error('Source session not found');
|
|
542
542
|
}
|
|
543
543
|
sourceFilePath = sourceSession.filePath;
|
|
544
544
|
} else if (sourceType === 'gemini') {
|
|
545
|
-
const sourceSession = getGeminiSessionById(sessionId);
|
|
545
|
+
const sourceSession = await getGeminiSessionById(sessionId);
|
|
546
546
|
if (!sourceSession) {
|
|
547
547
|
throw new Error('Source session not found');
|
|
548
548
|
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const { NATIVE_PATHS } = require('../../../config/paths');
|
|
7
|
+
|
|
8
|
+
const CLAUDE_PROJECTS_DIR = NATIVE_PATHS?.claude?.projects || '';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get Claude user-visible text from a message content block.
|
|
12
|
+
* Shared with sessions.js API for display consistency.
|
|
13
|
+
*/
|
|
14
|
+
function getClaudeUserText(content) {
|
|
15
|
+
if (typeof content === 'string') {
|
|
16
|
+
return content === 'Warmup' ? '' : content;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!Array.isArray(content)) {
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const parts = [];
|
|
24
|
+
for (const item of content) {
|
|
25
|
+
if (item?.type === 'text' && item.text) {
|
|
26
|
+
parts.push(item.text);
|
|
27
|
+
} else if (item?.type === 'image') {
|
|
28
|
+
parts.push('[图片]');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return parts.join('\n\n').trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse real project path from encoded name.
|
|
37
|
+
*/
|
|
38
|
+
function parseRealProjectPath(encodedName) {
|
|
39
|
+
const { parseRealProjectPath: fn } = require('../sessions');
|
|
40
|
+
return fn(encodedName);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Inventory all Claude session files.
|
|
45
|
+
* @returns {Promise<Array<{filePath: string, size: number, mtimeMs: number, sessionId: string, projectHint: string}>>}
|
|
46
|
+
*/
|
|
47
|
+
async function inventory() {
|
|
48
|
+
const descriptors = [];
|
|
49
|
+
|
|
50
|
+
if (!fs.existsSync(CLAUDE_PROJECTS_DIR)) {
|
|
51
|
+
return descriptors;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const projects = await fs.promises.readdir(CLAUDE_PROJECTS_DIR, { withFileTypes: true });
|
|
55
|
+
for (const entry of projects) {
|
|
56
|
+
if (!entry.isDirectory()) continue;
|
|
57
|
+
const projectName = entry.name;
|
|
58
|
+
const projectPath = path.join(CLAUDE_PROJECTS_DIR, projectName);
|
|
59
|
+
|
|
60
|
+
let files;
|
|
61
|
+
try {
|
|
62
|
+
files = await fs.promises.readdir(projectPath);
|
|
63
|
+
} catch (_) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const f of files) {
|
|
68
|
+
if (!f.endsWith('.jsonl') || f.startsWith('agent-')) continue;
|
|
69
|
+
const filePath = path.join(projectPath, f);
|
|
70
|
+
let stat;
|
|
71
|
+
try {
|
|
72
|
+
stat = await fs.promises.stat(filePath);
|
|
73
|
+
} catch (_) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
descriptors.push({
|
|
77
|
+
filePath,
|
|
78
|
+
size: stat.size,
|
|
79
|
+
mtimeMs: stat.mtimeMs,
|
|
80
|
+
sessionId: f.replace(/\.jsonl$/, ''),
|
|
81
|
+
projectHint: projectName
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return descriptors;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Parse a Claude session file into normalized session + messages.
|
|
91
|
+
* @param {{filePath: string, size: number, mtimeMs: number, sessionId: string, projectHint: string}} descriptor
|
|
92
|
+
* @returns {Promise<{session: object, messages: Array}>}
|
|
93
|
+
*/
|
|
94
|
+
async function parse(descriptor) {
|
|
95
|
+
const { filePath, size, mtimeMs, sessionId, projectHint } = descriptor;
|
|
96
|
+
const { fullPath, projectName: displayName } = parseRealProjectPath(projectHint);
|
|
97
|
+
|
|
98
|
+
const messages = [];
|
|
99
|
+
let firstMessage = null;
|
|
100
|
+
let gitBranch = null;
|
|
101
|
+
let provider = null;
|
|
102
|
+
let model = null;
|
|
103
|
+
let startedAt = null;
|
|
104
|
+
let updatedAt = null;
|
|
105
|
+
let usageJson = null;
|
|
106
|
+
const extra = {};
|
|
107
|
+
|
|
108
|
+
let userMessageNumber = 0;
|
|
109
|
+
|
|
110
|
+
const stream = fs.createReadStream(filePath, { encoding: 'utf8' });
|
|
111
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
112
|
+
|
|
113
|
+
for await (const line of rl) {
|
|
114
|
+
if (!line.trim()) continue;
|
|
115
|
+
|
|
116
|
+
let json;
|
|
117
|
+
try {
|
|
118
|
+
json = JSON.parse(line);
|
|
119
|
+
} catch (_) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Extract metadata
|
|
124
|
+
if (json.type === 'metadata' || json.type === 'summary') {
|
|
125
|
+
if (json.cwd) extra.cwd = json.cwd;
|
|
126
|
+
if (json.gitBranch) gitBranch = json.gitBranch;
|
|
127
|
+
if (json.timestamp && !startedAt) {
|
|
128
|
+
startedAt = new Date(json.timestamp).getTime();
|
|
129
|
+
}
|
|
130
|
+
if (json.model) {
|
|
131
|
+
model = json.model;
|
|
132
|
+
provider = json.provider || null;
|
|
133
|
+
}
|
|
134
|
+
if (json.usage) usageJson = JSON.stringify(json.usage);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Determine role
|
|
139
|
+
let role = json.role;
|
|
140
|
+
if (!role) {
|
|
141
|
+
if (json.type === 'user' || json.type === 'human') role = 'user';
|
|
142
|
+
else if (json.type === 'assistant' || json.type === 'ai') role = 'assistant';
|
|
143
|
+
else if (json.type === 'system') role = 'system';
|
|
144
|
+
else role = json.type || 'unknown';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Extract message content
|
|
148
|
+
let content = '';
|
|
149
|
+
let subtype = null;
|
|
150
|
+
|
|
151
|
+
if (role === 'user') {
|
|
152
|
+
content = getClaudeUserText(json.content || json.message || '');
|
|
153
|
+
if (!content && json.type === 'Warmup') {
|
|
154
|
+
content = ''; // Warmup excluded
|
|
155
|
+
}
|
|
156
|
+
} else if (role === 'assistant') {
|
|
157
|
+
if (Array.isArray(json.content)) {
|
|
158
|
+
const parts = [];
|
|
159
|
+
for (const block of json.content) {
|
|
160
|
+
if (block.type === 'text' && block.text) {
|
|
161
|
+
parts.push(block.text);
|
|
162
|
+
} else if (block.type === 'tool_use') {
|
|
163
|
+
subtype = 'tool_use';
|
|
164
|
+
parts.push(`[调用工具: ${block.name || 'unknown'}]`);
|
|
165
|
+
} else if (block.type === 'tool_result') {
|
|
166
|
+
subtype = subtype || 'tool_result';
|
|
167
|
+
if (typeof block.content === 'string') {
|
|
168
|
+
parts.push(block.content);
|
|
169
|
+
}
|
|
170
|
+
} else if (block.type === 'thinking') {
|
|
171
|
+
subtype = subtype || 'thinking';
|
|
172
|
+
parts.push(block.thinking || '');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
content = parts.join('\n').trim();
|
|
176
|
+
} else if (typeof json.content === 'string') {
|
|
177
|
+
content = json.content;
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
content = typeof json.content === 'string' ? json.content : (typeof json.message === 'string' ? json.message : '');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Build message
|
|
184
|
+
const msg = {
|
|
185
|
+
messageId: json.uuid || json.id || null,
|
|
186
|
+
role,
|
|
187
|
+
type: json.type || role,
|
|
188
|
+
subtype,
|
|
189
|
+
content,
|
|
190
|
+
timestamp: json.timestamp ? new Date(json.timestamp).getTime() : null,
|
|
191
|
+
model: json.model || model || null,
|
|
192
|
+
provider: json.provider || provider || null,
|
|
193
|
+
extraJson: null
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
if (role === 'user') {
|
|
197
|
+
userMessageNumber++;
|
|
198
|
+
msg.userMessageNumber = userMessageNumber;
|
|
199
|
+
if (!firstMessage && content) {
|
|
200
|
+
firstMessage = content;
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
msg.userMessageNumber = null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
messages.push(msg);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
rl.close();
|
|
210
|
+
stream.destroy();
|
|
211
|
+
|
|
212
|
+
if (messages.length > 0) {
|
|
213
|
+
const lastTs = messages[messages.length - 1].timestamp;
|
|
214
|
+
if (lastTs) updatedAt = lastTs;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const session = {
|
|
218
|
+
sessionId,
|
|
219
|
+
projectName: projectHint,
|
|
220
|
+
projectDisplayName: displayName,
|
|
221
|
+
projectFullPath: fullPath,
|
|
222
|
+
firstMessage: firstMessage || null,
|
|
223
|
+
gitBranch,
|
|
224
|
+
provider,
|
|
225
|
+
model,
|
|
226
|
+
startedAt,
|
|
227
|
+
updatedAt: updatedAt || mtimeMs,
|
|
228
|
+
usageJson,
|
|
229
|
+
extraJson: JSON.stringify(extra)
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
return { session, messages: messages.map(m => ({ ...m, extraJson: m.extraJson ? JSON.stringify(m.extraJson) : null })) };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = { inventory, parse, getClaudeUserText };
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { getCodexDir } = require('../codex-config');
|
|
6
|
+
const { extractSessionMeta, extractMessages, extractTokenUsage, readJSONL } = require('../codex-parser');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Scan all Codex session files recursively.
|
|
10
|
+
* @returns {Array<{filePath: string, size: number, mtimeMs: number, sessionId: string}>}
|
|
11
|
+
*/
|
|
12
|
+
function scanSessionFiles() {
|
|
13
|
+
const results = [];
|
|
14
|
+
const sessionsDir = path.join(getCodexDir(), 'sessions');
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(sessionsDir)) {
|
|
17
|
+
return results;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function scanDir(dir) {
|
|
21
|
+
let entries;
|
|
22
|
+
try {
|
|
23
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
24
|
+
} catch (_) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
const fullPath = path.join(dir, entry.name);
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
scanDir(fullPath);
|
|
31
|
+
} else if (entry.isFile() && entry.name.match(/^rollout-.*\.jsonl$/)) {
|
|
32
|
+
try {
|
|
33
|
+
const stat = fs.statSync(fullPath);
|
|
34
|
+
results.push({
|
|
35
|
+
filePath: fullPath,
|
|
36
|
+
size: stat.size,
|
|
37
|
+
mtimeMs: stat.mtimeMs,
|
|
38
|
+
sessionId: entry.name.replace(/\.jsonl$/, '')
|
|
39
|
+
});
|
|
40
|
+
} catch (_) {}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
scanDir(sessionsDir);
|
|
46
|
+
return results;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract project name from Codex session metadata.
|
|
51
|
+
*/
|
|
52
|
+
function extractCodexProjectName(meta) {
|
|
53
|
+
if (!meta) return 'unknown';
|
|
54
|
+
if (typeof meta.cwd === 'string' && meta.cwd.trim()) {
|
|
55
|
+
// Use last directory of cwd as project name
|
|
56
|
+
const cleaned = meta.cwd.trim().replace(/[/\\]$/, '');
|
|
57
|
+
if (cleaned) {
|
|
58
|
+
const parts = cleaned.split(/[/\\]/);
|
|
59
|
+
const last = parts[parts.length - 1];
|
|
60
|
+
if (last) return last;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return 'unknown';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Inventory all Codex session files.
|
|
68
|
+
* @returns {Promise<Array>}
|
|
69
|
+
*/
|
|
70
|
+
async function inventory() {
|
|
71
|
+
const files = scanSessionFiles();
|
|
72
|
+
return files.map(f => ({
|
|
73
|
+
filePath: f.filePath,
|
|
74
|
+
size: f.size,
|
|
75
|
+
mtimeMs: f.mtimeMs,
|
|
76
|
+
sessionId: f.sessionId,
|
|
77
|
+
projectHint: null // Codex determines project from session content
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parse a Codex session file.
|
|
83
|
+
*/
|
|
84
|
+
async function parse(descriptor) {
|
|
85
|
+
const { filePath, size, mtimeMs, sessionId } = descriptor;
|
|
86
|
+
const lines = readJSONL(filePath);
|
|
87
|
+
|
|
88
|
+
const meta = extractSessionMeta(lines);
|
|
89
|
+
const messages = extractMessages(lines);
|
|
90
|
+
const tokenUsage = extractTokenUsage(lines);
|
|
91
|
+
|
|
92
|
+
const projectName = extractCodexProjectName(meta);
|
|
93
|
+
const usageJson = tokenUsage ? JSON.stringify(tokenUsage) : null;
|
|
94
|
+
|
|
95
|
+
// Build normalized messages
|
|
96
|
+
const normalizedMessages = messages.map((msg, idx) => {
|
|
97
|
+
let content = '';
|
|
98
|
+
if (typeof msg.content === 'string') {
|
|
99
|
+
content = msg.content;
|
|
100
|
+
} else if (Array.isArray(msg.content)) {
|
|
101
|
+
const parts = [];
|
|
102
|
+
for (const block of msg.content) {
|
|
103
|
+
if (typeof block === 'string') {
|
|
104
|
+
parts.push(block);
|
|
105
|
+
} else if (block.type === 'text' && block.text) {
|
|
106
|
+
parts.push(block.text);
|
|
107
|
+
} else if (block.type === 'tool_use' || block.type === 'tool_call') {
|
|
108
|
+
parts.push(`[调用工具: ${block.name || 'unknown'}]`);
|
|
109
|
+
} else if (block.type === 'tool_result') {
|
|
110
|
+
if (typeof block.content === 'string') {
|
|
111
|
+
parts.push(block.content);
|
|
112
|
+
}
|
|
113
|
+
} else if (block.type === 'thinking' || block.type === 'reasoning') {
|
|
114
|
+
parts.push(block.thinking || block.reasoning || '');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
content = parts.join('\n').trim();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const isUser = msg.role === 'user' || msg.role === 'human';
|
|
121
|
+
return {
|
|
122
|
+
messageId: msg.id || `codex-${sessionId}-${idx}`,
|
|
123
|
+
role: msg.role || 'unknown',
|
|
124
|
+
type: msg.type || msg.role || 'unknown',
|
|
125
|
+
subtype: msg.subtype || null,
|
|
126
|
+
content,
|
|
127
|
+
timestamp: msg.timestamp ? new Date(msg.timestamp).getTime() : null,
|
|
128
|
+
model: msg.model || (meta ? meta.model : null) || null,
|
|
129
|
+
provider: null,
|
|
130
|
+
userMessageNumber: isUser ? (msg.userMessageNumber != null ? msg.userMessageNumber : idx + 1) : null,
|
|
131
|
+
extraJson: null
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const firstUserMsg = normalizedMessages.find(m => m.role === 'user' && m.content);
|
|
136
|
+
const lastMsg = normalizedMessages[normalizedMessages.length - 1];
|
|
137
|
+
|
|
138
|
+
const session = {
|
|
139
|
+
sessionId,
|
|
140
|
+
projectName,
|
|
141
|
+
projectDisplayName: projectName,
|
|
142
|
+
projectFullPath: null,
|
|
143
|
+
firstMessage: firstUserMsg ? firstUserMsg.content : null,
|
|
144
|
+
gitBranch: meta ? (meta.gitBranch || null) : null,
|
|
145
|
+
provider: null,
|
|
146
|
+
model: meta ? (meta.model || null) : null,
|
|
147
|
+
startedAt: normalizedMessages[0] ? normalizedMessages[0].timestamp : null,
|
|
148
|
+
updatedAt: lastMsg ? (lastMsg.timestamp || mtimeMs) : mtimeMs,
|
|
149
|
+
usageJson,
|
|
150
|
+
extraJson: JSON.stringify({
|
|
151
|
+
cwd: meta ? meta.cwd : null
|
|
152
|
+
})
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return { session, messages: normalizedMessages };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = { inventory, parse, extractCodexProjectName };
|