codebuddy-stats 1.0.0 → 1.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.
@@ -0,0 +1,211 @@
1
+ import fs from 'node:fs/promises';
2
+ import fsSync from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import crypto from 'node:crypto';
6
+ import { getWorkspaceStorageDir } from './paths.js';
7
+ // 缓存已解析的 CodeBuddy Code 路径名
8
+ const codePathCache = new Map();
9
+ /**
10
+ * 从 folder URI 提取纯路径(用于计算 MD5)
11
+ */
12
+ function extractPathFromUri(folderUri) {
13
+ // 处理本地文件路径: file:///path/to/folder
14
+ if (folderUri.startsWith('file://')) {
15
+ try {
16
+ const url = new URL(folderUri);
17
+ return decodeURIComponent(url.pathname);
18
+ }
19
+ catch {
20
+ return decodeURIComponent(folderUri.replace('file://', ''));
21
+ }
22
+ }
23
+ // 处理远程路径: vscode-remote://codebuddy-remote-ssh%2B.../path
24
+ if (folderUri.startsWith('vscode-remote://')) {
25
+ try {
26
+ const url = new URL(folderUri);
27
+ return decodeURIComponent(url.pathname);
28
+ }
29
+ catch {
30
+ const match = folderUri.match(/vscode-remote:\/\/[^/]+(.+)$/);
31
+ if (match?.[1]) {
32
+ return decodeURIComponent(match[1]);
33
+ }
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+ /**
39
+ * 格式化路径用于显示(简化 home 目录)
40
+ */
41
+ function formatDisplayPath(p) {
42
+ const home = os.homedir();
43
+ if (p.startsWith(home)) {
44
+ return '~' + p.slice(home.length);
45
+ }
46
+ return p;
47
+ }
48
+ /**
49
+ * 从 folder URI 生成用于显示的友好路径
50
+ */
51
+ function getDisplayPath(folderUri) {
52
+ // 本地路径
53
+ if (folderUri.startsWith('file://')) {
54
+ const p = extractPathFromUri(folderUri);
55
+ if (p) {
56
+ return formatDisplayPath(p);
57
+ }
58
+ }
59
+ // 远程路径
60
+ if (folderUri.startsWith('vscode-remote://')) {
61
+ const p = extractPathFromUri(folderUri);
62
+ if (p) {
63
+ const hostMatch = folderUri.match(/vscode-remote:\/\/codebuddy-remote-ssh%2B([^/]+)/);
64
+ if (hostMatch?.[1]) {
65
+ let host = decodeURIComponent(hostMatch[1]);
66
+ host = host.replace(/_x([0-9a-fA-F]{2})_/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
67
+ host = host.replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
68
+ if (host.includes('@')) {
69
+ const parts = host.split('@');
70
+ host = parts[parts.length - 1]?.split(':')[0] || host;
71
+ }
72
+ if (host.length > 20) {
73
+ host = host.slice(0, 17) + '...';
74
+ }
75
+ return `[${host}]${p}`;
76
+ }
77
+ return `[remote]${p}`;
78
+ }
79
+ }
80
+ return folderUri;
81
+ }
82
+ /**
83
+ * 计算路径的 MD5 hash(CodeBuddyExtension 使用纯路径计算)
84
+ */
85
+ function computePathHash(p) {
86
+ return crypto.createHash('md5').update(p).digest('hex');
87
+ }
88
+ /**
89
+ * 加载所有工作区映射
90
+ */
91
+ export async function loadWorkspaceMappings() {
92
+ const mappings = new Map();
93
+ const storageDir = getWorkspaceStorageDir();
94
+ let entries = [];
95
+ try {
96
+ entries = await fs.readdir(storageDir, { withFileTypes: true });
97
+ }
98
+ catch {
99
+ return mappings;
100
+ }
101
+ for (const entry of entries) {
102
+ if (!entry.isDirectory())
103
+ continue;
104
+ const workspaceJsonPath = path.join(storageDir, entry.name, 'workspace.json');
105
+ try {
106
+ const content = await fs.readFile(workspaceJsonPath, 'utf8');
107
+ const data = JSON.parse(content);
108
+ const folderUri = data.folder;
109
+ if (!folderUri)
110
+ continue;
111
+ const extractedPath = extractPathFromUri(folderUri);
112
+ if (!extractedPath)
113
+ continue;
114
+ const hash = computePathHash(extractedPath);
115
+ const displayPath = getDisplayPath(folderUri);
116
+ mappings.set(hash, { hash, folderUri, displayPath });
117
+ }
118
+ catch {
119
+ // 跳过无法读取的文件
120
+ }
121
+ }
122
+ return mappings;
123
+ }
124
+ /**
125
+ * 检查路径是否存在(同步版本,用于路径探测)
126
+ */
127
+ function pathExistsSync(p) {
128
+ try {
129
+ fsSync.accessSync(p);
130
+ return true;
131
+ }
132
+ catch {
133
+ return false;
134
+ }
135
+ }
136
+ /**
137
+ * 尝试将 CodeBuddy Code 的项目名(路径中 / 替换为 -)还原为真实路径
138
+ * 使用回溯搜索,因为目录名本身可能包含 -
139
+ *
140
+ * 例如: "Users-anoti-Documents-project-codebudy-cost-analyzer"
141
+ * -> "/Users/anoti/Documents/project/codebudy-cost-analyzer"
142
+ */
143
+ function tryResolveCodePath(name) {
144
+ // 检查缓存
145
+ const cached = codePathCache.get(name);
146
+ if (cached !== undefined) {
147
+ return cached || null;
148
+ }
149
+ const parts = name.split('-');
150
+ if (parts.length < 2) {
151
+ codePathCache.set(name, '');
152
+ return null;
153
+ }
154
+ // 回溯搜索:尝试不同的分割方式
155
+ function backtrack(index, currentPath) {
156
+ if (index >= parts.length) {
157
+ // 检查完整路径是否存在
158
+ if (pathExistsSync(currentPath)) {
159
+ return currentPath;
160
+ }
161
+ return null;
162
+ }
163
+ // 尝试从当前位置开始,合并不同数量的 parts
164
+ for (let end = index; end < parts.length; end++) {
165
+ const segment = parts.slice(index, end + 1).join('-');
166
+ const newPath = currentPath ? `${currentPath}/${segment}` : `/${segment}`;
167
+ // 如果这不是最后一段,检查目录是否存在
168
+ if (end < parts.length - 1) {
169
+ if (pathExistsSync(newPath)) {
170
+ const result = backtrack(end + 1, newPath);
171
+ if (result)
172
+ return result;
173
+ }
174
+ }
175
+ else {
176
+ // 最后一段,检查完整路径
177
+ if (pathExistsSync(newPath)) {
178
+ return newPath;
179
+ }
180
+ }
181
+ }
182
+ return null;
183
+ }
184
+ const result = backtrack(0, '');
185
+ codePathCache.set(name, result || '');
186
+ return result;
187
+ }
188
+ /**
189
+ * 解析项目名称
190
+ * - MD5 hash (32位十六进制): 从 IDE workspaceMappings 查找
191
+ * - 路径格式 (包含 -): 尝试还原 CodeBuddy Code 的路径格式
192
+ */
193
+ export function resolveProjectName(name, mappings) {
194
+ // IDE source: MD5 hash
195
+ if (mappings && /^[a-f0-9]{32}$/.test(name)) {
196
+ const mapping = mappings.get(name);
197
+ if (mapping) {
198
+ return mapping.displayPath;
199
+ }
200
+ }
201
+ // Code source: 路径中 / 替换为 - 的格式
202
+ // 特征:以大写字母开头(如 Users-、home-),包含 -
203
+ if (/^[A-Za-z]/.test(name) && name.includes('-')) {
204
+ const resolved = tryResolveCodePath(name);
205
+ if (resolved) {
206
+ return formatDisplayPath(resolved);
207
+ }
208
+ }
209
+ return name;
210
+ }
211
+ //# sourceMappingURL=workspace-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-resolver.js","sourceRoot":"","sources":["../../src/lib/workspace-resolver.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebuddy-stats",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "repository": {
package/src/index.ts CHANGED
@@ -1,12 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
3
6
  import blessed from 'blessed'
4
7
 
5
8
  import { loadUsageData } from './lib/data-loader.js'
6
9
  import type { AnalysisData } from './lib/data-loader.js'
7
- import { shortenProjectName } from './lib/paths.js'
10
+ import { resolveProjectName } from './lib/workspace-resolver.js'
8
11
  import { formatCost, formatNumber, formatPercent, formatTokens, truncate } from './lib/utils.js'
9
12
 
13
+ // 读取 package.json 获取版本号
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
15
+ const pkgPath = path.resolve(__dirname, '../package.json')
16
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { version: string }
17
+ const VERSION = pkg.version
18
+
10
19
  type CliOptions = {
11
20
  days: number | null
12
21
  noTui: boolean
@@ -73,7 +82,7 @@ function getHeatChar(cost: number, maxCost: number): string {
73
82
  }
74
83
 
75
84
  // 渲染 Overview 视图
76
- function renderOverview(box: any, data: AnalysisData, width: number): void {
85
+ function renderOverview(box: any, data: AnalysisData, width: number, note: string): void {
77
86
  const { dailySummary, grandTotal, topModel, topProject, cacheHitRate, activeDays } = data
78
87
  const heatmap = generateHeatmapData(dailySummary)
79
88
 
@@ -159,15 +168,19 @@ function renderOverview(box: any, data: AnalysisData, width: number): void {
159
168
  }
160
169
  if (topProject) {
161
170
  const projectMaxLen = width >= 100 ? 60 : 35
162
- const shortName = shortenProjectName(topProject.name)
171
+ const shortName = resolveProjectName(topProject.name, data.workspaceMappings)
163
172
  content += `{cyan-fg}Top project:{/cyan-fg} ${truncate(shortName, projectMaxLen)} (${formatCost(topProject.cost)})\n`
164
173
  }
165
174
 
175
+ if (note) {
176
+ content += `\n{gray-fg}备注:${note}{/gray-fg}\n`
177
+ }
178
+
166
179
  box.setContent(content)
167
180
  }
168
181
 
169
182
  // 渲染 By Model 视图
170
- function renderByModel(box: any, data: AnalysisData, width: number): void {
183
+ function renderByModel(box: any, data: AnalysisData, width: number, note: string): void {
171
184
  const { modelTotals, grandTotal } = data
172
185
  const sorted = Object.entries(modelTotals).sort((a, b) => b[1].cost - a[1].cost)
173
186
 
@@ -207,11 +220,15 @@ function renderByModel(box: any, data: AnalysisData, width: number): void {
207
220
  formatTokens(grandTotal.tokens).padStart(12) +
208
221
  '{/bold}\n'
209
222
 
223
+ if (note) {
224
+ content += `\n{gray-fg}备注:${note}{/gray-fg}\n`
225
+ }
226
+
210
227
  box.setContent(content)
211
228
  }
212
229
 
213
230
  // 渲染 By Project 视图
214
- function renderByProject(box: any, data: AnalysisData, width: number): void {
231
+ function renderByProject(box: any, data: AnalysisData, width: number, note: string): void {
215
232
  const { projectTotals, grandTotal } = data
216
233
  const sorted = Object.entries(projectTotals).sort((a, b) => b[1].cost - a[1].cost)
217
234
 
@@ -232,7 +249,7 @@ function renderByProject(box: any, data: AnalysisData, width: number): void {
232
249
 
233
250
  for (const [projectName, stats] of sorted) {
234
251
  // 简化项目名
235
- const shortName = shortenProjectName(projectName)
252
+ const shortName = resolveProjectName(projectName, data.workspaceMappings)
236
253
  content +=
237
254
  truncate(shortName, projectCol - 1).padEnd(projectCol) +
238
255
  formatCost(stats.cost).padStart(12) +
@@ -250,11 +267,15 @@ function renderByProject(box: any, data: AnalysisData, width: number): void {
250
267
  formatTokens(grandTotal.tokens).padStart(12) +
251
268
  '{/bold}\n'
252
269
 
270
+ if (note) {
271
+ content += `\n{gray-fg}备注:${note}{/gray-fg}\n`
272
+ }
273
+
253
274
  box.setContent(content)
254
275
  }
255
276
 
256
277
  // 渲染 Daily 视图
257
- function renderDaily(box: any, data: AnalysisData, scrollOffset = 0, width: number): void {
278
+ function renderDaily(box: any, data: AnalysisData, scrollOffset = 0, width: number, note: string): void {
258
279
  const { dailySummary, dailyData } = data
259
280
  const sortedDates = Object.keys(dailySummary).sort().reverse()
260
281
 
@@ -303,7 +324,7 @@ function renderDaily(box: any, data: AnalysisData, scrollOffset = 0, width: numb
303
324
  }
304
325
  }
305
326
 
306
- const shortProject = shortenProjectName(topProject.name)
327
+ const shortProject = resolveProjectName(topProject.name, data.workspaceMappings)
307
328
 
308
329
  content +=
309
330
  date.padEnd(dateCol) +
@@ -318,6 +339,10 @@ function renderDaily(box: any, data: AnalysisData, scrollOffset = 0, width: numb
318
339
  content += `\n{gray-fg}Showing ${scrollOffset + 1}-${Math.min(scrollOffset + 20, sortedDates.length)} of ${sortedDates.length} days (↑↓ to scroll){/gray-fg}`
319
340
  }
320
341
 
342
+ if (note) {
343
+ content += `\n\n{gray-fg}备注:${note}{/gray-fg}\n`
344
+ }
345
+
321
346
  box.setContent(content)
322
347
  }
323
348
 
@@ -338,7 +363,8 @@ function printTextReport(data: AnalysisData): void {
338
363
  console.log(`\nTop model: ${topModel.id} (${formatCost(topModel.cost)})`)
339
364
  }
340
365
  if (topProject) {
341
- console.log(`Top project: ${topProject.name}`)
366
+ const shortName = resolveProjectName(topProject.name, data.workspaceMappings)
367
+ console.log(`Top project: ${shortName}`)
342
368
  console.log(` (${formatCost(topProject.cost)})`)
343
369
  }
344
370
 
@@ -353,7 +379,7 @@ function printTextReport(data: AnalysisData): void {
353
379
  for (const [project, stats] of Object.entries(projectTotals)
354
380
  .sort((a, b) => b[1].cost - a[1].cost)
355
381
  .slice(0, 10)) {
356
- const shortName = shortenProjectName(project)
382
+ const shortName = resolveProjectName(project, data.workspaceMappings)
357
383
  console.log(` ${truncate(shortName, 40)}: ${formatCost(stats.cost)}`) // eslint-disable-line no-console
358
384
  }
359
385
 
@@ -365,7 +391,8 @@ async function main(): Promise<void> {
365
391
  const options = parseArgs()
366
392
 
367
393
  console.log('Loading data...')
368
- let data = await loadUsageData({ days: options.days })
394
+ let currentSource: 'code' | 'ide' = 'code'
395
+ let data = await loadUsageData({ days: options.days, source: currentSource })
369
396
 
370
397
  if (options.noTui) {
371
398
  printTextReport(data)
@@ -376,6 +403,8 @@ async function main(): Promise<void> {
376
403
  const screen = blessed.screen({
377
404
  smartCSR: true,
378
405
  title: 'CodeBuddy Cost Analyzer',
406
+ forceUnicode: true,
407
+ fullUnicode: true,
379
408
  })
380
409
 
381
410
  // Tab 状态
@@ -438,6 +467,18 @@ async function main(): Promise<void> {
438
467
  // 更新 Tab 栏
439
468
  function updateTabBar(): void {
440
469
  let content = ' Cost Analysis '
470
+
471
+ content += '{gray-fg}Source:{/gray-fg} '
472
+ if (currentSource === 'code') {
473
+ content += '{black-fg}{green-bg} Code {/green-bg}{/black-fg} '
474
+ content += '{gray-fg}IDE{/gray-fg} '
475
+ } else {
476
+ content += '{gray-fg}Code{/gray-fg} '
477
+ content += '{black-fg}{green-bg} IDE {/green-bg}{/black-fg} '
478
+ }
479
+
480
+ content += '{gray-fg}Views:{/gray-fg} '
481
+
441
482
  for (let i = 0; i < tabs.length; i++) {
442
483
  if (i === currentTab) {
443
484
  content += `{black-fg}{green-bg} ${tabs[i]} {/green-bg}{/black-fg} `
@@ -445,25 +486,31 @@ async function main(): Promise<void> {
445
486
  content += `{gray-fg}${tabs[i]}{/gray-fg} `
446
487
  }
447
488
  }
448
- content += ' {gray-fg}(Tab to switch){/gray-fg}'
489
+ content += ' {gray-fg}(Tab view, s source){/gray-fg}'
449
490
  tabBar.setContent(content)
450
491
  }
451
492
 
452
493
  // 更新内容
453
494
  function updateContent(): void {
454
495
  const width = Number(screen.width) || 80
496
+
497
+ const note =
498
+ currentSource === 'code'
499
+ ? `针对 CodeBuddy Code ≤ 2.20.0 版本产生的数据,由于没有请求级别的 model ID,用量是基于当前 CodeBuddy Code 设置的 model ID(${data.defaultModelId})计算价格的`
500
+ : 'IDE 的 usage 不包含缓存命中/写入 tokens,无法计算缓存相关价格与命中率;成本按 input/output tokens 估算'
501
+
455
502
  switch (currentTab) {
456
503
  case 0:
457
- renderOverview(contentBox, data, width)
504
+ renderOverview(contentBox, data, width, note)
458
505
  break
459
506
  case 1:
460
- renderByModel(contentBox, data, width)
507
+ renderByModel(contentBox, data, width, note)
461
508
  break
462
509
  case 2:
463
- renderByProject(contentBox, data, width)
510
+ renderByProject(contentBox, data, width, note)
464
511
  break
465
512
  case 3:
466
- renderDaily(contentBox, data, dailyScrollOffset, width)
513
+ renderDaily(contentBox, data, dailyScrollOffset, width, note)
467
514
  break
468
515
  }
469
516
  }
@@ -471,9 +518,12 @@ async function main(): Promise<void> {
471
518
  // 更新状态栏
472
519
  function updateStatusBar(): void {
473
520
  const daysInfo = options.days ? `Last ${options.days} days` : 'All time'
474
- statusBar.setContent(
475
- ` ${daysInfo} | Total: ${formatCost(data.grandTotal.cost)} | q quit, Tab switch, r refresh`
476
- )
521
+ const sourceInfo = currentSource === 'code' ? 'Code' : 'IDE'
522
+ const leftContent = ` ${daysInfo} | Source: ${sourceInfo} | Total: ${formatCost(data.grandTotal.cost)} | q quit, Tab view, s source, r refresh`
523
+ const rightContent = `v${VERSION} `
524
+ const width = Number(screen.width) || 80
525
+ const padding = Math.max(0, width - leftContent.length - rightContent.length)
526
+ statusBar.setContent(leftContent + ' '.repeat(padding) + rightContent)
477
527
  }
478
528
 
479
529
  // 键盘事件
@@ -519,7 +569,7 @@ async function main(): Promise<void> {
519
569
  statusBar.setContent(' {yellow-fg}Reloading...{/yellow-fg}')
520
570
  screen.render()
521
571
  try {
522
- data = await loadUsageData({ days: options.days })
572
+ data = await loadUsageData({ days: options.days, source: currentSource })
523
573
  dailyScrollOffset = 0
524
574
  updateTabBar()
525
575
  updateContent()
@@ -530,6 +580,22 @@ async function main(): Promise<void> {
530
580
  screen.render()
531
581
  })
532
582
 
583
+ screen.key(['s'], async () => {
584
+ statusBar.setContent(' {yellow-fg}Switching source...{/yellow-fg}')
585
+ screen.render()
586
+ try {
587
+ currentSource = currentSource === 'code' ? 'ide' : 'code'
588
+ data = await loadUsageData({ days: options.days, source: currentSource })
589
+ dailyScrollOffset = 0
590
+ updateTabBar()
591
+ updateContent()
592
+ updateStatusBar()
593
+ } catch (err) {
594
+ statusBar.setContent(` {red-fg}Switch source failed: ${String(err)}{/red-fg}`)
595
+ }
596
+ screen.render()
597
+ })
598
+
533
599
  // 监听窗口大小变化
534
600
  screen.on('resize', () => {
535
601
  updateContent()