lumencode 1.2.0 → 1.3.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,67 +1,69 @@
1
- /**
2
- * AI 工具日志解析器基类
3
- * 所有具体解析器必须继承此类并实现抽象方法
4
- */
5
- export class BaseParser {
6
- constructor() {
7
- if (this.constructor === BaseParser) {
8
- throw new Error('BaseParser 是抽象类,不能直接实例化');
9
- }
10
- }
11
-
12
- /**
13
- * 返回工具元信息
14
- * @returns {{name: string, displayName: string, defaultDir: string, envVar: string}}
15
- */
16
- getInfo() {
17
- throw new Error(`${this.constructor.name}.getInfo() 未实现`);
18
- }
19
-
20
- /**
21
- * 检测该工具的数据目录是否存在且有有效数据
22
- * @param {Object} config - 完整配置对象
23
- * @returns {Promise<boolean>}
24
- */
25
- async detect(config) {
26
- throw new Error(`${this.constructor.name}.detect() 未实现`);
27
- }
28
-
29
- /**
30
- * 解析日志文件,返回 UsageRecord[]
31
- * @param {Object} config - 完整配置对象
32
- * @param {Object} options - 解析选项
33
- * @returns {Promise<Array>}
34
- */
35
- async parse(config, options = {}) {
36
- throw new Error(`${this.constructor.name}.parse() 未实现`);
37
- }
38
-
39
- /**
40
- * 获取该工具的数据目录路径(从配置或环境变量)
41
- * @param {Object} config
42
- * @returns {string|null}
43
- */
44
- getDataDir(config) {
45
- const info = this.getInfo();
46
- const configKey = `${info.name}Dir`;
47
- if (config[configKey] && config[configKey] !== '') {
48
- return config[configKey];
49
- }
50
- const envVal = process.env[info.envVar];
51
- if (envVal) return envVal;
52
- const home = process.env.HOME || process.env.USERPROFILE;
53
- if (home) {
54
- return home + info.defaultDir.replace(/^~/, '');
55
- }
56
- return null;
57
- }
58
-
59
- /**
60
- * 获取工具版本号(子类可覆写)
61
- * @param {Object} config
62
- * @returns {Promise<string|null>}
63
- */
64
- async getVersion(config) {
65
- return null;
66
- }
67
- }
1
+ import { homedir } from 'os';
2
+
3
+ /**
4
+ * AI 工具日志解析器基类
5
+ * 所有具体解析器必须继承此类并实现抽象方法
6
+ */
7
+ export class BaseParser {
8
+ constructor() {
9
+ if (this.constructor === BaseParser) {
10
+ throw new Error('BaseParser 是抽象类,不能直接实例化');
11
+ }
12
+ }
13
+
14
+ /**
15
+ * 返回工具元信息
16
+ * @returns {{name: string, displayName: string, defaultDir: string, envVar: string}}
17
+ */
18
+ getInfo() {
19
+ throw new Error(`${this.constructor.name}.getInfo() 未实现`);
20
+ }
21
+
22
+ /**
23
+ * 检测该工具的数据目录是否存在且有有效数据
24
+ * @param {Object} config - 完整配置对象
25
+ * @returns {Promise<boolean>}
26
+ */
27
+ async detect(config) {
28
+ throw new Error(`${this.constructor.name}.detect() 未实现`);
29
+ }
30
+
31
+ /**
32
+ * 解析日志文件,返回 UsageRecord[]
33
+ * @param {Object} config - 完整配置对象
34
+ * @param {Object} options - 解析选项
35
+ * @returns {Promise<Array>}
36
+ */
37
+ async parse(config, options = {}) {
38
+ throw new Error(`${this.constructor.name}.parse() 未实现`);
39
+ }
40
+
41
+ /**
42
+ * 获取该工具的数据目录路径(从配置或环境变量)
43
+ * @param {Object} config
44
+ * @returns {string|null}
45
+ */
46
+ getDataDir(config) {
47
+ const info = this.getInfo();
48
+ const configKey = `${info.name}Dir`;
49
+ if (config[configKey] && config[configKey] !== '') {
50
+ return config[configKey];
51
+ }
52
+ const envVal = process.env[info.envVar];
53
+ if (envVal) return envVal;
54
+ const home = homedir();
55
+ if (home) {
56
+ return home + info.defaultDir.replace(/^~/, '');
57
+ }
58
+ return null;
59
+ }
60
+
61
+ /**
62
+ * 获取工具版本号(子类可覆写)
63
+ * @param {Object} config
64
+ * @returns {Promise<string|null>}
65
+ */
66
+ async getVersion(config) {
67
+ return null;
68
+ }
69
+ }
@@ -1,29 +1,34 @@
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
- }
1
+ import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
2
+ import { join, dirname, basename } from 'path';
3
+ import { execFileSync } from 'child_process';
4
+ import { BaseParser } from './base.js';
5
+ import { createUsageRecord } from '../models/usage-record.js';
6
+ import { getCachedFileRecords } from '../cache.js';
7
+ import { getProjectDisplayName } from '../aggregate.js';
8
+
9
+ const gitRootCache = new Map();
10
+
11
+ function normalizePath(value) {
12
+ return String(value || '').replace(/\\/g, '/').replace(/\/$/, '');
13
+ }
14
+
15
+ function resolveGitRoot(cwd) {
16
+ const normalized = normalizePath(cwd);
17
+ if (!normalized) return '';
18
+ if (gitRootCache.has(normalized)) return gitRootCache.get(normalized);
19
+ let root = normalized;
20
+ try {
21
+ root = normalizePath(execFileSync('git', ['rev-parse', '--show-toplevel'], {
22
+ cwd: normalized,
23
+ encoding: 'utf8',
24
+ stdio: ['ignore', 'pipe', 'ignore'],
25
+ }).trim());
26
+ } catch {
27
+ root = normalized;
28
+ }
29
+ gitRootCache.set(normalized, root);
30
+ return root;
31
+ }
27
32
 
28
33
  export class ClaudeParser extends BaseParser {
29
34
  getInfo() {
@@ -48,7 +53,7 @@ export class ClaudeParser extends BaseParser {
48
53
 
49
54
  async parse(config, options = {}) {
50
55
  const dir = this.getDataDir(config);
51
- const { excludeProjects = [], includeProjects = null } = options;
56
+ const { excludeProjects = [] } = options;
52
57
  const records = [];
53
58
 
54
59
  if (!dir) return records;
@@ -64,27 +69,15 @@ export class ClaudeParser extends BaseParser {
64
69
  return records;
65
70
  }
66
71
 
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;
72
+ // includeProjects 过滤交由外层 parseAllEnabledTools 按 basename 统一处理
73
+ // 不在内层按 encoded 路径精确匹配,避免路径编码差异导致 session 全部丢失
78
74
 
79
75
  for (const projDir of dirs) {
80
- if (encodedIncludes && !encodedIncludes.has(projDir)) continue;
81
-
82
76
  const projPath = join(projectsDir, projDir);
83
77
  const allEntries = readdirSync(projPath);
84
78
  const jsonlFiles = allEntries.filter(f => f.endsWith('.jsonl') && !f.includes('subagents'));
85
79
 
86
- // 优先使用 includeProject 原始路径(避免 decodeProjectName 的有损解码)
87
- const projName = (encodedToOriginal && encodedToOriginal.get(projDir)) || decodeProjectName(projDir);
80
+ const projName = getProjectDisplayName(projDir);
88
81
 
89
82
  // 并行读取所有 JSONL 文件(减少串行 IO 等待)
90
83
  const fileResults = await Promise.all(
@@ -97,14 +90,14 @@ export class ClaudeParser extends BaseParser {
97
90
  for (const r of fileRecords) {
98
91
  result.push(this._convertToUsageRecord(r, projName, sessionIdFromFile));
99
92
  }
100
- } catch {}
93
+ } catch (e) { console.warn(`[claude] 读取文件记录失败: ${filePath}`, e.message); }
101
94
  // 子 agent 日志
102
95
  try {
103
96
  const subRecords = this._parseSubagentFiles(dirname(filePath));
104
97
  for (const r of subRecords) {
105
98
  result.push(this._convertToUsageRecord(r, projName, sessionIdFromFile));
106
99
  }
107
- } catch {}
100
+ } catch (e) { console.warn(`[claude] 解析子agent失败: ${filePath}`, e.message); }
108
101
  return result;
109
102
  })
110
103
  );
@@ -128,7 +121,7 @@ export class ClaudeParser extends BaseParser {
128
121
  for (const r of subRecords) {
129
122
  records.push(this._convertToUsageRecord(r, projName, uuidDir));
130
123
  }
131
- } catch {}
124
+ } catch (e) { console.warn(`[claude] 扫描UUID子目录subagent失败: ${uuidDir}`, e.message); }
132
125
  }
133
126
  }
134
127
  }
@@ -136,8 +129,13 @@ export class ClaudeParser extends BaseParser {
136
129
  return records;
137
130
  }
138
131
 
139
- _convertToUsageRecord(raw, projectDir, fallbackSessionId = '') {
140
- return createUsageRecord({
132
+ _convertToUsageRecord(raw, projectDir, fallbackSessionId = '') {
133
+ // 优先将 cwd 归一到 Git 根目录,避免 monorepo 子目录会话与仓库提交失配
134
+ let projectName = projectDir || '';
135
+ if (raw.cwd) {
136
+ projectName = resolveGitRoot(raw.cwd) || projectName;
137
+ }
138
+ return createUsageRecord({
141
139
  timestamp: raw.timestamp || '',
142
140
  tool: 'claude',
143
141
  sessionId: raw.sessionId || fallbackSessionId || '',
@@ -147,7 +145,7 @@ export class ClaudeParser extends BaseParser {
147
145
  cacheReadTokens: raw.tokens?.cacheRead || 0,
148
146
  cacheWriteTokens: raw.tokens?.cacheCreate || 0,
149
147
  costUSD: raw.costUSD ?? null,
150
- project: projectDir || '',
148
+ project: projectName,
151
149
  metadata: {
152
150
  type: raw.type,
153
151
  role: raw.role,
@@ -182,9 +180,9 @@ export class ClaudeParser extends BaseParser {
182
180
  if (obj.isApiErrorMessage === true) continue;
183
181
  records.push(this._normalizeRawRecord(obj));
184
182
  }
185
- } catch {}
183
+ } catch (e) { /* JSONL 单行解析失败,跳过 */ }
186
184
  }
187
- } catch {}
185
+ } catch (e) { console.warn(`[claude] 读取subagent文件失败: ${file}`, e.message); }
188
186
  }
189
187
 
190
188
  for (const r of records) {
@@ -220,7 +218,7 @@ export class ClaudeParser extends BaseParser {
220
218
  sessionId: entry.sessionId,
221
219
  cwd: entry.projectPath || '',
222
220
  gitBranch: entry.gitBranch || '',
223
- project: '',
221
+ project: projName || '',
224
222
  tokens: { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 },
225
223
  isSidechain: false,
226
224
  isSubagent: false,
@@ -242,7 +240,7 @@ export class ClaudeParser extends BaseParser {
242
240
  sessionId: entry.sessionId,
243
241
  cwd: entry.projectPath || '',
244
242
  gitBranch: entry.gitBranch || '',
245
- project: '',
243
+ project: projName || '',
246
244
  tokens: { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 },
247
245
  isSidechain: false,
248
246
  isSubagent: false,
@@ -255,7 +253,7 @@ export class ClaudeParser extends BaseParser {
255
253
  });
256
254
  }
257
255
  }
258
- } catch {}
256
+ } catch (e) { console.warn(`[claude] 解析sessions-index失败: ${projPath}`, e.message); }
259
257
  return records;
260
258
  }
261
259
 
@@ -6,6 +6,7 @@ import { createUsageRecord } from '../models/usage-record.js';
6
6
  // 文件级解析缓存:基于 mtime
7
7
  const _codexFileCache = new Map();
8
8
  const CODEX_CACHE_MAX = 200;
9
+ const CODEX_MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
9
10
 
10
11
  function getCachedCodexParse(filePath, parseFn) {
11
12
  try {
@@ -97,22 +98,23 @@ export class CodexParser extends BaseParser {
97
98
  if (statSync(sessionsDir).isDirectory()) {
98
99
  files.push(...this._walkDir(sessionsDir));
99
100
  }
100
- } catch {}
101
+ } catch (e) { console.warn("[codex] parse error", e.message); }
101
102
 
102
103
  const archivedDir = join(dir, 'archived_sessions');
103
104
  try {
104
- if (statSync(archivedDir).isDirectory()) {
105
+ if (existsSync(archivedDir) && statSync(archivedDir).isDirectory()) {
105
106
  const archived = readdirSync(archivedDir).filter(f => f.endsWith('.jsonl'));
106
107
  for (const f of archived) {
107
108
  files.push(join(archivedDir, f));
108
109
  }
109
110
  }
110
- } catch {}
111
+ } catch (e) { console.warn("[codex] parse error", e.message); }
111
112
 
112
113
  return files;
113
114
  }
114
115
 
115
- _walkDir(dir) {
116
+ _walkDir(dir, depth = 0) {
117
+ if (depth > 10) return [];
116
118
  const results = [];
117
119
  try {
118
120
  const entries = readdirSync(dir);
@@ -121,13 +123,13 @@ export class CodexParser extends BaseParser {
121
123
  try {
122
124
  const stat = statSync(fullPath);
123
125
  if (stat.isDirectory()) {
124
- results.push(...this._walkDir(fullPath));
126
+ results.push(...this._walkDir(fullPath, depth + 1));
125
127
  } else if (entry.endsWith('.jsonl')) {
126
128
  results.push(fullPath);
127
129
  }
128
- } catch {}
130
+ } catch (e) { console.warn("[codex] parse error", e.message); }
129
131
  }
130
- } catch {}
132
+ } catch (e) { console.warn("[codex] parse error", e.message); }
131
133
  return results;
132
134
  }
133
135
 
@@ -160,6 +162,11 @@ export class CodexParser extends BaseParser {
160
162
  }
161
163
 
162
164
  _parseFile(filePath) {
165
+ const { size } = statSync(filePath);
166
+ if (size > CODEX_MAX_FILE_SIZE) {
167
+ console.warn(`[codex] 文件过大 (${(size / 1024 / 1024).toFixed(0)}MB),跳过: ${filePath}`);
168
+ return [];
169
+ }
163
170
  const content = readFileSync(filePath, 'utf-8');
164
171
  const lines = content.split('\n').filter(l => l.trim());
165
172
 
@@ -254,7 +261,7 @@ export class CodexParser extends BaseParser {
254
261
  }));
255
262
  }
256
263
  }
257
- } catch {}
264
+ } catch (e) { console.warn("[codex] parse error", e.message); }
258
265
  }
259
266
 
260
267
  return records;
@@ -294,6 +301,11 @@ export class CodexParser extends BaseParser {
294
301
  let rows = useCache ? _codexStateCache.rows : null;
295
302
 
296
303
  if (!rows) {
304
+ const dbStat = statSync(dbPath);
305
+ if (dbStat.size > CODEX_MAX_FILE_SIZE) {
306
+ console.warn(`[codex] state.db 过大 (${(dbStat.size / 1024 / 1024).toFixed(0)}MB),跳过`);
307
+ return records;
308
+ }
297
309
  const initSqlJs = (await import('sql.js')).default;
298
310
  const SQL = await initSqlJs();
299
311
  const dbBuf = readFileSync(dbPath);
@@ -343,7 +355,7 @@ export class CodexParser extends BaseParser {
343
355
  }));
344
356
  }
345
357
  }
346
- } catch {}
358
+ } catch (e) { console.warn("[codex] parse error", e.message); }
347
359
  return records;
348
360
  }
349
361