codebuddy-stats 1.1.3 → 1.1.4

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/dist/index.js CHANGED
@@ -188,7 +188,7 @@ function renderByModel(box, data, width, note) {
188
188
  content +=
189
189
  '{underline}' +
190
190
  'Model'.padEnd(modelCol) +
191
- 'Cost'.padStart(12) +
191
+ '~Cost'.padStart(12) +
192
192
  'Requests'.padStart(12) +
193
193
  'Tokens'.padStart(12) +
194
194
  'Avg/Req'.padStart(10) +
@@ -229,7 +229,7 @@ function renderByProject(box, data, width, note) {
229
229
  content +=
230
230
  '{underline}' +
231
231
  'Project'.padEnd(projectCol) +
232
- 'Cost'.padStart(12) +
232
+ '~Cost'.padStart(12) +
233
233
  'Requests'.padStart(12) +
234
234
  'Tokens'.padStart(12) +
235
235
  '{/underline}\n';
@@ -264,8 +264,9 @@ function renderDaily(box, data, scrollOffset = 0, width, note) {
264
264
  const availableWidth = width - 6; // padding
265
265
  const dateCol = 12;
266
266
  const costCol = 12;
267
+ const tokensCol = 10;
267
268
  const reqCol = 10;
268
- const fixedCols = dateCol + costCol + reqCol;
269
+ const fixedCols = dateCol + costCol + tokensCol + reqCol;
269
270
  const remainingWidth = availableWidth - fixedCols;
270
271
  const modelCol = Math.max(15, Math.min(25, Math.floor(remainingWidth * 0.4)));
271
272
  const projectCol = Math.max(20, remainingWidth - modelCol);
@@ -273,7 +274,8 @@ function renderDaily(box, data, scrollOffset = 0, width, note) {
273
274
  content +=
274
275
  '{underline}' +
275
276
  'Date'.padEnd(dateCol) +
276
- 'Cost'.padStart(costCol) +
277
+ '~Cost'.padStart(costCol) +
278
+ 'Tokens'.padStart(tokensCol) +
277
279
  'Requests'.padStart(reqCol) +
278
280
  'Top Model'.padStart(modelCol) +
279
281
  'Top Project'.padStart(projectCol) +
@@ -304,6 +306,7 @@ function renderDaily(box, data, scrollOffset = 0, width, note) {
304
306
  content +=
305
307
  date.padEnd(dateCol) +
306
308
  formatCost(daySummary.cost).padStart(costCol) +
309
+ formatTokens(daySummary.tokens).padStart(tokensCol) +
307
310
  formatNumber(daySummary.requests).padStart(reqCol) +
308
311
  truncate(topModel.id, modelCol - 1).padStart(modelCol) +
309
312
  truncate(shortProject, projectCol - 1).padStart(projectCol) +
@@ -3,7 +3,7 @@ import fsSync from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
- import { getWorkspaceStorageDir } from './paths.js';
6
+ import { getIdeDataDir, getWorkspaceStorageDir } from './paths.js';
7
7
  // 缓存已解析的 CodeBuddy Code 路径名
8
8
  const codePathCache = new Map();
9
9
  /**
@@ -86,38 +86,381 @@ function computePathHash(p) {
86
86
  return crypto.createHash('md5').update(p).digest('hex');
87
87
  }
88
88
  /**
89
- * 加载所有工作区映射
89
+ * 从 CodeBuddyIDE 的 file-tree 数据中反推出 workspace hash -> 真实路径
90
+ *
91
+ * 背景:Remote SSH 场景下,server 侧 `workspaceStorage/<hash>/workspace.json` 可能不存在,
92
+ * 但 CodeBuddyExtension 会把对话关联的文件路径写入 `file-tree.json`,可据此还原工作区根目录。
90
93
  */
91
- export async function loadWorkspaceMappings() {
92
- const mappings = new Map();
93
- const storageDir = getWorkspaceStorageDir();
94
- let entries = [];
94
+ async function loadWorkspaceMappingsFromIdeFileTree() {
95
+ const out = new Map();
96
+ const root = getIdeDataDir();
97
+ async function pathExists(p) {
98
+ try {
99
+ await fs.access(p);
100
+ return true;
101
+ }
102
+ catch {
103
+ return false;
104
+ }
105
+ }
106
+ function collectFilePaths(node, acc, depth = 0) {
107
+ if (depth > 10)
108
+ return;
109
+ if (typeof node === 'string')
110
+ return;
111
+ if (Array.isArray(node)) {
112
+ for (const item of node)
113
+ collectFilePaths(item, acc, depth + 1);
114
+ return;
115
+ }
116
+ if (!node || typeof node !== 'object')
117
+ return;
118
+ for (const [k, v] of Object.entries(node)) {
119
+ if (k === 'filePath' && typeof v === 'string' && v) {
120
+ acc.push(v);
121
+ continue;
122
+ }
123
+ if (k === 'createdEntries' && Array.isArray(v)) {
124
+ for (const e of v) {
125
+ if (typeof e === 'string' && e)
126
+ acc.push(e);
127
+ }
128
+ continue;
129
+ }
130
+ collectFilePaths(v, acc, depth + 1);
131
+ }
132
+ }
133
+ function resolveWorkspacePathFromFilePaths(hash, filePaths) {
134
+ for (const raw of filePaths) {
135
+ const p = extractPathFromUri(raw) || (path.isAbsolute(raw) ? raw : null);
136
+ if (!p)
137
+ continue;
138
+ let cur = p;
139
+ for (let i = 0; i < 50; i++) {
140
+ if (computePathHash(cur) === hash)
141
+ return cur;
142
+ const parent = path.dirname(cur);
143
+ if (parent === cur)
144
+ break;
145
+ cur = parent;
146
+ }
147
+ }
148
+ return null;
149
+ }
150
+ // 扫描 `Data/*/CodeBuddyIDE/*/file-tree/*/<convId>/file-tree.json`
151
+ let level1 = [];
95
152
  try {
96
- entries = await fs.readdir(storageDir, { withFileTypes: true });
153
+ level1 = await fs.readdir(root, { withFileTypes: true });
97
154
  }
98
155
  catch {
99
- return mappings;
156
+ return out;
100
157
  }
101
- for (const entry of entries) {
102
- if (!entry.isDirectory())
158
+ for (const dirent of level1) {
159
+ if (!dirent.isDirectory())
103
160
  continue;
104
- const workspaceJsonPath = path.join(storageDir, entry.name, 'workspace.json');
161
+ const codeBuddyIdeRoot = path.join(root, dirent.name, 'CodeBuddyIDE');
162
+ if (!(await pathExists(codeBuddyIdeRoot)))
163
+ continue;
164
+ // 兼容两种结构:
165
+ // 1) CodeBuddyIDE/file-tree
166
+ // 2) CodeBuddyIDE/<profile>/file-tree(当前主流)
167
+ const profileDirs = [];
168
+ if (await pathExists(path.join(codeBuddyIdeRoot, 'file-tree'))) {
169
+ profileDirs.push(codeBuddyIdeRoot);
170
+ }
171
+ let nested = [];
105
172
  try {
106
- const content = await fs.readFile(workspaceJsonPath, 'utf8');
107
- const data = JSON.parse(content);
108
- const folderUri = data.folder;
109
- if (!folderUri)
173
+ nested = await fs.readdir(codeBuddyIdeRoot, { withFileTypes: true });
174
+ }
175
+ catch {
176
+ nested = [];
177
+ }
178
+ for (const child of nested) {
179
+ if (!child.isDirectory())
110
180
  continue;
111
- const extractedPath = extractPathFromUri(folderUri);
112
- if (!extractedPath)
181
+ const profileDir = path.join(codeBuddyIdeRoot, child.name);
182
+ if (await pathExists(path.join(profileDir, 'file-tree'))) {
183
+ profileDirs.push(profileDir);
184
+ }
185
+ }
186
+ for (const profileDir of profileDirs) {
187
+ const fileTreeDir = path.join(profileDir, 'file-tree');
188
+ let workspaces = [];
189
+ try {
190
+ workspaces = await fs.readdir(fileTreeDir, { withFileTypes: true });
191
+ }
192
+ catch {
113
193
  continue;
114
- const hash = computePathHash(extractedPath);
115
- const displayPath = getDisplayPath(folderUri);
116
- mappings.set(hash, { hash, folderUri, displayPath });
194
+ }
195
+ for (const ws of workspaces) {
196
+ if (!ws.isDirectory())
197
+ continue;
198
+ const workspaceHash = ws.name;
199
+ if (!/^[a-f0-9]{32}$/.test(workspaceHash))
200
+ continue;
201
+ if (out.has(workspaceHash))
202
+ continue;
203
+ const workspaceDir = path.join(fileTreeDir, workspaceHash);
204
+ let convDirs = [];
205
+ try {
206
+ convDirs = await fs.readdir(workspaceDir, { withFileTypes: true });
207
+ }
208
+ catch {
209
+ continue;
210
+ }
211
+ let resolved = null;
212
+ for (const conv of convDirs) {
213
+ if (!conv.isDirectory())
214
+ continue;
215
+ const fileTreeJson = path.join(workspaceDir, conv.name, 'file-tree.json');
216
+ try {
217
+ const raw = await fs.readFile(fileTreeJson, 'utf8');
218
+ const parsed = JSON.parse(raw);
219
+ const paths = [];
220
+ collectFilePaths(parsed, paths);
221
+ resolved = resolveWorkspacePathFromFilePaths(workspaceHash, paths);
222
+ if (resolved)
223
+ break;
224
+ }
225
+ catch {
226
+ // ignore
227
+ }
228
+ }
229
+ if (!resolved)
230
+ continue;
231
+ const folderUri = 'file://' + resolved;
232
+ out.set(workspaceHash, {
233
+ hash: workspaceHash,
234
+ folderUri,
235
+ displayPath: getDisplayPath(folderUri),
236
+ });
237
+ }
238
+ }
239
+ }
240
+ return out;
241
+ }
242
+ /**
243
+ * 从 CodeBuddyIDE 的 history/messages 中反推出 workspace hash -> 真实路径
244
+ *
245
+ * 场景:有些对话没有触发 file-tree 落盘,但 messages 里常包含 tool-result 的绝对路径。
246
+ */
247
+ async function loadWorkspaceMappingsFromIdeHistory() {
248
+ const out = new Map();
249
+ const root = getIdeDataDir();
250
+ async function pathExists(p) {
251
+ try {
252
+ await fs.access(p);
253
+ return true;
117
254
  }
118
255
  catch {
119
- // 跳过无法读取的文件
256
+ return false;
257
+ }
258
+ }
259
+ async function readFileHeadUtf8(filePath, bytes = 64 * 1024) {
260
+ const fh = await fs.open(filePath, 'r');
261
+ try {
262
+ const buf = Buffer.alloc(bytes);
263
+ const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
264
+ return buf.toString('utf8', 0, bytesRead);
120
265
  }
266
+ finally {
267
+ await fh.close();
268
+ }
269
+ }
270
+ function extractCandidatePathsFromText(text) {
271
+ const set = new Set();
272
+ const uriRe = /(vscode-remote:\/\/[^\s"']+|file:\/\/[^\s"']+)/g;
273
+ for (const m of text.matchAll(uriRe)) {
274
+ const v = m[1];
275
+ if (v)
276
+ set.add(v);
277
+ }
278
+ const absPathRe = /\/[A-Za-z0-9._~@%+=:,\-]+(?:\/[A-Za-z0-9._~@%+=:,\-]+)+/g;
279
+ for (const m of text.matchAll(absPathRe)) {
280
+ const v = m[0];
281
+ if (v)
282
+ set.add(v);
283
+ }
284
+ return [...set];
285
+ }
286
+ function resolveWorkspacePathFromCandidates(hash, candidates) {
287
+ for (const raw of candidates) {
288
+ const p = extractPathFromUri(raw) || (path.isAbsolute(raw) ? raw : null);
289
+ if (!p)
290
+ continue;
291
+ let cur = p;
292
+ for (let i = 0; i < 50; i++) {
293
+ if (computePathHash(cur) === hash)
294
+ return cur;
295
+ const parent = path.dirname(cur);
296
+ if (parent === cur)
297
+ break;
298
+ cur = parent;
299
+ }
300
+ }
301
+ return null;
302
+ }
303
+ let level1 = [];
304
+ try {
305
+ level1 = await fs.readdir(root, { withFileTypes: true });
306
+ }
307
+ catch {
308
+ return out;
309
+ }
310
+ for (const dirent of level1) {
311
+ if (!dirent.isDirectory())
312
+ continue;
313
+ const codeBuddyIdeRoot = path.join(root, dirent.name, 'CodeBuddyIDE');
314
+ if (!(await pathExists(codeBuddyIdeRoot)))
315
+ continue;
316
+ const profileDirs = [];
317
+ if (await pathExists(path.join(codeBuddyIdeRoot, 'history'))) {
318
+ profileDirs.push(codeBuddyIdeRoot);
319
+ }
320
+ let nested = [];
321
+ try {
322
+ nested = await fs.readdir(codeBuddyIdeRoot, { withFileTypes: true });
323
+ }
324
+ catch {
325
+ nested = [];
326
+ }
327
+ for (const child of nested) {
328
+ if (!child.isDirectory())
329
+ continue;
330
+ const profileDir = path.join(codeBuddyIdeRoot, child.name);
331
+ if (await pathExists(path.join(profileDir, 'history'))) {
332
+ profileDirs.push(profileDir);
333
+ }
334
+ }
335
+ for (const profileDir of profileDirs) {
336
+ const historyDir = path.join(profileDir, 'history');
337
+ let workspaces = [];
338
+ try {
339
+ workspaces = await fs.readdir(historyDir, { withFileTypes: true });
340
+ }
341
+ catch {
342
+ continue;
343
+ }
344
+ for (const ws of workspaces) {
345
+ if (!ws.isDirectory())
346
+ continue;
347
+ const workspaceHash = ws.name;
348
+ if (!/^[a-f0-9]{32}$/.test(workspaceHash))
349
+ continue;
350
+ if (out.has(workspaceHash))
351
+ continue;
352
+ const workspaceDir = path.join(historyDir, workspaceHash);
353
+ let convDirs = [];
354
+ try {
355
+ convDirs = await fs.readdir(workspaceDir, { withFileTypes: true });
356
+ }
357
+ catch {
358
+ continue;
359
+ }
360
+ let resolved = null;
361
+ for (const conv of convDirs) {
362
+ if (!conv.isDirectory())
363
+ continue;
364
+ const messagesDir = path.join(workspaceDir, conv.name, 'messages');
365
+ if (!(await pathExists(messagesDir)))
366
+ continue;
367
+ let msgFiles = [];
368
+ try {
369
+ msgFiles = await fs.readdir(messagesDir, { withFileTypes: true });
370
+ }
371
+ catch {
372
+ continue;
373
+ }
374
+ let tried = 0;
375
+ for (const msg of msgFiles) {
376
+ if (!msg.isFile() || !msg.name.endsWith('.json'))
377
+ continue;
378
+ const msgPath = path.join(messagesDir, msg.name);
379
+ tried += 1;
380
+ if (tried > 30)
381
+ break;
382
+ try {
383
+ const head = await readFileHeadUtf8(msgPath);
384
+ const candidates = extractCandidatePathsFromText(head);
385
+ resolved = resolveWorkspacePathFromCandidates(workspaceHash, candidates);
386
+ if (resolved)
387
+ break;
388
+ }
389
+ catch {
390
+ // ignore
391
+ }
392
+ }
393
+ if (resolved)
394
+ break;
395
+ }
396
+ if (!resolved)
397
+ continue;
398
+ const folderUri = 'file://' + resolved;
399
+ out.set(workspaceHash, {
400
+ hash: workspaceHash,
401
+ folderUri,
402
+ displayPath: getDisplayPath(folderUri),
403
+ });
404
+ }
405
+ }
406
+ }
407
+ return out;
408
+ }
409
+ /**
410
+ * 加载所有工作区映射
411
+ */
412
+ export async function loadWorkspaceMappings() {
413
+ const mappings = new Map();
414
+ // 1) 尝试从客户端 workspaceStorage/workspace.json 解析
415
+ const storageDir = getWorkspaceStorageDir();
416
+ try {
417
+ const entries = await fs.readdir(storageDir, { withFileTypes: true });
418
+ for (const entry of entries) {
419
+ if (!entry.isDirectory())
420
+ continue;
421
+ const workspaceJsonPath = path.join(storageDir, entry.name, 'workspace.json');
422
+ try {
423
+ const content = await fs.readFile(workspaceJsonPath, 'utf8');
424
+ const data = JSON.parse(content);
425
+ const folderUri = data.folder;
426
+ if (!folderUri)
427
+ continue;
428
+ const extractedPath = extractPathFromUri(folderUri);
429
+ if (!extractedPath)
430
+ continue;
431
+ const hash = computePathHash(extractedPath);
432
+ const displayPath = getDisplayPath(folderUri);
433
+ mappings.set(hash, { hash, folderUri, displayPath });
434
+ }
435
+ catch {
436
+ // ignore
437
+ }
438
+ }
439
+ }
440
+ catch {
441
+ // ignore
442
+ }
443
+ // 2) Remote SSH 等场景的兜底:从 CodeBuddyIDE file-tree 反推
444
+ try {
445
+ const ideMappings = await loadWorkspaceMappingsFromIdeFileTree();
446
+ for (const [hash, mapping] of ideMappings) {
447
+ if (!mappings.has(hash))
448
+ mappings.set(hash, mapping);
449
+ }
450
+ }
451
+ catch {
452
+ // ignore
453
+ }
454
+ // 3) 进一步兜底:从 CodeBuddyIDE history/messages 反推(tool-result 常带绝对路径)
455
+ try {
456
+ const ideMappings = await loadWorkspaceMappingsFromIdeHistory();
457
+ for (const [hash, mapping] of ideMappings) {
458
+ if (!mappings.has(hash))
459
+ mappings.set(hash, mapping);
460
+ }
461
+ }
462
+ catch {
463
+ // ignore
121
464
  }
122
465
  return mappings;
123
466
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebuddy-stats",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "files": [