@geoqiao/pi-usage 0.1.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.
Files changed (62) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +222 -0
  3. package/bin/pi-usage.js +69 -0
  4. package/data/models.dev-LICENSE +21 -0
  5. package/data/prices.json +2678 -0
  6. package/extensions/usage-report.js +36 -0
  7. package/package.json +51 -0
  8. package/src/analytics.js +189 -0
  9. package/src/collect.js +34 -0
  10. package/src/network.js +25 -0
  11. package/src/report.js +43 -0
  12. package/vendor/vibe-usage/NOTICE.md +58 -0
  13. package/vendor/vibe-usage/src/cindy-roots.js +85 -0
  14. package/vendor/vibe-usage/src/claude-roots.js +165 -0
  15. package/vendor/vibe-usage/src/cline-roots.js +40 -0
  16. package/vendor/vibe-usage/src/codex-roots.js +46 -0
  17. package/vendor/vibe-usage/src/craft-roots.js +15 -0
  18. package/vendor/vibe-usage/src/extra-roots.js +312 -0
  19. package/vendor/vibe-usage/src/parsers/aggregate.js +196 -0
  20. package/vendor/vibe-usage/src/parsers/alma.js +94 -0
  21. package/vendor/vibe-usage/src/parsers/amp.js +156 -0
  22. package/vendor/vibe-usage/src/parsers/antigravity-db.js +359 -0
  23. package/vendor/vibe-usage/src/parsers/antigravity.js +530 -0
  24. package/vendor/vibe-usage/src/parsers/cindy-ledger.js +157 -0
  25. package/vendor/vibe-usage/src/parsers/claude-code.js +372 -0
  26. package/vendor/vibe-usage/src/parsers/cline.js +92 -0
  27. package/vendor/vibe-usage/src/parsers/codex-cache.js +138 -0
  28. package/vendor/vibe-usage/src/parsers/codex.js +1198 -0
  29. package/vendor/vibe-usage/src/parsers/contract.js +55 -0
  30. package/vendor/vibe-usage/src/parsers/copilot-cli.js +128 -0
  31. package/vendor/vibe-usage/src/parsers/craft-agent.js +21 -0
  32. package/vendor/vibe-usage/src/parsers/cursor.js +262 -0
  33. package/vendor/vibe-usage/src/parsers/dimagent.js +127 -0
  34. package/vendor/vibe-usage/src/parsers/droid.js +113 -0
  35. package/vendor/vibe-usage/src/parsers/dsh.js +563 -0
  36. package/vendor/vibe-usage/src/parsers/fs-utils.js +36 -0
  37. package/vendor/vibe-usage/src/parsers/gemini-cli.js +190 -0
  38. package/vendor/vibe-usage/src/parsers/grok.js +395 -0
  39. package/vendor/vibe-usage/src/parsers/hermes.js +123 -0
  40. package/vendor/vibe-usage/src/parsers/index.js +61 -0
  41. package/vendor/vibe-usage/src/parsers/kimi-code.js +467 -0
  42. package/vendor/vibe-usage/src/parsers/kiro.js +788 -0
  43. package/vendor/vibe-usage/src/parsers/mcode.js +182 -0
  44. package/vendor/vibe-usage/src/parsers/mimocode.js +88 -0
  45. package/vendor/vibe-usage/src/parsers/omp.js +10 -0
  46. package/vendor/vibe-usage/src/parsers/openclaw.js +142 -0
  47. package/vendor/vibe-usage/src/parsers/opencode.js +151 -0
  48. package/vendor/vibe-usage/src/parsers/pi-coding-agent.js +27 -0
  49. package/vendor/vibe-usage/src/parsers/pi-session-jsonl.js +166 -0
  50. package/vendor/vibe-usage/src/parsers/qwen-code.js +122 -0
  51. package/vendor/vibe-usage/src/parsers/roo-code.js +123 -0
  52. package/vendor/vibe-usage/src/parsers/sqlite.js +148 -0
  53. package/vendor/vibe-usage/src/parsers/trae-cli.js +171 -0
  54. package/vendor/vibe-usage/src/parsers/workbuddy.js +322 -0
  55. package/vendor/vibe-usage/src/parsers/zcode.js +115 -0
  56. package/vendor/vibe-usage/src/pi-roots.js +125 -0
  57. package/vendor/vibe-usage/src/tools.js +422 -0
  58. package/vendor/vibe-usage/src/workbuddy-roots.js +22 -0
  59. package/vendor/vibe-usage/upstream-files.json +48 -0
  60. package/web/report.css +10 -0
  61. package/web/report.html +81 -0
  62. package/web/report.js +310 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 eko24ive
4
+ Copyright (c) 2026 geoqiao
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,222 @@
1
+ # @geoqiao/pi-usage
2
+
3
+ **获取 AI 编程工具的使用数据,在本机计价、分析并生成可离线打开的 HTML 报告;不上传统计数据。**
4
+
5
+ 参考 [Vibe Usage](https://github.com/vibe-cafe/vibe-usage) 的解析代码和登录后仪表盘布局。
6
+ 独立维护,非 VibeCafé 官方产品。无运行时 npm 依赖,无后台服务,无 LLM 分析调用。
7
+
8
+ ## 安装与使用
9
+
10
+ 需要 **Node.js ≥ 22.15**。
11
+
12
+ ```bash
13
+ pi install npm:@geoqiao/pi-usage
14
+ # 不安装 Pi 扩展,仅运行 CLI:
15
+ npx @geoqiao/pi-usage --days 90
16
+ ```
17
+
18
+ 安装后在 Pi 中运行(已打开的 Pi 先 `/reload`):
19
+
20
+ ```text
21
+ /usage-report
22
+ /usage-report 30
23
+ ```
24
+
25
+ 命令不触发模型回合,不将统计结果注入对话;UI 提示报告的本地路径。
26
+ 生成过程在独立 Node 子进程中执行,避免同步解析阻塞 Pi。退出或重载 Pi 会取消该进程。
27
+
28
+ 也可从源码运行,无需构建:
29
+
30
+ ```bash
31
+ # 在 pi-tools 仓库根目录运行
32
+ pi install ./packages/pi-usage
33
+ node packages/pi-usage/bin/pi-usage.js --days 90
34
+ ```
35
+
36
+ 默认写入 `~/.pi/usage/reports/report-<随机后缀>/`。双击 `index.html` 即可打开;不需要本地 Web 服务器。
37
+ 每次生成独立快照,不会覆盖旧报告。
38
+
39
+ | 文件 | 内容 |
40
+ |---|---|
41
+ | `index.html` | 数据、脚本和样式全部内嵌的交互报告 |
42
+ | `details.csv` | 解析器原始粒度 × Harness × 模型 × 项目 × 终端 × 请求类型的 token 与费用明细 |
43
+ | `sessions.csv` | 会话时长、消息数、开始和结束时间 |
44
+ | `usage.json` | 字段白名单后的桶、会话、读取状态和价格;可再次离线分析 |
45
+
46
+ CSV 使用 UTF-8 BOM、标准引号转义与公式注入防护,可直接在 Excel 中打开。
47
+ POSIX 上新建报告目录权限为 0700,文件为 0600。报告含项目名和终端名,仍属于私人文件;不要放进自动同步的公共目录。
48
+
49
+ ## 命令行
50
+
51
+ ```bash
52
+ pi-usage --days 90 --timezone Asia/Shanghai
53
+ pi-usage --sources pi-coding-agent,claude-code,codex --days 30
54
+ pi-usage --out /absolute/path/to/empty-report-directory
55
+ pi-usage --prices /absolute/path/to/prices.json
56
+ pi-usage --input /absolute/path/to/usage.json --days 90 --offline
57
+ pi-usage --offline
58
+ pi-usage --list-sources
59
+ pi-usage --help
60
+ ```
61
+
62
+ `--days` 为包含今天的 1–3660 个日历日,默认 90;日期按 `--timezone` 分组,默认系统 IANA 时区。
63
+ 报告只能筛选采集窗口内的数据,不能凭空补回已删除的日志。`--out` 必须为空目录;非空即拒绝写入。
64
+ `--input` 接受 `{ "buckets": [...], "sessions": [...] }`,不读取数据源,忽略原有费用并用本地价格重新计算。
65
+ 输入文件的其他字段不会被保留;来源完整性未验证,也不会自动沿用外部 CSV 中的疑似重复标记。
66
+
67
+ ## 交互分析看板
68
+
69
+ 每次主动执行命令读取已有数据并生成快照,不做定时采集。默认回答 **「这段时间用了多少,主要用在哪?」**:含缓存 Token 优先,金额辅助。浅色分析画布、图标工具栏与四个视图代替长篇摘要;桌面首屏并列展示核心图表,移动端改为单列。
70
+
71
+ | 视图 / 操作 | 内容 |
72
+ |---|---|
73
+ | 看板 | 四类 Token 指标、日堆叠趋势、请求构成环图、Harness 排名、可切换维度排名、每日频数与分位区间 |
74
+ | 联动筛选 | 日期 / Harness / 模型 / 项目 / 终端 / 请求类型组合筛选;点击柱形、排名或请求类型加入同一筛选状态,标签可单独移除或清除全部 |
75
+ | 日分布 | 六项 Token 指标的区间图与 Min / P25 / P50 / P75 / P90 / Max;完整日金额显示有效与排除样本,精确表格及每日数值可展开 / 核对 |
76
+ | 模型计价 | 对当前筛选的每天四类 Token 逐日重新计价;共用线性刻度区间图与全部六个金额,默认 P50 排序,可切换 P90 / Min / Max |
77
+ | 明细 | 日级聚合完整数值、分页与排序;顶部下载图标导出当前筛选的日级 CSV |
78
+ | 信息入口 | 顶部 ⓘ / 快照状态打开来源、价格出处与口径;图表旁 ⓘ 打开对应说明,Escape 关闭并返回焦点 |
79
+
80
+ 页面明细及顶部下载按 **日期 × Harness × 模型 × 项目 × 终端 × 请求类型** 聚合,CSV 附 `knownCost`(已知小计)、`estimatedCost`(完整金额或空)、`coverage`(可计价 Token 比例)。
81
+ 同目录自动生成的 `details.csv` 与 `usage.json` 仍保留解析器原始粒度,供进一步分析;没有改变源数据或解析缓存。
82
+ Token 四类颜色跨指标、趋势和排名保持一致;请求类型单独成图,不混成第五类 Token。排名默认前 5 项,可展开全部并在面板内滚动;长名称用省略显示,悬停或辅助技术可读取全名。页面不展示工时、生产力或分时活跃指标;会话数据仍保留在 JSON / sessions.csv。
83
+ 日期快捷项以报告截止日为准,旧报告不会随当前日期变化或自动更新。
84
+
85
+ ### 请求类型与 Harness
86
+
87
+ UI 的 **Harness 对应 `source` 字段**,包括 Pi、Claude Code、Codex,也保留 Cursor 等来源;不表示都在本机运行。新的 `requestType` 与 Token 类型是两个独立维度:
88
+
89
+ | requestType | 页面标签 | 口径 |
90
+ |---|---|---|
91
+ | `non_tool` | 非工具调用请求 | 完整响应可确认没有工具调用 |
92
+ | `tool` | 含工具调用请求 | 响应中有工具调用或明确的工具调用结束标记;文字与调用混合也归此类 |
93
+ | `other` | 其他(无法判定) | 旧汇总、缺少完整响应、关联不可靠或来源尚不支持分类 |
94
+
95
+ 整条请求的 usage 只归一类,不按工具数重复计数,也不表示工具自身消耗。构成图的占比按**含缓存 Token**计算,不是请求次数;三个入口(下拉、构成按钮、排名下钻)使用同一筛选状态。环图中央显示可分类 Token 比例,灰色明确保留其他 / 未判定。可逐项移除筛选或清除全部。
96
+
97
+ 分类支持与剩余 `other` 原因:
98
+
99
+ | Harness | 可审计的分类证据 | 仍归 other 的情况 |
100
+ |---|---|---|
101
+ | Pi / Oh My Pi、Claude Code | 原始完整响应与工具调用;流式片段及副本保留正面工具证据,usage 只计一次 | 缺少响应或正常完成证据 |
102
+ | Codex | 已知请求边界之间的完整 response_item 与推进的单次 token_count;新版 token_usage_record 额外核对 usage / response / turn / thread 关联 | 累计量回退、缺边界/输出、冲突 ID、损坏记录、旧账本;不把整个 turn 视为一次请求 |
103
+ | ZCode | part.message_id 精确关联 assistant message;有 tool 优先,无 tool 且 finish=stop 才归 non_tool | 缺 part 表/字段、损坏 part、未结束或 content-filter 等结束原因 |
104
+ | Kimi Code | 新格式匹配 step UUID / turn / step.end.usage 与 usage.record;旧格式 StepBegin / StepRetry 至完整 StatusUpdate.token_usage | 缺 step 边界、usage 不匹配、中断、无法关联的 session scope / 压缩账本;不混入子代理事件 |
105
+ | 其他来源 | 尚未建立可靠请求级关联 | 保留 other,不猜测 |
106
+
107
+ Codex 的 token_usage_record 仅用于核对完成证据,不叠加到既有 token_count 计量,因此分类补全不改变四类 Token、去重或价格口径。解析缓存算法已升级,旧结果及增量尾缓存会自动失效。导入旧 JSON 时缺失的 `requestType` 默认归其他;更新 HTML 不能恢复旧汇总中丢失的信息,需要重新读取日志。
108
+
109
+ Min / Max 是当前样本日的最小 / 最大观测值,与四个分位同样本;目标模型逐日计价后再取极值。区间图以细线表示 Min–Max、色带表示 P25–P75、圆点表示 P50、菱形表示 P90。Token 分位各行独立刻度,模型计价共用刻度。不是单次请求极值、预算边界或未来上下限。移动端趋势按可用宽度重绘;表格可局部横向滚动,第一列保持可见。
110
+
111
+ ## 本地价格表
112
+
113
+ 随包提供 **2026-09-05 models.dev 社区价格快照**:159 个基础模型 / 318 个精确模型标识(含 provider 前缀)。
114
+ 不是从聊天中的费用拟合而来,也不声称逐条验证过厂商官网;每条记录保留提供方和文档来源。
115
+ 只取直接提供方的基础文本费率,不随意选代理商价格。运行时不会自动下载或更新价格。
116
+
117
+ 用 `--prices` 指定本地 JSON 覆盖;**每个覆盖项必须完整提供四项费率**:
118
+
119
+ ```json
120
+ {
121
+ "models": {
122
+ "my-model": {
123
+ "input": 5,
124
+ "cacheRead": 0.5,
125
+ "output": 30,
126
+ "reasoning": 30
127
+ }
128
+ }
129
+ }
130
+ ```
131
+
132
+ 单位:**美元 / 百万 token**。允许非负有限数字;`cacheRead: null` 表示缓存价格未知,遇到缓存用量时该桶不计价。
133
+ 按完整模型标识匹配,允许唯一的大小写差异;不随意删除 `#service_tier=...` 后缀或模糊猜测别名。特殊档位可按其完整标识自行覆盖。
134
+
135
+ ```text
136
+ estimatedCost = (inputTokens × input
137
+ + cachedInputTokens × cacheRead
138
+ + outputTokens × output
139
+ + reasoningOutputTokens × reasoning) / 1,000,000
140
+ ```
141
+
142
+ 未知模型和缺失费率的费用为 `null`,不是零。完全未定价的分组显示「未定价」,部分定价明确标识;金额占比仅相对于已知小计。覆盖率按**含缓存 Token 数**计算,不代表真实费用覆盖率;不会据此自动切换排名指标。
143
+ Token 分位与完整日金额分位分开展示,后者标出有效 / 排除日期数量及样本偏差风险,另列全部样本日的已知金额小计分布,不将其冒充完整金额。
144
+ 分位与模型计价共用日期样本,默认仅有用量日。「将无记录日按 0 纳入」是显式假设,不表示已确认当天未使用。
145
+ 模拟按每天整体重计价后求分位,**不把各类 Token 的分位乘价再相加**。目标费率齐备即可模拟原模型未定价的用量;不展示实际节省、质量或生产力推断。
146
+
147
+ ### 估算限制
148
+
149
+ 上游将缓存写入并入 `inputTokens`,无法恢复独立缓存写入数及溢价;因此该部分只能按普通输入费率估算。
150
+ 桶也不保留每次请求的上下文长度,无法重建长上下文阶梯价。价格快照用于全部历史日期,不是历史有效期价格表。
151
+ 不含媒体计费、税费、批量折扣、实际订阅扣款、赠送额度等。覆盖率只说明能套用费率,不保证账单精度。
152
+
153
+ ## 数据源与隐私边界
154
+
155
+ 保留上游 0.10.21 的全部 **28 个 parser**:
156
+
157
+ | 类别 | 工具 |
158
+ |---|---|
159
+ | CLI / 会话日志 | Claude Code、Codex、Grok、Copilot CLI、CraftAgent、Gemini CLI、OpenClaw、Oh My Pi、Pi、Qwen Code、Kimi Code、Amp、Droid、DeepSeek Harness、Trae CLI、WorkBuddy |
160
+ | 本地 DB / 编辑器存储 | Alma、DimAgent、OpenCode、Hermes、Kiro、MiniMax Code、MiMoCode、Cline、Roo Code、ZCode |
161
+ | 来源服务读取 | Cursor:用本机已登录凭据从 cursor.com 下载使用明细;Antigravity:本地 DB,旧版加密历史可通过本机 127.0.0.1 只读 RPC 获取 |
162
+
163
+ 另继承上游 Cindy 本地账本读取,归并到 Codex / Pi,不新增独立 source。
164
+ 继承各 parser 的默认目录、环境变量覆盖、归档和去重规则。常用覆盖如 `CODEX_HOME`、`CLAUDE_CONFIG_DIR`、`VIBE_USAGE_PI_SESSION_DIRS`;完整来源约定以固定版本上游代码为准。
165
+ 不读取 `~/.vibe-usage/config.json`,其中配置的额外根目录不会自动继承;可用对应来源环境变量设置目录。
166
+ 不同工具可提供的数据不同,部分仅有 token,没有会话。缺少或格式不支持的记录可能被上游跳过,不能承诺还原已丢失的历史。
167
+
168
+ **允许获取源数据,不允许上传采集结果:**
169
+
170
+ - 不包含上游 `api.js`、`sync.js`、账号配置、daemon 或上传入口,不需要 VibeCafé API key。
171
+ - 唯一网络边界只允许 Cursor 的固定 GET 导出地址,以及 Antigravity 本机的两种读取 RPC;拒绝 HTTP 重定向和自定义 Cursor 服务地址。
172
+ - 不把统计数据、项目名、消息内容发送给 VibeCafé、模型服务或遥测服务。Cursor 请求仅携带向该来源认证所需的已有凭据。
173
+ - HTML 使用 CSP 禁止连接、远程资源、表单和嵌入对象;唯一脚本以 SHA-256 授权。没有 CDN、远程字体、追踪像素或外链资源。
174
+ - `--offline` 进一步禁止所有来源网络请求;Cursor 无法读取,Antigravity 只能解析本地 DB。
175
+ - 只保存白名单统计字段,不保存 prompt / 回复 / 代码正文。解析器会在本机读取日志来提取用量。
176
+
177
+ Codex 使用独立可丢弃缓存 `~/.pi/usage/cache`(`PI_USAGE_CACHE_DIR` 可覆盖),不碰上游上传状态。
178
+ 首次索引超过上游非交互预算时会报告「部分数据」,再次生成可续建缓存。所有报告均展示读取状态,部分失败不阻止其他来源生成报告。
179
+ 没有后台采集或自动上传任务。**本 package 不卸载或停止已有 Vibe Usage daemon;它若仍在运行,会继续独立上传。**
180
+
181
+ ### 会话与去重
182
+
183
+ 会话按开始日期整体归属;时长不按跨午夜裁剪。活跃秒数是上游能观测到的首条回复至该轮最后事件的估计,并非用户工时。
184
+ 并行会话时长可能相加超过自然时间。会话没有模型、token 或费用,仅作为独立导出保留,不做不可靠的日期 / 项目 join。
185
+ `totalTokens` 不包含缓存读取;`allTokens` 包含缓存读取,两者均单独展示。
186
+ 复用来源级消息 / fork 去重,不应用此前聊天中服务端匿名副本的启发式去重,避免误删本地不同项目的相同用量。
187
+
188
+ ## 开发与验证
189
+
190
+ ```bash
191
+ pnpm --filter @geoqiao/pi-usage test
192
+ pnpm --filter @geoqiao/pi-usage typecheck
193
+ pnpm --filter @geoqiao/pi-usage pack:check
194
+ ```
195
+
196
+ 这是无构建的 JavaScript 包;`typecheck` 工作区钩子执行 JavaScript 语法检查和未修改上游文件哈希验证,不是 TypeScript 类型推导。
197
+ `node:test` 覆盖计价 / 分位、隐私边界、CSV/HTML 安全、CLI、Pi 命令和保留的上游 parser 回归测试。
198
+ 部分上游 SQLite 测试需系统 `sqlite3`;运行时 Node 22.15+ 可使用内置 SQLite。
199
+
200
+ 前端验证(合成数据,不读取个人日志):
201
+
202
+ ```bash
203
+ node packages/pi-usage/scripts/demo.js /tmp/pi-usage-demo
204
+ PLAYWRIGHT_MCP_ALLOW_UNRESTRICTED_FILE_ACCESS=1 playwright-cli -s=pi-usage open file:///tmp/pi-usage-demo/index.html
205
+ playwright-cli -s=pi-usage run-code --filename=packages/pi-usage/scripts/browser-check.js
206
+ playwright-cli -s=pi-usage close
207
+ ```
208
+
209
+ 使用已有 `playwright-cli`,不作为运行时依赖。检查桌面 / 390px 移动端全部视图、组合筛选与下钻、键盘日期查询、信息对话框焦点、长名称与 HTML/CSV 防护、未定价与空状态、分位样本、模型计价排序、分页,以及实际 CSV 内容与筛选后原数据总量 / 日聚合条数一致。验证零外部请求;合成截图写入 `/tmp/pi-usage-bi-demo-*.png`。脚本也可验收已有报告;私有报告请在仓库外的临时目录运行浏览器,以免下载和浏览器快照进入工作区。
210
+ 更新价格需维护者先单独下载公开 `https://models.dev/api.json`,再执行:
211
+
212
+ ```bash
213
+ node packages/pi-usage/scripts/prices.js /path/to/downloaded-catalog.json YYYY-MM-DD
214
+ ```
215
+
216
+ 快照保留来源 URL、下载日期、内容 SHA-256;用户运行报告不会触发此流程。
217
+
218
+ ## 许可与归属
219
+
220
+ MIT。上游解析器版本、提交、原始哈希和本地补丁记录于 [NOTICE](vendor/vibe-usage/NOTICE.md)。
221
+ 价格快照来自 [models.dev](https://models.dev),保留 [MIT license](data/models.dev-LICENSE)。
222
+ 页面是独立实现,只参考仪表盘视觉与信息布局,不包含 VibeCafé 网站代码、品牌资源或个人数据。
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { collect, SOURCES } from '../src/collect.js';
5
+ import { dateKey, shiftDate, validatePrices, normalizeData } from '../src/analytics.js';
6
+ import { writeReport } from '../src/report.js';
7
+
8
+ const HELP = `pi-usage — 获取使用数据,本地生成分析报告;不上传统计数据。
9
+
10
+ pi-usage [--days 90] [--out /path/to/empty-directory]
11
+ --sources pi-coding-agent,codex 只采集指定工具(默认全部 28 类)
12
+ --prices /path/prices.json 本地价格覆盖:{ "models": { "model-id": { "input": 5, "cacheRead": 0.5, "output": 30, "reasoning": 30 } } }
13
+ --timezone Asia/Shanghai 日期分组时区(默认系统时区)
14
+ --input /path/usage.json 从本地 buckets / sessions 重新分析,不采集数据源
15
+ --offline 禁止来源网络请求(Cursor 不可用;Antigravity 仅本地 DB)
16
+ --list-sources 列出数据源
17
+ --help 查看帮助
18
+
19
+ 报告为独立 HTML,双击打开;同目录包含 details.csv、sessions.csv、usage.json。
20
+ 成本为价格快照下的估算,不是账单;未定价模型不会按 0 元计算。
21
+ `;
22
+
23
+ async function main(args = process.argv.slice(2)) {
24
+ const { values } = parseArgs({ args, options: {
25
+ days: { type: 'string', default: '90' }, out: { type: 'string' }, sources: { type: 'string' },
26
+ prices: { type: 'string' }, timezone: { type: 'string' }, input: { type: 'string' },
27
+ offline: { type: 'boolean' }, 'list-sources': { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
28
+ } });
29
+ if (values.help) { console.log(HELP); return; }
30
+ if (values['list-sources']) { console.log(SOURCES.join('\n')); return; }
31
+ const days = Number(values.days);
32
+ if (!Number.isInteger(days) || days < 1 || days > 3660) throw new Error('--days 必须是 1–3660 的整数');
33
+ const timeZone = values.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
34
+ new Intl.DateTimeFormat('en', { timeZone });
35
+ const selected = values.sources ? [...new Set(values.sources.split(','))] : SOURCES;
36
+ if (selected.some(id => !SOURCES.includes(id))) throw new Error('未知数据源;使用 --list-sources 查看');
37
+ const snapshot = JSON.parse(await readFile(new URL('../data/prices.json', import.meta.url), 'utf8'));
38
+ const overrides = values.prices ? JSON.parse(await readFile(values.prices, 'utf8')).models : {};
39
+ validatePrices(snapshot.models); validatePrices(overrides);
40
+ const prices = { ...snapshot.models, ...Object.fromEntries(Object.entries(overrides).map(([id, rate]) => [id, {
41
+ input: rate.input, output: rate.output, cacheRead: rate.cacheRead, reasoning: rate.reasoning,
42
+ provider: 'local-override', reference: '用户本地价格表',
43
+ }])) };
44
+ if (values.offline) process.env.PI_USAGE_OFFLINE = '1';
45
+ const now = new Date();
46
+ const to = dateKey(now, timeZone), from = shiftDate(to, 1 - days);
47
+ let data;
48
+ if (values.input) {
49
+ const raw = JSON.parse(await readFile(values.input, 'utf8'));
50
+ data = { ...normalizeData(raw, { timeZone, prices }), statuses: [{ source: 'local-import', state: 'ok', note: '从本地文件重新计价;来源完整性未验证。' }] };
51
+ } else {
52
+ data = await collect({ sources: selected, timeZone, prices, onProgress: source => console.error(`读取 ${source}…`) });
53
+ }
54
+ data.buckets = data.buckets.filter(row => row.date >= from && row.date <= to);
55
+ data.sessions = data.sessions.filter(row => row.date >= from && row.date <= to);
56
+ const report = {
57
+ schemaVersion: 1, generatedAt: now.toISOString(), from, to, timeZone,
58
+ priceSnapshot: { date: snapshot.snapshotDate, source: snapshot.source, overrideCount: Object.keys(overrides).length },
59
+ prices, ...data,
60
+ };
61
+ const file = await writeReport(report, values.out);
62
+ console.log(file);
63
+ if (data.statuses.some(s => s.state === 'error' || s.state === 'partial' || s.warningCount > 0)) {
64
+ console.error('注意:部分数据源未完整读取;请查看报告中的数据源状态。');
65
+ }
66
+ return file;
67
+ }
68
+
69
+ main().catch(error => { console.error(`pi-usage: ${error.message}`); process.exitCode = 1; });
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 models.dev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.