lumencode 0.4.4 → 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/README.md +54 -38
- package/index.js +128 -48
- package/lib/aggregate.js +40 -6
- package/lib/cache.js +8 -0
- package/lib/git.js +75 -20
- 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 +53 -16
- package/lib/server.js +191 -30
- package/package.json +1 -1
- package/public/api.js +6 -0
- package/public/app.js +827 -636
- package/public/charts.js +285 -95
- package/public/config.js +22 -21
- package/public/git-insights.js +39 -113
- package/public/index.html +728 -341
- package/public/style.css +829 -1701
- package/public/ui-state.js +8 -67
- package/public/utils.js +10 -0
- package/public/work-report.js +1 -22
package/lib/parsers/opencode.js
CHANGED
|
@@ -1,216 +1,236 @@
|
|
|
1
|
-
import { existsSync, readFileSync, statSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import { BaseParser } from './base.js';
|
|
4
|
-
import { createUsageRecord } from '../models/usage-record.js';
|
|
5
|
-
|
|
6
|
-
// SQLite 数据库文件大小上限(500MB),超过则跳过解析避免 OOM
|
|
7
|
-
const MAX_DB_SIZE = 500 * 1024 * 1024;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
}
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { BaseParser } from './base.js';
|
|
4
|
+
import { createUsageRecord } from '../models/usage-record.js';
|
|
5
|
+
|
|
6
|
+
// SQLite 数据库文件大小上限(500MB),超过则跳过解析避免 OOM
|
|
7
|
+
const MAX_DB_SIZE = 500 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
// 增量解析缓存:基于 opencode.db 的 mtime
|
|
10
|
+
const _opencodeCache = {
|
|
11
|
+
dbPath: '',
|
|
12
|
+
mtimeMs: 0,
|
|
13
|
+
size: 0,
|
|
14
|
+
records: null,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export class OpencodeParser extends BaseParser {
|
|
18
|
+
getInfo() {
|
|
19
|
+
return {
|
|
20
|
+
name: 'opencode',
|
|
21
|
+
displayName: 'OpenCode',
|
|
22
|
+
defaultDir: '~/.local/share/opencode',
|
|
23
|
+
envVar: 'OPENCODE_DATA_DIR',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async detect(config) {
|
|
28
|
+
const dir = this.getDataDir(config);
|
|
29
|
+
if (!dir) return false;
|
|
30
|
+
try {
|
|
31
|
+
return existsSync(join(dir, 'opencode.db'));
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async parse(config, options = {}) {
|
|
38
|
+
const dir = this.getDataDir(config);
|
|
39
|
+
const records = [];
|
|
40
|
+
if (!dir) return records;
|
|
41
|
+
|
|
42
|
+
const dbPath = join(dir, 'opencode.db');
|
|
43
|
+
if (!existsSync(dbPath)) return records;
|
|
44
|
+
|
|
45
|
+
// 检查文件大小,避免大文件导致 Array buffer allocation failed
|
|
46
|
+
const fileSize = statSync(dbPath).size;
|
|
47
|
+
if (fileSize > MAX_DB_SIZE) {
|
|
48
|
+
console.warn(`OpenCode: opencode.db 过大 (${(fileSize / 1024 / 1024).toFixed(0)}MB),跳过解析`);
|
|
49
|
+
return records;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 增量缓存:如果 db 文件未变化,直接返回缓存
|
|
53
|
+
const { mtimeMs } = statSync(dbPath);
|
|
54
|
+
if (_opencodeCache.dbPath === dbPath && _opencodeCache.mtimeMs === mtimeMs && _opencodeCache.size === fileSize && _opencodeCache.records) {
|
|
55
|
+
return _opencodeCache.records;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const initSqlJs = (await import('sql.js')).default;
|
|
60
|
+
const SQL = await initSqlJs();
|
|
61
|
+
const dbBuf = readFileSync(dbPath);
|
|
62
|
+
const db = new SQL.Database(dbBuf);
|
|
63
|
+
|
|
64
|
+
// 读取 session -> project 映射
|
|
65
|
+
const sessionMap = {};
|
|
66
|
+
// 不同版本 schema 不同,探测可用列
|
|
67
|
+
let sessCols;
|
|
68
|
+
try {
|
|
69
|
+
const colInfo = db.exec("PRAGMA table_info(session)");
|
|
70
|
+
sessCols = colInfo[0] ? colInfo[0].values.map(r => r[1]) : [];
|
|
71
|
+
} catch { sessCols = []; }
|
|
72
|
+
const hasPath = sessCols.includes('path');
|
|
73
|
+
const hasDir = sessCols.includes('directory');
|
|
74
|
+
const sessSelect = hasDir
|
|
75
|
+
? `SELECT id, directory${hasPath ? ', path' : ''} FROM session`
|
|
76
|
+
: 'SELECT id FROM session';
|
|
77
|
+
try {
|
|
78
|
+
const sessRows = db.exec(sessSelect);
|
|
79
|
+
if (sessRows[0]) {
|
|
80
|
+
for (const row of sessRows[0].values) {
|
|
81
|
+
const id = row[0];
|
|
82
|
+
const dir = hasDir ? (row[1] || '') : '';
|
|
83
|
+
const p = hasPath ? (row[2] || '') : '';
|
|
84
|
+
sessionMap[id] = (p || dir || '').replace(/\\/g, '/');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch {}
|
|
88
|
+
|
|
89
|
+
// 读取所有 message
|
|
90
|
+
const msgRows = db.exec(
|
|
91
|
+
"SELECT id, session_id, time_created, data FROM message ORDER BY time_created"
|
|
92
|
+
);
|
|
93
|
+
if (!msgRows[0]) { db.close(); return records; }
|
|
94
|
+
|
|
95
|
+
// 计算 delta tokens(message 中 tokens 是累计值)
|
|
96
|
+
let lastTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
97
|
+
|
|
98
|
+
for (const [msgId, sessionId, timeCreated, dataStr] of msgRows[0].values) {
|
|
99
|
+
let data;
|
|
100
|
+
try { data = JSON.parse(dataStr); } catch { continue; }
|
|
101
|
+
|
|
102
|
+
const role = data.role || '';
|
|
103
|
+
const timestamp = new Date(timeCreated).toISOString();
|
|
104
|
+
const project = sessionMap[sessionId] || '';
|
|
105
|
+
const model = data.modelID || data.model?.modelID || '';
|
|
106
|
+
|
|
107
|
+
// User messages for scenario classification
|
|
108
|
+
if (role === 'user') {
|
|
109
|
+
const text = this._extractUserText(db, msgId);
|
|
110
|
+
if (text) {
|
|
111
|
+
records.push(createUsageRecord({
|
|
112
|
+
timestamp,
|
|
113
|
+
tool: 'opencode',
|
|
114
|
+
sessionId: sessionId || '',
|
|
115
|
+
model: '',
|
|
116
|
+
inputTokens: 0,
|
|
117
|
+
outputTokens: 0,
|
|
118
|
+
project,
|
|
119
|
+
metadata: { type: 'user', text },
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Assistant messages with token usage
|
|
126
|
+
if (role === 'assistant' && data.tokens) {
|
|
127
|
+
const t = data.tokens;
|
|
128
|
+
const current = {
|
|
129
|
+
input: t.input || 0,
|
|
130
|
+
output: t.output || 0,
|
|
131
|
+
cacheRead: t.cache?.read || 0,
|
|
132
|
+
cacheWrite: t.cache?.write || 0,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const delta = {
|
|
136
|
+
input: Math.max(0, current.input - lastTokens.input),
|
|
137
|
+
output: Math.max(0, current.output - lastTokens.output),
|
|
138
|
+
cacheRead: Math.max(0, current.cacheRead - lastTokens.cacheRead),
|
|
139
|
+
cacheWrite: Math.max(0, current.cacheWrite - lastTokens.cacheWrite),
|
|
140
|
+
};
|
|
141
|
+
lastTokens = current;
|
|
142
|
+
|
|
143
|
+
// Collect tool calls from parts
|
|
144
|
+
const toolCalls = this._extractToolCalls(db, msgId);
|
|
145
|
+
|
|
146
|
+
if (delta.input > 0 || delta.output > 0) {
|
|
147
|
+
records.push(createUsageRecord({
|
|
148
|
+
timestamp,
|
|
149
|
+
tool: 'opencode',
|
|
150
|
+
sessionId: sessionId || '',
|
|
151
|
+
model,
|
|
152
|
+
inputTokens: delta.input,
|
|
153
|
+
outputTokens: delta.output,
|
|
154
|
+
cacheReadTokens: delta.cacheRead,
|
|
155
|
+
cacheWriteTokens: delta.cacheWrite,
|
|
156
|
+
costUSD: data.cost ?? null,
|
|
157
|
+
project,
|
|
158
|
+
metadata: {
|
|
159
|
+
type: 'assistant',
|
|
160
|
+
toolCalls,
|
|
161
|
+
reasoningOutputTokens: t.reasoning || 0,
|
|
162
|
+
},
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
db.close();
|
|
169
|
+
|
|
170
|
+
// 更新增量缓存
|
|
171
|
+
_opencodeCache.dbPath = dbPath;
|
|
172
|
+
_opencodeCache.mtimeMs = mtimeMs;
|
|
173
|
+
_opencodeCache.size = fileSize;
|
|
174
|
+
_opencodeCache.records = records;
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.warn('OpenCode parse error:', err.message);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return records;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
_extractUserText(db, msgId) {
|
|
183
|
+
try {
|
|
184
|
+
const rows = db.exec(
|
|
185
|
+
"SELECT data FROM part WHERE message_id = ? AND json_extract(data, '$.type') = 'text'",
|
|
186
|
+
[msgId]
|
|
187
|
+
);
|
|
188
|
+
if (rows[0]?.values?.length) {
|
|
189
|
+
const parts = [];
|
|
190
|
+
for (const [dataStr] of rows[0].values) {
|
|
191
|
+
const d = JSON.parse(dataStr);
|
|
192
|
+
if (d.text) parts.push(d.text);
|
|
193
|
+
}
|
|
194
|
+
return parts.join(' ').trim();
|
|
195
|
+
}
|
|
196
|
+
} catch {}
|
|
197
|
+
return '';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_extractToolCalls(db, msgId) {
|
|
201
|
+
const calls = [];
|
|
202
|
+
try {
|
|
203
|
+
const rows = db.exec(
|
|
204
|
+
"SELECT data FROM part WHERE message_id = ? AND json_extract(data, '$.type') = 'tool'",
|
|
205
|
+
[msgId]
|
|
206
|
+
);
|
|
207
|
+
if (rows[0]?.values) {
|
|
208
|
+
for (const [dataStr] of rows[0].values) {
|
|
209
|
+
const d = JSON.parse(dataStr);
|
|
210
|
+
const name = d.name || d.tool || 'unknown';
|
|
211
|
+
calls.push({ name });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} catch {}
|
|
215
|
+
return calls;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async getVersion(config) {
|
|
219
|
+
const dir = this.getDataDir(config);
|
|
220
|
+
if (!dir) return null;
|
|
221
|
+
const dbPath = join(dir, 'opencode.db');
|
|
222
|
+
if (!existsSync(dbPath)) return null;
|
|
223
|
+
try {
|
|
224
|
+
if (statSync(dbPath).size > MAX_DB_SIZE) return null;
|
|
225
|
+
const initSqlJs = (await import('sql.js')).default;
|
|
226
|
+
const SQL = await initSqlJs();
|
|
227
|
+
const dbBuf = readFileSync(dbPath);
|
|
228
|
+
const db = new SQL.Database(dbBuf);
|
|
229
|
+
const rows = db.exec("SELECT value FROM settings WHERE key = 'version'");
|
|
230
|
+
db.close();
|
|
231
|
+
return rows[0]?.values?.[0]?.[0] || null;
|
|
232
|
+
} catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
package/lib/record-utils.js
CHANGED
|
@@ -1,35 +1,36 @@
|
|
|
1
|
-
// UsageRecord 兼容辅助函数
|
|
2
|
-
// 统一处理新格式(inputTokens/outputTokens)和旧格式(tokens.input/tokens.output)
|
|
3
|
-
|
|
4
|
-
export function getInputTokens(r) {
|
|
5
|
-
if (r.inputTokens !== undefined) return r.inputTokens;
|
|
6
|
-
return r.tokens?.input || 0;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function getOutputTokens(r) {
|
|
10
|
-
if (r.outputTokens !== undefined) return r.outputTokens;
|
|
11
|
-
return r.tokens?.output || 0;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function getCacheRead(r) {
|
|
15
|
-
if (r.cacheReadTokens !== undefined) return r.cacheReadTokens;
|
|
16
|
-
return r.tokens?.cacheRead || 0;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function getCacheCreate(r) {
|
|
20
|
-
if (r.cacheWriteTokens !== undefined) return r.cacheWriteTokens;
|
|
21
|
-
return r.tokens?.cacheCreate || 0;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function getModel(r) {
|
|
25
|
-
return r.model || '';
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function isAssistantRecord(r) {
|
|
29
|
-
if (r.metadata?.type === 'assistant') return true;
|
|
30
|
-
if (r.metadata?.type === 'user') return false;
|
|
31
|
-
if (r.tool === 'codex') return true;
|
|
32
|
-
if (r.tool === 'opencode' && r.metadata?.role !== 'user') return true;
|
|
33
|
-
|
|
34
|
-
return
|
|
35
|
-
|
|
1
|
+
// UsageRecord 兼容辅助函数
|
|
2
|
+
// 统一处理新格式(inputTokens/outputTokens)和旧格式(tokens.input/tokens.output)
|
|
3
|
+
|
|
4
|
+
export function getInputTokens(r) {
|
|
5
|
+
if (r.inputTokens !== undefined) return r.inputTokens;
|
|
6
|
+
return r.tokens?.input || 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getOutputTokens(r) {
|
|
10
|
+
if (r.outputTokens !== undefined) return r.outputTokens;
|
|
11
|
+
return r.tokens?.output || 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getCacheRead(r) {
|
|
15
|
+
if (r.cacheReadTokens !== undefined) return r.cacheReadTokens;
|
|
16
|
+
return r.tokens?.cacheRead || 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getCacheCreate(r) {
|
|
20
|
+
if (r.cacheWriteTokens !== undefined) return r.cacheWriteTokens;
|
|
21
|
+
return r.tokens?.cacheCreate || 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getModel(r) {
|
|
25
|
+
return r.model || '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isAssistantRecord(r) {
|
|
29
|
+
if (r.metadata?.type === 'assistant') return true;
|
|
30
|
+
if (r.metadata?.type === 'user') return false;
|
|
31
|
+
if (r.tool === 'codex') return true;
|
|
32
|
+
if (r.tool === 'opencode' && r.metadata?.role !== 'user') return true;
|
|
33
|
+
// 兼容 Claude Code 新版日志:type 可能统一为 'user',用 role 区分 user/assistant
|
|
34
|
+
if (!r.tool && (r.type === 'assistant' || r.role === 'assistant')) return true;
|
|
35
|
+
return false;
|
|
36
|
+
}
|