lumencode 1.0.0 → 1.1.0
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/index.js +74 -63
- package/lib/git.js +51 -18
- package/lib/parsers/claude.js +321 -316
- package/lib/parsers/codex.js +360 -316
- package/lib/parsers/opencode.js +236 -216
- package/lib/record-utils.js +36 -35
- package/lib/report.js +41 -4
- package/lib/server.js +573 -523
- package/package.json +1 -1
- package/public/app.js +827 -809
- package/public/style.css +3 -2
package/lib/parsers/claude.js
CHANGED
|
@@ -1,316 +1,321 @@
|
|
|
1
|
-
import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
2
|
-
import { join, dirname, basename } from 'path';
|
|
3
|
-
import { BaseParser } from './base.js';
|
|
4
|
-
import { createUsageRecord } from '../models/usage-record.js';
|
|
5
|
-
import { getCachedFileRecords } from '../cache.js';
|
|
6
|
-
|
|
7
|
-
function encodeProjectPath(projectPath) {
|
|
8
|
-
return projectPath
|
|
9
|
-
.replace(/:[\\/]/, '--')
|
|
10
|
-
.replace(/[\\/]/g, '-');
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function decodeProjectName(dirName) {
|
|
14
|
-
let decoded = dirName
|
|
15
|
-
.replace(/^([A-Z])-/, '$1:/')
|
|
16
|
-
.replace(/--/g, '/')
|
|
17
|
-
.replace(/-/g, '/');
|
|
18
|
-
if (dirName.startsWith('...')) {
|
|
19
|
-
decoded = '.../' + decoded.slice(3);
|
|
20
|
-
}
|
|
21
|
-
decoded = decoded.replace(/\/+$/, '');
|
|
22
|
-
if (/^[A-Z]:$/.test(decoded)) {
|
|
23
|
-
decoded = decoded + ' [空路径]';
|
|
24
|
-
}
|
|
25
|
-
return decoded || '[未知项目]';
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export class ClaudeParser extends BaseParser {
|
|
29
|
-
getInfo() {
|
|
30
|
-
return {
|
|
31
|
-
name: 'claude',
|
|
32
|
-
displayName: 'Claude Code',
|
|
33
|
-
defaultDir: '~/.claude',
|
|
34
|
-
envVar: 'CLAUDE_CONFIG_DIR',
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async detect(config) {
|
|
39
|
-
const dir = this.getDataDir(config);
|
|
40
|
-
if (!dir) return false;
|
|
41
|
-
try {
|
|
42
|
-
const projectsDir = join(dir, 'projects');
|
|
43
|
-
return statSync(projectsDir).isDirectory();
|
|
44
|
-
} catch {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async parse(config, options = {}) {
|
|
50
|
-
const dir = this.getDataDir(config);
|
|
51
|
-
const { excludeProjects = [], includeProjects = null } = options;
|
|
52
|
-
const records = [];
|
|
53
|
-
|
|
54
|
-
if (!dir) return records;
|
|
55
|
-
|
|
56
|
-
const projectsDir = join(dir, 'projects');
|
|
57
|
-
let dirs;
|
|
58
|
-
try {
|
|
59
|
-
dirs = readdirSync(projectsDir).filter(d => {
|
|
60
|
-
const full = join(projectsDir, d);
|
|
61
|
-
return statSync(full).isDirectory() && !excludeProjects.includes(d);
|
|
62
|
-
});
|
|
63
|
-
} catch {
|
|
64
|
-
return records;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// includeProjects 过滤 + 反查正确项目名
|
|
68
|
-
const normalizedIp = includeProjects
|
|
69
|
-
? includeProjects.map(p => p.replace(/\\/g, '/').replace(/\/+/g, '/').replace(/\/$/, ''))
|
|
70
|
-
: null;
|
|
71
|
-
const encodedIncludes = normalizedIp
|
|
72
|
-
? new Set(normalizedIp.map(p => encodeProjectPath(p)))
|
|
73
|
-
: null;
|
|
74
|
-
// encoded → 原始 includeProject 路径的反查表
|
|
75
|
-
const encodedToOriginal = normalizedIp
|
|
76
|
-
? new Map(normalizedIp.map(p => [encodeProjectPath(p), p]))
|
|
77
|
-
: null;
|
|
78
|
-
|
|
79
|
-
for (const projDir of dirs) {
|
|
80
|
-
if (encodedIncludes && !encodedIncludes.has(projDir)) continue;
|
|
81
|
-
|
|
82
|
-
const projPath = join(projectsDir, projDir);
|
|
83
|
-
const allEntries = readdirSync(projPath);
|
|
84
|
-
const jsonlFiles = allEntries.filter(f => f.endsWith('.jsonl') && !f.includes('subagents'));
|
|
85
|
-
|
|
86
|
-
// 优先使用 includeProject 原始路径(避免 decodeProjectName 的有损解码)
|
|
87
|
-
const projName = (encodedToOriginal && encodedToOriginal.get(projDir)) || decodeProjectName(projDir);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
1
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
2
|
+
import { join, dirname, basename } from 'path';
|
|
3
|
+
import { BaseParser } from './base.js';
|
|
4
|
+
import { createUsageRecord } from '../models/usage-record.js';
|
|
5
|
+
import { getCachedFileRecords } from '../cache.js';
|
|
6
|
+
|
|
7
|
+
function encodeProjectPath(projectPath) {
|
|
8
|
+
return projectPath
|
|
9
|
+
.replace(/:[\\/]/, '--')
|
|
10
|
+
.replace(/[\\/]/g, '-');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function decodeProjectName(dirName) {
|
|
14
|
+
let decoded = dirName
|
|
15
|
+
.replace(/^([A-Z])-/, '$1:/')
|
|
16
|
+
.replace(/--/g, '/')
|
|
17
|
+
.replace(/-/g, '/');
|
|
18
|
+
if (dirName.startsWith('...')) {
|
|
19
|
+
decoded = '.../' + decoded.slice(3);
|
|
20
|
+
}
|
|
21
|
+
decoded = decoded.replace(/\/+$/, '');
|
|
22
|
+
if (/^[A-Z]:$/.test(decoded)) {
|
|
23
|
+
decoded = decoded + ' [空路径]';
|
|
24
|
+
}
|
|
25
|
+
return decoded || '[未知项目]';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class ClaudeParser extends BaseParser {
|
|
29
|
+
getInfo() {
|
|
30
|
+
return {
|
|
31
|
+
name: 'claude',
|
|
32
|
+
displayName: 'Claude Code',
|
|
33
|
+
defaultDir: '~/.claude',
|
|
34
|
+
envVar: 'CLAUDE_CONFIG_DIR',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async detect(config) {
|
|
39
|
+
const dir = this.getDataDir(config);
|
|
40
|
+
if (!dir) return false;
|
|
41
|
+
try {
|
|
42
|
+
const projectsDir = join(dir, 'projects');
|
|
43
|
+
return statSync(projectsDir).isDirectory();
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async parse(config, options = {}) {
|
|
50
|
+
const dir = this.getDataDir(config);
|
|
51
|
+
const { excludeProjects = [], includeProjects = null } = options;
|
|
52
|
+
const records = [];
|
|
53
|
+
|
|
54
|
+
if (!dir) return records;
|
|
55
|
+
|
|
56
|
+
const projectsDir = join(dir, 'projects');
|
|
57
|
+
let dirs;
|
|
58
|
+
try {
|
|
59
|
+
dirs = readdirSync(projectsDir).filter(d => {
|
|
60
|
+
const full = join(projectsDir, d);
|
|
61
|
+
return statSync(full).isDirectory() && !excludeProjects.includes(d);
|
|
62
|
+
});
|
|
63
|
+
} catch {
|
|
64
|
+
return records;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// includeProjects 过滤 + 反查正确项目名
|
|
68
|
+
const normalizedIp = includeProjects
|
|
69
|
+
? includeProjects.map(p => p.replace(/\\/g, '/').replace(/\/+/g, '/').replace(/\/$/, ''))
|
|
70
|
+
: null;
|
|
71
|
+
const encodedIncludes = normalizedIp
|
|
72
|
+
? new Set(normalizedIp.map(p => encodeProjectPath(p)))
|
|
73
|
+
: null;
|
|
74
|
+
// encoded → 原始 includeProject 路径的反查表
|
|
75
|
+
const encodedToOriginal = normalizedIp
|
|
76
|
+
? new Map(normalizedIp.map(p => [encodeProjectPath(p), p]))
|
|
77
|
+
: null;
|
|
78
|
+
|
|
79
|
+
for (const projDir of dirs) {
|
|
80
|
+
if (encodedIncludes && !encodedIncludes.has(projDir)) continue;
|
|
81
|
+
|
|
82
|
+
const projPath = join(projectsDir, projDir);
|
|
83
|
+
const allEntries = readdirSync(projPath);
|
|
84
|
+
const jsonlFiles = allEntries.filter(f => f.endsWith('.jsonl') && !f.includes('subagents'));
|
|
85
|
+
|
|
86
|
+
// 优先使用 includeProject 原始路径(避免 decodeProjectName 的有损解码)
|
|
87
|
+
const projName = (encodedToOriginal && encodedToOriginal.get(projDir)) || decodeProjectName(projDir);
|
|
88
|
+
|
|
89
|
+
// 并行读取所有 JSONL 文件(减少串行 IO 等待)
|
|
90
|
+
const fileResults = await Promise.all(
|
|
91
|
+
jsonlFiles.map(async (file) => {
|
|
92
|
+
const filePath = join(projPath, file);
|
|
93
|
+
const sessionIdFromFile = file.replace(/\.jsonl$/, '');
|
|
94
|
+
const result = [];
|
|
95
|
+
try {
|
|
96
|
+
const fileRecords = getCachedFileRecords(filePath);
|
|
97
|
+
for (const r of fileRecords) {
|
|
98
|
+
result.push(this._convertToUsageRecord(r, projName, sessionIdFromFile));
|
|
99
|
+
}
|
|
100
|
+
} catch {}
|
|
101
|
+
// 子 agent 日志
|
|
102
|
+
try {
|
|
103
|
+
const subRecords = this._parseSubagentFiles(dirname(filePath));
|
|
104
|
+
for (const r of subRecords) {
|
|
105
|
+
result.push(this._convertToUsageRecord(r, projName, sessionIdFromFile));
|
|
106
|
+
}
|
|
107
|
+
} catch {}
|
|
108
|
+
return result;
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
for (const fr of fileResults) records.push(...fr);
|
|
112
|
+
|
|
113
|
+
// 当无主 JSONL 文件时,扫描 sessions-index.json 和 UUID 子目录
|
|
114
|
+
if (jsonlFiles.length === 0) {
|
|
115
|
+
// 解析 sessions-index.json 生成会话元数据记录
|
|
116
|
+
const indexRecords = this._parseSessionsIndex(projPath, projName);
|
|
117
|
+
records.push(...indexRecords);
|
|
118
|
+
|
|
119
|
+
// 扫描 UUID 子目录中的 subagent JSONL 文件
|
|
120
|
+
const uuidDirs = allEntries.filter(d => {
|
|
121
|
+
const full = join(projPath, d);
|
|
122
|
+
try { return statSync(full).isDirectory() && /^[0-9a-f]{8}-/i.test(d); } catch { return false; }
|
|
123
|
+
});
|
|
124
|
+
for (const uuidDir of uuidDirs) {
|
|
125
|
+
const sessionDir = join(projPath, uuidDir);
|
|
126
|
+
try {
|
|
127
|
+
const subRecords = this._parseSubagentFiles(sessionDir);
|
|
128
|
+
for (const r of subRecords) {
|
|
129
|
+
records.push(this._convertToUsageRecord(r, projName, uuidDir));
|
|
130
|
+
}
|
|
131
|
+
} catch {}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return records;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_convertToUsageRecord(raw, projectDir, fallbackSessionId = '') {
|
|
140
|
+
return createUsageRecord({
|
|
141
|
+
timestamp: raw.timestamp || '',
|
|
142
|
+
tool: 'claude',
|
|
143
|
+
sessionId: raw.sessionId || fallbackSessionId || '',
|
|
144
|
+
model: raw.model || '',
|
|
145
|
+
inputTokens: raw.tokens?.input || 0,
|
|
146
|
+
outputTokens: raw.tokens?.output || 0,
|
|
147
|
+
cacheReadTokens: raw.tokens?.cacheRead || 0,
|
|
148
|
+
cacheWriteTokens: raw.tokens?.cacheCreate || 0,
|
|
149
|
+
costUSD: raw.costUSD ?? null,
|
|
150
|
+
project: projectDir || '',
|
|
151
|
+
metadata: {
|
|
152
|
+
type: raw.type,
|
|
153
|
+
role: raw.role,
|
|
154
|
+
text: raw.text,
|
|
155
|
+
toolCalls: raw.toolCalls,
|
|
156
|
+
isSubagent: raw.isSubagent,
|
|
157
|
+
isSidechain: raw.isSidechain,
|
|
158
|
+
speed: raw.speed,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
_parseSubagentFiles(sessionDir) {
|
|
164
|
+
const subagentsDir = join(sessionDir, 'subagents');
|
|
165
|
+
const records = [];
|
|
166
|
+
try {
|
|
167
|
+
if (!statSync(subagentsDir).isDirectory()) return records;
|
|
168
|
+
} catch {
|
|
169
|
+
return records;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const files = readdirSync(subagentsDir).filter(f => f.endsWith('.jsonl'));
|
|
173
|
+
for (const file of files) {
|
|
174
|
+
try {
|
|
175
|
+
const content = readFileSync(join(subagentsDir, file), 'utf-8');
|
|
176
|
+
for (const line of content.split('\n')) {
|
|
177
|
+
const trimmed = line.trim();
|
|
178
|
+
if (!trimmed) continue;
|
|
179
|
+
try {
|
|
180
|
+
const obj = JSON.parse(trimmed);
|
|
181
|
+
if (obj.type === 'user' || obj.type === 'assistant') {
|
|
182
|
+
if (obj.isApiErrorMessage === true) continue;
|
|
183
|
+
records.push(this._normalizeRawRecord(obj));
|
|
184
|
+
}
|
|
185
|
+
} catch {}
|
|
186
|
+
}
|
|
187
|
+
} catch {}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const r of records) {
|
|
191
|
+
r.isSubagent = true;
|
|
192
|
+
}
|
|
193
|
+
return records;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 解析 sessions-index.json,从会话索引生成元数据记录
|
|
197
|
+
_parseSessionsIndex(projPath, projName) {
|
|
198
|
+
const indexPath = join(projPath, 'sessions-index.json');
|
|
199
|
+
if (!existsSync(indexPath)) return [];
|
|
200
|
+
|
|
201
|
+
const records = [];
|
|
202
|
+
try {
|
|
203
|
+
const raw = JSON.parse(readFileSync(indexPath, 'utf-8'));
|
|
204
|
+
const entries = raw?.entries || [];
|
|
205
|
+
for (const entry of entries) {
|
|
206
|
+
if (!entry.sessionId) continue;
|
|
207
|
+
// 检查对应的 .jsonl 是否还存在,存在则跳过(由主流程解析)
|
|
208
|
+
const jsonlPath = entry.fullPath || join(projPath, entry.sessionId + '.jsonl');
|
|
209
|
+
if (existsSync(jsonlPath)) continue;
|
|
210
|
+
|
|
211
|
+
// 从索引条目创建占位 user+assistant 记录,确保会话被统计
|
|
212
|
+
const ts = entry.created || entry.modified || '';
|
|
213
|
+
records.push({
|
|
214
|
+
type: 'user',
|
|
215
|
+
role: 'user',
|
|
216
|
+
timestamp: ts,
|
|
217
|
+
model: '',
|
|
218
|
+
text: entry.firstPrompt || entry.summary || '',
|
|
219
|
+
toolCalls: [],
|
|
220
|
+
sessionId: entry.sessionId,
|
|
221
|
+
cwd: entry.projectPath || '',
|
|
222
|
+
gitBranch: entry.gitBranch || '',
|
|
223
|
+
project: '',
|
|
224
|
+
tokens: { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 },
|
|
225
|
+
isSidechain: false,
|
|
226
|
+
isSubagent: false,
|
|
227
|
+
messageId: '',
|
|
228
|
+
requestId: '',
|
|
229
|
+
costUSD: null,
|
|
230
|
+
isApiError: false,
|
|
231
|
+
speed: 'standard',
|
|
232
|
+
_fromIndex: true,
|
|
233
|
+
});
|
|
234
|
+
if (entry.messageCount > 0) {
|
|
235
|
+
records.push({
|
|
236
|
+
type: 'assistant',
|
|
237
|
+
role: 'assistant',
|
|
238
|
+
timestamp: entry.modified || ts,
|
|
239
|
+
model: '',
|
|
240
|
+
text: '',
|
|
241
|
+
toolCalls: [],
|
|
242
|
+
sessionId: entry.sessionId,
|
|
243
|
+
cwd: entry.projectPath || '',
|
|
244
|
+
gitBranch: entry.gitBranch || '',
|
|
245
|
+
project: '',
|
|
246
|
+
tokens: { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 },
|
|
247
|
+
isSidechain: false,
|
|
248
|
+
isSubagent: false,
|
|
249
|
+
messageId: '',
|
|
250
|
+
requestId: '',
|
|
251
|
+
costUSD: null,
|
|
252
|
+
isApiError: false,
|
|
253
|
+
speed: 'standard',
|
|
254
|
+
_fromIndex: true,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} catch {}
|
|
259
|
+
return records;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
_normalizeRawRecord(obj) {
|
|
263
|
+
const msg = obj.message || {};
|
|
264
|
+
const content = msg.content || '';
|
|
265
|
+
let text = '';
|
|
266
|
+
let toolCalls = [];
|
|
267
|
+
|
|
268
|
+
if (typeof content === 'string') {
|
|
269
|
+
text = content;
|
|
270
|
+
} else if (Array.isArray(content)) {
|
|
271
|
+
for (const c of content) {
|
|
272
|
+
if (!c || typeof c !== 'object') continue;
|
|
273
|
+
if (c.type === 'text') text += (c.text || '');
|
|
274
|
+
if (c.type === 'tool_use') {
|
|
275
|
+
toolCalls.push({ name: c.name || '', input: c.input || {} });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
text = text.trim();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const usage = obj.usage || msg.usage || {};
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
type: obj.type,
|
|
285
|
+
role: msg.role || '',
|
|
286
|
+
timestamp: obj.timestamp || '',
|
|
287
|
+
model: msg.model || '',
|
|
288
|
+
text: text.trim(),
|
|
289
|
+
toolCalls,
|
|
290
|
+
sessionId: obj.sessionId || '',
|
|
291
|
+
cwd: obj.cwd || '',
|
|
292
|
+
gitBranch: obj.gitBranch || '',
|
|
293
|
+
project: '',
|
|
294
|
+
tokens: {
|
|
295
|
+
input: usage.input_tokens || 0,
|
|
296
|
+
output: usage.output_tokens || 0,
|
|
297
|
+
cacheCreate: usage.cache_creation_input_tokens || usage.cache_creation?.ephemeral_5m_input_tokens || 0,
|
|
298
|
+
cacheRead: usage.cache_read_input_tokens || 0,
|
|
299
|
+
},
|
|
300
|
+
isSidechain: obj.isSidechain || false,
|
|
301
|
+
isSubagent: false,
|
|
302
|
+
messageId: msg.id || '',
|
|
303
|
+
requestId: obj.requestId || '',
|
|
304
|
+
costUSD: obj.costUSD ?? null,
|
|
305
|
+
isApiError: obj.isApiErrorMessage || false,
|
|
306
|
+
speed: usage.speed || 'standard',
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async getVersion() {
|
|
311
|
+
try {
|
|
312
|
+
const { execSync } = await import('child_process');
|
|
313
|
+
const cmd = process.platform === 'win32' ? 'claude --version' : 'claude --version 2>/dev/null';
|
|
314
|
+
const out = execSync(cmd, { encoding: 'utf8', timeout: 5000 }).trim();
|
|
315
|
+
const m = out.match(/(\d+\.\d+\.\d+)/);
|
|
316
|
+
return m ? m[1] : out.split(' ')[0];
|
|
317
|
+
} catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|