tokentracker-cli 0.65.0 → 0.65.2

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.
Files changed (54) hide show
  1. package/README.de.md +476 -0
  2. package/README.ja.md +1 -1
  3. package/README.ko.md +1 -1
  4. package/README.md +1 -1
  5. package/README.zh-CN.md +1 -1
  6. package/dashboard/dist/assets/{ActivityHeatmap-Ccf0I3uP.js → ActivityHeatmap-CggJI8SR.js} +1 -1
  7. package/dashboard/dist/assets/{Card-BSF8dYEt.js → Card-5au9t9nC.js} +1 -1
  8. package/dashboard/dist/assets/{ConfirmModal-Bjp6eg2f.js → ConfirmModal-DdczuiTm.js} +1 -1
  9. package/dashboard/dist/assets/{DashboardPage-BInQkoDe.js → DashboardPage-VAYxz_kf.js} +2 -2
  10. package/dashboard/dist/assets/{DevicePage-B0yxbDDR.js → DevicePage-DQJlqpU3.js} +1 -1
  11. package/dashboard/dist/assets/{DialogTitle-uw-Oa5Qv.js → DialogTitle-DR64sBQk.js} +1 -1
  12. package/dashboard/dist/assets/{FadeIn-DPIBqXRl.js → FadeIn-CGzcyAaJ.js} +1 -1
  13. package/dashboard/dist/assets/{HeaderGithubStar-DDXfJ1uC.js → HeaderGithubStar-DiJi3EYZ.js} +1 -1
  14. package/dashboard/dist/assets/{IpCheckPage-KC93tPlt.js → IpCheckPage-CoGzED0h.js} +1 -1
  15. package/dashboard/dist/assets/{LandingPage-Itx2uPWI.js → LandingPage-D_h4MI77.js} +1 -1
  16. package/dashboard/dist/assets/{LeaderboardAvatar-Bv44b_Kl.js → LeaderboardAvatar-BuSHrfaq.js} +1 -1
  17. package/dashboard/dist/assets/{LeaderboardPage-DDq5gzyz.js → LeaderboardPage-aP0oQNBq.js} +2 -2
  18. package/dashboard/dist/assets/{LeaderboardProfileModal-LQyYrCf1.js → LeaderboardProfileModal-zWSPrjXm.js} +1 -1
  19. package/dashboard/dist/assets/{LeaderboardProfilePage-3you3mbA.js → LeaderboardProfilePage-BNR__NEp.js} +1 -1
  20. package/dashboard/dist/assets/{LimitsPage-CrLvR8vE.js → LimitsPage-CbwEV4tL.js} +1 -1
  21. package/dashboard/dist/assets/{LocalOnlyNotice-Di1gnfIi.js → LocalOnlyNotice-DFxhTdeP.js} +1 -1
  22. package/dashboard/dist/assets/{LoginPage-f9QHz_uZ.js → LoginPage-6m5w2iay.js} +1 -1
  23. package/dashboard/dist/assets/{PopoverPopup-7rjAxAqk.js → PopoverPopup-6bH8WcTu.js} +1 -1
  24. package/dashboard/dist/assets/{ResetPasswordPage-DKA7HGpJ.js → ResetPasswordPage-ByOumS6-.js} +1 -1
  25. package/dashboard/dist/assets/{Select-2gikPo1a.js → Select-DrNJJE2o.js} +1 -1
  26. package/dashboard/dist/assets/{SelectItemText-DZx33XF1.js → SelectItemText-BTHh_2vw.js} +1 -1
  27. package/dashboard/dist/assets/SettingsPage-BIxrrxUF.js +1 -0
  28. package/dashboard/dist/assets/{SkillsPage-B6BJ9UZp.js → SkillsPage-Cg8iXYaS.js} +1 -1
  29. package/dashboard/dist/assets/{WidgetsPage-D8p3Tx7E.js → WidgetsPage-Bi8d2YwV.js} +1 -1
  30. package/dashboard/dist/assets/{WrappedPage-cXEfhti9.js → WrappedPage-Cc2kLDpE.js} +1 -1
  31. package/dashboard/dist/assets/{agent-logos-D3iFlaWg.js → agent-logos-CzNQ3IaQ.js} +1 -1
  32. package/dashboard/dist/assets/{arrow-up-right-D87Aw_ZN.js → arrow-up-right-FgMhJ3Pu.js} +1 -1
  33. package/dashboard/dist/assets/{download-BtcEs1OB.js → download-Bo9t1yfc.js} +1 -1
  34. package/dashboard/dist/assets/{main-DqnXui98.js → main-6K89gDOX.js} +21 -20
  35. package/dashboard/dist/assets/{main-CGx8BJ4O.css → main-BHwig9Um.css} +1 -1
  36. package/dashboard/dist/assets/{use-limits-display-prefs-DAxlRSu2.js → use-limits-display-prefs-DN1nv1HE.js} +1 -1
  37. package/dashboard/dist/assets/{use-native-settings-CgvB_B6W.js → use-native-settings-6ygLVyJu.js} +1 -1
  38. package/dashboard/dist/assets/{use-ordered-list-DOfX6aSy.js → use-ordered-list-li19cWJh.js} +1 -1
  39. package/dashboard/dist/assets/{use-quality-per-dollar-pref-kEbwQwk2.js → use-quality-per-dollar-pref-rNMT2kff.js} +1 -1
  40. package/dashboard/dist/assets/{use-usage-limits-la-KPfJj.js → use-usage-limits-kVHyIhp3.js} +1 -1
  41. package/dashboard/dist/assets/{useCurrency-LDRg7ST0.js → useCurrency-g0G5pwGx.js} +1 -1
  42. package/dashboard/dist/assets/{useScrollLock-lP1Is9IY.js → useScrollLock-Dg7JyjFK.js} +1 -1
  43. package/dashboard/dist/index.html +2 -2
  44. package/dashboard/dist/share.html +2 -2
  45. package/package.json +1 -1
  46. package/src/commands/init.js +3 -2
  47. package/src/commands/serve.js +59 -1
  48. package/src/commands/status.js +26 -17
  49. package/src/commands/sync.js +672 -98
  50. package/src/lib/openclaw-session-plugin.js +2 -2
  51. package/src/lib/pricing/seed-snapshot.json +1 -1
  52. package/src/lib/rollout.js +602 -178
  53. package/src/lib/wsl-probe.js +181 -0
  54. package/dashboard/dist/assets/SettingsPage-CwHPmuZV.js +0 -1
@@ -6,6 +6,7 @@ const readline = require("node:readline");
6
6
  const crypto = require("node:crypto");
7
7
  const { ensureDir } = require("./fs");
8
8
  const { readSqliteJsonRows } = require("./sqlite-reader");
9
+ const wsl = require("./wsl-probe");
9
10
 
10
11
  const DEFAULT_SOURCE = "codex";
11
12
  const DEFAULT_MODEL = "unknown";
@@ -13,9 +14,25 @@ const BUCKET_SEPARATOR = "|";
13
14
  const CLAUDE_MEM_OBSERVER_PATH_SEGMENT = "--claude-mem-observer-sessions";
14
15
  const CLAUDE_MEM_OBSERVER_PROJECT_REF =
15
16
  "https://local.tokentracker/claude-mem/observer-sessions";
17
+ const PROJECT_ABSENT_CONTEXT_RESCAN_MS = 24 * 60 * 60 * 1000;
18
+ const DEFAULT_CODEX_COLD_SKIP_RECENT_DAYS = 2;
16
19
 
17
- async function listRolloutFiles(sessionsDir) {
20
+ async function listRolloutFiles(sessionsDir, options = {}) {
18
21
  const out = [];
22
+ const dayInventoryCache =
23
+ options?.dayInventoryCache && typeof options.dayInventoryCache === "object"
24
+ ? options.dayInventoryCache
25
+ : null;
26
+ const stats = options?.stats && typeof options.stats === "object" ? options.stats : null;
27
+ if (dayInventoryCache) {
28
+ if (dayInventoryCache.version !== 1) {
29
+ dayInventoryCache.days = {};
30
+ }
31
+ dayInventoryCache.version = 1;
32
+ if (!dayInventoryCache.days || typeof dayInventoryCache.days !== "object") {
33
+ dayInventoryCache.days = {};
34
+ }
35
+ }
19
36
  const years = await safeReadDir(sessionsDir);
20
37
  for (const y of years) {
21
38
  if (!/^[0-9]{4}$/.test(y.name) || !y.isDirectory()) continue;
@@ -28,12 +45,8 @@ async function listRolloutFiles(sessionsDir) {
28
45
  for (const d of days) {
29
46
  if (!/^[0-9]{2}$/.test(d.name) || !d.isDirectory()) continue;
30
47
  const dayDir = path.join(monthDir, d.name);
31
- const files = await safeReadDir(dayDir);
32
- for (const f of files) {
33
- if (!f.isFile()) continue;
34
- if (!f.name.startsWith("rollout-") || !f.name.endsWith(".jsonl")) continue;
35
- out.push(path.join(dayDir, f.name));
36
- }
48
+ const files = await listRolloutDayFiles(dayDir, { dayInventoryCache, stats });
49
+ out.push(...files);
37
50
  }
38
51
  }
39
52
  }
@@ -42,6 +55,58 @@ async function listRolloutFiles(sessionsDir) {
42
55
  return out;
43
56
  }
44
57
 
58
+ async function listRolloutDayFiles(dayDir, { dayInventoryCache, stats } = {}) {
59
+ const cacheDays = dayInventoryCache?.days;
60
+ const dayStat = dayInventoryCache ? await fs.stat(dayDir).catch(() => null) : null;
61
+ const statKey = dayStat && dayStat.isDirectory() ? directoryInventoryStatKey(dayStat) : null;
62
+ const cached = cacheDays && cacheDays[dayDir];
63
+ if (
64
+ statKey &&
65
+ cached &&
66
+ typeof cached === "object" &&
67
+ cached.statKey === statKey &&
68
+ Array.isArray(cached.files) &&
69
+ cached.files.every(isRolloutFileName)
70
+ ) {
71
+ if (stats) stats.dayInventoryCacheHits = Number(stats.dayInventoryCacheHits || 0) + 1;
72
+ return cached.files.map((name) => path.join(dayDir, name));
73
+ }
74
+
75
+ if (stats && cached) stats.dayInventoryCacheMisses = Number(stats.dayInventoryCacheMisses || 0) + 1;
76
+ const entries = await safeReadDir(dayDir);
77
+ const files = [];
78
+ for (const f of entries) {
79
+ if (!f.isFile()) continue;
80
+ if (!isRolloutFileName(f.name)) continue;
81
+ files.push(f.name);
82
+ }
83
+ files.sort((a, b) => a.localeCompare(b));
84
+ if (cacheDays && statKey) {
85
+ cacheDays[dayDir] = {
86
+ statKey,
87
+ files,
88
+ updatedAt: new Date().toISOString(),
89
+ };
90
+ }
91
+ return files.map((name) => path.join(dayDir, name));
92
+ }
93
+
94
+ function isRolloutFileName(name) {
95
+ if (typeof name !== "string") return false;
96
+ if (name.includes("/") || name.includes("\\")) return false;
97
+ if (name !== path.basename(name)) return false;
98
+ return name.startsWith("rollout-") && name.endsWith(".jsonl");
99
+ }
100
+
101
+ function directoryInventoryStatKey(st) {
102
+ return [
103
+ Number(st.ino || 0),
104
+ Number(st.size || 0),
105
+ Number(st.mtimeMs || 0),
106
+ Number(st.ctimeMs || 0),
107
+ ].join(":");
108
+ }
109
+
45
110
  // Collect rollout-*.jsonl at ANY depth under dir. listRolloutFiles requires the
46
111
  // strict YYYY/MM/DD/ nesting Codex itself writes, but Codex-Manager archives
47
112
  // sessions FLAT into ~/.codex/archived_sessions/ (issue #187), so the strict
@@ -119,6 +184,8 @@ async function parseRolloutIncremental({
119
184
  const projectTouchedBuckets = projectEnabled ? new Set() : null;
120
185
  const projectMetaCache = projectEnabled ? new Map() : null;
121
186
  const publicRepoCache = projectEnabled ? new Map() : null;
187
+ const projectFreshnessCache = projectEnabled ? new Map() : null;
188
+ const projectFreshnessNowMs = Date.now();
122
189
  const touchedBuckets = new Set();
123
190
  const defaultSource = normalizeSourceInput(source) || DEFAULT_SOURCE;
124
191
 
@@ -154,7 +221,29 @@ async function parseRolloutIncremental({
154
221
  const lastTotal = sameInode && !truncated ? prev.lastTotal || null : null;
155
222
  const lastModel = sameInode && !truncated ? prev.lastModel || null : null;
156
223
 
157
- if (!projectEnabled && sameInode && !truncated && startOffset >= st.size) {
224
+ const codexProjectFastPath = projectEnabled && fileSource === DEFAULT_SOURCE;
225
+ const projectOffset = sameInode && !truncated ? Number(prev.projectOffset || 0) : 0;
226
+ const projectUpToDate =
227
+ codexProjectFastPath &&
228
+ typeof publicRepoResolver !== "function" &&
229
+ projectOffset >= st.size &&
230
+ (await isProjectFileContextFresh(prev?.projectFileContext, {
231
+ freshnessCache: projectFreshnessCache,
232
+ nowMs: projectFreshnessNowMs,
233
+ }));
234
+ const projectContextOnlyScan =
235
+ codexProjectFastPath &&
236
+ typeof publicRepoResolver !== "function" &&
237
+ sameInode &&
238
+ !truncated &&
239
+ startOffset >= st.size &&
240
+ !projectUpToDate;
241
+ if (
242
+ sameInode &&
243
+ !truncated &&
244
+ startOffset >= st.size &&
245
+ (!projectEnabled || projectUpToDate)
246
+ ) {
158
247
  if (cb) {
159
248
  cb({
160
249
  index: idx + 1,
@@ -180,33 +269,57 @@ async function parseRolloutIncremental({
180
269
  const projectRef = projectContext?.projectRef || null;
181
270
  const projectKey = projectContext?.projectKey || null;
182
271
 
183
- const result = await parseRolloutFile({
184
- filePath,
185
- fileStat: st,
186
- startOffset,
187
- lastTotal,
188
- lastModel,
189
- hourlyState,
190
- touchedBuckets,
191
- source: fileSource,
192
- projectState,
193
- projectTouchedBuckets,
194
- projectRef,
195
- projectKey,
196
- projectMetaCache,
197
- publicRepoCache,
198
- publicRepoResolver,
199
- seenCodexEvents,
200
- sessionId: codexSessionIdFromPath(filePath),
201
- });
272
+ const result = projectContextOnlyScan
273
+ ? await scanRolloutProjectFileContexts({
274
+ filePath,
275
+ fileStat: st,
276
+ lastTotal,
277
+ lastModel,
278
+ projectState,
279
+ projectMetaCache,
280
+ publicRepoCache,
281
+ publicRepoResolver,
282
+ projectContext,
283
+ })
284
+ : await parseRolloutFile({
285
+ filePath,
286
+ fileStat: st,
287
+ startOffset,
288
+ lastTotal,
289
+ lastModel,
290
+ hourlyState,
291
+ touchedBuckets,
292
+ source: fileSource,
293
+ projectState,
294
+ projectTouchedBuckets,
295
+ projectRef,
296
+ projectKey,
297
+ projectMetaCache,
298
+ publicRepoCache,
299
+ publicRepoResolver,
300
+ projectContext,
301
+ seenCodexEvents,
302
+ sessionId: codexSessionIdFromPath(filePath),
303
+ });
202
304
 
203
- cursors.files[key] = {
305
+ const nextCursor = {
204
306
  inode,
205
307
  offset: result.endOffset,
206
308
  lastTotal: result.lastTotal,
207
309
  lastModel: result.lastModel,
208
310
  updatedAt: new Date().toISOString(),
209
311
  };
312
+ if (codexProjectFastPath) {
313
+ nextCursor.projectOffset = result.endOffset;
314
+ nextCursor.projectFileContext = buildProjectFileContext(
315
+ result.projectFileContexts,
316
+ projectFreshnessNowMs,
317
+ );
318
+ } else if (Number.isFinite(prev?.projectOffset)) {
319
+ nextCursor.projectOffset = prev.projectOffset;
320
+ if (prev?.projectFileContext) nextCursor.projectFileContext = prev.projectFileContext;
321
+ }
322
+ cursors.files[key] = nextCursor;
210
323
 
211
324
  filesProcessed += 1;
212
325
  eventsAggregated += result.eventsAggregated;
@@ -238,6 +351,108 @@ async function parseRolloutIncremental({
238
351
  return { filesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
239
352
  }
240
353
 
354
+ async function filterColdCodexRolloutFiles({
355
+ rolloutFiles,
356
+ cursors,
357
+ projectEnabled = false,
358
+ auditDue = false,
359
+ nowMs = Date.now(),
360
+ recentDays = DEFAULT_CODEX_COLD_SKIP_RECENT_DAYS,
361
+ } = {}) {
362
+ const files = Array.isArray(rolloutFiles) ? rolloutFiles : [];
363
+ if (auditDue || !cursors?.files || typeof cursors.files !== "object") {
364
+ return { rolloutFiles: files, skipped: 0 };
365
+ }
366
+
367
+ const activeDates = activeCodexRolloutDates(files, { nowMs, recentDays });
368
+ const freshnessCache = new Map();
369
+ const out = [];
370
+ let skipped = 0;
371
+
372
+ for (const entry of files) {
373
+ const filePath = typeof entry === "string" ? entry : entry?.path;
374
+ const source =
375
+ typeof entry === "string"
376
+ ? DEFAULT_SOURCE
377
+ : normalizeSourceInput(entry?.source) || DEFAULT_SOURCE;
378
+ if (!filePath || source !== DEFAULT_SOURCE) {
379
+ out.push(entry);
380
+ continue;
381
+ }
382
+
383
+ const rolloutDate = rolloutDateFromPath(filePath);
384
+ if (!rolloutDate || activeDates.has(rolloutDate)) {
385
+ out.push(entry);
386
+ continue;
387
+ }
388
+
389
+ const prev = cursors.files[filePath];
390
+ const cachedSize = Number(prev?.offset);
391
+ if (!Number.isFinite(cachedSize) || cachedSize <= 0) {
392
+ out.push(entry);
393
+ continue;
394
+ }
395
+
396
+ if (projectEnabled) {
397
+ const projectOffset = Number(prev?.projectOffset);
398
+ if (!Number.isFinite(projectOffset) || projectOffset < cachedSize) {
399
+ out.push(entry);
400
+ continue;
401
+ }
402
+ if (
403
+ !(await isProjectFileContextFresh(prev?.projectFileContext, {
404
+ freshnessCache,
405
+ nowMs,
406
+ }))
407
+ ) {
408
+ out.push(entry);
409
+ continue;
410
+ }
411
+ }
412
+
413
+ skipped += 1;
414
+ }
415
+
416
+ return { rolloutFiles: out, skipped };
417
+ }
418
+
419
+ function activeCodexRolloutDates(
420
+ rolloutFiles,
421
+ { nowMs = Date.now(), recentDays = DEFAULT_CODEX_COLD_SKIP_RECENT_DAYS } = {},
422
+ ) {
423
+ const active = new Set();
424
+ const days = Math.max(1, Math.floor(Number(recentDays) || 1));
425
+ const now = new Date(nowMs);
426
+ if (Number.isFinite(now.getTime())) {
427
+ for (let i = 0; i < days; i += 1) {
428
+ const d = new Date(now.getFullYear(), now.getMonth(), now.getDate() - i);
429
+ active.add(formatLocalDate(d));
430
+ }
431
+ }
432
+
433
+ let latest = null;
434
+ for (const entry of Array.isArray(rolloutFiles) ? rolloutFiles : []) {
435
+ const filePath = typeof entry === "string" ? entry : entry?.path;
436
+ const source =
437
+ typeof entry === "string"
438
+ ? DEFAULT_SOURCE
439
+ : normalizeSourceInput(entry?.source) || DEFAULT_SOURCE;
440
+ if (source !== DEFAULT_SOURCE) continue;
441
+ const rolloutDate = rolloutDateFromPath(filePath);
442
+ if (rolloutDate && (!latest || rolloutDate > latest)) latest = rolloutDate;
443
+ }
444
+ if (latest) active.add(latest);
445
+
446
+ return active;
447
+ }
448
+
449
+ function formatLocalDate(value) {
450
+ const year = value.getFullYear();
451
+ const month = String(value.getMonth() + 1).padStart(2, "0");
452
+ const day = String(value.getDate()).padStart(2, "0");
453
+ return `${year}-${month}-${day}`;
454
+ }
455
+
241
456
  async function parseClaudeIncremental({
242
457
  projectFiles,
243
458
  cursors,
@@ -352,7 +567,7 @@ async function parseClaudeIncremental({
352
567
  } else {
353
568
  projectRef = null;
354
569
  projectKey = null;
355
- nextProjectFileContext = { absent: true };
570
+ nextProjectFileContext = buildProjectFileContext(null);
356
571
  }
357
572
  }
358
573
 
@@ -928,13 +1143,16 @@ async function parseRolloutFile({
928
1143
  projectMetaCache,
929
1144
  publicRepoCache,
930
1145
  publicRepoResolver,
1146
+ projectContext,
931
1147
  seenCodexEvents,
932
1148
  sessionId,
933
1149
  }) {
934
1150
  const st = fileStat || (await fs.stat(filePath));
935
1151
  const endOffset = st.size;
1152
+ const projectFileContexts = [];
1153
+ addProjectFileContext(projectFileContexts, projectContext);
936
1154
  if (startOffset >= endOffset) {
937
- return { endOffset, lastTotal, lastModel, eventsAggregated: 0 };
1155
+ return { endOffset, lastTotal, lastModel, eventsAggregated: 0, projectFileContexts };
938
1156
  }
939
1157
 
940
1158
  const stream = fssync.createReadStream(filePath, { encoding: "utf8", start: startOffset });
@@ -996,6 +1214,7 @@ async function parseRolloutFile({
996
1214
  currentCwd = nextCwd;
997
1215
  currentProjectRef = context?.projectRef || null;
998
1216
  currentProjectKey = context?.projectKey || null;
1217
+ addProjectFileContext(projectFileContexts, context);
999
1218
  }
1000
1219
  }
1001
1220
  continue;
@@ -1066,7 +1285,73 @@ async function parseRolloutFile({
1066
1285
  eventsAggregated += 1;
1067
1286
  }
1068
1287
 
1069
- return { endOffset, lastTotal: totals, lastModel: model, eventsAggregated };
1288
+ return { endOffset, lastTotal: totals, lastModel: model, eventsAggregated, projectFileContexts };
1289
+ }
1290
+
1291
+ async function scanRolloutProjectFileContexts({
1292
+ filePath,
1293
+ fileStat,
1294
+ lastTotal,
1295
+ lastModel,
1296
+ projectState,
1297
+ projectMetaCache,
1298
+ publicRepoCache,
1299
+ publicRepoResolver,
1300
+ projectContext,
1301
+ }) {
1302
+ const st = fileStat || (await fs.stat(filePath));
1303
+ const endOffset = st.size;
1304
+ const projectFileContexts = [];
1305
+ addProjectFileContext(projectFileContexts, projectContext);
1306
+ if (!projectState || endOffset <= 0) {
1307
+ return { endOffset, lastTotal, lastModel, eventsAggregated: 0, projectFileContexts };
1308
+ }
1309
+
1310
+ const stream = fssync.createReadStream(filePath, { encoding: "utf8", start: 0 });
1311
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
1312
+ let currentCwd = null;
1313
+
1314
+ for await (const line of rl) {
1315
+ if (
1316
+ !line ||
1317
+ !(
1318
+ line.includes('"turn_context"') ||
1319
+ line.includes('"session_meta"')
1320
+ ) ||
1321
+ !line.includes('"cwd"')
1322
+ ) {
1323
+ continue;
1324
+ }
1325
+
1326
+ let obj;
1327
+ try {
1328
+ obj = JSON.parse(line);
1329
+ } catch (_e) {
1330
+ continue;
1331
+ }
1332
+ if (
1333
+ (obj?.type !== "turn_context" && obj?.type !== "session_meta") ||
1334
+ !obj?.payload ||
1335
+ typeof obj.payload !== "object" ||
1336
+ typeof obj.payload.cwd !== "string"
1337
+ ) {
1338
+ continue;
1339
+ }
1340
+
1341
+ const nextCwd = obj.payload.cwd.trim();
1342
+ if (!nextCwd || nextCwd === currentCwd) continue;
1343
+ const context = await resolveProjectContextForPath({
1344
+ startDir: nextCwd,
1345
+ projectMetaCache,
1346
+ publicRepoCache,
1347
+ publicRepoResolver,
1348
+ projectState,
1349
+ });
1350
+ currentCwd = nextCwd;
1351
+ addProjectFileContext(projectFileContexts, context);
1352
+ }
1353
+
1354
+ return { endOffset, lastTotal, lastModel, eventsAggregated: 0, projectFileContexts };
1070
1355
  }
1071
1356
 
1072
1357
  async function parseClaudeFile({
@@ -2135,9 +2420,9 @@ async function resolveProjectMetaForPath(startDir, cache) {
2135
2420
 
2136
2421
  const configPath = await resolveGitConfigPath(current);
2137
2422
  if (configPath) {
2423
+ const configStat = await fs.stat(configPath).catch(() => null);
2138
2424
  const remoteUrl = await readGitRemoteUrl(configPath);
2139
2425
  const projectRef = canonicalizeProjectRef(remoteUrl);
2140
- const configStat = await fs.stat(configPath).catch(() => null);
2141
2426
  const meta = {
2142
2427
  projectRef: projectRef || null,
2143
2428
  repoRoot: current,
@@ -2164,6 +2449,86 @@ async function resolveProjectMetaForPath(startDir, cache) {
2164
2449
  return null;
2165
2450
  }
2166
2451
 
2452
+ function addProjectFileContext(contexts, projectContext) {
2453
+ if (!Array.isArray(contexts) || !projectContext || typeof projectContext !== "object") return;
2454
+ const configPath =
2455
+ typeof projectContext.configPath === "string" && projectContext.configPath
2456
+ ? projectContext.configPath
2457
+ : null;
2458
+ if (!configPath) return;
2459
+ contexts.push(projectContext);
2460
+ }
2461
+
2462
+ function buildProjectFileContext(projectContexts, checkedAtMs = Date.now()) {
2463
+ const contexts = Array.isArray(projectContexts)
2464
+ ? projectContexts
2465
+ : projectContexts && typeof projectContexts === "object"
2466
+ ? [projectContexts]
2467
+ : [];
2468
+ const seen = new Set();
2469
+ const configs = [];
2470
+ for (const context of contexts) {
2471
+ const configPath =
2472
+ typeof context?.configPath === "string" && context.configPath ? context.configPath : null;
2473
+ if (!configPath || seen.has(configPath)) continue;
2474
+ seen.add(configPath);
2475
+ configs.push({
2476
+ configPath,
2477
+ configMtimeMs: Number.isFinite(context.configMtimeMs) ? context.configMtimeMs : null,
2478
+ configSize: Number.isFinite(context.configSize) ? context.configSize : null,
2479
+ });
2480
+ }
2481
+ if (configs.length === 0) return { absent: true, checkedAtMs };
2482
+ if (configs.length > 1) return { configs };
2483
+ const [config] = configs;
2484
+ return {
2485
+ ...config,
2486
+ configs,
2487
+ };
2488
+ }
2489
+
2490
+ async function isProjectFileContextFresh(projectFileContext, options = {}) {
2491
+ if (!projectFileContext || typeof projectFileContext !== "object") return false;
2492
+ const nowMs = Number.isFinite(options.nowMs) ? options.nowMs : Date.now();
2493
+ const absentTtlMs = Number.isFinite(options.absentTtlMs)
2494
+ ? options.absentTtlMs
2495
+ : PROJECT_ABSENT_CONTEXT_RESCAN_MS;
2496
+ const freshnessCache =
2497
+ options.freshnessCache && typeof options.freshnessCache === "object"
2498
+ ? options.freshnessCache
2499
+ : null;
2500
+ if (projectFileContext.absent === true) {
2501
+ const checkedAtMs = Number(projectFileContext.checkedAtMs);
2502
+ return Number.isFinite(checkedAtMs) && checkedAtMs > 0 && nowMs - checkedAtMs < absentTtlMs;
2503
+ }
2504
+ if (Array.isArray(projectFileContext.configs)) {
2505
+ if (projectFileContext.configs.length === 0) return false;
2506
+ for (const config of projectFileContext.configs) {
2507
+ if (!(await isSingleProjectConfigFresh(config, freshnessCache))) return false;
2508
+ }
2509
+ return true;
2510
+ }
2511
+ return isSingleProjectConfigFresh(projectFileContext, freshnessCache);
2512
+ }
2513
+
2514
+ async function isSingleProjectConfigFresh(projectFileContext, freshnessCache = null) {
2515
+ const configPath =
2516
+ typeof projectFileContext.configPath === "string" ? projectFileContext.configPath : null;
2517
+ if (!configPath) return false;
2518
+ let st;
2519
+ if (freshnessCache && freshnessCache.has(configPath)) {
2520
+ st = freshnessCache.get(configPath);
2521
+ } else {
2522
+ st = await fs.stat(configPath).catch(() => null);
2523
+ if (freshnessCache) freshnessCache.set(configPath, st);
2524
+ }
2525
+ if (!st || !st.isFile()) return false;
2526
+ return (
2527
+ st.mtimeMs === projectFileContext.configMtimeMs &&
2528
+ st.size === projectFileContext.configSize
2529
+ );
2530
+ }
2531
+
2167
2532
  function hashRepoRoot(repoRoot) {
2168
2533
  return crypto.createHash("sha256").update(String(repoRoot)).digest("hex");
2169
2534
  }
@@ -2283,6 +2648,9 @@ async function resolveProjectContextForPath({
2283
2648
  projectKey: meta?.projectKey || null,
2284
2649
  status: meta?.status || "blocked",
2285
2650
  repoRootHash: meta?.repoRootHash || repoRootHash,
2651
+ configPath: projectMeta.configPath || null,
2652
+ configMtimeMs: projectMeta.configMtimeMs ?? null,
2653
+ configSize: projectMeta.configSize ?? null,
2286
2654
  };
2287
2655
  recordProjectMeta(projectState, normalized);
2288
2656
  const configFields = {
@@ -2306,30 +2674,6 @@ async function resolveProjectContextForPath({
2306
2674
  };
2307
2675
  }
2308
2676
 
2309
- // Builds a small, cursor-persistable freshness fingerprint for a resolved
2310
- // project context's git config file, so a later sync can tell whether the
2311
- // resolution is still valid without re-walking the filesystem.
2312
- function buildProjectFileContext(context) {
2313
- const configPath = typeof context?.configPath === "string" ? context.configPath : null;
2314
- if (!configPath) return { absent: true };
2315
- return {
2316
- configPath,
2317
- configMtimeMs: Number.isFinite(context.configMtimeMs) ? context.configMtimeMs : null,
2318
- configSize: Number.isFinite(context.configSize) ? context.configSize : null,
2319
- };
2320
- }
2321
-
2322
- async function isProjectFileContextFresh(projectFileContext) {
2323
- if (!projectFileContext || typeof projectFileContext !== "object") return false;
2324
- if (projectFileContext.absent === true) return false;
2325
- const configPath =
2326
- typeof projectFileContext.configPath === "string" ? projectFileContext.configPath : null;
2327
- if (!configPath) return false;
2328
- const st = await fs.stat(configPath).catch(() => null);
2329
- if (!st || !st.isFile()) return false;
2330
- return st.mtimeMs === projectFileContext.configMtimeMs && st.size === projectFileContext.configSize;
2331
- }
2332
-
2333
2677
  // Claude Code session files log the launch cwd on message lines (top-level
2334
2678
  // "cwd" field) — unlike the file's on-disk location under
2335
2679
  // ~/.claude/projects/<dash-encoded-cwd>/, which is not itself inside the
@@ -3446,115 +3790,75 @@ function resolveHermesPath(env = process.env, deps = {}) {
3446
3790
  // On Windows, scan BOTH the native install (%LOCALAPPDATA%\hermes) and
3447
3791
  // any WSL distros running Hermes Agent. Each install has its own independent
3448
3792
  // state.db with different sessions, so there is no double-counting risk.
3449
- // WSL is preferred when both exist because development happens in WSL (#87).
3793
+ // TOKENTRACKER_WSL_MODE controls which source takes priority.
3450
3794
  if (process.platform === "win32") {
3451
3795
  let nativePath = null;
3452
- const localAppData = typeof env.LOCALAPPDATA === "string" ? env.LOCALAPPDATA.trim() : "";
3453
- if (localAppData.length > 0) {
3454
- const candidate = path.join(localAppData, "hermes");
3455
- try {
3456
- if (fssync.existsSync(candidate)) nativePath = candidate;
3457
- } catch (_e) { }
3796
+ if (wsl.shouldProbeNative(env)) {
3797
+ const localAppData = typeof env.LOCALAPPDATA === "string" ? env.LOCALAPPDATA.trim() : "";
3798
+ if (localAppData.length > 0) {
3799
+ const candidate = path.join(localAppData, "hermes");
3800
+ try {
3801
+ if (fssync.existsSync(candidate)) nativePath = candidate;
3802
+ } catch (_e) { }
3803
+ }
3458
3804
  }
3459
- const wslPath = discoverWslHermesHome(deps);
3460
- if (wslPath) return wslPath;
3461
- if (nativePath) return nativePath;
3805
+ const wslPath = wsl.shouldProbeWsl(env) ? discoverWslHermesHome(deps) : null;
3806
+ const picked = wsl.pickWin32Path({ wslValue: wslPath, nativeValue: nativePath, env, platform: "win32" });
3807
+ if (picked) return picked;
3808
+ const mode = wsl.getWslMode(env);
3809
+ if (mode === "wsl-only" || mode === "native-only") return null;
3462
3810
  }
3463
3811
  return defaultPath;
3464
3812
  }
3465
3813
 
3466
- // ── WSL auto-discovery (Windows host, Hermes installed inside a distro) ──────
3814
+ // ── WSL auto-discovery (Windows host, tools inside a distro) ──────────────────
3467
3815
  // `wsl.exe -l -v` prints UTF-16LE; the per-distro `whoami` runs a Linux process
3468
3816
  // so its stdout is UTF-8. We capture buffers and decode per-call.
3469
3817
  function defaultRunWsl(args, { utf16 = false } = {}) {
3470
- const { execFileSync } = require("node:child_process");
3471
- const buf = execFileSync("wsl.exe", args, {
3472
- timeout: 5000,
3473
- windowsHide: true,
3474
- maxBuffer: 1024 * 1024,
3475
- stdio: ["ignore", "pipe", "ignore"],
3476
- });
3477
- return utf16 ? buf.toString("utf16le") : buf.toString("utf8");
3818
+ return wsl.defaultRunWsl(args, { utf16 });
3478
3819
  }
3479
3820
 
3480
3821
  // Parse `wsl.exe -l -v` output into [{ name, version, isDefault }]. The default
3481
3822
  // distro is prefixed with `*`; the VERSION column (1 or 2) decides which UNC
3482
3823
  // alias to try first (simonlpaige, #87).
3483
3824
  function parseWslListVerbose(raw) {
3484
- if (typeof raw !== "string") return [];
3485
- const distros = [];
3486
- for (const line of raw.split(/\r?\n/)) {
3487
- const clean = line.replace(//g, "").replace(//g, "").trim();
3488
- if (!clean) continue;
3489
- const cells = clean.split(/\s+/);
3490
- let isDefault = false;
3491
- let idx = 0;
3492
- if (cells[0] === "*") {
3493
- isDefault = true;
3494
- idx = 1;
3495
- }
3496
- const name = cells[idx];
3497
- if (!name || name === "NAME") continue; // skip header row
3498
- const version = parseInt(cells[cells.length - 1], 10);
3499
- distros.push({
3500
- name,
3501
- version: Number.isFinite(version) ? version : null,
3502
- isDefault,
3503
- });
3504
- }
3505
- return distros;
3825
+ return wsl.parseWslListVerbose(raw);
3506
3826
  }
3507
3827
 
3508
3828
  // Default distro first, then listed order — matches what a user expects when
3509
3829
  // they have one primary distro plus extras.
3510
3830
  function probeWslDistros(deps = {}) {
3511
- const runWsl = deps.runWsl || defaultRunWsl;
3512
- let raw;
3513
- try {
3514
- raw = runWsl(["-l", "-v"], { utf16: true });
3515
- } catch (_e) {
3516
- return [];
3517
- }
3518
- const distros = parseWslListVerbose(raw);
3519
- return distros.sort((a, b) => (b.isDefault ? 1 : 0) - (a.isDefault ? 1 : 0));
3831
+ return wsl.probeWslDistros(deps);
3520
3832
  }
3521
3833
 
3522
3834
  // Probe each WSL distro for ~/.hermes via UNC. The Linux username rarely equals
3523
3835
  // %USERNAME%, so we ask the distro directly with `whoami` rather than guessing
3524
3836
  // (simonlpaige, #87).
3525
3837
  function discoverWslHermesHome(deps = {}) {
3526
- const runWsl = deps.runWsl || defaultRunWsl;
3527
- const existsSync = deps.existsSync || fssync.existsSync;
3528
- const distros = probeWslDistros(deps);
3529
- for (const distro of distros) {
3530
- let user;
3531
- try {
3532
- user = String(runWsl(["-d", distro.name, "-e", "whoami"], { utf16: false }) || "").trim();
3533
- } catch (_e) {
3534
- user = "";
3535
- }
3536
- if (!user) continue;
3537
- // \\wsl$\ is the legacy alias, \\wsl.localhost\ the newer one. WSL1 distros
3538
- // sometimes only resolve via \\wsl.localhost\, so order by version.
3539
- const roots = distro.version === 1
3540
- ? ["\\\\wsl.localhost\\", "\\\\wsl$\\"]
3541
- : ["\\\\wsl$\\", "\\\\wsl.localhost\\"];
3542
- for (const root of roots) {
3543
- const candidate = `${root}${distro.name}\\home\\${user}\\.hermes`;
3544
- try {
3545
- if (existsSync(candidate)) return candidate;
3546
- } catch (_e) { }
3547
- }
3548
- }
3549
- return null;
3838
+ return wsl.discoverWslHome(".hermes", deps);
3839
+ }
3840
+
3841
+ function pickWin32ProviderPath({ env = process.env, nativeValue, wslProviderDir, wslValue }) {
3842
+ const nativeCandidate = wsl.shouldProbeNative(env) ? nativeValue : null;
3843
+ const wslCandidate = wslValue !== undefined
3844
+ ? wslValue
3845
+ : (wsl.shouldProbeWsl(env) ? wsl.discoverWslHome(wslProviderDir) : null);
3846
+ return wsl.pickWin32Path({
3847
+ wslValue: wslCandidate,
3848
+ nativeValue: nativeCandidate,
3849
+ env,
3850
+ platform: "win32",
3851
+ });
3550
3852
  }
3551
3853
 
3552
3854
  function resolveHermesDbPath(env = process.env) {
3553
- return path.join(resolveHermesPath(env), "state.db");
3855
+ const hermesPath = resolveHermesPath(env);
3856
+ return hermesPath ? path.join(hermesPath, "state.db") : null;
3554
3857
  }
3555
3858
 
3556
3859
  function resolveAllHermesDBPaths({ hermesPath, dbPath } = {}) {
3557
3860
  const hermesDir = hermesPath ?? (dbPath ? path.dirname(dbPath) : resolveHermesPath());
3861
+ if (!hermesDir) return { default: null, profiles: {} };
3558
3862
  const defaultDbPath = dbPath ?? path.join(hermesDir, "state.db");
3559
3863
  const profilePaths = {};
3560
3864
  try {
@@ -3586,28 +3890,11 @@ function sqliteStringLiteral(value) {
3586
3890
  // / SMB bridge can't grant the locks SQLite asks for — even after `wsl
3587
3891
  // --shutdown`. Detect those paths so we can snapshot the DB locally first.
3588
3892
  function isUncPath(p) {
3589
- return typeof p === "string" && (p.startsWith("\\\\") || p.startsWith("//"));
3893
+ return wsl.isUncPath(p);
3590
3894
  }
3591
3895
 
3592
3896
  function snapshotSqliteDb(dbPath) {
3593
- const tmpRoot = fssync.mkdtempSync(
3594
- path.join(require("node:os").tmpdir(), "tokentracker-hermes-snap-"),
3595
- );
3596
- const target = path.join(tmpRoot, path.basename(dbPath));
3597
- fssync.copyFileSync(dbPath, target);
3598
- // Best-effort copy of SQLite sidecars; missing -wal/-shm/-journal is fine.
3599
- for (const suffix of ["-wal", "-shm", "-journal"]) {
3600
- const src = dbPath + suffix;
3601
- try {
3602
- if (fssync.existsSync(src)) fssync.copyFileSync(src, target + suffix);
3603
- } catch (_e) { }
3604
- }
3605
- return {
3606
- path: target,
3607
- cleanup() {
3608
- try { fssync.rmSync(tmpRoot, { recursive: true, force: true }); } catch (_e) { }
3609
- },
3610
- };
3897
+ return wsl.snapshotSqliteDb(dbPath);
3611
3898
  }
3612
3899
 
3613
3900
  function readHermesSessions(dbPath, lastCompletedEpoch, unfinishedSessionIds = [], sqliteOptions = {}) {
@@ -4908,11 +5195,21 @@ async function parseKimiIncremental({ wireFiles, cursors, queuePath, onProgress,
4908
5195
  function resolveKimiCodeHome(env = process.env) {
4909
5196
  const home = require("node:os").homedir();
4910
5197
  const explicit = typeof env?.KIMI_CODE_HOME === "string" ? env.KIMI_CODE_HOME.trim() : "";
4911
- return explicit ? path.resolve(explicit) : path.join(home, ".kimi-code");
5198
+ if (explicit) return path.resolve(explicit);
5199
+ if (process.platform === "win32") {
5200
+ return pickWin32ProviderPath({
5201
+ env,
5202
+ nativeValue: path.join(home, ".kimi-code"),
5203
+ wslProviderDir: ".kimi-code",
5204
+ });
5205
+ }
5206
+ return path.join(home, ".kimi-code");
4912
5207
  }
4913
5208
 
4914
5209
  function resolveKimiCodeWireFiles(env = process.env) {
4915
- const sessionsDir = path.join(resolveKimiCodeHome(env), "sessions");
5210
+ const kimiHome = resolveKimiCodeHome(env);
5211
+ if (!kimiHome) return [];
5212
+ const sessionsDir = path.join(kimiHome, "sessions");
4916
5213
  if (!fssync.existsSync(sessionsDir)) return [];
4917
5214
  const files = [];
4918
5215
  const walk = (dir, depth) => {
@@ -4932,7 +5229,9 @@ function resolveKimiCodeWireFiles(env = process.env) {
4932
5229
  function resolveKimiCodeDefaultModel(env = process.env) {
4933
5230
  const fallback = "kimi-for-coding";
4934
5231
  try {
4935
- const cfgPath = path.join(resolveKimiCodeHome(env), "config.toml");
5232
+ const kimiHome = resolveKimiCodeHome(env);
5233
+ if (!kimiHome) return fallback;
5234
+ const cfgPath = path.join(kimiHome, "config.toml");
4936
5235
  const raw = fssync.readFileSync(cfgPath, "utf8");
4937
5236
  const m = raw.match(/^\s*default_model\s*=\s*"([^"]+)"/m);
4938
5237
  if (!m) return fallback;
@@ -5136,13 +5435,23 @@ async function parseKimiCodeIncremental({ wireFiles, cursors, queuePath, onProgr
5136
5435
 
5137
5436
  function resolveCodebuddyHome(env = process.env) {
5138
5437
  const home = env.HOME || require("node:os").homedir();
5139
- return env.CODEBUDDY_HOME || path.join(home, ".codebuddy");
5438
+ if (env.CODEBUDDY_HOME) return env.CODEBUDDY_HOME;
5439
+ if (process.platform === "win32") {
5440
+ return pickWin32ProviderPath({
5441
+ env,
5442
+ nativeValue: path.join(home, ".codebuddy"),
5443
+ wslProviderDir: ".codebuddy",
5444
+ });
5445
+ }
5446
+ return path.join(home, ".codebuddy");
5140
5447
  }
5141
5448
 
5142
5449
  function resolveCodebuddyDefaultModel(env = process.env) {
5143
5450
  const fallback = "codebuddy-unknown";
5144
5451
  try {
5145
- const settingsPath = path.join(resolveCodebuddyHome(env), "settings.json");
5452
+ const codebuddyHome = resolveCodebuddyHome(env);
5453
+ if (!codebuddyHome) return fallback;
5454
+ const settingsPath = path.join(codebuddyHome, "settings.json");
5146
5455
  const raw = fssync.readFileSync(settingsPath, "utf8");
5147
5456
  const parsed = JSON.parse(raw);
5148
5457
  if (parsed && typeof parsed === "object" && typeof parsed.model === "string" && parsed.model.trim()) {
@@ -5155,7 +5464,9 @@ function resolveCodebuddyDefaultModel(env = process.env) {
5155
5464
  }
5156
5465
 
5157
5466
  function resolveCodebuddyProjectFiles(env = process.env) {
5158
- const projectsDir = path.join(resolveCodebuddyHome(env), "projects");
5467
+ const codebuddyHome = resolveCodebuddyHome(env);
5468
+ if (!codebuddyHome) return [];
5469
+ const projectsDir = path.join(codebuddyHome, "projects");
5159
5470
  if (!fssync.existsSync(projectsDir)) return [];
5160
5471
  const files = [];
5161
5472
  try {
@@ -5430,13 +5741,23 @@ async function parseCodebuddyIncremental({
5430
5741
 
5431
5742
  function resolveWorkbuddyHome(env = process.env) {
5432
5743
  const home = env.HOME || require("node:os").homedir();
5433
- return env.WORKBUDDY_HOME || path.join(home, ".workbuddy");
5744
+ if (env.WORKBUDDY_HOME) return env.WORKBUDDY_HOME;
5745
+ if (process.platform === "win32") {
5746
+ return pickWin32ProviderPath({
5747
+ env,
5748
+ nativeValue: path.join(home, ".workbuddy"),
5749
+ wslProviderDir: ".workbuddy",
5750
+ });
5751
+ }
5752
+ return path.join(home, ".workbuddy");
5434
5753
  }
5435
5754
 
5436
5755
  function resolveWorkbuddyDefaultModel(env = process.env) {
5437
5756
  const fallback = "auto";
5438
5757
  try {
5439
- const settingsPath = path.join(resolveWorkbuddyHome(env), "settings.json");
5758
+ const workbuddyHome = resolveWorkbuddyHome(env);
5759
+ if (!workbuddyHome) return fallback;
5760
+ const settingsPath = path.join(workbuddyHome, "settings.json");
5440
5761
  const raw = fssync.readFileSync(settingsPath, "utf8");
5441
5762
  const parsed = JSON.parse(raw);
5442
5763
  if (parsed && typeof parsed === "object" && typeof parsed.model === "string" && parsed.model.trim()) {
@@ -5452,7 +5773,9 @@ function resolveWorkbuddyDefaultModel(env = process.env) {
5452
5773
  // per-session conversation logs AND their nested subagents/agent-*.jsonl files
5453
5774
  // are both discovered. Non-.jsonl artefacts (tool-results/*.txt) are ignored.
5454
5775
  function resolveWorkbuddyProjectFiles(env = process.env) {
5455
- const projectsDir = path.join(resolveWorkbuddyHome(env), "projects");
5776
+ const workbuddyHome = resolveWorkbuddyHome(env);
5777
+ if (!workbuddyHome) return [];
5778
+ const projectsDir = path.join(workbuddyHome, "projects");
5456
5779
  if (!fssync.existsSync(projectsDir)) return [];
5457
5780
  const files = [];
5458
5781
  const walk = (dir) => {
@@ -5726,6 +6049,13 @@ function resolveOmpHome(env = process.env) {
5726
6049
  // Honor TokenTracker override first, then oh-my-pi upstream env vars.
5727
6050
  if (env.OMP_HOME) return env.OMP_HOME;
5728
6051
  if (env.PI_CONFIG_DIR) return path.join(home, env.PI_CONFIG_DIR);
6052
+ if (process.platform === "win32") {
6053
+ return pickWin32ProviderPath({
6054
+ env,
6055
+ nativeValue: path.join(home, ".omp"),
6056
+ wslProviderDir: ".omp",
6057
+ });
6058
+ }
5729
6059
  return path.join(home, ".omp");
5730
6060
  }
5731
6061
 
@@ -5763,11 +6093,14 @@ function resolveOmpAgentDir(env = process.env) {
5763
6093
  if (env.PI_CODING_AGENT_DIR && decidePiCodingAgentDirOwner(env) === "omp") {
5764
6094
  return expandHomePath(env.PI_CODING_AGENT_DIR, env);
5765
6095
  }
5766
- return path.join(resolveOmpHome(env), "agent");
6096
+ const ompHome = resolveOmpHome(env);
6097
+ return ompHome ? path.join(ompHome, "agent") : null;
5767
6098
  }
5768
6099
 
5769
6100
  function resolveOmpSessionFiles(env = process.env) {
5770
- const sessionsDir = path.join(resolveOmpAgentDir(env), "sessions");
6101
+ const agentDir = resolveOmpAgentDir(env);
6102
+ if (!agentDir) return [];
6103
+ const sessionsDir = path.join(agentDir, "sessions");
5771
6104
  if (!fssync.existsSync(sessionsDir)) return [];
5772
6105
  const files = [];
5773
6106
  try {
@@ -5843,14 +6176,28 @@ function resolveKilocodeRoots(env = process.env) {
5843
6176
  );
5844
6177
  } else if (process.platform === "win32") {
5845
6178
  const appData = env.APPDATA || path.join(home, "AppData", "Roaming");
5846
- candidates.push(
6179
+ const nativeRoots = [
5847
6180
  path.join(appData, "Code"),
5848
6181
  path.join(appData, "Code - Insiders"),
5849
6182
  path.join(appData, "Cursor"),
5850
6183
  path.join(appData, "CodeBuddy"),
5851
6184
  path.join(appData, "Windsurf"),
5852
6185
  path.join(appData, "VSCodium"),
5853
- );
6186
+ ];
6187
+ const wslRoots = [];
6188
+ if (wsl.shouldProbeWsl(env)) {
6189
+ for (const ide of ["Code", "Code - Insiders", "Cursor", "CodeBuddy", "Windsurf", "VSCodium"]) {
6190
+ const wslDir = wsl.discoverWslHome(`.config/${ide}`, { env });
6191
+ if (wslDir) wslRoots.push(wslDir);
6192
+ }
6193
+ }
6194
+ const nativeCandidates = wsl.shouldProbeNative(env) ? nativeRoots : [];
6195
+ const mode = wsl.getWslMode(env);
6196
+ if (mode === "native-first" || mode === "native-only") {
6197
+ candidates.push(...nativeCandidates, ...wslRoots);
6198
+ } else {
6199
+ candidates.push(...wslRoots, ...nativeCandidates);
6200
+ }
5854
6201
  } else {
5855
6202
  const xdg = env.XDG_CONFIG_HOME || path.join(home, ".config");
5856
6203
  candidates.push(
@@ -6178,7 +6525,17 @@ function resolveZedDbPath(env = process.env) {
6178
6525
  }
6179
6526
  if (process.platform === "win32") {
6180
6527
  const local = env.LOCALAPPDATA || path.join(home, "AppData", "Local");
6181
- return path.join(local, "Zed", "threads", "threads.db");
6528
+ const native = path.join(local, "Zed", "threads", "threads.db");
6529
+ let nativeExists = null;
6530
+ if (wsl.shouldProbeNative(env)) {
6531
+ try { if (fssync.existsSync(native)) nativeExists = native; } catch (_e) { }
6532
+ }
6533
+ const wslDir = wsl.shouldProbeWsl(env) ? wsl.discoverWslHome(".local/share/zed/threads", { env }) : null;
6534
+ const wslValue = wslDir ? path.join(wslDir, "threads.db") : null;
6535
+ const picked = wsl.pickWin32Path({ wslValue, nativeValue: nativeExists, env, platform: "win32" });
6536
+ if (picked) return picked;
6537
+ const mode = wsl.getWslMode(env);
6538
+ return mode === "wsl-only" || mode === "native-only" ? null : native;
6182
6539
  }
6183
6540
  const xdg = env.XDG_DATA_HOME || path.join(home, ".local", "share");
6184
6541
  return path.join(xdg, "zed", "threads", "threads.db");
@@ -6330,6 +6687,10 @@ async function parseZedIncremental({
6330
6687
  } = {}) {
6331
6688
  await ensureDir(path.dirname(queuePath));
6332
6689
  const resolvedDb = dbPath || resolveZedDbPath(env || process.env);
6690
+ if (!resolvedDb) {
6691
+ cursors.zed = { ...cursors.zed, updatedAt: new Date().toISOString() };
6692
+ return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
6693
+ }
6333
6694
  const zedState =
6334
6695
  cursors.zed && typeof cursors.zed === "object" ? cursors.zed : {};
6335
6696
  const threadTotals =
@@ -6547,7 +6908,20 @@ function resolveGooseDbPath(env = process.env) {
6547
6908
  );
6548
6909
  } else if (process.platform === "win32") {
6549
6910
  const appData = env.APPDATA || path.join(home, "AppData", "Roaming");
6550
- candidates.push(path.join(appData, "goose", "sessions", "sessions.db"));
6911
+ const native = path.join(appData, "goose", "sessions", "sessions.db");
6912
+ let nativeExists = null;
6913
+ if (wsl.shouldProbeNative(env)) {
6914
+ try { if (fssync.existsSync(native)) nativeExists = native; } catch (_e) { }
6915
+ }
6916
+ const wslDir = wsl.shouldProbeWsl(env) ? wsl.discoverWslHome(".local/share/goose/sessions", { env }) : null;
6917
+ const wslValue = wslDir ? path.join(wslDir, "sessions.db") : null;
6918
+ const picked = wsl.pickWin32Path({ wslValue, nativeValue: nativeExists, env, platform: "win32" });
6919
+ if (picked) candidates.push(picked);
6920
+ for (const c of candidates) {
6921
+ if (fssync.existsSync(c)) return c;
6922
+ }
6923
+ const mode = wsl.getWslMode(env);
6924
+ return mode === "wsl-only" || mode === "native-only" ? null : native;
6551
6925
  }
6552
6926
  const xdg = env.XDG_DATA_HOME || path.join(home, ".local", "share");
6553
6927
  candidates.push(
@@ -6559,7 +6933,7 @@ function resolveGooseDbPath(env = process.env) {
6559
6933
  for (const c of candidates) {
6560
6934
  if (fssync.existsSync(c)) return c;
6561
6935
  }
6562
- return candidates[0];
6936
+ return candidates[0] || null;
6563
6937
  }
6564
6938
 
6565
6939
  function parseGooseModelName(modelConfigJson) {
@@ -6654,6 +7028,11 @@ async function parseGooseIncremental({
6654
7028
  ? { ...gooseState.sessionTotals }
6655
7029
  : {};
6656
7030
 
7031
+ if (!resolvedDb) {
7032
+ cursors.goose = { ...gooseState, sessionTotals, updatedAt: new Date().toISOString() };
7033
+ return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
7034
+ }
7035
+
6657
7036
  const cursorDbMtime = Number.isFinite(gooseState.lastDbMtimeMs) ? gooseState.lastDbMtimeMs : 0;
6658
7037
  let currentMtime = 0;
6659
7038
  try {
@@ -6835,6 +7214,14 @@ function resolveDroidSessionsDirs(env = process.env) {
6835
7214
  return [path.join(expandHomePath(env.FACTORY_DIR.trim(), env), "sessions")];
6836
7215
  }
6837
7216
  const home = env.HOME || require("node:os").homedir();
7217
+ if (process.platform === "win32") {
7218
+ const picked = pickWin32ProviderPath({
7219
+ env,
7220
+ nativeValue: path.join(home, ".factory", "sessions"),
7221
+ wslProviderDir: ".factory/sessions",
7222
+ });
7223
+ return picked ? [picked] : [];
7224
+ }
6838
7225
  return [path.join(home, ".factory", "sessions")];
6839
7226
  }
6840
7227
 
@@ -7692,6 +8079,13 @@ async function parseOmpIncremental({
7692
8079
 
7693
8080
  function resolvePiHome(env = process.env) {
7694
8081
  const home = env.HOME || require("node:os").homedir();
8082
+ if (process.platform === "win32") {
8083
+ return pickWin32ProviderPath({
8084
+ env,
8085
+ nativeValue: path.join(home, ".pi"),
8086
+ wslProviderDir: ".pi",
8087
+ });
8088
+ }
7695
8089
  return path.join(home, ".pi");
7696
8090
  }
7697
8091
 
@@ -7702,7 +8096,8 @@ function resolvePiAgentDir(env = process.env) {
7702
8096
  if (env.PI_CODING_AGENT_DIR && decidePiCodingAgentDirOwner(env) === "pi") {
7703
8097
  return expandHomePath(env.PI_CODING_AGENT_DIR, env);
7704
8098
  }
7705
- return path.join(resolvePiHome(env), "agent");
8099
+ const piHome = resolvePiHome(env);
8100
+ return piHome ? path.join(piHome, "agent") : null;
7706
8101
  }
7707
8102
 
7708
8103
  // Defense in depth for invariant 2 (no double-count). Two explicit overrides
@@ -7711,11 +8106,16 @@ function resolvePiAgentDir(env = process.env) {
7711
8106
  // the install-signal disambiguator and would otherwise have both providers scan
7712
8107
  // the same sessions directory under different `source` tags.
7713
8108
  function piAgentDirCollidesWithOmp(env = process.env) {
7714
- return path.resolve(resolvePiAgentDir(env)) === path.resolve(resolveOmpAgentDir(env));
8109
+ const piAgentDir = resolvePiAgentDir(env);
8110
+ const ompAgentDir = resolveOmpAgentDir(env);
8111
+ if (!piAgentDir || !ompAgentDir) return false;
8112
+ return path.resolve(piAgentDir) === path.resolve(ompAgentDir);
7715
8113
  }
7716
8114
 
7717
8115
  function resolvePiSessionFiles(env = process.env) {
7718
- const sessionsDir = path.join(resolvePiAgentDir(env), "sessions");
8116
+ const agentDir = resolvePiAgentDir(env);
8117
+ if (!agentDir) return [];
8118
+ const sessionsDir = path.join(agentDir, "sessions");
7719
8119
  if (!fssync.existsSync(sessionsDir)) return [];
7720
8120
  const files = [];
7721
8121
  try {
@@ -7963,11 +8363,19 @@ async function parsePiIncremental({
7963
8363
  function resolveCraftConfigDir(env = process.env) {
7964
8364
  if (env.CRAFT_CONFIG_DIR) return env.CRAFT_CONFIG_DIR;
7965
8365
  const home = env.HOME || require("node:os").homedir();
8366
+ if (process.platform === "win32") {
8367
+ return pickWin32ProviderPath({
8368
+ env,
8369
+ nativeValue: path.join(home, ".craft-agent"),
8370
+ wslProviderDir: ".craft-agent",
8371
+ });
8372
+ }
7966
8373
  return path.join(home, ".craft-agent");
7967
8374
  }
7968
8375
 
7969
8376
  function resolveCraftWorkspaceRoots(env = process.env) {
7970
8377
  const configDir = resolveCraftConfigDir(env);
8378
+ if (!configDir) return [];
7971
8379
  const roots = new Set();
7972
8380
  // Always include the default workspaces directory so a fresh install (no
7973
8381
  // config.json yet) still gets discovered.
@@ -8261,13 +8669,22 @@ async function parseCraftIncremental({
8261
8669
  function resolveCopilotOtelPaths(env = process.env) {
8262
8670
  const home = env.HOME || require("node:os").homedir();
8263
8671
  const paths = new Set();
8264
- const defaultDir = path.join(home, ".copilot", "otel");
8265
- if (fssync.existsSync(defaultDir)) {
8672
+ const scanDir = (dir) => {
8673
+ if (!fssync.existsSync(dir)) return;
8266
8674
  try {
8267
- for (const entry of fssync.readdirSync(defaultDir)) {
8268
- if (entry.endsWith(".jsonl")) paths.add(path.join(defaultDir, entry));
8675
+ for (const entry of fssync.readdirSync(dir)) {
8676
+ if (entry.endsWith(".jsonl")) paths.add(path.join(dir, entry));
8269
8677
  }
8270
8678
  } catch (_e) {}
8679
+ };
8680
+ if (process.platform !== "win32" || wsl.shouldProbeNative(env)) {
8681
+ scanDir(path.join(home, ".copilot", "otel"));
8682
+ }
8683
+ if (process.platform === "win32") {
8684
+ if (wsl.shouldProbeWsl(env)) {
8685
+ const wslDir = wsl.discoverWslHome(".copilot/otel", { env });
8686
+ if (wslDir) scanDir(wslDir);
8687
+ }
8271
8688
  }
8272
8689
  const explicit = env.COPILOT_OTEL_FILE_EXPORTER_PATH;
8273
8690
  if (typeof explicit === "string" && explicit.trim() && fssync.existsSync(explicit)) {
@@ -8590,15 +9007,21 @@ const GROK_ESTIMATED_INPUT_RATIO = 0.8;
8590
9007
  const GROK_CURSOR_VERSION = 3;
8591
9008
 
8592
9009
  function resolveGrokBuildHome(env = process.env) {
8593
- return (
8594
- env.TOKENTRACKER_GROK_HOME ||
8595
- env.GROK_HOME ||
8596
- path.join(require("node:os").homedir(), ".grok")
8597
- );
9010
+ if (env.TOKENTRACKER_GROK_HOME) return env.TOKENTRACKER_GROK_HOME;
9011
+ if (env.GROK_HOME) return env.GROK_HOME;
9012
+ if (process.platform === "win32") {
9013
+ return pickWin32ProviderPath({
9014
+ env,
9015
+ nativeValue: path.join(require("node:os").homedir(), ".grok"),
9016
+ wslProviderDir: ".grok",
9017
+ });
9018
+ }
9019
+ return path.join(require("node:os").homedir(), ".grok");
8598
9020
  }
8599
9021
 
8600
9022
  function resolveGrokBuildSessions(env = process.env) {
8601
9023
  const home = resolveGrokBuildHome(env);
9024
+ if (!home) return [];
8602
9025
  const sessionsRoot = path.join(home, "sessions");
8603
9026
  if (!fssync.existsSync(sessionsRoot)) return [];
8604
9027
 
@@ -9472,6 +9895,7 @@ module.exports = {
9472
9895
  listRolloutFiles,
9473
9896
  listRolloutFilesDeep,
9474
9897
  codexSessionIdFromPath,
9898
+ filterColdCodexRolloutFiles,
9475
9899
  listClaudeProjectFiles,
9476
9900
  listGeminiSessionFiles,
9477
9901
  listOpencodeMessageFiles,