lumencode 1.2.0 → 1.3.1

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 CHANGED
@@ -11,7 +11,7 @@
11
11
  </p>
12
12
 
13
13
  <p align="center">
14
- 支持 <b>Claude Code · Codex · OpenCode</b> 三大 AI 编码工具 · 600+ 模型定价 · AI 贡献度归因 · 按项目独立汇报 · 自定义时间范围 · 一键飞书/钉钉周报
14
+ <b>AI 编码助手使用分析</b> —— 精确到每一行代码的 AI 归因 · 三工具统一 · 智能周报生成
15
15
  </p>
16
16
 
17
17
  <p align="center">
@@ -30,12 +30,12 @@
30
30
 
31
31
  | 场景 | 用 lumencode 解决 |
32
32
  |------|----------------------|
33
- | **写周报** | 选周报 点「工作汇报 复制」→ 粘贴飞书/钉钉。**3 秒搞定。** |
34
- | **证明 AI ROI** | 「67% 提交有 AI 参与,AI 辅助新增 4,200 行,费用 $12.5」**有数据,有底气。** |
35
- | **按项目汇报** | 配置多项目后,选择单个项目生成独立工作汇报,方便向不同项目负责人对齐 |
36
- | **对齐 Sprint 周期** | 除日/周/月外,支持自定义起止日期,不再被固定周期限制 |
37
- | **理解使用习惯** | 哪个项目用得最多?哪个模型最费 Token?什么时段是编码高峰?**一目了然。** |
38
- | **追踪 AI 成本** | 内置 **600+ 模型定价**(含 GLM、Kimi、Qwen、DeepSeek 等),自动算出等效 API 花销 |
33
+ | **精确量化 AI 贡献** | 不是模糊的"大概写了不少",而是精确到「4,200 行代码中 3,180 行由 AI 辅助完成」。**每一行都有数可查。** |
34
+ | **证明 AI ROI** | 周报自动生成:「本周 AI 辅助 12 个提交,节省约 8 小时编码时间,Token 花费 $18.5」。**老板一看就懂。** |
35
+ | **写周报/月报** | 选周期 → 点「工作汇报 → 复制」→ 粘贴飞书/钉钉。**3 秒搞定。** |
36
+ | **按项目汇报** | 多项目并行时,选择单个项目生成独立汇报,方便向不同项目负责人对齐 |
37
+ | **对齐 Sprint 周期** | 支持自定义起止日期,不再被日/周/月固定周期限制 |
38
+ | **追踪 AI 成本** | 600+ 模型内置定价(含 GLM、Kimi、Qwen、DeepSeek),自动算出等效 API 花销 |
39
39
 
40
40
  ---
41
41
 
@@ -63,8 +63,8 @@ npx lumencode serve
63
63
 
64
64
  | 亮点 | 说明 |
65
65
  |------|------|
66
+ | 🎯 **行级 AI 归因** | 通过 hook 步骤追踪,精确识别每一行代码的 AI 参与度。不是"这个提交 AI 帮忙了",而是"这行代码是 AI 写的" |
66
67
  | 🌐 **三工具统一** | Claude Code / Codex / OpenCode 数据全自动汇总,左侧标签一键切换 |
67
- | 🤖 **AI 贡献度量化** | 识别 `Co-Authored-By: Claude` 等签名,多层归因引擎量化 AI 在你代码中的实际占比 |
68
68
  | 📝 **自然语言工作汇报** | 详报/简报一键生成,支持标准 Markdown / 飞书 / 钉钉三种格式,每个板块附诊断解读 |
69
69
  | 📂 **按项目独立汇报** | 右侧面板选择项目,生成该项目的独立工作汇报(commits + AI 交互量 + 热点文件) |
70
70
  | 📅 **自定义时间范围** | 除日/周/月外,支持选择任意起止日期,方便对齐 Sprint 周期 |
@@ -202,6 +202,24 @@ v0.4.0 起支持 Claude Code、Codex、OpenCode 三种工具,**首次运行自
202
202
  | 排除项目 | 不希望统计的项目名称 |
203
203
  | 场景关键词 | 工作类型分类关键词 JSON |
204
204
 
205
+ ### 行级 AI 归因(可选增强)
206
+
207
+ 行级归因通过 AI 编程工具 hook 记录文件编辑步骤,用于把 AI 贡献从提交级/文件级细化到行级。Claude Code 优先使用 `PostToolBatch`,Codex 使用 `PostToolUse`,OpenCode 使用项目级插件。该功能默认按需启用:没有初始化数据库时 hook 会静默跳过,不影响正常使用。
208
+
209
+ ```bash
210
+ # 在需要统计的 Git 项目根目录执行
211
+ node index.js hooks status
212
+ node index.js hooks enable # 交互式选择工具、初始化 steps 并自动备份配置
213
+ ```
214
+
215
+ 开启时只会修改当前项目的本地配置(`.claude/settings.local.json`、`.codex/config.toml`、`.opencode/plugins/lumencode-step-tracker.js`),不会修改全局配置或其它项目。关闭可执行:
216
+
217
+ ```bash
218
+ node index.js hooks disable
219
+ ```
220
+
221
+ 数据写入当前项目的 `.ccusage/steps.db`。该数据库包含用于归因的文件快照,已在本项目 `.gitignore` 中默认忽略;如果在其它仓库启用,也建议忽略 `.ccusage/`。
222
+
205
223
  ### 模型定价数据
206
224
 
207
225
  - **本地表**:内置 590 个来自 [Portkey-AI/models](https://github.com/Portkey-AI/models) 的厂商原命名定价
@@ -232,6 +250,29 @@ v0.4.0 起支持 Claude Code、Codex、OpenCode 三种工具,**首次运行自
232
250
 
233
251
  ## 更新日志
234
252
 
253
+ ### v1.3.0 (2026-05-28) — 行级 AI 归因 & 交互式 Hooks 管理
254
+
255
+ - **行级 AI 归因** — 通过 hook 步骤追踪系统,将归因粒度从提交级细化到行级,精确识别每一行代码的 AI 参与度
256
+ - **交互式 hooks 管理** — Web UI 左下角新增 hooks 状态指示,支持一键开启/关闭,自动备份原始配置
257
+ - **Codex 统一 hook 捕获** — 支持 Codex 的 `PostToolUse` 钩子,实时记录文件编辑步骤
258
+ - **Claude batch hook 模式** — 支持 `PostToolBatch` 批处理钩子,减少性能开销
259
+ - **OpenCode 插件支持** — 新增 OpenCode `lumenccode-step-tracker.js` 插件,覆盖三大工具
260
+ - **报告诊断改进** — CLI 默认值优化,错误提示更友好
261
+ - **解析器稳定性增强** — Git 指标解析、工具解析器、跨解析器项目过滤全面加固
262
+
263
+ ### v1.2.0 (2026-05-26) — AI 置信度精度全面改进
264
+
265
+ - **基线校准** — 根据项目历史提交模式建立个人编码基线,区分「你的正常风格」与「AI 辅助风格」
266
+ - **负信号检测** — 识别纯人工提交的反向指标(如周末提交、短编辑会话),降低误判率
267
+ - **连续评分** — 从二元判断(是/否)升级为 0-100% 连续置信度,归因结果更细腻
268
+ - **归因所有权改进** — 优化多人协作场景下的 AI 贡献归属逻辑
269
+
270
+ ### v1.1.0 (2026-05-25) — 并发管道 & 分层归因
271
+
272
+ - **并发管道处理** — 数据解析与 Git 统计并行执行,大仓库分析速度显著提升
273
+ - **消除冗余 Git 调用** — 缓存重复查询,降低 I/O 开销
274
+ - **分层 AI 归因** — 显式签名 / Session 强关联 / 文件重叠三层置信度模型,归因更准确
275
+
235
276
  ### v1.0.0 (2026-05-24) — 项目级汇报 & 自定义时间
236
277
 
237
278
  - **按项目独立汇报** — 工作汇报右侧面板新增项目选择器,选定后自动筛选该项目数据,生成独立工作汇报(commits + AI 交互量 + 热点文件)
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Claude Code PostToolBatch hook for step tracking.
4
+ * Records one step per completed tool batch.
5
+ */
6
+ import { ORIGINS, recordToolBatch } from '../lib/capture-recorder.js';
7
+
8
+ async function readPayload() {
9
+ const chunks = [];
10
+ process.stdin.setEncoding('utf8');
11
+ for await (const chunk of process.stdin) chunks.push(chunk);
12
+ try {
13
+ return JSON.parse(chunks.join(''));
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ function normalizeToolCalls(payload) {
20
+ const rawCalls = payload.tool_calls || payload.toolCalls || payload.tools || [];
21
+ if (!Array.isArray(rawCalls)) return [];
22
+ return rawCalls.map(call => ({
23
+ toolUseId: call.tool_use_id || call.toolUseId || call.id,
24
+ toolName: call.tool_name || call.toolName || call.name,
25
+ toolInput: call.tool_input || call.toolInput || call.input || {},
26
+ toolResponse: call.tool_response || call.toolResponse || call.output,
27
+ }));
28
+ }
29
+
30
+ async function main() {
31
+ const payload = await readPayload();
32
+ if (!payload) process.exit(0);
33
+
34
+ const cwd = payload.cwd || payload.projectPath || process.cwd();
35
+ try {
36
+ await recordToolBatch({
37
+ origin: ORIGINS.CLAUDE_CODE,
38
+ sessionId: payload.session_id || payload.sessionId,
39
+ batchId: payload.batch_id || payload.batchId,
40
+ toolCalls: normalizeToolCalls(payload),
41
+ cwd,
42
+ timestamp: payload.timestamp || new Date().toISOString(),
43
+ });
44
+ } catch (e) {
45
+ if (process.env.DEBUG_STEP_TRACKER) console.error('[step-tracker]', e.message);
46
+ }
47
+
48
+ process.exit(0);
49
+ }
50
+
51
+ main().catch(() => process.exit(0));
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Codex PostToolUse hook adapter.
4
+ * Normalizes Codex hook payloads and records tool calls through capture-recorder.
5
+ */
6
+ import { ORIGINS, recordToolUse } from '../lib/capture-recorder.js';
7
+
8
+ function normalizeEventName(value) {
9
+ return String(value || '').replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
10
+ }
11
+
12
+ async function readPayload() {
13
+ const chunks = [];
14
+ process.stdin.setEncoding('utf8');
15
+ for await (const chunk of process.stdin) {
16
+ chunks.push(chunk);
17
+ }
18
+
19
+ try {
20
+ return JSON.parse(chunks.join(''));
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ async function main() {
27
+ const payload = await readPayload();
28
+ if (!payload) process.exit(0);
29
+
30
+ const eventName = normalizeEventName(
31
+ payload.hook_event_name || payload.hookEventName || payload.event
32
+ );
33
+ if (eventName !== 'posttooluse') process.exit(0);
34
+
35
+ const tool = payload.tool || {};
36
+ const cwd = payload.cwd || payload.projectPath || payload.workspace || process.cwd();
37
+
38
+ try {
39
+ await recordToolUse({
40
+ origin: ORIGINS.CODEX_CLI,
41
+ sessionId: payload.session_id || payload.sessionId,
42
+ toolUseId: payload.tool_use_id || payload.toolUseId || payload.call_id || payload.callId,
43
+ toolName: payload.tool_name || payload.toolName || tool.name,
44
+ toolInput: payload.tool_input || payload.toolInput || payload.input || tool.input,
45
+ toolResponse: payload.tool_response || payload.toolResponse || payload.output || tool.output,
46
+ cwd,
47
+ timestamp: payload.timestamp || new Date().toISOString(),
48
+ });
49
+ } catch (e) {
50
+ if (process.env.DEBUG_STEP_TRACKER) console.error('[step-tracker]', e.message);
51
+ }
52
+
53
+ process.exit(0);
54
+ }
55
+
56
+ main().catch(() => process.exit(0));
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Initialize step tracking database for the current project.
4
+ * Usage: node hooks/init-steps.js
5
+ */
6
+ import { initStepTracking } from '../lib/hooks-manager.js';
7
+
8
+ const stats = await initStepTracking(process.cwd());
9
+ console.log(`Step tracking initialized at .ccusage/steps.db`);
10
+ console.log(` Steps: ${stats.stepCount}, Sessions: ${stats.sessionCount}`);
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Install PostToolUse hook into Codex config.
4
+ * Usage: node hooks/install-codex.js
5
+ */
6
+ import { enableCodexHooks } from '../lib/hooks-manager.js';
7
+
8
+ const result = enableCodexHooks(process.cwd(), { backup: false });
9
+ console.log(result.changed ? `Codex hook installed in ${result.configPath}` : 'Codex hook already installed.');
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Install PostToolUse hook into Claude Code settings.
4
+ * Usage: node hooks/install.js
5
+ */
6
+ import { enableClaudeHooks } from '../lib/hooks-manager.js';
7
+
8
+ const result = enableClaudeHooks(process.cwd(), { backup: false });
9
+
10
+ if (result.changed) {
11
+ console.log(`Hook installed in ${result.configPath}`);
12
+ } else {
13
+ console.log('Hook already installed.');
14
+ }
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * OpenCode tool.execute.after adapter for step tracking.
4
+ */
5
+ import { ORIGINS, recordToolUse } from '../lib/capture-recorder.js';
6
+
7
+ async function readPayload() {
8
+ const chunks = [];
9
+ process.stdin.setEncoding('utf8');
10
+ for await (const chunk of process.stdin) chunks.push(chunk);
11
+ try {
12
+ return JSON.parse(chunks.join(''));
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ async function main() {
19
+ const payload = await readPayload();
20
+ if (!payload) process.exit(0);
21
+
22
+ const input = payload.input || {};
23
+ const output = payload.output || {};
24
+ const tool = payload.tool || input.tool || {};
25
+ const cwd = payload.cwd || input.cwd || input.directory || payload.directory || process.cwd();
26
+
27
+ try {
28
+ await recordToolUse({
29
+ origin: ORIGINS.OPENCODE,
30
+ sessionId: payload.sessionId || payload.session_id || input.sessionID || input.sessionId || input.session_id || input.session?.id,
31
+ toolUseId: payload.toolUseId || payload.tool_use_id || input.toolCallID || input.toolUseId || input.id,
32
+ toolName: payload.toolName || payload.tool_name || input.toolName || input.tool || tool.name,
33
+ toolInput: payload.toolInput || payload.tool_input || output.args || input.args || input.toolInput || {},
34
+ toolResponse: payload.toolResponse || payload.tool_response || output.result || output,
35
+ cwd,
36
+ timestamp: payload.timestamp || new Date().toISOString(),
37
+ });
38
+ } catch (e) {
39
+ if (process.env.DEBUG_STEP_TRACKER) console.error('[step-tracker]', e.message);
40
+ }
41
+
42
+ process.exit(0);
43
+ }
44
+
45
+ main().catch(() => process.exit(0));
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Claude Code PostToolUse hook for step tracking.
4
+ * Reads JSON payload from stdin, records the tool call as a step.
5
+ * Silently no-ops if .ccusage/steps.db doesn't exist.
6
+ */
7
+ import { ORIGINS, recordToolUse } from '../lib/capture-recorder.js';
8
+
9
+ async function main() {
10
+ const chunks = [];
11
+ process.stdin.setEncoding('utf8');
12
+ for await (const chunk of process.stdin) {
13
+ chunks.push(chunk);
14
+ }
15
+
16
+ let payload;
17
+ try {
18
+ payload = JSON.parse(chunks.join(''));
19
+ } catch { process.exit(0); }
20
+
21
+ const cwd = payload.cwd || payload.projectPath || process.cwd();
22
+
23
+ try {
24
+ await recordToolUse({
25
+ origin: ORIGINS.CLAUDE_CODE,
26
+ sessionId: payload.session_id || payload.sessionId,
27
+ toolUseId: payload.tool_use_id || payload.toolUseId,
28
+ toolName: payload.tool_name || payload.toolName,
29
+ toolInput: payload.tool_input || payload.toolInput,
30
+ toolResponse: payload.tool_response || payload.toolResponse,
31
+ cwd,
32
+ timestamp: payload.timestamp || new Date().toISOString(),
33
+ });
34
+ } catch (e) {
35
+ // Never fail the agent
36
+ if (process.env.DEBUG_STEP_TRACKER) console.error('[step-tracker]', e.message);
37
+ }
38
+
39
+ process.exit(0);
40
+ }
41
+
42
+ main().catch(() => process.exit(0));
package/index.js CHANGED
@@ -5,21 +5,185 @@ import { getGitStatsForMultipleReposAsync, invalidateGitCache, finalizeGitStats,
5
5
  import { invalidateFileCache } from './lib/cache.js';
6
6
  import { generateReport, generateWorkReport } from './lib/report.js';
7
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';
8
+ import { detectClaudeDir, deriveProjectPaths } from './lib/parser.js';
9
+ import { identifyBillingBlocks } from './lib/blocks.js';
10
+ import { registerParser, parseAllEnabledTools, detectAvailableTools } 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
+ import { createInterface } from 'readline';
16
+ import { stdin as input, stdout as output } from 'process';
17
+ import { enableHooks, disableHooks, getHooksStatus, HOOK_TOOLS, initStepTracking } from './lib/hooks-manager.js';
15
18
 
16
19
  // 注册所有解析器
17
20
  registerParser(ClaudeParser);
18
21
  registerParser(CodexParser);
19
22
  registerParser(OpencodeParser);
20
23
 
21
- const args = process.argv.slice(2);
22
- const command = args[0];
24
+ const args = process.argv.slice(2);
25
+ const command = args[0];
26
+
27
+ function parseHookTools(values) {
28
+ const raw = values.length > 0 ? values : ['claude', 'codex', 'opencode'];
29
+ const tools = new Set();
30
+ for (const value of raw) {
31
+ for (const part of value.split(',')) {
32
+ const tool = part.trim().toLowerCase();
33
+ if (!tool) continue;
34
+ if (tool === 'claude' || tool === 'claude-code') tools.add(HOOK_TOOLS.CLAUDE);
35
+ else if (tool === 'codex') tools.add(HOOK_TOOLS.CODEX);
36
+ else if (tool === 'opencode' || tool === 'open-code') tools.add(HOOK_TOOLS.OPENCODE);
37
+ else throw new Error(`不支持的 hooks 工具: ${part}`);
38
+ }
39
+ }
40
+ return [...tools];
41
+ }
42
+
43
+ function detectedHookTools(status = getHooksStatus(process.cwd())) {
44
+ const tools = [];
45
+ if (status.claude.configExists || status.claude.enabled) tools.push(HOOK_TOOLS.CLAUDE);
46
+ if (status.codex.configExists || status.codex.enabled) tools.push(HOOK_TOOLS.CODEX);
47
+ if (status.opencode.configExists || status.opencode.enabled) tools.push(HOOK_TOOLS.OPENCODE);
48
+ return tools.length > 0 ? tools : [HOOK_TOOLS.CLAUDE, HOOK_TOOLS.CODEX, HOOK_TOOLS.OPENCODE];
49
+ }
50
+
51
+ function formatEnabled(value) {
52
+ return value ? '已开启' : '未开启';
53
+ }
54
+
55
+ function printHooksStatus(status) {
56
+ console.log('Hooks 状态:');
57
+ const claudeMode = status.claude.batchEnabled ? 'batch' : status.claude.legacyEnabled ? 'legacy' : '';
58
+ console.log(`- Claude Code: ${status.claude.invalid ? '配置文件 JSON 无效' : formatEnabled(status.claude.enabled)}${claudeMode ? ` (${claudeMode})` : ''}`);
59
+ console.log(`- Codex: ${formatEnabled(status.codex.enabled)}`);
60
+ console.log(`- OpenCode: ${formatEnabled(status.opencode.enabled)}`);
61
+ console.log(`- steps 数据库: ${status.stepsInitialized ? '已初始化' : '未初始化'}`);
62
+ console.log(`- 项目: ${status.projectRoot}`);
63
+ }
64
+
65
+ function printHookResults(results, action) {
66
+ for (const result of results) {
67
+ const name = hookToolName(result.tool);
68
+ console.log(`- ${name}: ${result.changed ? action : '无需变更'} (${result.configPath})`);
69
+ if (result.backupPath) console.log(` 备份: ${result.backupPath}`);
70
+ }
71
+ }
72
+
73
+ function hookToolName(tool) {
74
+ if (tool === HOOK_TOOLS.CLAUDE) return 'Claude Code';
75
+ if (tool === HOOK_TOOLS.CODEX) return 'Codex';
76
+ return 'OpenCode';
77
+ }
78
+
79
+ function createPromptSession() {
80
+ const rl = createInterface({ input, output });
81
+ const lines = [];
82
+ const waiters = [];
83
+ let closed = false;
84
+
85
+ rl.on('line', line => {
86
+ const waiter = waiters.shift();
87
+ if (waiter) waiter(line);
88
+ else lines.push(line);
89
+ });
90
+ rl.on('close', () => {
91
+ closed = true;
92
+ while (waiters.length > 0) waiters.shift()('');
93
+ });
94
+
95
+ return {
96
+ async ask(prompt) {
97
+ output.write(prompt);
98
+ if (lines.length > 0) return lines.shift();
99
+ if (closed) return '';
100
+ return new Promise(resolve => waiters.push(resolve));
101
+ },
102
+ close() {
103
+ rl.close();
104
+ },
105
+ };
106
+ }
107
+
108
+ async function promptHookTools(defaultTools, rl) {
109
+ console.log('检测到:');
110
+ defaultTools.forEach((tool, index) => {
111
+ console.log(`[${index + 1}] ${hookToolName(tool)}`);
112
+ });
113
+
114
+ const answer = await rl.ask('请选择要开启 hooks 的工具(例如 1,2,直接回车选择全部): ');
115
+ const raw = answer.trim();
116
+ if (!raw) return defaultTools;
117
+ const selected = [];
118
+ for (const part of raw.split(',')) {
119
+ const idx = Number(part.trim());
120
+ if (!Number.isInteger(idx) || idx < 1 || idx > defaultTools.length) {
121
+ throw new Error(`无效选择: ${part}`);
122
+ }
123
+ selected.push(defaultTools[idx - 1]);
124
+ }
125
+ return [...new Set(selected)];
126
+ }
127
+
128
+ async function confirmHooksEnable(tools, status, rl) {
129
+ console.log('即将开启 AI 工具 hooks。');
130
+ console.log('操作类型: 修改当前项目的本地 AI 工具配置文件。');
131
+ console.log(`影响范围: ${tools.includes(HOOK_TOOLS.CLAUDE) ? '.claude/settings.local.json ' : ''}${tools.includes(HOOK_TOOLS.CODEX) ? '.codex/config.toml' : ''}`);
132
+ console.log('用途: 记录 PostToolUse 事件,用于行级 AI 归因。');
133
+ console.log(`steps 数据库: ${status.stepsInitialized ? '已初始化' : '将初始化 .ccusage/steps.db'}`);
134
+ console.log('风险: 只启用当前项目 hooks,不修改全局配置或其它项目。');
135
+
136
+ const answer = await rl.ask('请输入“确认”继续: ');
137
+ return answer.trim() === '确认' || answer.trim().toLowerCase() === 'yes' || answer.trim().toLowerCase() === 'y';
138
+ }
139
+
140
+ async function handleHooksCommand() {
141
+ const subcommand = args[1] || 'status';
142
+ if (subcommand === 'init') {
143
+ const stats = await initStepTracking(process.cwd());
144
+ console.log(`Step tracking initialized at .ccusage/steps.db`);
145
+ console.log(` Steps: ${stats.stepCount}, Sessions: ${stats.sessionCount}`);
146
+ return;
147
+ }
148
+ if (subcommand === 'status') {
149
+ printHooksStatus(getHooksStatus(process.cwd()));
150
+ return;
151
+ }
152
+
153
+ const yes = args.includes('--yes') || args.includes('-y');
154
+ const toolArgs = args.slice(2).filter(arg => arg !== '--yes' && arg !== '-y');
155
+ const status = getHooksStatus(process.cwd());
156
+ const rl = createPromptSession();
157
+ let tools;
158
+ try {
159
+ tools = toolArgs.length > 0
160
+ ? parseHookTools(toolArgs)
161
+ : await promptHookTools(detectedHookTools(status), rl);
162
+
163
+ if (subcommand === 'enable') {
164
+ if (!yes && !(await confirmHooksEnable(tools, status, rl))) {
165
+ console.log('已取消,未修改配置。');
166
+ return;
167
+ }
168
+ const stats = await initStepTracking(process.cwd());
169
+ console.log(`Step tracking initialized at .ccusage/steps.db`);
170
+ console.log(` Steps: ${stats.stepCount}, Sessions: ${stats.sessionCount}`);
171
+ const results = enableHooks(process.cwd(), tools, { backup: true });
172
+ printHookResults(results, '已开启');
173
+ return;
174
+ }
175
+
176
+ if (subcommand === 'disable') {
177
+ const results = disableHooks(process.cwd(), tools, { backup: true });
178
+ printHookResults(results, '已关闭');
179
+ return;
180
+ }
181
+ } finally {
182
+ rl.close();
183
+ }
184
+
185
+ console.log('未知 hooks 命令。用法: node index.js hooks status|enable|disable|init [claude,codex] [--yes]');
186
+ }
23
187
 
24
188
  function loadCliConfig() {
25
189
  let config = loadConfig();
@@ -40,8 +204,16 @@ function loadCliConfig() {
40
204
  }
41
205
 
42
206
  // 日期参数
43
- let dateArg = new Date().toISOString().slice(0, 10);
207
+ let dateArg = (() => { const n = new Date(); return `${n.getFullYear()}-${String(n.getMonth()+1).padStart(2,'0')}-${String(n.getDate()).padStart(2,'0')}`; })();
208
+ const skipArgs = new Set();
209
+ for (let i = 0; i < args.length; i++) {
210
+ if (args[i] === '--projects' || args[i] === '--start' || args[i] === '--end') {
211
+ skipArgs.add(i);
212
+ skipArgs.add(i + 1);
213
+ }
214
+ }
44
215
  for (let i = 2; i < args.length; i++) {
216
+ if (skipArgs.has(i)) continue;
45
217
  if (!args[i].startsWith('--')) {
46
218
  dateArg = args[i];
47
219
  break;
@@ -121,7 +293,10 @@ async function buildReportData(period, dateArg, config, effectiveIncludeProjects
121
293
  extendedEnd.setDate(extendedEnd.getDate() + 2);
122
294
  const extendedEndStr = extendedEnd.toISOString().slice(0, 10) + 'T23:59:59';
123
295
  let gs = await getGitStatsForMultipleReposAsync(toolRepos, start, extendedEndStr);
124
- gs = finalizeGitStats(gs, sessions);
296
+ gs = await finalizeGitStats(gs, sessions, {
297
+ attribution: config.aiAttribution,
298
+ stepTracking: config.stepTracking,
299
+ });
125
300
  if (gs.commitList) {
126
301
  const windowStart = start;
127
302
  const windowEnd = end + 'T23:59:59';
@@ -232,7 +407,16 @@ async function buildReportData(period, dateArg, config, effectiveIncludeProjects
232
407
  }
233
408
  }
234
409
 
235
- return { usageStats, gitStats, reposConfigured, sessions: slimSessions, start, end, trendData, prevStats, billingBlocks, toolBreakdown: mergedBreakdown, projectDetails };
410
+ // 工具检测诊断:记录每个工具的检测状态和数据目录
411
+ const diagnostics = {};
412
+ try {
413
+ const availableTools = await detectAvailableTools(config);
414
+ for (const t of availableTools) {
415
+ diagnostics[t.name] = { detected: t.detected, dataDir: t.dataDir || null };
416
+ }
417
+ } catch {}
418
+
419
+ return { usageStats, gitStats, reposConfigured, sessions: slimSessions, start, end, trendData, prevStats, billingBlocks, toolBreakdown: mergedBreakdown, projectDetails, _diagnostics: diagnostics };
236
420
  }
237
421
 
238
422
  if (!command || command === 'help' || command === '--help') {
@@ -263,9 +447,16 @@ if (!command || command === 'help' || command === '--help') {
263
447
  lumencode report daily --projects D://fzwork
264
448
  lumencode report weekly 2026-05-15 --projects D://fzwork,E://play/idea
265
449
  lumencode report daily --work
266
- lumencode report daily --work --brief
267
- lumencode serve
268
- lumencode init
450
+ lumencode report daily --work --brief
451
+ node index.js serve
452
+ node index.js init
453
+ node index.js hooks status
454
+ node index.js hooks enable
455
+ node index.js hooks disable
456
+ node index.js hooks:init
457
+ node index.js hooks:install
458
+ node index.js hooks:install-claude
459
+ node index.js hooks:install-codex
269
460
 
270
461
  零配置:
271
462
  首次运行自动检测 Claude 日志目录和项目路径,无需手动配置。
@@ -274,12 +465,32 @@ if (!command || command === 'help' || command === '--help') {
274
465
  process.exit(0);
275
466
  }
276
467
 
277
- if (command === 'init') {
278
- initConfig(args[1]);
279
- process.exit(0);
280
- }
281
-
282
- if (command === 'serve') {
468
+ if (command === 'init') {
469
+ initConfig(args[1]);
470
+ process.exit(0);
471
+ }
472
+
473
+ if (command === 'hooks') {
474
+ await handleHooksCommand();
475
+ process.exit(0);
476
+ }
477
+
478
+ if (command === 'hooks:init') {
479
+ await import('./hooks/init-steps.js');
480
+ process.exit(0);
481
+ }
482
+
483
+ if (command === 'hooks:install' || command === 'hooks:install-claude') {
484
+ await import('./hooks/install.js');
485
+ process.exit(0);
486
+ }
487
+
488
+ if (command === 'hooks:install-codex') {
489
+ await import('./hooks/install-codex.js');
490
+ process.exit(0);
491
+ }
492
+
493
+ if (command === 'serve') {
283
494
  const { config, effectiveIncludeProjects, configPath } = loadCliConfig();
284
495
  startServer(config, effectiveIncludeProjects, buildReportData, configPath);
285
496
  } else {
@@ -321,7 +532,10 @@ if (command === 'serve') {
321
532
  console.log('正在统计 Git 指标...');
322
533
  const sessions = groupBySessions(filtered);
323
534
  gitStats = await getGitStatsForMultipleReposAsync(config.repos, start, end + 'T23:59:59');
324
- gitStats = finalizeGitStats(gitStats, sessions);
535
+ gitStats = await finalizeGitStats(gitStats, sessions, {
536
+ attribution: config.aiAttribution,
537
+ stepTracking: config.stepTracking,
538
+ });
325
539
  }
326
540
 
327
541
  // 上一周期数据(用于工作汇报环比)