lumencode 0.4.3

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 ADDED
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env node
2
+ import { loadConfig, initConfig, getConfigPath } from './lib/config.js';
3
+ import { collectAllRecords, computeUsageStats, filterRecordsByPeriod, normalizeProjectPath, computeTrendData, computePrevPeriodRange, groupBySessions } from './lib/aggregate.js';
4
+ import { getGitStatsForMultipleReposAsync, invalidateGitCache, finalizeGitStats, computeCommitTypes, computeFileHotspots } from './lib/git.js';
5
+ import { invalidateFileCache } from './lib/cache.js';
6
+ import { generateReport, generateWorkReport } from './lib/report.js';
7
+ import { startServer } from './lib/server.js';
8
+ import { detectClaudeDir, deriveProjectPaths } from './lib/parser.js';
9
+ import { identifyBillingBlocks } from './lib/blocks.js';
10
+ import { registerParser, parseAllEnabledTools } from './lib/parsers/index.js';
11
+ import { ClaudeParser } from './lib/parsers/claude.js';
12
+ import { CodexParser } from './lib/parsers/codex.js';
13
+ import { OpencodeParser } from './lib/parsers/opencode.js';
14
+ import { initPricing, preloadUnknownPricing } from './lib/pricing-loader.js';
15
+
16
+ // 注册所有解析器
17
+ registerParser(ClaudeParser);
18
+ registerParser(CodexParser);
19
+ registerParser(OpencodeParser);
20
+
21
+ const args = process.argv.slice(2);
22
+ const command = args[0];
23
+
24
+ function loadCliConfig() {
25
+ let config = loadConfig();
26
+
27
+ // 零配置:自动检测 claudeDir
28
+ if (!config.claudeDir || config.claudeDir === '') {
29
+ config.claudeDir = detectClaudeDir() || config.claudeDir;
30
+ }
31
+
32
+ // 零配置:自动推导项目路径(从 cwd 字段)
33
+ if ((!config.repos || config.repos.length === 0) && config.claudeDir) {
34
+ try {
35
+ const derived = deriveProjectPaths(config.claudeDir, config.excludeProjects || []);
36
+ if (derived.length > 0) {
37
+ config._autoRepos = derived;
38
+ }
39
+ } catch {}
40
+ }
41
+
42
+ // 日期参数
43
+ let dateArg = new Date().toISOString().slice(0, 10);
44
+ for (let i = 2; i < args.length; i++) {
45
+ if (!args[i].startsWith('--')) {
46
+ dateArg = args[i];
47
+ break;
48
+ }
49
+ }
50
+
51
+ // --projects 参数
52
+ let includeProjects = null;
53
+ const projectsIdx = args.indexOf('--projects');
54
+ if (projectsIdx !== -1 && args[projectsIdx + 1]) {
55
+ includeProjects = args[projectsIdx + 1].split(',').map(p => p.trim());
56
+ }
57
+
58
+ // 推导 includeProjects
59
+ let effectiveIncludeProjects = includeProjects;
60
+ if (!effectiveIncludeProjects && config.repos && config.repos.length > 0) {
61
+ effectiveIncludeProjects = config.repos.map(r => normalizeProjectPath(r));
62
+ } else if (!effectiveIncludeProjects && config._autoRepos && config._autoRepos.length > 0) {
63
+ effectiveIncludeProjects = config._autoRepos.map(r => normalizeProjectPath(r));
64
+ }
65
+
66
+ // 自动推导的 repos 也用于 Git 统计
67
+ if ((!config.repos || config.repos.length === 0) && config._autoRepos) {
68
+ config.repos = config._autoRepos;
69
+ }
70
+
71
+ const configPath = getConfigPath();
72
+ return { config, dateArg, effectiveIncludeProjects, configPath };
73
+ }
74
+
75
+ async function buildReportData(period, dateArg, config, effectiveIncludeProjects, tool = 'all') {
76
+ // 使用新的多工具解析入口
77
+ const { records, toolBreakdown } = await parseAllEnabledTools(config, {
78
+ excludeProjects: config.excludeProjects,
79
+ includeProjects: effectiveIncludeProjects,
80
+ });
81
+
82
+ if (records.length === 0) {
83
+ return null;
84
+ }
85
+
86
+ // 预加载未知模型定价
87
+ await preloadUnknownPricing(records);
88
+
89
+ // 按工具过滤
90
+ const toolRecords = tool !== 'all' ? records.filter(r => r.tool === tool) : records;
91
+ if (toolRecords.length === 0) {
92
+ return null;
93
+ }
94
+
95
+ // 其余逻辑保持不变
96
+ const { filtered, start, end } = filterRecordsByPeriod(toolRecords, period, dateArg);
97
+ const usageStats = computeUsageStats(filtered, config.scenarioKeywords, config.costMode);
98
+ const sessions = groupBySessions(filtered);
99
+
100
+ let gitStats = null;
101
+ if (config.repos && config.repos.length > 0) {
102
+ // 按工具过滤后,只统计该工具覆盖的项目对应的 repos
103
+ const coveredBases = new Set(filtered.map(r => {
104
+ const p = r.project || '';
105
+ return p.replace(/\\/g, '/').replace(/\/$/, '').split('/').pop();
106
+ }).filter(Boolean));
107
+ const toolRepos = config.repos.filter(r => coveredBases.has(r.replace(/\\/g, '/').replace(/\/$/, '').split('/').pop()));
108
+ if (toolRepos.length > 0) {
109
+ // 扩展 git 查询窗口 +2 天,以匹配 session 跨天的延迟提交
110
+ const extendedEnd = new Date(end);
111
+ extendedEnd.setDate(extendedEnd.getDate() + 2);
112
+ const extendedEndStr = extendedEnd.toISOString().slice(0, 10) + 'T23:59:59';
113
+ gitStats = await getGitStatsForMultipleReposAsync(toolRepos, start, extendedEndStr);
114
+ gitStats = finalizeGitStats(gitStats, sessions);
115
+ // 归因已完成,过滤 commitList 到原始窗口,重算基础统计
116
+ if (gitStats.commitList) {
117
+ const windowStart = start;
118
+ const windowEnd = end + 'T23:59:59';
119
+ const inWindow = gitStats.commitList.filter(c => (c.date || '') >= windowStart && (c.date || '') <= windowEnd);
120
+ gitStats.commits = inWindow.length;
121
+ gitStats.linesAdded = inWindow.reduce((s, c) => s + (c.linesAdded || 0), 0);
122
+ gitStats.linesDeleted = inWindow.reduce((s, c) => s + (c.linesDeleted || 0), 0);
123
+ gitStats.filesChanged = new Set(inWindow.flatMap(c => (c.files || []).map(f => f.path))).size;
124
+ // commitList 保留全部(含跨天),前端 drill-down 需要
125
+ // 但提交类型和热点只基于窗口内
126
+ gitStats.commitTypes = computeCommitTypes(inWindow);
127
+ gitStats.fileHotspots = computeFileHotspots(inWindow, 10);
128
+ }
129
+ }
130
+ }
131
+
132
+ const trendData = computeTrendData(toolRecords, period, dateArg);
133
+
134
+ // Previous period stats
135
+ const prevRange = computePrevPeriodRange(period, dateArg);
136
+ const prevFiltered = toolRecords.filter(r => {
137
+ if (!r.timestamp) return false;
138
+ const date = r.timestamp.slice(0, 10);
139
+ return date >= prevRange.start && date <= prevRange.end;
140
+ });
141
+ const prevStats = prevFiltered.length > 0 ? computeUsageStats(prevFiltered, config.scenarioKeywords, config.costMode) : null;
142
+
143
+ const slimSessions = sessions.map(s => ({
144
+ id: s.id,
145
+ project: s.project,
146
+ startTime: s.startTime,
147
+ endTime: s.endTime,
148
+ requests: s.requests,
149
+ commits: s.commits || [],
150
+ }));
151
+
152
+ const billingBlocks = identifyBillingBlocks(filtered, config.blockQuota ? 5 : 5, config.costMode);
153
+
154
+ const reposConfigured = !!(config.repos && config.repos.length > 0);
155
+ return { usageStats, gitStats, reposConfigured, sessions: slimSessions, start, end, trendData, prevStats, billingBlocks, toolBreakdown };
156
+ }
157
+
158
+ if (!command || command === 'help' || command === '--help') {
159
+ console.log(`
160
+ 用法: lumencode <命令> [周期] [日期] [选项]
161
+
162
+ 命令:
163
+ report 生成使用报告(默认命令)
164
+ serve 启动 Web 服务(默认端口 4567)
165
+ init 初始化配置文件
166
+ help 显示帮助信息
167
+
168
+ 周期:
169
+ daily 日报(默认)
170
+ weekly 周报
171
+ monthly 月报
172
+
173
+ 日期:
174
+ 指定报告的参考日期,格式 YYYY-MM-DD(默认今天)
175
+
176
+ 选项:
177
+ --projects 只统计指定项目,多个项目用逗号分隔
178
+ --work 输出工作汇报版本(Markdown 格式)
179
+ --brief 配合 --work 使用,输出简报(3-5 句话)
180
+
181
+ 示例:
182
+ lumencode report daily 2026-05-15
183
+ lumencode report daily --projects D://fzwork
184
+ lumencode report weekly 2026-05-15 --projects D://fzwork,E://play/idea
185
+ lumencode report daily --work
186
+ lumencode report daily --work --brief
187
+ lumencode serve
188
+ lumencode init
189
+
190
+ 零配置:
191
+ 首次运行自动检测 Claude 日志目录和项目路径,无需手动配置。
192
+ 如需自定义,运行 lumencode init 或在 Web 模式下点击设置。
193
+ `);
194
+ process.exit(0);
195
+ }
196
+
197
+ if (command === 'init') {
198
+ initConfig(args[1]);
199
+ process.exit(0);
200
+ }
201
+
202
+ if (command === 'serve') {
203
+ const { config, effectiveIncludeProjects, configPath } = loadCliConfig();
204
+ startServer(config, effectiveIncludeProjects, buildReportData, configPath);
205
+ } else {
206
+ // report command (default)
207
+ const period = args[1] || 'daily';
208
+ const isWorkMode = args.includes('--work');
209
+ const isBrief = args.includes('--brief');
210
+ const { config, dateArg, effectiveIncludeProjects } = loadCliConfig();
211
+
212
+ console.log('正在扫描 AI 编码助手日志...');
213
+ const { records, toolBreakdown } = await parseAllEnabledTools(config, {
214
+ excludeProjects: config.excludeProjects,
215
+ includeProjects: effectiveIncludeProjects,
216
+ });
217
+
218
+ // 预加载未知模型定价
219
+ await preloadUnknownPricing(records);
220
+
221
+ if (records.length === 0) {
222
+ console.log('未找到任何会话记录。可能原因:');
223
+ console.log(` 1. 日志目录不存在或路径错误`);
224
+ console.log(` 2. 该目录下没有可解析的数据`);
225
+ console.log('请运行 lumencode init 创建配置文件,或在 Web 模式下点击设置按钮配置。');
226
+ process.exit(1);
227
+ }
228
+
229
+ const projectSet = new Set(records.map(r => r.project).filter(Boolean));
230
+ const toolNames = Object.keys(toolBreakdown || {});
231
+ console.log(`已加载 ${records.length} 条记录,${projectSet.size} 个项目,工具: ${toolNames.join(', ')}`);
232
+
233
+ const { filtered, start, end } = filterRecordsByPeriod(records, period, dateArg);
234
+ console.log(`筛选 ${period} 数据: ${start} ~ ${end},共 ${filtered.length} 条记录`);
235
+
236
+ const usageStats = computeUsageStats(filtered, config.scenarioKeywords, config.costMode);
237
+ usageStats.toolBreakdown = toolBreakdown;
238
+
239
+ let gitStats = null;
240
+ if (config.repos && config.repos.length > 0) {
241
+ console.log('正在统计 Git 指标...');
242
+ const sessions = groupBySessions(filtered);
243
+ gitStats = await getGitStatsForMultipleReposAsync(config.repos, start, end + 'T23:59:59');
244
+ gitStats = finalizeGitStats(gitStats, sessions);
245
+ }
246
+
247
+ // 上一周期数据(用于工作汇报环比)
248
+ const prevRange = computePrevPeriodRange(period, dateArg);
249
+ const prevFiltered = records.filter(r => {
250
+ if (!r.timestamp) return false;
251
+ const date = r.timestamp.slice(0, 10);
252
+ return date >= prevRange.start && date <= prevRange.end;
253
+ });
254
+ const prevStats = prevFiltered.length > 0 ? computeUsageStats(prevFiltered, config.scenarioKeywords, config.costMode) : null;
255
+
256
+ const report = isWorkMode
257
+ ? generateWorkReport(usageStats, gitStats, period, start, end, prevStats, { level: isBrief ? 'brief' : 'detailed' })
258
+ : generateReport(usageStats, gitStats, period, start, end);
259
+ console.log(report);
260
+ }
261
+
262
+ function fmtNum(n) {
263
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
264
+ if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K';
265
+ return String(n);
266
+ }