codebuddy-stats 1.0.0 → 1.1.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 +120 -0
- package/dist/index.js +76 -19
- package/dist/lib/data-loader.js +289 -57
- package/dist/lib/paths.js +31 -8
- package/dist/lib/pricing.js +0 -1
- package/dist/lib/utils.js +0 -1
- package/dist/lib/workspace-resolver.js +210 -0
- package/package.json +4 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/data-loader.js.map +0 -1
- package/dist/lib/paths.js.map +0 -1
- package/dist/lib/pricing.js.map +0 -1
- package/dist/lib/utils.js.map +0 -1
- package/index.js +0 -16
- package/src/index.ts +0 -549
- package/src/lib/data-loader.ts +0 -302
- package/src/lib/paths.ts +0 -45
- package/src/lib/pricing.ts +0 -128
- package/src/lib/utils.ts +0 -61
- package/tsconfig.json +0 -25
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# CodeBuddy Stats
|
|
2
|
+
|
|
3
|
+
一个用于分析 CodeBuddy 系列产品使用成本的命令行工具,支持交互式 TUI 界面和纯文本输出。
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- **双数据源支持** - 同时支持 CodeBuddy Code 和 CodeBuddy IDE(VS Code 扩展)
|
|
8
|
+
- **成本热力图** - 可视化每日 AI 使用成本分布
|
|
9
|
+
- **模型统计** - 按模型分类的费用、请求数、Token 用量
|
|
10
|
+
- **项目统计** - 按项目分类的费用汇总
|
|
11
|
+
- **每日明细** - 查看每日详细使用情况
|
|
12
|
+
- **缓存命中率** - 显示 prompt cache 命中率(Code 模式)
|
|
13
|
+
- **多模型定价** - 支持 GPT-5.2、Claude 4.5、Gemini 等模型
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g codebuddy-stats
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 使用方法
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# 启动交互式 TUI 界面
|
|
25
|
+
cbs
|
|
26
|
+
|
|
27
|
+
# 或使用完整命令名
|
|
28
|
+
codebuddy-stats
|
|
29
|
+
|
|
30
|
+
# 纯文本输出模式
|
|
31
|
+
cbs --no-tui
|
|
32
|
+
|
|
33
|
+
# 只显示最近 7 天的数据
|
|
34
|
+
cbs --days 7
|
|
35
|
+
|
|
36
|
+
# 显示帮助
|
|
37
|
+
cbs --help
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## TUI 界面操作
|
|
41
|
+
|
|
42
|
+
| 按键 | 功能 |
|
|
43
|
+
| ----------- | ------------------------ |
|
|
44
|
+
| `Tab` | 切换到下一个视图 |
|
|
45
|
+
| `Shift+Tab` | 切换到上一个视图 |
|
|
46
|
+
| `s` | 切换数据源(Code / IDE) |
|
|
47
|
+
| `↑` / `k` | 向上滚动 (Daily 视图) |
|
|
48
|
+
| `↓` / `j` | 向下滚动 (Daily 视图) |
|
|
49
|
+
| `r` | 刷新数据 |
|
|
50
|
+
| `q` | 退出 |
|
|
51
|
+
|
|
52
|
+
## 视图说明
|
|
53
|
+
|
|
54
|
+
### Overview
|
|
55
|
+
|
|
56
|
+
显示成本热力图和汇总统计,包括:
|
|
57
|
+
|
|
58
|
+
- 总费用、总 Token 数、总请求数
|
|
59
|
+
- 活跃天数、缓存命中率、日均费用
|
|
60
|
+
- 使用最多的模型和项目
|
|
61
|
+
|
|
62
|
+
### By Model
|
|
63
|
+
|
|
64
|
+
按 AI 模型分类的详细统计表格,包含每个模型的费用、请求数、Token 数和平均每次请求费用。
|
|
65
|
+
|
|
66
|
+
### By Project
|
|
67
|
+
|
|
68
|
+
按项目分类的费用统计,方便了解不同项目的 AI 使用成本。
|
|
69
|
+
|
|
70
|
+
工具会自动将项目标识解析为可读路径:
|
|
71
|
+
- **Code 模式**: 将 `Users-anoti-Documents-project-xxx` 还原为 `~/Documents/project/xxx`
|
|
72
|
+
- **IDE 模式**: 将 MD5 hash 映射为实际工作区路径
|
|
73
|
+
|
|
74
|
+
### Daily
|
|
75
|
+
|
|
76
|
+
每日使用明细,显示日期、费用、请求数以及当天使用最多的模型和项目。
|
|
77
|
+
|
|
78
|
+
## 支持的模型
|
|
79
|
+
|
|
80
|
+
| 模型 | 输入价格 | 输出价格 |
|
|
81
|
+
| --------------- | -------- | -------- |
|
|
82
|
+
| GPT-5.2 | $1.75/M | $14.00/M |
|
|
83
|
+
| GPT-5.1 / GPT-5 | $1.25/M | $10.00/M |
|
|
84
|
+
| GPT-5-mini | $0.25/M | $2.00/M |
|
|
85
|
+
| GPT-5-nano | $0.05/M | $0.40/M |
|
|
86
|
+
| Claude Opus 4.5 | $5.00/M | $25.00/M |
|
|
87
|
+
| Claude 4.5 | $3.00/M | $15.00/M |
|
|
88
|
+
| Gemini 3 Pro | $2.00/M | $12.00/M |
|
|
89
|
+
| Gemini 2.5 Pro | $1.25/M | $10.00/M |
|
|
90
|
+
|
|
91
|
+
_价格单位:USD / 1M tokens,部分模型支持分层定价_
|
|
92
|
+
|
|
93
|
+
## 数据来源
|
|
94
|
+
|
|
95
|
+
工具支持两种数据源,可在 TUI 界面中按 `s` 键切换:
|
|
96
|
+
|
|
97
|
+
### CodeBuddy Code(CLI 版本)
|
|
98
|
+
|
|
99
|
+
- **macOS**: `~/.codebuddy/projects/`
|
|
100
|
+
- **Windows**: `%APPDATA%/CodeBuddy/projects/`
|
|
101
|
+
- **Linux**: `$XDG_CONFIG_HOME/codebuddy/projects/` 或 `~/.codebuddy/projects/`
|
|
102
|
+
|
|
103
|
+
特点:包含完整的缓存命中/写入 token 数据,可计算缓存命中率和精确成本。
|
|
104
|
+
|
|
105
|
+
### CodeBuddy IDE(VS Code 扩展)
|
|
106
|
+
|
|
107
|
+
- **macOS**: `~/Library/Application Support/CodeBuddyExtension/Data/`
|
|
108
|
+
- **Windows**: `%APPDATA%/CodeBuddyExtension/Data/`
|
|
109
|
+
- **Linux**: `$XDG_CONFIG_HOME/CodeBuddyExtension/Data/` 或 `~/.config/CodeBuddyExtension/Data/`
|
|
110
|
+
|
|
111
|
+
特点:基于 input/output tokens 估算成本,不包含缓存相关数据。
|
|
112
|
+
|
|
113
|
+
## 系统要求
|
|
114
|
+
|
|
115
|
+
- Node.js >= 18
|
|
116
|
+
- 终端支持 Unicode 字符(用于热力图显示)
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
ISC
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
2
5
|
import blessed from 'blessed';
|
|
3
6
|
import { loadUsageData } from './lib/data-loader.js';
|
|
4
|
-
import {
|
|
7
|
+
import { resolveProjectName } from './lib/workspace-resolver.js';
|
|
5
8
|
import { formatCost, formatNumber, formatPercent, formatTokens, truncate } from './lib/utils.js';
|
|
9
|
+
// 读取 package.json 获取版本号
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const pkgPath = path.resolve(__dirname, '../package.json');
|
|
12
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
13
|
+
const VERSION = pkg.version;
|
|
6
14
|
// 解析命令行参数
|
|
7
15
|
function parseArgs() {
|
|
8
16
|
const args = process.argv.slice(2);
|
|
@@ -59,7 +67,7 @@ function getHeatChar(cost, maxCost) {
|
|
|
59
67
|
return '█';
|
|
60
68
|
}
|
|
61
69
|
// 渲染 Overview 视图
|
|
62
|
-
function renderOverview(box, data, width) {
|
|
70
|
+
function renderOverview(box, data, width, note) {
|
|
63
71
|
const { dailySummary, grandTotal, topModel, topProject, cacheHitRate, activeDays } = data;
|
|
64
72
|
const heatmap = generateHeatmapData(dailySummary);
|
|
65
73
|
// 根据宽度计算热力图周数
|
|
@@ -135,13 +143,16 @@ function renderOverview(box, data, width) {
|
|
|
135
143
|
}
|
|
136
144
|
if (topProject) {
|
|
137
145
|
const projectMaxLen = width >= 100 ? 60 : 35;
|
|
138
|
-
const shortName =
|
|
146
|
+
const shortName = resolveProjectName(topProject.name, data.workspaceMappings);
|
|
139
147
|
content += `{cyan-fg}Top project:{/cyan-fg} ${truncate(shortName, projectMaxLen)} (${formatCost(topProject.cost)})\n`;
|
|
140
148
|
}
|
|
149
|
+
if (note) {
|
|
150
|
+
content += `\n{gray-fg}备注:${note}{/gray-fg}\n`;
|
|
151
|
+
}
|
|
141
152
|
box.setContent(content);
|
|
142
153
|
}
|
|
143
154
|
// 渲染 By Model 视图
|
|
144
|
-
function renderByModel(box, data, width) {
|
|
155
|
+
function renderByModel(box, data, width, note) {
|
|
145
156
|
const { modelTotals, grandTotal } = data;
|
|
146
157
|
const sorted = Object.entries(modelTotals).sort((a, b) => b[1].cost - a[1].cost);
|
|
147
158
|
// 根据宽度计算列宽
|
|
@@ -176,10 +187,13 @@ function renderByModel(box, data, width) {
|
|
|
176
187
|
formatNumber(grandTotal.requests).padStart(12) +
|
|
177
188
|
formatTokens(grandTotal.tokens).padStart(12) +
|
|
178
189
|
'{/bold}\n';
|
|
190
|
+
if (note) {
|
|
191
|
+
content += `\n{gray-fg}备注:${note}{/gray-fg}\n`;
|
|
192
|
+
}
|
|
179
193
|
box.setContent(content);
|
|
180
194
|
}
|
|
181
195
|
// 渲染 By Project 视图
|
|
182
|
-
function renderByProject(box, data, width) {
|
|
196
|
+
function renderByProject(box, data, width, note) {
|
|
183
197
|
const { projectTotals, grandTotal } = data;
|
|
184
198
|
const sorted = Object.entries(projectTotals).sort((a, b) => b[1].cost - a[1].cost);
|
|
185
199
|
// 根据宽度计算列宽
|
|
@@ -197,7 +211,7 @@ function renderByProject(box, data, width) {
|
|
|
197
211
|
'{/underline}\n';
|
|
198
212
|
for (const [projectName, stats] of sorted) {
|
|
199
213
|
// 简化项目名
|
|
200
|
-
const shortName =
|
|
214
|
+
const shortName = resolveProjectName(projectName, data.workspaceMappings);
|
|
201
215
|
content +=
|
|
202
216
|
truncate(shortName, projectCol - 1).padEnd(projectCol) +
|
|
203
217
|
formatCost(stats.cost).padStart(12) +
|
|
@@ -213,10 +227,13 @@ function renderByProject(box, data, width) {
|
|
|
213
227
|
formatNumber(grandTotal.requests).padStart(12) +
|
|
214
228
|
formatTokens(grandTotal.tokens).padStart(12) +
|
|
215
229
|
'{/bold}\n';
|
|
230
|
+
if (note) {
|
|
231
|
+
content += `\n{gray-fg}备注:${note}{/gray-fg}\n`;
|
|
232
|
+
}
|
|
216
233
|
box.setContent(content);
|
|
217
234
|
}
|
|
218
235
|
// 渲染 Daily 视图
|
|
219
|
-
function renderDaily(box, data, scrollOffset = 0, width) {
|
|
236
|
+
function renderDaily(box, data, scrollOffset = 0, width, note) {
|
|
220
237
|
const { dailySummary, dailyData } = data;
|
|
221
238
|
const sortedDates = Object.keys(dailySummary).sort().reverse();
|
|
222
239
|
// 根据宽度计算列宽
|
|
@@ -259,7 +276,7 @@ function renderDaily(box, data, scrollOffset = 0, width) {
|
|
|
259
276
|
topProject = { name: project, cost: projectCost };
|
|
260
277
|
}
|
|
261
278
|
}
|
|
262
|
-
const shortProject =
|
|
279
|
+
const shortProject = resolveProjectName(topProject.name, data.workspaceMappings);
|
|
263
280
|
content +=
|
|
264
281
|
date.padEnd(dateCol) +
|
|
265
282
|
formatCost(daySummary.cost).padStart(costCol) +
|
|
@@ -271,6 +288,9 @@ function renderDaily(box, data, scrollOffset = 0, width) {
|
|
|
271
288
|
if (sortedDates.length > 20) {
|
|
272
289
|
content += `\n{gray-fg}Showing ${scrollOffset + 1}-${Math.min(scrollOffset + 20, sortedDates.length)} of ${sortedDates.length} days (↑↓ to scroll){/gray-fg}`;
|
|
273
290
|
}
|
|
291
|
+
if (note) {
|
|
292
|
+
content += `\n\n{gray-fg}备注:${note}{/gray-fg}\n`;
|
|
293
|
+
}
|
|
274
294
|
box.setContent(content);
|
|
275
295
|
}
|
|
276
296
|
// 纯文本输出模式
|
|
@@ -287,7 +307,8 @@ function printTextReport(data) {
|
|
|
287
307
|
console.log(`\nTop model: ${topModel.id} (${formatCost(topModel.cost)})`);
|
|
288
308
|
}
|
|
289
309
|
if (topProject) {
|
|
290
|
-
|
|
310
|
+
const shortName = resolveProjectName(topProject.name, data.workspaceMappings);
|
|
311
|
+
console.log(`Top project: ${shortName}`);
|
|
291
312
|
console.log(` (${formatCost(topProject.cost)})`);
|
|
292
313
|
}
|
|
293
314
|
console.log('\n' + '-'.repeat(50));
|
|
@@ -300,7 +321,7 @@ function printTextReport(data) {
|
|
|
300
321
|
for (const [project, stats] of Object.entries(projectTotals)
|
|
301
322
|
.sort((a, b) => b[1].cost - a[1].cost)
|
|
302
323
|
.slice(0, 10)) {
|
|
303
|
-
const shortName =
|
|
324
|
+
const shortName = resolveProjectName(project, data.workspaceMappings);
|
|
304
325
|
console.log(` ${truncate(shortName, 40)}: ${formatCost(stats.cost)}`); // eslint-disable-line no-console
|
|
305
326
|
}
|
|
306
327
|
console.log('\n' + '='.repeat(50) + '\n');
|
|
@@ -309,7 +330,8 @@ function printTextReport(data) {
|
|
|
309
330
|
async function main() {
|
|
310
331
|
const options = parseArgs();
|
|
311
332
|
console.log('Loading data...');
|
|
312
|
-
let
|
|
333
|
+
let currentSource = 'code';
|
|
334
|
+
let data = await loadUsageData({ days: options.days, source: currentSource });
|
|
313
335
|
if (options.noTui) {
|
|
314
336
|
printTextReport(data);
|
|
315
337
|
return;
|
|
@@ -318,6 +340,8 @@ async function main() {
|
|
|
318
340
|
const screen = blessed.screen({
|
|
319
341
|
smartCSR: true,
|
|
320
342
|
title: 'CodeBuddy Cost Analyzer',
|
|
343
|
+
forceUnicode: true,
|
|
344
|
+
fullUnicode: true,
|
|
321
345
|
});
|
|
322
346
|
// Tab 状态
|
|
323
347
|
const tabs = ['Overview', 'By Model', 'By Project', 'Daily'];
|
|
@@ -374,6 +398,16 @@ async function main() {
|
|
|
374
398
|
// 更新 Tab 栏
|
|
375
399
|
function updateTabBar() {
|
|
376
400
|
let content = ' Cost Analysis ';
|
|
401
|
+
content += '{gray-fg}Source:{/gray-fg} ';
|
|
402
|
+
if (currentSource === 'code') {
|
|
403
|
+
content += '{black-fg}{green-bg} Code {/green-bg}{/black-fg} ';
|
|
404
|
+
content += '{gray-fg}IDE{/gray-fg} ';
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
content += '{gray-fg}Code{/gray-fg} ';
|
|
408
|
+
content += '{black-fg}{green-bg} IDE {/green-bg}{/black-fg} ';
|
|
409
|
+
}
|
|
410
|
+
content += '{gray-fg}Views:{/gray-fg} ';
|
|
377
411
|
for (let i = 0; i < tabs.length; i++) {
|
|
378
412
|
if (i === currentTab) {
|
|
379
413
|
content += `{black-fg}{green-bg} ${tabs[i]} {/green-bg}{/black-fg} `;
|
|
@@ -382,31 +416,39 @@ async function main() {
|
|
|
382
416
|
content += `{gray-fg}${tabs[i]}{/gray-fg} `;
|
|
383
417
|
}
|
|
384
418
|
}
|
|
385
|
-
content += ' {gray-fg}(Tab
|
|
419
|
+
content += ' {gray-fg}(Tab view, s source){/gray-fg}';
|
|
386
420
|
tabBar.setContent(content);
|
|
387
421
|
}
|
|
388
422
|
// 更新内容
|
|
389
423
|
function updateContent() {
|
|
390
424
|
const width = Number(screen.width) || 80;
|
|
425
|
+
const note = currentSource === 'code'
|
|
426
|
+
? `针对 CodeBuddy Code ≤ 2.20.0 版本产生的数据,由于没有请求级别的 model ID,用量是基于当前 CodeBuddy Code 设置的 model ID(${data.defaultModelId})计算价格的`
|
|
427
|
+
: 'IDE 的 usage 不包含缓存命中/写入 tokens,无法计算缓存相关价格与命中率;成本按 input/output tokens 估算';
|
|
391
428
|
switch (currentTab) {
|
|
392
429
|
case 0:
|
|
393
|
-
renderOverview(contentBox, data, width);
|
|
430
|
+
renderOverview(contentBox, data, width, note);
|
|
394
431
|
break;
|
|
395
432
|
case 1:
|
|
396
|
-
renderByModel(contentBox, data, width);
|
|
433
|
+
renderByModel(contentBox, data, width, note);
|
|
397
434
|
break;
|
|
398
435
|
case 2:
|
|
399
|
-
renderByProject(contentBox, data, width);
|
|
436
|
+
renderByProject(contentBox, data, width, note);
|
|
400
437
|
break;
|
|
401
438
|
case 3:
|
|
402
|
-
renderDaily(contentBox, data, dailyScrollOffset, width);
|
|
439
|
+
renderDaily(contentBox, data, dailyScrollOffset, width, note);
|
|
403
440
|
break;
|
|
404
441
|
}
|
|
405
442
|
}
|
|
406
443
|
// 更新状态栏
|
|
407
444
|
function updateStatusBar() {
|
|
408
445
|
const daysInfo = options.days ? `Last ${options.days} days` : 'All time';
|
|
409
|
-
|
|
446
|
+
const sourceInfo = currentSource === 'code' ? 'Code' : 'IDE';
|
|
447
|
+
const leftContent = ` ${daysInfo} | Source: ${sourceInfo} | Total: ${formatCost(data.grandTotal.cost)} | q quit, Tab view, s source, r refresh`;
|
|
448
|
+
const rightContent = `v${VERSION} `;
|
|
449
|
+
const width = Number(screen.width) || 80;
|
|
450
|
+
const padding = Math.max(0, width - leftContent.length - rightContent.length);
|
|
451
|
+
statusBar.setContent(leftContent + ' '.repeat(padding) + rightContent);
|
|
410
452
|
}
|
|
411
453
|
// 键盘事件
|
|
412
454
|
screen.key(['tab'], () => {
|
|
@@ -446,7 +488,7 @@ async function main() {
|
|
|
446
488
|
statusBar.setContent(' {yellow-fg}Reloading...{/yellow-fg}');
|
|
447
489
|
screen.render();
|
|
448
490
|
try {
|
|
449
|
-
data = await loadUsageData({ days: options.days });
|
|
491
|
+
data = await loadUsageData({ days: options.days, source: currentSource });
|
|
450
492
|
dailyScrollOffset = 0;
|
|
451
493
|
updateTabBar();
|
|
452
494
|
updateContent();
|
|
@@ -457,6 +499,22 @@ async function main() {
|
|
|
457
499
|
}
|
|
458
500
|
screen.render();
|
|
459
501
|
});
|
|
502
|
+
screen.key(['s'], async () => {
|
|
503
|
+
statusBar.setContent(' {yellow-fg}Switching source...{/yellow-fg}');
|
|
504
|
+
screen.render();
|
|
505
|
+
try {
|
|
506
|
+
currentSource = currentSource === 'code' ? 'ide' : 'code';
|
|
507
|
+
data = await loadUsageData({ days: options.days, source: currentSource });
|
|
508
|
+
dailyScrollOffset = 0;
|
|
509
|
+
updateTabBar();
|
|
510
|
+
updateContent();
|
|
511
|
+
updateStatusBar();
|
|
512
|
+
}
|
|
513
|
+
catch (err) {
|
|
514
|
+
statusBar.setContent(` {red-fg}Switch source failed: ${String(err)}{/red-fg}`);
|
|
515
|
+
}
|
|
516
|
+
screen.render();
|
|
517
|
+
});
|
|
460
518
|
// 监听窗口大小变化
|
|
461
519
|
screen.on('resize', () => {
|
|
462
520
|
updateContent();
|
|
@@ -472,4 +530,3 @@ main().catch(err => {
|
|
|
472
530
|
console.error('Error:', err);
|
|
473
531
|
process.exit(1);
|
|
474
532
|
});
|
|
475
|
-
//# sourceMappingURL=index.js.map
|