codebuddy-stats 1.1.2 → 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
@@ -40,19 +40,6 @@ Options:
40
40
  }
41
41
  return options;
42
42
  }
43
- // 生成热力图数据
44
- function generateHeatmapData(dailySummary) {
45
- const sortedDates = Object.keys(dailySummary).sort();
46
- if (sortedDates.length === 0)
47
- return { dates: [], costs: [], maxCost: 0 };
48
- const costs = sortedDates.map(d => dailySummary[d]?.cost ?? 0);
49
- const maxCost = Math.max(...costs);
50
- return {
51
- dates: sortedDates,
52
- costs,
53
- maxCost,
54
- };
55
- }
56
43
  // 获取热力图字符
57
44
  function getHeatChar(cost, maxCost) {
58
45
  if (cost === 0)
@@ -69,7 +56,6 @@ function getHeatChar(cost, maxCost) {
69
56
  // 渲染 Overview 视图
70
57
  function renderOverview(box, data, width, note) {
71
58
  const { dailySummary, grandTotal, topModel, topProject, cacheHitRate, activeDays } = data;
72
- const heatmap = generateHeatmapData(dailySummary);
73
59
  // 根据宽度计算热力图周数
74
60
  const availableWidth = width - 10;
75
61
  const maxWeeks = Math.min(Math.floor(availableWidth / 2), 26); // 最多 26 周 (半年)
@@ -96,7 +82,43 @@ function renderOverview(box, data, width, note) {
96
82
  }
97
83
  weeks.push(week);
98
84
  }
99
- const maxCost = heatmap.maxCost || 1;
85
+ // 以“当前热力图窗口”的最大值做归一化(避免历史极值导致近期全是浅色)
86
+ const visibleCosts = [];
87
+ for (const week of weeks) {
88
+ for (const date of week) {
89
+ if (!date || date > todayStr)
90
+ continue;
91
+ visibleCosts.push(dailySummary[date]?.cost ?? 0);
92
+ }
93
+ }
94
+ const maxCost = Math.max(...visibleCosts, 0) || 1;
95
+ // 月份标尺(在列上方标注月份变化)
96
+ const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
97
+ const colWidth = 2; // 每周一列:字符 + 空格
98
+ const heatStartCol = 4; // 左侧周几标签宽度
99
+ const headerLen = heatStartCol + weeks.length * colWidth;
100
+ const monthHeader = Array.from({ length: headerLen }, () => ' ');
101
+ let lastMonth = -1;
102
+ let lastPlacedAt = -999;
103
+ for (let i = 0; i < weeks.length; i++) {
104
+ const week = weeks[i];
105
+ const repDate = week.find(d => d && d <= todayStr) ?? week[0];
106
+ if (!repDate)
107
+ continue;
108
+ const m = new Date(repDate).getMonth();
109
+ if (m !== lastMonth) {
110
+ const label = monthNames[m];
111
+ const pos = heatStartCol + i * colWidth;
112
+ // 避免月份标签过于拥挤/相互覆盖
113
+ if (pos - lastPlacedAt >= 4 && pos + label.length <= monthHeader.length) {
114
+ for (let k = 0; k < label.length; k++)
115
+ monthHeader[pos + k] = label[k];
116
+ lastPlacedAt = pos;
117
+ }
118
+ lastMonth = m;
119
+ }
120
+ }
121
+ content += `{gray-fg}${monthHeader.join('').trimEnd()}{/gray-fg}\n`;
100
122
  const dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
101
123
  for (let dayOfWeek = 0; dayOfWeek < 7; dayOfWeek++) {
102
124
  let row = dayLabels[dayOfWeek].padEnd(4);
@@ -114,6 +136,8 @@ function renderOverview(box, data, width, note) {
114
136
  }
115
137
  content += row + '\n';
116
138
  }
139
+ const rangeStart = weeks[0]?.[0] ?? todayStr;
140
+ content += `{gray-fg}Range: ${rangeStart} → ${todayStr}{/gray-fg}\n`;
117
141
  content += ' Less {gray-fg}·░▒▓{/gray-fg}{white-fg}█{/white-fg} More\n\n';
118
142
  // 汇总指标 - 根据宽度决定布局
119
143
  const avgDailyCost = activeDays > 0 ? grandTotal.cost / activeDays : 0;
@@ -164,7 +188,7 @@ function renderByModel(box, data, width, note) {
164
188
  content +=
165
189
  '{underline}' +
166
190
  'Model'.padEnd(modelCol) +
167
- 'Cost'.padStart(12) +
191
+ '~Cost'.padStart(12) +
168
192
  'Requests'.padStart(12) +
169
193
  'Tokens'.padStart(12) +
170
194
  'Avg/Req'.padStart(10) +
@@ -205,7 +229,7 @@ function renderByProject(box, data, width, note) {
205
229
  content +=
206
230
  '{underline}' +
207
231
  'Project'.padEnd(projectCol) +
208
- 'Cost'.padStart(12) +
232
+ '~Cost'.padStart(12) +
209
233
  'Requests'.padStart(12) +
210
234
  'Tokens'.padStart(12) +
211
235
  '{/underline}\n';
@@ -240,8 +264,9 @@ function renderDaily(box, data, scrollOffset = 0, width, note) {
240
264
  const availableWidth = width - 6; // padding
241
265
  const dateCol = 12;
242
266
  const costCol = 12;
267
+ const tokensCol = 10;
243
268
  const reqCol = 10;
244
- const fixedCols = dateCol + costCol + reqCol;
269
+ const fixedCols = dateCol + costCol + tokensCol + reqCol;
245
270
  const remainingWidth = availableWidth - fixedCols;
246
271
  const modelCol = Math.max(15, Math.min(25, Math.floor(remainingWidth * 0.4)));
247
272
  const projectCol = Math.max(20, remainingWidth - modelCol);
@@ -249,7 +274,8 @@ function renderDaily(box, data, scrollOffset = 0, width, note) {
249
274
  content +=
250
275
  '{underline}' +
251
276
  'Date'.padEnd(dateCol) +
252
- 'Cost'.padStart(costCol) +
277
+ '~Cost'.padStart(costCol) +
278
+ 'Tokens'.padStart(tokensCol) +
253
279
  'Requests'.padStart(reqCol) +
254
280
  'Top Model'.padStart(modelCol) +
255
281
  'Top Project'.padStart(projectCol) +
@@ -280,6 +306,7 @@ function renderDaily(box, data, scrollOffset = 0, width, note) {
280
306
  content +=
281
307
  date.padEnd(dateCol) +
282
308
  formatCost(daySummary.cost).padStart(costCol) +
309
+ formatTokens(daySummary.tokens).padStart(tokensCol) +
283
310
  formatNumber(daySummary.requests).padStart(reqCol) +
284
311
  truncate(topModel.id, modelCol - 1).padStart(modelCol) +
285
312
  truncate(shortProject, projectCol - 1).padStart(projectCol) +
@@ -444,10 +471,29 @@ async function main() {
444
471
  function updateStatusBar() {
445
472
  const daysInfo = options.days ? `Last ${options.days} days` : 'All time';
446
473
  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} `;
474
+ const rightContent = `v${VERSION}`;
449
475
  const width = Number(screen.width) || 80;
450
- const padding = Math.max(0, width - leftContent.length - rightContent.length);
476
+ // 根据剩余宽度决定左侧内容详细程度(预留版本号空间)
477
+ const reservedForRight = rightContent.length + 2; // 版本号 + 两侧空格
478
+ const availableForLeft = width - reservedForRight;
479
+ let leftContent;
480
+ const fullContent = ` ${daysInfo} | Source: ${sourceInfo} | Total: ${formatCost(data.grandTotal.cost)} | q quit, Tab view, s source, r refresh`;
481
+ const mediumContent = ` ${daysInfo} | ${sourceInfo} | ${formatCost(data.grandTotal.cost)} | q/Tab/s/r`;
482
+ const shortContent = ` ${sourceInfo} | ${formatCost(data.grandTotal.cost)} | q/Tab/s/r`;
483
+ const minContent = ` ${formatCost(data.grandTotal.cost)}`;
484
+ if (fullContent.length <= availableForLeft) {
485
+ leftContent = fullContent;
486
+ }
487
+ else if (mediumContent.length <= availableForLeft) {
488
+ leftContent = mediumContent;
489
+ }
490
+ else if (shortContent.length <= availableForLeft) {
491
+ leftContent = shortContent;
492
+ }
493
+ else {
494
+ leftContent = minContent;
495
+ }
496
+ const padding = Math.max(1, width - leftContent.length - rightContent.length);
451
497
  statusBar.setContent(leftContent + ' '.repeat(padding) + rightContent);
452
498
  }
453
499
  // 键盘事件
@@ -517,7 +563,9 @@ async function main() {
517
563
  });
518
564
  // 监听窗口大小变化
519
565
  screen.on('resize', () => {
566
+ updateTabBar();
520
567
  updateContent();
568
+ updateStatusBar();
521
569
  screen.render();
522
570
  });
523
571
  // 初始渲染
package/dist/lib/paths.js CHANGED
@@ -24,7 +24,7 @@ export function getConfigDir() {
24
24
  * 获取 CodeBuddy IDE (CodeBuddyExtension) 数据目录
25
25
  * - macOS: ~/Library/Application Support/CodeBuddyExtension/Data
26
26
  * - Windows: %APPDATA%/CodeBuddyExtension/Data
27
- * - Linux: $XDG_CONFIG_HOME/CodeBuddyExtension/Data 或 ~/.config/CodeBuddyExtension/Data
27
+ * - Linux: $XDG_DATA_HOME/CodeBuddyExtension/Data 或 ~/.local/share/CodeBuddyExtension/Data
28
28
  */
29
29
  export function getIdeDataDir() {
30
30
  if (process.platform === 'darwin') {
@@ -34,8 +34,8 @@ export function getIdeDataDir() {
34
34
  const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
35
35
  return path.join(appData, 'CodeBuddyExtension', 'Data');
36
36
  }
37
- const xdgConfigHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
38
- return path.join(xdgConfigHome, 'CodeBuddyExtension', 'Data');
37
+ const xdgDataHome = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
38
+ return path.join(xdgDataHome, 'CodeBuddyExtension', 'Data');
39
39
  }
40
40
  /**
41
41
  * 获取 CodeBuddy IDE 的 workspaceStorage 目录
@@ -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.2",
3
+ "version": "1.1.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "files": [