tokentracker-cli 0.64.2 → 0.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/dist/assets/ActivityHeatmap-Ccf0I3uP.js +42 -0
- package/dashboard/dist/assets/{Card-Cg-6DDbN.js → Card-BSF8dYEt.js} +1 -1
- package/dashboard/dist/assets/ConfirmModal-Bjp6eg2f.js +1 -0
- package/dashboard/dist/assets/DashboardPage-BInQkoDe.js +19 -0
- package/dashboard/dist/assets/{DevicePage-C78nzmZm.js → DevicePage-B0yxbDDR.js} +1 -1
- package/dashboard/dist/assets/DialogTitle-uw-Oa5Qv.js +1 -0
- package/dashboard/dist/assets/{FadeIn-RZKrtHup.js → FadeIn-DPIBqXRl.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-B1gFlLrg.js → HeaderGithubStar-DDXfJ1uC.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-RrZDmOFh.js → IpCheckPage-KC93tPlt.js} +1 -1
- package/dashboard/dist/assets/LandingPage-Itx2uPWI.js +4415 -0
- package/dashboard/dist/assets/{LeaderboardAvatar-_tiTs_tT.js → LeaderboardAvatar-Bv44b_Kl.js} +1 -1
- package/dashboard/dist/assets/LeaderboardPage-DDq5gzyz.js +2 -0
- package/dashboard/dist/assets/{LeaderboardProfileModal-BiQREZ6Z.js → LeaderboardProfileModal-LQyYrCf1.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfilePage-DU43hEuh.js → LeaderboardProfilePage-3you3mbA.js} +2 -2
- package/dashboard/dist/assets/LimitsPage-CrLvR8vE.js +3 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-Dvcf7F9s.js → LocalOnlyNotice-Di1gnfIi.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-UlbsYFKJ.js → LoginPage-f9QHz_uZ.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-BsBKLiJu.js → PopoverPopup-7rjAxAqk.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-BuKemqhr.js → ResetPasswordPage-DKA7HGpJ.js} +1 -1
- package/dashboard/dist/assets/{Select-9oplHX46.js → Select-2gikPo1a.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-CnL08w_W.js → SelectItemText-DZx33XF1.js} +1 -1
- package/dashboard/dist/assets/SettingsPage-CwHPmuZV.js +1 -0
- package/dashboard/dist/assets/SkillsPage-B6BJ9UZp.js +1 -0
- package/dashboard/dist/assets/WidgetsPage-D8p3Tx7E.js +1 -0
- package/dashboard/dist/assets/{WrappedPage-DPAz_bit.js → WrappedPage-cXEfhti9.js} +1 -1
- package/dashboard/dist/assets/{agent-logos-uSGgZshc.js → agent-logos-D3iFlaWg.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-9sot4sNP.js → arrow-up-right-D87Aw_ZN.js} +1 -1
- package/dashboard/dist/assets/{download-3O0nnnqv.js → download-BtcEs1OB.js} +1 -1
- package/dashboard/dist/assets/index-BFIcLxb4.js +2 -0
- package/dashboard/dist/assets/main-CGx8BJ4O.css +1 -0
- package/dashboard/dist/assets/{main-C5qSiLnL.js → main-DqnXui98.js} +30 -19
- package/dashboard/dist/assets/{use-limits-display-prefs-Bh7PFn6x.js → use-limits-display-prefs-DAxlRSu2.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-MKCjJWeQ.js → use-native-settings-CgvB_B6W.js} +1 -1
- package/dashboard/dist/assets/use-ordered-list-DOfX6aSy.js +5 -0
- package/dashboard/dist/assets/{use-quality-per-dollar-pref-D-m9WqCz.js → use-quality-per-dollar-pref-kEbwQwk2.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-CCu4bGI4.js → use-usage-limits-la-KPfJj.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-BZmZ49MO.js → useCurrency-LDRg7ST0.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-BdBmNNmw.js → useScrollLock-lP1Is9IY.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +1 -1
- package/src/cli.js +7 -0
- package/src/commands/status.js +38 -0
- package/src/commands/sync.js +1 -0
- package/src/lib/pricing/curated-overrides.json +1 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/project-usage-purge.js +13 -2
- package/src/lib/rollout.js +364 -44
- package/src/lib/usage-limits.js +210 -2
- package/dashboard/dist/assets/ActivityHeatmap-CrW1u40M.js +0 -42
- package/dashboard/dist/assets/DashboardPage-Bl8jvnt-.js +0 -19
- package/dashboard/dist/assets/DialogTitle-COVyZQVB.js +0 -1
- package/dashboard/dist/assets/LandingPage-CV9EvgTg.js +0 -4411
- package/dashboard/dist/assets/LeaderboardPage-2tltO9IR.js +0 -6
- package/dashboard/dist/assets/LimitsPage-C7xuyH5P.js +0 -3
- package/dashboard/dist/assets/SettingsPage-CeqgABWP.js +0 -1
- package/dashboard/dist/assets/SkillsPage-CW6aD8GD.js +0 -1
- package/dashboard/dist/assets/WidgetsPage-DtOkMdxK.js +0 -1
- package/dashboard/dist/assets/index-BeoRn2gJ.js +0 -2
- package/dashboard/dist/assets/info-CfCBZRtQ.js +0 -1
- package/dashboard/dist/assets/main-DWsITcdg.css +0 -1
|
@@ -8,9 +8,10 @@ async function purgeProjectUsage({
|
|
|
8
8
|
projectQueuePath,
|
|
9
9
|
projectQueueStatePath,
|
|
10
10
|
projectState,
|
|
11
|
+
cursors,
|
|
11
12
|
}) {
|
|
12
13
|
if (!projectKey || !projectQueuePath || !projectQueueStatePath || !projectState) {
|
|
13
|
-
return { removed: 0, kept: 0, removedBuckets: 0 };
|
|
14
|
+
return { removed: 0, kept: 0, removedBuckets: 0, removedProjectCursors: 0 };
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
let previousOffset = 0;
|
|
@@ -37,6 +38,16 @@ async function purgeProjectUsage({
|
|
|
37
38
|
}
|
|
38
39
|
projectState.buckets = buckets;
|
|
39
40
|
|
|
41
|
+
let removedProjectCursors = 0;
|
|
42
|
+
const files = cursors?.files && typeof cursors.files === "object" ? cursors.files : {};
|
|
43
|
+
for (const cursor of Object.values(files)) {
|
|
44
|
+
if (!cursor || typeof cursor !== "object") continue;
|
|
45
|
+
if (cursor.project?.projectKey === projectKey) {
|
|
46
|
+
delete cursor.project;
|
|
47
|
+
removedProjectCursors += 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
let removed = 0;
|
|
41
52
|
let kept = 0;
|
|
42
53
|
let nextOffset = 0;
|
|
@@ -100,7 +111,7 @@ async function purgeProjectUsage({
|
|
|
100
111
|
|
|
101
112
|
await fsp.writeFile(projectQueueStatePath, JSON.stringify({ offset: nextOffset }), "utf8");
|
|
102
113
|
|
|
103
|
-
return { removed, kept, removedBuckets };
|
|
114
|
+
return { removed, kept, removedBuckets, removedProjectCursors };
|
|
104
115
|
}
|
|
105
116
|
|
|
106
117
|
module.exports = { purgeProjectUsage };
|
package/src/lib/rollout.js
CHANGED
|
@@ -290,7 +290,32 @@ async function parseClaudeIncremental({
|
|
|
290
290
|
const truncated = sameInode && prevOffset > st.size;
|
|
291
291
|
const startOffset = sameInode && !truncated ? prevOffset : 0;
|
|
292
292
|
|
|
293
|
-
|
|
293
|
+
// Claude's launch cwd is fixed for a session file's lifetime, so once
|
|
294
|
+
// resolved (found, or confirmed absent) from content it never needs
|
|
295
|
+
// re-extracting. Cache it — and the resolved project's git-config
|
|
296
|
+
// freshness fingerprint — on the cursor so idle files with project
|
|
297
|
+
// tracking enabled don't re-walk the filesystem on every sync.
|
|
298
|
+
//
|
|
299
|
+
// "No cwd found" / "cwd not inside a git checkout" only count as settled
|
|
300
|
+
// for genuinely idle files (no new bytes) — there's nothing new to
|
|
301
|
+
// attribute either way. A file that's still growing gets retried: an
|
|
302
|
+
// early sync can catch a session file before its cwd-bearing line lands,
|
|
303
|
+
// and treating that as permanent would silently drop the rest of the
|
|
304
|
+
// session's usage from the project queue.
|
|
305
|
+
const idle = sameInode && !truncated && startOffset >= st.size;
|
|
306
|
+
const cachedCwd = sameInode && !truncated ? prev?.claudeCwd : undefined;
|
|
307
|
+
const cachedProjectFileContext =
|
|
308
|
+
sameInode && !truncated ? prev?.projectFileContext || null : null;
|
|
309
|
+
const cachedContextAbsent = cachedProjectFileContext?.absent === true;
|
|
310
|
+
const projectContextFresh =
|
|
311
|
+
projectEnabled && cachedProjectFileContext && !cachedContextAbsent
|
|
312
|
+
? await isProjectFileContextFresh(cachedProjectFileContext)
|
|
313
|
+
: false;
|
|
314
|
+
const projectKnownAbsent = cachedCwd === null || cachedContextAbsent;
|
|
315
|
+
const projectSettled =
|
|
316
|
+
!projectEnabled || (idle && projectKnownAbsent) || projectContextFresh;
|
|
317
|
+
|
|
318
|
+
if (idle && projectSettled) {
|
|
294
319
|
if (cb) {
|
|
295
320
|
cb({
|
|
296
321
|
index: idx + 1,
|
|
@@ -304,17 +329,32 @@ async function parseClaudeIncremental({
|
|
|
304
329
|
continue;
|
|
305
330
|
}
|
|
306
331
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
332
|
+
let projectRef = sameInode && !truncated ? prev?.projectRef || null : null;
|
|
333
|
+
let projectKey = sameInode && !truncated ? prev?.projectKey || null : null;
|
|
334
|
+
let nextCwd = cachedCwd;
|
|
335
|
+
let nextProjectFileContext = cachedProjectFileContext;
|
|
336
|
+
|
|
337
|
+
if (projectEnabled && !projectSettled) {
|
|
338
|
+
if (nextCwd === undefined || nextCwd === null) {
|
|
339
|
+
nextCwd = await resolveClaudeFileCwd(filePath);
|
|
340
|
+
}
|
|
341
|
+
if (nextCwd) {
|
|
342
|
+
const projectContext = await resolveProjectContextForPath({
|
|
343
|
+
startDir: nextCwd,
|
|
310
344
|
projectMetaCache,
|
|
311
345
|
publicRepoCache,
|
|
312
346
|
publicRepoResolver,
|
|
313
347
|
projectState,
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
348
|
+
});
|
|
349
|
+
projectRef = projectContext?.projectRef || null;
|
|
350
|
+
projectKey = projectContext?.projectKey || null;
|
|
351
|
+
nextProjectFileContext = buildProjectFileContext(projectContext);
|
|
352
|
+
} else {
|
|
353
|
+
projectRef = null;
|
|
354
|
+
projectKey = null;
|
|
355
|
+
nextProjectFileContext = { absent: true };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
318
358
|
|
|
319
359
|
const result = await parseClaudeFile({
|
|
320
360
|
filePath,
|
|
@@ -334,6 +374,14 @@ async function parseClaudeIncremental({
|
|
|
334
374
|
inode,
|
|
335
375
|
offset: result.endOffset,
|
|
336
376
|
updatedAt: new Date().toISOString(),
|
|
377
|
+
...(projectEnabled
|
|
378
|
+
? {
|
|
379
|
+
claudeCwd: nextCwd === undefined ? null : nextCwd,
|
|
380
|
+
projectFileContext: nextProjectFileContext,
|
|
381
|
+
projectRef,
|
|
382
|
+
projectKey,
|
|
383
|
+
}
|
|
384
|
+
: {}),
|
|
337
385
|
};
|
|
338
386
|
|
|
339
387
|
filesProcessed += 1;
|
|
@@ -412,6 +460,26 @@ async function parseGeminiIncremental({
|
|
|
412
460
|
const key = filePath;
|
|
413
461
|
const prev = cursors.files[key] || null;
|
|
414
462
|
const inode = st.ino || 0;
|
|
463
|
+
const size = Number.isFinite(st.size) ? st.size : 0;
|
|
464
|
+
const mtimeMs = Number.isFinite(st.mtimeMs) ? st.mtimeMs : 0;
|
|
465
|
+
const sameFileMetadata =
|
|
466
|
+
prev &&
|
|
467
|
+
prev.inode === inode &&
|
|
468
|
+
prev.size === size &&
|
|
469
|
+
prev.mtimeMs === mtimeMs;
|
|
470
|
+
if (!projectEnabled && sameFileMetadata) {
|
|
471
|
+
if (cb) {
|
|
472
|
+
cb({
|
|
473
|
+
index: idx + 1,
|
|
474
|
+
total: totalFiles,
|
|
475
|
+
filePath,
|
|
476
|
+
filesProcessed,
|
|
477
|
+
eventsAggregated,
|
|
478
|
+
bucketsQueued: touchedBuckets.size,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
415
483
|
let startIndex = prev && prev.inode === inode ? Number(prev.lastIndex || -1) : -1;
|
|
416
484
|
let lastTotals = prev && prev.inode === inode ? prev.lastTotals || null : null;
|
|
417
485
|
let lastModel = prev && prev.inode === inode ? prev.lastModel || null : null;
|
|
@@ -427,12 +495,38 @@ async function parseGeminiIncremental({
|
|
|
427
495
|
: null;
|
|
428
496
|
const projectRef = projectContext?.projectRef || null;
|
|
429
497
|
const projectKey = projectContext?.projectKey || null;
|
|
498
|
+
const projectCursor =
|
|
499
|
+
projectKey &&
|
|
500
|
+
prev?.project &&
|
|
501
|
+
prev.project.projectKey === projectKey &&
|
|
502
|
+
prev.project.projectRef === projectRef
|
|
503
|
+
? prev.project
|
|
504
|
+
: null;
|
|
505
|
+
const projectUpToDate =
|
|
506
|
+
projectKey &&
|
|
507
|
+
sameFileMetadata &&
|
|
508
|
+
projectCursor &&
|
|
509
|
+
Number(projectCursor.lastIndex ?? -1) >= Number(prev?.lastIndex ?? -1);
|
|
510
|
+
if (projectUpToDate) {
|
|
511
|
+
if (cb) {
|
|
512
|
+
cb({
|
|
513
|
+
index: idx + 1,
|
|
514
|
+
total: totalFiles,
|
|
515
|
+
filePath,
|
|
516
|
+
filesProcessed,
|
|
517
|
+
eventsAggregated,
|
|
518
|
+
bucketsQueued: touchedBuckets.size,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
430
523
|
|
|
431
524
|
const result = await parseGeminiFile({
|
|
432
525
|
filePath,
|
|
433
526
|
startIndex,
|
|
434
527
|
lastTotals,
|
|
435
528
|
lastModel,
|
|
529
|
+
projectCursor,
|
|
436
530
|
hourlyState,
|
|
437
531
|
touchedBuckets,
|
|
438
532
|
source: fileSource,
|
|
@@ -444,11 +538,25 @@ async function parseGeminiIncremental({
|
|
|
444
538
|
|
|
445
539
|
cursors.files[key] = {
|
|
446
540
|
inode,
|
|
541
|
+
size,
|
|
542
|
+
mtimeMs,
|
|
447
543
|
lastIndex: result.lastIndex,
|
|
448
544
|
lastTotals: result.lastTotals,
|
|
449
545
|
lastModel: result.lastModel,
|
|
450
546
|
updatedAt: new Date().toISOString(),
|
|
451
547
|
};
|
|
548
|
+
if (projectKey) {
|
|
549
|
+
cursors.files[key].project = {
|
|
550
|
+
projectKey,
|
|
551
|
+
projectRef,
|
|
552
|
+
lastIndex: result.projectLastIndex,
|
|
553
|
+
lastTotals: result.projectLastTotals,
|
|
554
|
+
lastModel: result.projectLastModel,
|
|
555
|
+
updatedAt: new Date().toISOString(),
|
|
556
|
+
};
|
|
557
|
+
} else if (prev?.project) {
|
|
558
|
+
cursors.files[key].project = prev.project;
|
|
559
|
+
}
|
|
452
560
|
|
|
453
561
|
filesProcessed += 1;
|
|
454
562
|
eventsAggregated += result.eventsAggregated;
|
|
@@ -1070,6 +1178,7 @@ async function parseGeminiFile({
|
|
|
1070
1178
|
startIndex,
|
|
1071
1179
|
lastTotals,
|
|
1072
1180
|
lastModel,
|
|
1181
|
+
projectCursor,
|
|
1073
1182
|
hourlyState,
|
|
1074
1183
|
touchedBuckets,
|
|
1075
1184
|
source,
|
|
@@ -1098,57 +1207,96 @@ async function parseGeminiFile({
|
|
|
1098
1207
|
let eventsAggregated = 0;
|
|
1099
1208
|
let model = typeof lastModel === "string" ? lastModel : null;
|
|
1100
1209
|
let totals = lastTotals && typeof lastTotals === "object" ? lastTotals : null;
|
|
1101
|
-
const
|
|
1210
|
+
const projectActive = Boolean(projectKey && projectState && projectTouchedBuckets);
|
|
1211
|
+
let projectStartIndex =
|
|
1212
|
+
projectActive && Number.isFinite(projectCursor?.lastIndex)
|
|
1213
|
+
? Number(projectCursor.lastIndex)
|
|
1214
|
+
: -1;
|
|
1215
|
+
let projectModel =
|
|
1216
|
+
projectActive && typeof projectCursor?.lastModel === "string"
|
|
1217
|
+
? projectCursor.lastModel
|
|
1218
|
+
: null;
|
|
1219
|
+
let projectTotals =
|
|
1220
|
+
projectActive && projectCursor?.lastTotals && typeof projectCursor.lastTotals === "object"
|
|
1221
|
+
? projectCursor.lastTotals
|
|
1222
|
+
: null;
|
|
1223
|
+
if (projectActive && projectStartIndex >= messages.length) {
|
|
1224
|
+
projectStartIndex = -1;
|
|
1225
|
+
projectTotals = null;
|
|
1226
|
+
projectModel = null;
|
|
1227
|
+
}
|
|
1228
|
+
const hourlyBegin = Number.isFinite(startIndex) ? startIndex + 1 : 0;
|
|
1229
|
+
const projectBegin = projectActive ? projectStartIndex + 1 : Number.POSITIVE_INFINITY;
|
|
1230
|
+
const begin = Math.min(hourlyBegin, projectBegin);
|
|
1102
1231
|
|
|
1103
1232
|
for (let idx = begin; idx < messages.length; idx++) {
|
|
1104
1233
|
const msg = messages[idx];
|
|
1105
1234
|
if (!msg || typeof msg !== "object") continue;
|
|
1106
1235
|
|
|
1107
1236
|
const normalizedModel = normalizeModelInput(msg.model);
|
|
1108
|
-
if (normalizedModel) model = normalizedModel;
|
|
1237
|
+
if (normalizedModel && idx >= hourlyBegin) model = normalizedModel;
|
|
1238
|
+
if (normalizedModel && idx >= projectBegin) projectModel = normalizedModel;
|
|
1109
1239
|
|
|
1110
1240
|
const timestamp = typeof msg.timestamp === "string" ? msg.timestamp : null;
|
|
1111
1241
|
const currentTotals = normalizeGeminiTokens(msg.tokens);
|
|
1112
|
-
if (!timestamp || !currentTotals) {
|
|
1242
|
+
if (idx >= hourlyBegin && (!timestamp || !currentTotals)) {
|
|
1113
1243
|
totals = currentTotals || totals;
|
|
1114
|
-
continue;
|
|
1115
1244
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1245
|
+
if (idx >= projectBegin && (!timestamp || !currentTotals)) {
|
|
1246
|
+
projectTotals = currentTotals || projectTotals;
|
|
1247
|
+
}
|
|
1248
|
+
if (!timestamp || !currentTotals) {
|
|
1120
1249
|
continue;
|
|
1121
1250
|
}
|
|
1122
|
-
delta.conversation_count = 1;
|
|
1123
1251
|
|
|
1124
|
-
|
|
1125
|
-
if (
|
|
1126
|
-
|
|
1127
|
-
|
|
1252
|
+
let bucketStart = null;
|
|
1253
|
+
if (idx >= hourlyBegin) {
|
|
1254
|
+
const delta = diffGeminiTotals(currentTotals, totals);
|
|
1255
|
+
if (!delta || isAllZeroUsage(delta)) {
|
|
1256
|
+
totals = currentTotals;
|
|
1257
|
+
} else {
|
|
1258
|
+
delta.conversation_count = 1;
|
|
1259
|
+
bucketStart = toUtcHalfHourStart(timestamp);
|
|
1260
|
+
if (bucketStart) {
|
|
1261
|
+
const bucket = getHourlyBucket(hourlyState, source, model, bucketStart);
|
|
1262
|
+
addTotals(bucket.totals, delta);
|
|
1263
|
+
touchedBuckets.add(bucketKey(source, model, bucketStart));
|
|
1264
|
+
eventsAggregated += 1;
|
|
1265
|
+
}
|
|
1266
|
+
totals = currentTotals;
|
|
1267
|
+
}
|
|
1128
1268
|
}
|
|
1129
1269
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1270
|
+
if (idx >= projectBegin) {
|
|
1271
|
+
const projectDelta = diffGeminiTotals(currentTotals, projectTotals);
|
|
1272
|
+
if (!projectDelta || isAllZeroUsage(projectDelta)) {
|
|
1273
|
+
projectTotals = currentTotals;
|
|
1274
|
+
continue;
|
|
1275
|
+
}
|
|
1276
|
+
projectDelta.conversation_count = 1;
|
|
1277
|
+
bucketStart = bucketStart || toUtcHalfHourStart(timestamp);
|
|
1278
|
+
if (bucketStart) {
|
|
1279
|
+
const projectBucket = getProjectBucket(
|
|
1280
|
+
projectState,
|
|
1281
|
+
projectKey,
|
|
1282
|
+
source,
|
|
1283
|
+
bucketStart,
|
|
1284
|
+
projectRef,
|
|
1285
|
+
);
|
|
1286
|
+
addTotals(projectBucket.totals, projectDelta);
|
|
1287
|
+
projectTouchedBuckets.add(projectBucketKey(projectKey, source, bucketStart));
|
|
1288
|
+
}
|
|
1289
|
+
projectTotals = currentTotals;
|
|
1143
1290
|
}
|
|
1144
|
-
eventsAggregated += 1;
|
|
1145
|
-
totals = currentTotals;
|
|
1146
1291
|
}
|
|
1147
1292
|
|
|
1148
1293
|
return {
|
|
1149
1294
|
lastIndex: messages.length - 1,
|
|
1150
1295
|
lastTotals: totals,
|
|
1151
1296
|
lastModel: model,
|
|
1297
|
+
projectLastIndex: projectActive ? messages.length - 1 : null,
|
|
1298
|
+
projectLastTotals: projectTotals,
|
|
1299
|
+
projectLastModel: projectModel,
|
|
1152
1300
|
eventsAggregated,
|
|
1153
1301
|
};
|
|
1154
1302
|
}
|
|
@@ -1989,7 +2137,15 @@ async function resolveProjectMetaForPath(startDir, cache) {
|
|
|
1989
2137
|
if (configPath) {
|
|
1990
2138
|
const remoteUrl = await readGitRemoteUrl(configPath);
|
|
1991
2139
|
const projectRef = canonicalizeProjectRef(remoteUrl);
|
|
1992
|
-
const
|
|
2140
|
+
const configStat = await fs.stat(configPath).catch(() => null);
|
|
2141
|
+
const meta = {
|
|
2142
|
+
projectRef: projectRef || null,
|
|
2143
|
+
repoRoot: current,
|
|
2144
|
+
configPath,
|
|
2145
|
+
configMtimeMs:
|
|
2146
|
+
configStat && Number.isFinite(configStat.mtimeMs) ? configStat.mtimeMs : null,
|
|
2147
|
+
configSize: configStat && Number.isFinite(configStat.size) ? configStat.size : null,
|
|
2148
|
+
};
|
|
1993
2149
|
if (cache) {
|
|
1994
2150
|
for (const entry of visited) cache.set(entry, meta);
|
|
1995
2151
|
}
|
|
@@ -2129,16 +2285,86 @@ async function resolveProjectContextForPath({
|
|
|
2129
2285
|
repoRootHash: meta?.repoRootHash || repoRootHash,
|
|
2130
2286
|
};
|
|
2131
2287
|
recordProjectMeta(projectState, normalized);
|
|
2288
|
+
const configFields = {
|
|
2289
|
+
configPath: projectMeta.configPath || null,
|
|
2290
|
+
configMtimeMs: projectMeta.configMtimeMs ?? null,
|
|
2291
|
+
configSize: projectMeta.configSize ?? null,
|
|
2292
|
+
};
|
|
2132
2293
|
if (normalized.status !== "public_verified") {
|
|
2133
|
-
return {
|
|
2294
|
+
return {
|
|
2295
|
+
projectRef: normalized.projectRef,
|
|
2296
|
+
projectKey: null,
|
|
2297
|
+
status: normalized.status,
|
|
2298
|
+
...configFields,
|
|
2299
|
+
};
|
|
2134
2300
|
}
|
|
2135
2301
|
return {
|
|
2136
2302
|
projectRef: normalized.projectRef,
|
|
2137
2303
|
projectKey: normalized.projectKey,
|
|
2138
2304
|
status: normalized.status,
|
|
2305
|
+
...configFields,
|
|
2139
2306
|
};
|
|
2140
2307
|
}
|
|
2141
2308
|
|
|
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
|
+
// Claude Code session files log the launch cwd on message lines (top-level
|
|
2334
|
+
// "cwd" field) — unlike the file's on-disk location under
|
|
2335
|
+
// ~/.claude/projects/<dash-encoded-cwd>/, which is not itself inside the
|
|
2336
|
+
// real project's git checkout and can never resolve a project via directory
|
|
2337
|
+
// walk. The encoded name can't be decoded reliably either (dashes in the
|
|
2338
|
+
// real path collide with the path-separator encoding), so read the real cwd
|
|
2339
|
+
// from content instead. It's set once at session start and bounded scan is
|
|
2340
|
+
// cheap since it's a top-level field on the first several lines.
|
|
2341
|
+
const CLAUDE_CWD_SCAN_MAX_BYTES = 65536;
|
|
2342
|
+
|
|
2343
|
+
async function resolveClaudeFileCwd(filePath) {
|
|
2344
|
+
const stream = fssync.createReadStream(filePath, {
|
|
2345
|
+
encoding: "utf8",
|
|
2346
|
+
start: 0,
|
|
2347
|
+
end: CLAUDE_CWD_SCAN_MAX_BYTES,
|
|
2348
|
+
});
|
|
2349
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
2350
|
+
try {
|
|
2351
|
+
for await (const line of rl) {
|
|
2352
|
+
if (!line || !line.includes('"cwd"')) continue;
|
|
2353
|
+
let obj;
|
|
2354
|
+
try {
|
|
2355
|
+
obj = JSON.parse(line);
|
|
2356
|
+
} catch (_e) {
|
|
2357
|
+
continue;
|
|
2358
|
+
}
|
|
2359
|
+
if (typeof obj?.cwd === "string" && obj.cwd.trim()) return obj.cwd.trim();
|
|
2360
|
+
}
|
|
2361
|
+
} finally {
|
|
2362
|
+
rl.close();
|
|
2363
|
+
stream.close?.();
|
|
2364
|
+
}
|
|
2365
|
+
return null;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2142
2368
|
async function resolveGitConfigPath(rootDir) {
|
|
2143
2369
|
const gitPath = path.join(rootDir, ".git");
|
|
2144
2370
|
const st = await fs.stat(gitPath).catch(() => null);
|
|
@@ -3210,28 +3436,119 @@ async function parseKiroIncremental({ dbPath, jsonlPath, cursors, queuePath, onP
|
|
|
3210
3436
|
// Hermes Agent — SQLite-based (sessions table in ~/.hermes/state.db)
|
|
3211
3437
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
3212
3438
|
|
|
3213
|
-
function resolveHermesPath(env = process.env) {
|
|
3439
|
+
function resolveHermesPath(env = process.env, deps = {}) {
|
|
3214
3440
|
const override = env.TOKENTRACKER_HERMES_HOME;
|
|
3215
3441
|
if (typeof override === "string" && override.trim().length > 0) {
|
|
3216
3442
|
return override.trim();
|
|
3217
3443
|
}
|
|
3218
3444
|
const home = require("node:os").homedir();
|
|
3219
3445
|
const defaultPath = path.join(home, ".hermes");
|
|
3220
|
-
//
|
|
3221
|
-
//
|
|
3222
|
-
//
|
|
3446
|
+
// On Windows, scan BOTH the native install (%LOCALAPPDATA%\hermes) and
|
|
3447
|
+
// any WSL distros running Hermes Agent. Each install has its own independent
|
|
3448
|
+
// 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).
|
|
3223
3450
|
if (process.platform === "win32") {
|
|
3451
|
+
let nativePath = null;
|
|
3224
3452
|
const localAppData = typeof env.LOCALAPPDATA === "string" ? env.LOCALAPPDATA.trim() : "";
|
|
3225
3453
|
if (localAppData.length > 0) {
|
|
3226
|
-
const
|
|
3454
|
+
const candidate = path.join(localAppData, "hermes");
|
|
3227
3455
|
try {
|
|
3228
|
-
if (fssync.existsSync(
|
|
3456
|
+
if (fssync.existsSync(candidate)) nativePath = candidate;
|
|
3229
3457
|
} catch (_e) { }
|
|
3230
3458
|
}
|
|
3459
|
+
const wslPath = discoverWslHermesHome(deps);
|
|
3460
|
+
if (wslPath) return wslPath;
|
|
3461
|
+
if (nativePath) return nativePath;
|
|
3231
3462
|
}
|
|
3232
3463
|
return defaultPath;
|
|
3233
3464
|
}
|
|
3234
3465
|
|
|
3466
|
+
// ── WSL auto-discovery (Windows host, Hermes installed inside a distro) ──────
|
|
3467
|
+
// `wsl.exe -l -v` prints UTF-16LE; the per-distro `whoami` runs a Linux process
|
|
3468
|
+
// so its stdout is UTF-8. We capture buffers and decode per-call.
|
|
3469
|
+
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");
|
|
3478
|
+
}
|
|
3479
|
+
|
|
3480
|
+
// Parse `wsl.exe -l -v` output into [{ name, version, isDefault }]. The default
|
|
3481
|
+
// distro is prefixed with `*`; the VERSION column (1 or 2) decides which UNC
|
|
3482
|
+
// alias to try first (simonlpaige, #87).
|
|
3483
|
+
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;
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
// Default distro first, then listed order — matches what a user expects when
|
|
3509
|
+
// they have one primary distro plus extras.
|
|
3510
|
+
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));
|
|
3520
|
+
}
|
|
3521
|
+
|
|
3522
|
+
// Probe each WSL distro for ~/.hermes via UNC. The Linux username rarely equals
|
|
3523
|
+
// %USERNAME%, so we ask the distro directly with `whoami` rather than guessing
|
|
3524
|
+
// (simonlpaige, #87).
|
|
3525
|
+
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;
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3235
3552
|
function resolveHermesDbPath(env = process.env) {
|
|
3236
3553
|
return path.join(resolveHermesPath(env), "state.db");
|
|
3237
3554
|
}
|
|
@@ -9165,6 +9482,9 @@ module.exports = {
|
|
|
9165
9482
|
resolveKiroJsonlPath,
|
|
9166
9483
|
resolveHermesPath,
|
|
9167
9484
|
resolveHermesDbPath,
|
|
9485
|
+
parseWslListVerbose,
|
|
9486
|
+
probeWslDistros,
|
|
9487
|
+
discoverWslHermesHome,
|
|
9168
9488
|
resolveCopilotOtelPaths,
|
|
9169
9489
|
parseRolloutIncremental,
|
|
9170
9490
|
parseClaudeIncremental,
|