ai-code-stats 0.1.0__tar.gz

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.
Files changed (51) hide show
  1. ai_code_stats-0.1.0/PKG-INFO +179 -0
  2. ai_code_stats-0.1.0/README.md +164 -0
  3. ai_code_stats-0.1.0/pyproject.toml +32 -0
  4. ai_code_stats-0.1.0/setup.cfg +4 -0
  5. ai_code_stats-0.1.0/src/ai_code_stats/__init__.py +15 -0
  6. ai_code_stats-0.1.0/src/ai_code_stats/agents/__init__.py +1 -0
  7. ai_code_stats-0.1.0/src/ai_code_stats/agents/base.py +40 -0
  8. ai_code_stats-0.1.0/src/ai_code_stats/agents/claude_code.py +95 -0
  9. ai_code_stats-0.1.0/src/ai_code_stats/agents/codex.py +174 -0
  10. ai_code_stats-0.1.0/src/ai_code_stats/agents/registry.py +25 -0
  11. ai_code_stats-0.1.0/src/ai_code_stats/attribution.py +141 -0
  12. ai_code_stats-0.1.0/src/ai_code_stats/classify.py +203 -0
  13. ai_code_stats-0.1.0/src/ai_code_stats/cli.py +216 -0
  14. ai_code_stats-0.1.0/src/ai_code_stats/config.py +171 -0
  15. ai_code_stats-0.1.0/src/ai_code_stats/diffutil.py +96 -0
  16. ai_code_stats-0.1.0/src/ai_code_stats/githook/__init__.py +1 -0
  17. ai_code_stats-0.1.0/src/ai_code_stats/githook/post_commit.py +214 -0
  18. ai_code_stats-0.1.0/src/ai_code_stats/gitutil.py +51 -0
  19. ai_code_stats-0.1.0/src/ai_code_stats/hooks/__init__.py +1 -0
  20. ai_code_stats-0.1.0/src/ai_code_stats/hooks/session_event.py +14 -0
  21. ai_code_stats-0.1.0/src/ai_code_stats/hooks/tool_event.py +141 -0
  22. ai_code_stats-0.1.0/src/ai_code_stats/identity.py +89 -0
  23. ai_code_stats-0.1.0/src/ai_code_stats/install/__init__.py +5 -0
  24. ai_code_stats-0.1.0/src/ai_code_stats/install/agent_install.py +182 -0
  25. ai_code_stats-0.1.0/src/ai_code_stats/install/git_install.py +114 -0
  26. ai_code_stats-0.1.0/src/ai_code_stats/models.py +237 -0
  27. ai_code_stats-0.1.0/src/ai_code_stats/paths.py +85 -0
  28. ai_code_stats-0.1.0/src/ai_code_stats/py.typed +0 -0
  29. ai_code_stats-0.1.0/src/ai_code_stats/reporters/__init__.py +1 -0
  30. ai_code_stats-0.1.0/src/ai_code_stats/reporters/base.py +60 -0
  31. ai_code_stats-0.1.0/src/ai_code_stats/reporters/command.py +45 -0
  32. ai_code_stats-0.1.0/src/ai_code_stats/reporters/http_webhook.py +79 -0
  33. ai_code_stats-0.1.0/src/ai_code_stats/reporters/json_file.py +24 -0
  34. ai_code_stats-0.1.0/src/ai_code_stats/reporters/registry.py +104 -0
  35. ai_code_stats-0.1.0/src/ai_code_stats/storage.py +119 -0
  36. ai_code_stats-0.1.0/src/ai_code_stats/tokens.py +68 -0
  37. ai_code_stats-0.1.0/src/ai_code_stats/util.py +39 -0
  38. ai_code_stats-0.1.0/src/ai_code_stats.egg-info/PKG-INFO +179 -0
  39. ai_code_stats-0.1.0/src/ai_code_stats.egg-info/SOURCES.txt +49 -0
  40. ai_code_stats-0.1.0/src/ai_code_stats.egg-info/dependency_links.txt +1 -0
  41. ai_code_stats-0.1.0/src/ai_code_stats.egg-info/entry_points.txt +2 -0
  42. ai_code_stats-0.1.0/src/ai_code_stats.egg-info/requires.txt +7 -0
  43. ai_code_stats-0.1.0/src/ai_code_stats.egg-info/top_level.txt +1 -0
  44. ai_code_stats-0.1.0/tests/test_agents.py +129 -0
  45. ai_code_stats-0.1.0/tests/test_attribution.py +101 -0
  46. ai_code_stats-0.1.0/tests/test_classify.py +82 -0
  47. ai_code_stats-0.1.0/tests/test_e2e.py +143 -0
  48. ai_code_stats-0.1.0/tests/test_install.py +70 -0
  49. ai_code_stats-0.1.0/tests/test_reporters.py +70 -0
  50. ai_code_stats-0.1.0/tests/test_schema.py +59 -0
  51. ai_code_stats-0.1.0/tests/test_tokens.py +30 -0
@@ -0,0 +1,179 @@
1
+ Metadata-Version: 2.4
2
+ Name: ai-code-stats
3
+ Version: 0.1.0
4
+ Summary: 统计 CodingAgent (Claude Code / Codex) 的 AI 代码采纳率、AI 代码行数与 token 消耗,按 git 仓库 × 提交人维度上报
5
+ Author: ai-code-stats
6
+ License: MIT
7
+ Keywords: claude-code,codex,git,metrics,ai-coding
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ Provides-Extra: http
11
+ Requires-Dist: requests>=2.25; extra == "http"
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=7.0; extra == "dev"
14
+ Requires-Dist: jsonschema>=4.0; extra == "dev"
15
+
16
+ # ai-code-stats
17
+
18
+ 统计 **CodingAgent(Claude Code / Codex)生成代码的采纳率、AI 代码行数与 token 消耗**,
19
+ 按 **git 仓库 × 提交人** 维度,在每次提交时上报。上报后端可插拔(HTTP / 本地文件 / 自定义命令),
20
+ 数据用带版本的 JSON Schema 定义,跨 macOS / Windows / Linux。
21
+
22
+ > 📖 **完整使用说明(安装/配置/上报示例/排查)见 [docs/USAGE.md](docs/USAGE.md)。**
23
+
24
+ ## 它能回答什么
25
+
26
+ - 这次提交里 **AI 写了多少行**、**人最终采纳了多少**(采纳率)。
27
+ - 每次提交的 **总代码行数 / AI 代码行数 / AI 占比**,分「全量」和「有效代码」两种口径。
28
+ - 这次提交关联的 AI **token 消耗**(input / output / cache)。
29
+
30
+ ## 工作原理
31
+
32
+ ```
33
+ AI 编辑 (PostToolUse 钩子) git 提交 (post-commit / post-merge 钩子)
34
+ ┌─────────────────────────┐ ┌──────────────────────────────────────┐
35
+ │ 解析 Edit/Write/apply_patch│ │ 取 commit 变更(含重命名检测) │
36
+ │ 新增行 → 归一化 + 哈希 │ ───▶ │ 与窗口内 AI 指纹做「多重集消费式匹配」 │
37
+ │ 标记是否「有效代码」 │ pending│ 算 采纳率 / AI 占比 / token │
38
+ │ 落 .git/ai-code-stats/ │ │ 组 JSON 信封 → 派发各 Reporter │
39
+ └─────────────────────────┘ └──────────────────────────────────────┘
40
+ ```
41
+
42
+ - **采纳率** = 落入本次 commit 的 AI 行数 / 窗口内 AI 生成的行数。
43
+ - **AI 占比** = 匹配到 AI 指纹的 commit 新增行 / commit 总新增行。
44
+ - 匹配基于**归一化内容哈希**,所以即使 AI 写的代码被移动到别的文件也能命中。
45
+
46
+ ## 安装
47
+
48
+ 需要 Python ≥ 3.9 与 git。
49
+
50
+ ```bash
51
+ pip install ai-code-stats # 或:pip install -e .(开发)
52
+
53
+ # 在目标仓库根目录执行,安装 git 钩子 + Claude + Codex 钩子
54
+ ai-code-stats install
55
+
56
+ # 只装某一项 / 预览不写入
57
+ ai-code-stats install --git
58
+ ai-code-stats install --claude --scope user # 写 ~/.claude/settings.json
59
+ ai-code-stats install --codex --dry-run
60
+
61
+ # 卸载(幂等,保留你自己的钩子内容)
62
+ ai-code-stats uninstall
63
+ ```
64
+
65
+ > Codex 钩子写入 `$CODEX_HOME/config.toml`(默认 `~/.codex/config.toml`)。由于 Codex 钩子
66
+ > schema 仍在演进,安装后建议 `ai-code-stats install --codex --dry-run` 核对,并确认你的
67
+ > Codex 版本支持内联 `[[hooks.PostToolUse]]`。
68
+
69
+ ## 配置
70
+
71
+ 解析顺序(后者覆盖前者):内置默认 → 用户级 `config.json` → 仓库 `.ai-code-stats.json` →
72
+ `AI_CODE_STATS_CONFIG` 指向的文件。字符串支持 `${ENV:VAR}` 注入密钥。
73
+
74
+ ```jsonc
75
+ {
76
+ "enabled": true,
77
+ "privacy": {
78
+ "store_plaintext": true, // 本地是否保留 AI 行明文(仅落在 .git/ 内)
79
+ "redact_in_reports": true // 上报只含统计数字,不含源码
80
+ },
81
+ "files": {
82
+ "include": [], // 为空=按已知代码扩展名统计;非空=只统计匹配项
83
+ "exclude": ["**/node_modules/**", "**/*.min.js", "package-lock.json"]
84
+ },
85
+ "attribution": {
86
+ "count_modes": ["raw", "effective"],
87
+ "primary": "effective", // 主指标用「有效代码」口径
88
+ "merge_strategy": "skip", // merge 提交:skip 或 first_parent
89
+ "detect_renames": true
90
+ },
91
+ "reporters": [
92
+ { "type": "json_file", "path": "{repo_data}/reports.jsonl" },
93
+ { "type": "http_webhook",
94
+ "url": "https://metrics.example.com/ingest",
95
+ "headers": { "Authorization": "Bearer ${ENV:AI_CODE_STATS_TOKEN}" },
96
+ "mapping": { // 把信封映射成任意后端 schema(点路径取值)
97
+ "repo": "data.repo_id",
98
+ "rate": "data.ai.effective.adoption_rate",
99
+ "tokens": "data.tokens.total"
100
+ }
101
+ },
102
+ { "type": "command", "argv": ["my-forwarder"] } // 信封 JSON 经 stdin 传入
103
+ ]
104
+ }
105
+ ```
106
+
107
+ ### 统计口径
108
+
109
+ - **raw(全量)**:所有新增/删除行。
110
+ - **effective(有效代码)**:剔除空行与纯注释行(按语言注释语法识别)。
111
+
112
+ ### 文件过滤
113
+
114
+ 默认只统计已知代码语言扩展名的文件,并排除 lock 文件、生成产物、vendored 目录、二进制。
115
+ 可用 `files.include` / `files.exclude`(glob,支持 `**`)定制。
116
+
117
+ ## 数据契约
118
+
119
+ `schemas/` 下三份带版本的 JSON Schema:
120
+
121
+ | Schema | 用途 |
122
+ |--------|------|
123
+ | `ai_edit_event.schema.json` | 单次 AI 编辑事件(本地暂存) |
124
+ | `commit_stat.schema.json` | 一次提交的完整统计 |
125
+ | `report_envelope.schema.json` | 上报统一信封 |
126
+
127
+ 信封示例:
128
+
129
+ ```json
130
+ {
131
+ "schema_version": "1.0",
132
+ "kind": "commit_stat",
133
+ "produced_at": "2026-06-15T08:00:00Z",
134
+ "producer": { "plugin": "ai-code-stats", "version": "0.1.0", "os": "darwin" },
135
+ "data": {
136
+ "repo_id": "github.com/org/repo",
137
+ "commit": { "sha": "…", "branch": "main", "is_merge": false },
138
+ "committer": { "name": "Dev", "email": "dev@x.com" },
139
+ "totals": { "files_changed": 2, "raw": { "lines_added": 5 }, "effective": { "lines_added": 3 } },
140
+ "ai": {
141
+ "raw": { "ai_lines_added": 4, "adoption_rate": 1.0, "ai_share_of_commit": 0.8 },
142
+ "effective": { "ai_lines_added": 3, "adoption_rate": 1.0, "ai_share_of_commit": 1.0 }
143
+ },
144
+ "tokens": { "input": 120, "output": 30, "total": 150 }
145
+ }
146
+ }
147
+ ```
148
+
149
+ ## 常用命令
150
+
151
+ ```bash
152
+ ai-code-stats status # 查看待归因事件与 token 快照
153
+ ai-code-stats report # 打印当前 HEAD 的统计信封(不发送、不消费)
154
+ ai-code-stats flush # 重试发送失败的上报队列
155
+ ```
156
+
157
+ ## 隐私
158
+
159
+ - AI 行**明文只落在仓库内 `.git/ai-code-stats/`**,不会被提交(在 `.git/` 下)。
160
+ - 上报默认 `redact_in_reports=true`,**只发统计数字**,不含源码。
161
+ - 需要更强隐私可设 `privacy.store_plaintext=false`,本地只存哈希。
162
+
163
+ ## 已知限制
164
+
165
+ - `merge` 提交默认跳过归因(diff 含合并噪声),可配 `first_parent`。
166
+ - `rebase` / `cherry-pick` / `commit --amend` 下采纳率为近似值。
167
+ - token 归属按「自上次提交以来该 session 的累计增量」估算,跨多仓库并行会有近似。
168
+
169
+ ## 开发
170
+
171
+ ```bash
172
+ PYTHONPATH=src python3 -m pytest # 运行测试
173
+ PYTHONPATH=src python3 -m ai_code_stats.cli --help
174
+ ```
175
+
176
+ 架构分层:`agents/`(Agent 适配)· `classify`(过滤/分类)· `attribution`(归因)·
177
+ `tokens`(token 聚合)· `reporters/`(可插拔上报)· `githook/`(提交统计)· `install/`(安装器)。
178
+ 新增上报后端:实现 `reporters/base.Reporter` 并在 `reporters/registry.REPORTER_TYPES` 注册。
179
+ 新增 Agent:实现 `agents/base.AgentAdapter` 并在 `agents/registry` 注册。
@@ -0,0 +1,164 @@
1
+ # ai-code-stats
2
+
3
+ 统计 **CodingAgent(Claude Code / Codex)生成代码的采纳率、AI 代码行数与 token 消耗**,
4
+ 按 **git 仓库 × 提交人** 维度,在每次提交时上报。上报后端可插拔(HTTP / 本地文件 / 自定义命令),
5
+ 数据用带版本的 JSON Schema 定义,跨 macOS / Windows / Linux。
6
+
7
+ > 📖 **完整使用说明(安装/配置/上报示例/排查)见 [docs/USAGE.md](docs/USAGE.md)。**
8
+
9
+ ## 它能回答什么
10
+
11
+ - 这次提交里 **AI 写了多少行**、**人最终采纳了多少**(采纳率)。
12
+ - 每次提交的 **总代码行数 / AI 代码行数 / AI 占比**,分「全量」和「有效代码」两种口径。
13
+ - 这次提交关联的 AI **token 消耗**(input / output / cache)。
14
+
15
+ ## 工作原理
16
+
17
+ ```
18
+ AI 编辑 (PostToolUse 钩子) git 提交 (post-commit / post-merge 钩子)
19
+ ┌─────────────────────────┐ ┌──────────────────────────────────────┐
20
+ │ 解析 Edit/Write/apply_patch│ │ 取 commit 变更(含重命名检测) │
21
+ │ 新增行 → 归一化 + 哈希 │ ───▶ │ 与窗口内 AI 指纹做「多重集消费式匹配」 │
22
+ │ 标记是否「有效代码」 │ pending│ 算 采纳率 / AI 占比 / token │
23
+ │ 落 .git/ai-code-stats/ │ │ 组 JSON 信封 → 派发各 Reporter │
24
+ └─────────────────────────┘ └──────────────────────────────────────┘
25
+ ```
26
+
27
+ - **采纳率** = 落入本次 commit 的 AI 行数 / 窗口内 AI 生成的行数。
28
+ - **AI 占比** = 匹配到 AI 指纹的 commit 新增行 / commit 总新增行。
29
+ - 匹配基于**归一化内容哈希**,所以即使 AI 写的代码被移动到别的文件也能命中。
30
+
31
+ ## 安装
32
+
33
+ 需要 Python ≥ 3.9 与 git。
34
+
35
+ ```bash
36
+ pip install ai-code-stats # 或:pip install -e .(开发)
37
+
38
+ # 在目标仓库根目录执行,安装 git 钩子 + Claude + Codex 钩子
39
+ ai-code-stats install
40
+
41
+ # 只装某一项 / 预览不写入
42
+ ai-code-stats install --git
43
+ ai-code-stats install --claude --scope user # 写 ~/.claude/settings.json
44
+ ai-code-stats install --codex --dry-run
45
+
46
+ # 卸载(幂等,保留你自己的钩子内容)
47
+ ai-code-stats uninstall
48
+ ```
49
+
50
+ > Codex 钩子写入 `$CODEX_HOME/config.toml`(默认 `~/.codex/config.toml`)。由于 Codex 钩子
51
+ > schema 仍在演进,安装后建议 `ai-code-stats install --codex --dry-run` 核对,并确认你的
52
+ > Codex 版本支持内联 `[[hooks.PostToolUse]]`。
53
+
54
+ ## 配置
55
+
56
+ 解析顺序(后者覆盖前者):内置默认 → 用户级 `config.json` → 仓库 `.ai-code-stats.json` →
57
+ `AI_CODE_STATS_CONFIG` 指向的文件。字符串支持 `${ENV:VAR}` 注入密钥。
58
+
59
+ ```jsonc
60
+ {
61
+ "enabled": true,
62
+ "privacy": {
63
+ "store_plaintext": true, // 本地是否保留 AI 行明文(仅落在 .git/ 内)
64
+ "redact_in_reports": true // 上报只含统计数字,不含源码
65
+ },
66
+ "files": {
67
+ "include": [], // 为空=按已知代码扩展名统计;非空=只统计匹配项
68
+ "exclude": ["**/node_modules/**", "**/*.min.js", "package-lock.json"]
69
+ },
70
+ "attribution": {
71
+ "count_modes": ["raw", "effective"],
72
+ "primary": "effective", // 主指标用「有效代码」口径
73
+ "merge_strategy": "skip", // merge 提交:skip 或 first_parent
74
+ "detect_renames": true
75
+ },
76
+ "reporters": [
77
+ { "type": "json_file", "path": "{repo_data}/reports.jsonl" },
78
+ { "type": "http_webhook",
79
+ "url": "https://metrics.example.com/ingest",
80
+ "headers": { "Authorization": "Bearer ${ENV:AI_CODE_STATS_TOKEN}" },
81
+ "mapping": { // 把信封映射成任意后端 schema(点路径取值)
82
+ "repo": "data.repo_id",
83
+ "rate": "data.ai.effective.adoption_rate",
84
+ "tokens": "data.tokens.total"
85
+ }
86
+ },
87
+ { "type": "command", "argv": ["my-forwarder"] } // 信封 JSON 经 stdin 传入
88
+ ]
89
+ }
90
+ ```
91
+
92
+ ### 统计口径
93
+
94
+ - **raw(全量)**:所有新增/删除行。
95
+ - **effective(有效代码)**:剔除空行与纯注释行(按语言注释语法识别)。
96
+
97
+ ### 文件过滤
98
+
99
+ 默认只统计已知代码语言扩展名的文件,并排除 lock 文件、生成产物、vendored 目录、二进制。
100
+ 可用 `files.include` / `files.exclude`(glob,支持 `**`)定制。
101
+
102
+ ## 数据契约
103
+
104
+ `schemas/` 下三份带版本的 JSON Schema:
105
+
106
+ | Schema | 用途 |
107
+ |--------|------|
108
+ | `ai_edit_event.schema.json` | 单次 AI 编辑事件(本地暂存) |
109
+ | `commit_stat.schema.json` | 一次提交的完整统计 |
110
+ | `report_envelope.schema.json` | 上报统一信封 |
111
+
112
+ 信封示例:
113
+
114
+ ```json
115
+ {
116
+ "schema_version": "1.0",
117
+ "kind": "commit_stat",
118
+ "produced_at": "2026-06-15T08:00:00Z",
119
+ "producer": { "plugin": "ai-code-stats", "version": "0.1.0", "os": "darwin" },
120
+ "data": {
121
+ "repo_id": "github.com/org/repo",
122
+ "commit": { "sha": "…", "branch": "main", "is_merge": false },
123
+ "committer": { "name": "Dev", "email": "dev@x.com" },
124
+ "totals": { "files_changed": 2, "raw": { "lines_added": 5 }, "effective": { "lines_added": 3 } },
125
+ "ai": {
126
+ "raw": { "ai_lines_added": 4, "adoption_rate": 1.0, "ai_share_of_commit": 0.8 },
127
+ "effective": { "ai_lines_added": 3, "adoption_rate": 1.0, "ai_share_of_commit": 1.0 }
128
+ },
129
+ "tokens": { "input": 120, "output": 30, "total": 150 }
130
+ }
131
+ }
132
+ ```
133
+
134
+ ## 常用命令
135
+
136
+ ```bash
137
+ ai-code-stats status # 查看待归因事件与 token 快照
138
+ ai-code-stats report # 打印当前 HEAD 的统计信封(不发送、不消费)
139
+ ai-code-stats flush # 重试发送失败的上报队列
140
+ ```
141
+
142
+ ## 隐私
143
+
144
+ - AI 行**明文只落在仓库内 `.git/ai-code-stats/`**,不会被提交(在 `.git/` 下)。
145
+ - 上报默认 `redact_in_reports=true`,**只发统计数字**,不含源码。
146
+ - 需要更强隐私可设 `privacy.store_plaintext=false`,本地只存哈希。
147
+
148
+ ## 已知限制
149
+
150
+ - `merge` 提交默认跳过归因(diff 含合并噪声),可配 `first_parent`。
151
+ - `rebase` / `cherry-pick` / `commit --amend` 下采纳率为近似值。
152
+ - token 归属按「自上次提交以来该 session 的累计增量」估算,跨多仓库并行会有近似。
153
+
154
+ ## 开发
155
+
156
+ ```bash
157
+ PYTHONPATH=src python3 -m pytest # 运行测试
158
+ PYTHONPATH=src python3 -m ai_code_stats.cli --help
159
+ ```
160
+
161
+ 架构分层:`agents/`(Agent 适配)· `classify`(过滤/分类)· `attribution`(归因)·
162
+ `tokens`(token 聚合)· `reporters/`(可插拔上报)· `githook/`(提交统计)· `install/`(安装器)。
163
+ 新增上报后端:实现 `reporters/base.Reporter` 并在 `reporters/registry.REPORTER_TYPES` 注册。
164
+ 新增 Agent:实现 `agents/base.AgentAdapter` 并在 `agents/registry` 注册。
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ai-code-stats"
7
+ version = "0.1.0"
8
+ description = "统计 CodingAgent (Claude Code / Codex) 的 AI 代码采纳率、AI 代码行数与 token 消耗,按 git 仓库 × 提交人维度上报"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "ai-code-stats" }]
13
+ keywords = ["claude-code", "codex", "git", "metrics", "ai-coding"]
14
+ dependencies = []
15
+
16
+ [project.optional-dependencies]
17
+ # HTTP webhook reporter 在无 requests 时回退到标准库 urllib,故 requests 为可选增强。
18
+ http = ["requests>=2.25"]
19
+ dev = ["pytest>=7.0", "jsonschema>=4.0"]
20
+
21
+ [project.scripts]
22
+ ai-code-stats = "ai_code_stats.cli:main"
23
+
24
+ [tool.setuptools.packages.find]
25
+ where = ["src"]
26
+
27
+ [tool.setuptools.package-data]
28
+ ai_code_stats = ["py.typed"]
29
+
30
+ [tool.pytest.ini_options]
31
+ testpaths = ["tests"]
32
+ addopts = "-q"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,15 @@
1
+ """ai-code-stats: 统计 CodingAgent 生成代码的采纳率、AI 代码行数与 token 消耗。
2
+
3
+ 按 git 仓库 × 提交人维度,在每次提交时上报本次提交的总代码行数、AI 代码行数、
4
+ AI 占比、采纳率以及 token 用量。支持 Claude Code 与 Codex 两种 Agent,
5
+ 上报后端可插拔(HTTP webhook / 本地 JSON 文件 / 自定义命令)。
6
+ """
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ # 数据契约版本:JSON Schema 与上报信封共用,破坏性变更时递增。
11
+ SCHEMA_VERSION = "1.0"
12
+
13
+ PLUGIN_NAME = "ai-code-stats"
14
+
15
+ __all__ = ["__version__", "SCHEMA_VERSION", "PLUGIN_NAME"]
@@ -0,0 +1 @@
1
+ """Agent 适配层:把不同 CodingAgent 的钩子载荷归一成统一的捕获结果。"""
@@ -0,0 +1,40 @@
1
+ """Agent 适配器抽象接口。
2
+
3
+ 每个适配器把某个 Agent 的钩子 stdin 载荷解析成统一的 :class:`HookCapture`:
4
+ 一次 hook 触发涉及的文件编辑列表 + 该 session 的累计 token 用量 + session/cwd。
5
+ 上层 ``hooks/tool_event`` 不关心 Agent 差异,只消费 ``HookCapture``。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from abc import ABC, abstractmethod
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Dict, List, Optional
13
+
14
+
15
+ @dataclass
16
+ class ParsedEdit:
17
+ """一次文件编辑解析结果。``added``/``removed`` 为去掉 diff 前缀后的纯文本行。"""
18
+
19
+ file_path: str
20
+ tool: str
21
+ added: List[str] = field(default_factory=list)
22
+ removed: List[str] = field(default_factory=list)
23
+
24
+
25
+ @dataclass
26
+ class HookCapture:
27
+ session_id: str
28
+ cwd: Optional[str]
29
+ edits: List[ParsedEdit] = field(default_factory=list)
30
+ # 该 session 到目前为止的累计 token 用量({} 表示未知)。
31
+ cumulative_usage: Dict[str, int] = field(default_factory=dict)
32
+
33
+
34
+ class AgentAdapter(ABC):
35
+ name: str = ""
36
+
37
+ @abstractmethod
38
+ def parse(self, payload: Dict[str, Any]) -> HookCapture:
39
+ """把钩子 stdin 载荷解析成 :class:`HookCapture`。"""
40
+ raise NotImplementedError
@@ -0,0 +1,95 @@
1
+ """Claude Code 适配器。
2
+
3
+ PostToolUse 钩子 stdin 载荷(JSON):
4
+ {
5
+ "session_id": "...",
6
+ "transcript_path": "/path/to/session.jsonl",
7
+ "cwd": "/abs/cwd",
8
+ "hook_event_name": "PostToolUse",
9
+ "tool_name": "Edit" | "Write" | "MultiEdit",
10
+ "tool_input": {...},
11
+ "tool_response": {...}
12
+ }
13
+
14
+ token 用量从 transcript JSONL 累计:每条 assistant 消息带 ``message.usage``。
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ from typing import Any, Dict, List
21
+
22
+ from ..diffutil import added_lines_between
23
+ from .base import AgentAdapter, HookCapture, ParsedEdit
24
+
25
+
26
+ class ClaudeCodeAdapter(AgentAdapter):
27
+ name = "claude_code"
28
+
29
+ def parse(self, payload: Dict[str, Any]) -> HookCapture:
30
+ session_id = str(payload.get("session_id", ""))
31
+ cwd = payload.get("cwd")
32
+ edits = self._parse_edits(payload)
33
+ usage = self._read_usage(payload.get("transcript_path"))
34
+ return HookCapture(
35
+ session_id=session_id, cwd=cwd, edits=edits, cumulative_usage=usage
36
+ )
37
+
38
+ # ------------------------------------------------------------------
39
+ def _parse_edits(self, payload: Dict[str, Any]) -> List[ParsedEdit]:
40
+ tool = str(payload.get("tool_name", ""))
41
+ ti = payload.get("tool_input") or {}
42
+ if not isinstance(ti, dict):
43
+ return []
44
+ file_path = ti.get("file_path") or ti.get("path") or ""
45
+
46
+ if tool == "Write":
47
+ content = ti.get("content", "") or ""
48
+ return [ParsedEdit(file_path=file_path, tool=tool,
49
+ added=content.splitlines(), removed=[])]
50
+
51
+ if tool == "Edit":
52
+ added, removed = added_lines_between(
53
+ ti.get("old_string", "") or "", ti.get("new_string", "") or ""
54
+ )
55
+ return [ParsedEdit(file_path=file_path, tool=tool, added=added, removed=removed)]
56
+
57
+ if tool == "MultiEdit":
58
+ added: List[str] = []
59
+ removed: List[str] = []
60
+ for e in ti.get("edits", []) or []:
61
+ a, r = added_lines_between(
62
+ e.get("old_string", "") or "", e.get("new_string", "") or ""
63
+ )
64
+ added.extend(a)
65
+ removed.extend(r)
66
+ return [ParsedEdit(file_path=file_path, tool=tool, added=added, removed=removed)]
67
+
68
+ # 其它工具(Bash/Read/...)不产生 AI 代码归因。
69
+ return []
70
+
71
+ def _read_usage(self, transcript_path: Any) -> Dict[str, int]:
72
+ agg = {"input": 0, "output": 0, "cache_read": 0}
73
+ if not transcript_path:
74
+ return {}
75
+ try:
76
+ with open(transcript_path, "r", encoding="utf-8") as fh:
77
+ for line in fh:
78
+ line = line.strip()
79
+ if not line:
80
+ continue
81
+ try:
82
+ obj = json.loads(line)
83
+ except json.JSONDecodeError:
84
+ continue
85
+ msg = obj.get("message") if isinstance(obj, dict) else None
86
+ usage = msg.get("usage") if isinstance(msg, dict) else None
87
+ if not isinstance(usage, dict):
88
+ continue
89
+ agg["input"] += int(usage.get("input_tokens", 0) or 0)
90
+ agg["input"] += int(usage.get("cache_creation_input_tokens", 0) or 0)
91
+ agg["output"] += int(usage.get("output_tokens", 0) or 0)
92
+ agg["cache_read"] += int(usage.get("cache_read_input_tokens", 0) or 0)
93
+ except OSError:
94
+ return {}
95
+ return agg