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.
@@ -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
- export class OpencodeParser extends BaseParser {
10
- getInfo() {
11
- return {
12
- name: 'opencode',
13
- displayName: 'OpenCode',
14
- defaultDir: '~/.local/share/opencode',
15
- envVar: 'OPENCODE_DATA_DIR',
16
- };
17
- }
18
-
19
- async detect(config) {
20
- const dir = this.getDataDir(config);
21
- if (!dir) return false;
22
- try {
23
- return existsSync(join(dir, 'opencode.db'));
24
- } catch {
25
- return false;
26
- }
27
- }
28
-
29
- async parse(config, options = {}) {
30
- const dir = this.getDataDir(config);
31
- const records = [];
32
- if (!dir) return records;
33
-
34
- const dbPath = join(dir, 'opencode.db');
35
- if (!existsSync(dbPath)) return records;
36
-
37
- // 检查文件大小,避免大文件导致 Array buffer allocation failed
38
- const fileSize = statSync(dbPath).size;
39
- if (fileSize > MAX_DB_SIZE) {
40
- console.warn(`OpenCode: opencode.db 过大 (${(fileSize / 1024 / 1024).toFixed(0)}MB),跳过解析`);
41
- return records;
42
- }
43
-
44
- try {
45
- const initSqlJs = (await import('sql.js')).default;
46
- const SQL = await initSqlJs();
47
- const dbBuf = readFileSync(dbPath);
48
- const db = new SQL.Database(dbBuf);
49
-
50
- // 读取 session -> project 映射
51
- const sessionMap = {};
52
- // 不同版本 schema 不同,探测可用列
53
- let sessCols;
54
- try {
55
- const colInfo = db.exec("PRAGMA table_info(session)");
56
- sessCols = colInfo[0] ? colInfo[0].values.map(r => r[1]) : [];
57
- } catch { sessCols = []; }
58
- const hasPath = sessCols.includes('path');
59
- const hasDir = sessCols.includes('directory');
60
- const sessSelect = hasDir
61
- ? `SELECT id, directory${hasPath ? ', path' : ''} FROM session`
62
- : 'SELECT id FROM session';
63
- try {
64
- const sessRows = db.exec(sessSelect);
65
- if (sessRows[0]) {
66
- for (const row of sessRows[0].values) {
67
- const id = row[0];
68
- const dir = hasDir ? (row[1] || '') : '';
69
- const p = hasPath ? (row[2] || '') : '';
70
- sessionMap[id] = (p || dir || '').replace(/\\/g, '/');
71
- }
72
- }
73
- } catch {}
74
-
75
- // 读取所有 message
76
- const msgRows = db.exec(
77
- "SELECT id, session_id, time_created, data FROM message ORDER BY time_created"
78
- );
79
- if (!msgRows[0]) { db.close(); return records; }
80
-
81
- // 计算 delta tokens(message 中 tokens 是累计值)
82
- let lastTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
83
-
84
- for (const [msgId, sessionId, timeCreated, dataStr] of msgRows[0].values) {
85
- let data;
86
- try { data = JSON.parse(dataStr); } catch { continue; }
87
-
88
- const role = data.role || '';
89
- const timestamp = new Date(timeCreated).toISOString();
90
- const project = sessionMap[sessionId] || '';
91
- const model = data.modelID || data.model?.modelID || '';
92
-
93
- // User messages for scenario classification
94
- if (role === 'user') {
95
- const text = this._extractUserText(db, msgId);
96
- if (text) {
97
- records.push(createUsageRecord({
98
- timestamp,
99
- tool: 'opencode',
100
- sessionId: sessionId || '',
101
- model: '',
102
- inputTokens: 0,
103
- outputTokens: 0,
104
- project,
105
- metadata: { type: 'user', text },
106
- }));
107
- }
108
- continue;
109
- }
110
-
111
- // Assistant messages with token usage
112
- if (role === 'assistant' && data.tokens) {
113
- const t = data.tokens;
114
- const current = {
115
- input: t.input || 0,
116
- output: t.output || 0,
117
- cacheRead: t.cache?.read || 0,
118
- cacheWrite: t.cache?.write || 0,
119
- };
120
-
121
- const delta = {
122
- input: Math.max(0, current.input - lastTokens.input),
123
- output: Math.max(0, current.output - lastTokens.output),
124
- cacheRead: Math.max(0, current.cacheRead - lastTokens.cacheRead),
125
- cacheWrite: Math.max(0, current.cacheWrite - lastTokens.cacheWrite),
126
- };
127
- lastTokens = current;
128
-
129
- // Collect tool calls from parts
130
- const toolCalls = this._extractToolCalls(db, msgId);
131
-
132
- if (delta.input > 0 || delta.output > 0) {
133
- records.push(createUsageRecord({
134
- timestamp,
135
- tool: 'opencode',
136
- sessionId: sessionId || '',
137
- model,
138
- inputTokens: delta.input,
139
- outputTokens: delta.output,
140
- cacheReadTokens: delta.cacheRead,
141
- cacheWriteTokens: delta.cacheWrite,
142
- costUSD: data.cost ?? null,
143
- project,
144
- metadata: {
145
- type: 'assistant',
146
- toolCalls,
147
- reasoningOutputTokens: t.reasoning || 0,
148
- },
149
- }));
150
- }
151
- }
152
- }
153
-
154
- db.close();
155
- } catch (err) {
156
- console.warn('OpenCode parse error:', err.message);
157
- }
158
-
159
- return records;
160
- }
161
-
162
- _extractUserText(db, msgId) {
163
- try {
164
- const rows = db.exec(
165
- "SELECT data FROM part WHERE message_id = ? AND json_extract(data, '$.type') = 'text'",
166
- [msgId]
167
- );
168
- if (rows[0]?.values?.length) {
169
- const parts = [];
170
- for (const [dataStr] of rows[0].values) {
171
- const d = JSON.parse(dataStr);
172
- if (d.text) parts.push(d.text);
173
- }
174
- return parts.join(' ').trim();
175
- }
176
- } catch {}
177
- return '';
178
- }
179
-
180
- _extractToolCalls(db, msgId) {
181
- const calls = [];
182
- try {
183
- const rows = db.exec(
184
- "SELECT data FROM part WHERE message_id = ? AND json_extract(data, '$.type') = 'tool'",
185
- [msgId]
186
- );
187
- if (rows[0]?.values) {
188
- for (const [dataStr] of rows[0].values) {
189
- const d = JSON.parse(dataStr);
190
- const name = d.name || d.tool || 'unknown';
191
- calls.push({ name });
192
- }
193
- }
194
- } catch {}
195
- return calls;
196
- }
197
-
198
- async getVersion(config) {
199
- const dir = this.getDataDir(config);
200
- if (!dir) return null;
201
- const dbPath = join(dir, 'opencode.db');
202
- if (!existsSync(dbPath)) return null;
203
- try {
204
- if (statSync(dbPath).size > MAX_DB_SIZE) return null;
205
- const initSqlJs = (await import('sql.js')).default;
206
- const SQL = await initSqlJs();
207
- const dbBuf = readFileSync(dbPath);
208
- const db = new SQL.Database(dbBuf);
209
- const rows = db.exec("SELECT value FROM settings WHERE key = 'version'");
210
- db.close();
211
- return rows[0]?.values?.[0]?.[0] || null;
212
- } catch {
213
- return null;
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
+ }
@@ -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
- if (r.type === 'assistant' && !r.tool) return true;
34
- return false;
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
+ }