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 +7 -4
- package/dist/lib/workspace-resolver.js +364 -21
- package/package.json +1 -1
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
|
-
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
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
|
-
|
|
153
|
+
level1 = await fs.readdir(root, { withFileTypes: true });
|
|
97
154
|
}
|
|
98
155
|
catch {
|
|
99
|
-
return
|
|
156
|
+
return out;
|
|
100
157
|
}
|
|
101
|
-
for (const
|
|
102
|
-
if (!
|
|
158
|
+
for (const dirent of level1) {
|
|
159
|
+
if (!dirent.isDirectory())
|
|
103
160
|
continue;
|
|
104
|
-
const
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
112
|
-
if (
|
|
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
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
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
|
}
|