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.
- ai_code_stats-0.1.0/PKG-INFO +179 -0
- ai_code_stats-0.1.0/README.md +164 -0
- ai_code_stats-0.1.0/pyproject.toml +32 -0
- ai_code_stats-0.1.0/setup.cfg +4 -0
- ai_code_stats-0.1.0/src/ai_code_stats/__init__.py +15 -0
- ai_code_stats-0.1.0/src/ai_code_stats/agents/__init__.py +1 -0
- ai_code_stats-0.1.0/src/ai_code_stats/agents/base.py +40 -0
- ai_code_stats-0.1.0/src/ai_code_stats/agents/claude_code.py +95 -0
- ai_code_stats-0.1.0/src/ai_code_stats/agents/codex.py +174 -0
- ai_code_stats-0.1.0/src/ai_code_stats/agents/registry.py +25 -0
- ai_code_stats-0.1.0/src/ai_code_stats/attribution.py +141 -0
- ai_code_stats-0.1.0/src/ai_code_stats/classify.py +203 -0
- ai_code_stats-0.1.0/src/ai_code_stats/cli.py +216 -0
- ai_code_stats-0.1.0/src/ai_code_stats/config.py +171 -0
- ai_code_stats-0.1.0/src/ai_code_stats/diffutil.py +96 -0
- ai_code_stats-0.1.0/src/ai_code_stats/githook/__init__.py +1 -0
- ai_code_stats-0.1.0/src/ai_code_stats/githook/post_commit.py +214 -0
- ai_code_stats-0.1.0/src/ai_code_stats/gitutil.py +51 -0
- ai_code_stats-0.1.0/src/ai_code_stats/hooks/__init__.py +1 -0
- ai_code_stats-0.1.0/src/ai_code_stats/hooks/session_event.py +14 -0
- ai_code_stats-0.1.0/src/ai_code_stats/hooks/tool_event.py +141 -0
- ai_code_stats-0.1.0/src/ai_code_stats/identity.py +89 -0
- ai_code_stats-0.1.0/src/ai_code_stats/install/__init__.py +5 -0
- ai_code_stats-0.1.0/src/ai_code_stats/install/agent_install.py +182 -0
- ai_code_stats-0.1.0/src/ai_code_stats/install/git_install.py +114 -0
- ai_code_stats-0.1.0/src/ai_code_stats/models.py +237 -0
- ai_code_stats-0.1.0/src/ai_code_stats/paths.py +85 -0
- ai_code_stats-0.1.0/src/ai_code_stats/py.typed +0 -0
- ai_code_stats-0.1.0/src/ai_code_stats/reporters/__init__.py +1 -0
- ai_code_stats-0.1.0/src/ai_code_stats/reporters/base.py +60 -0
- ai_code_stats-0.1.0/src/ai_code_stats/reporters/command.py +45 -0
- ai_code_stats-0.1.0/src/ai_code_stats/reporters/http_webhook.py +79 -0
- ai_code_stats-0.1.0/src/ai_code_stats/reporters/json_file.py +24 -0
- ai_code_stats-0.1.0/src/ai_code_stats/reporters/registry.py +104 -0
- ai_code_stats-0.1.0/src/ai_code_stats/storage.py +119 -0
- ai_code_stats-0.1.0/src/ai_code_stats/tokens.py +68 -0
- ai_code_stats-0.1.0/src/ai_code_stats/util.py +39 -0
- ai_code_stats-0.1.0/src/ai_code_stats.egg-info/PKG-INFO +179 -0
- ai_code_stats-0.1.0/src/ai_code_stats.egg-info/SOURCES.txt +49 -0
- ai_code_stats-0.1.0/src/ai_code_stats.egg-info/dependency_links.txt +1 -0
- ai_code_stats-0.1.0/src/ai_code_stats.egg-info/entry_points.txt +2 -0
- ai_code_stats-0.1.0/src/ai_code_stats.egg-info/requires.txt +7 -0
- ai_code_stats-0.1.0/src/ai_code_stats.egg-info/top_level.txt +1 -0
- ai_code_stats-0.1.0/tests/test_agents.py +129 -0
- ai_code_stats-0.1.0/tests/test_attribution.py +101 -0
- ai_code_stats-0.1.0/tests/test_classify.py +82 -0
- ai_code_stats-0.1.0/tests/test_e2e.py +143 -0
- ai_code_stats-0.1.0/tests/test_install.py +70 -0
- ai_code_stats-0.1.0/tests/test_reporters.py +70 -0
- ai_code_stats-0.1.0/tests/test_schema.py +59 -0
- 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,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
|