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.
@@ -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 };