tokentracker-cli 0.47.3 → 0.49.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/README.ja.md +3 -1
- package/README.ko.md +3 -1
- package/README.md +3 -1
- package/README.zh-CN.md +3 -1
- package/dashboard/dist/assets/{ActivityHeatmap-DNXs3lKI.js → ActivityHeatmap-CItg7FNN.js} +1 -1
- package/dashboard/dist/assets/{Card-DH3Er6A7.js → Card-B9jWqeQm.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-BwObNlNQ.js → DashboardPage-D-UdQdmb.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-C8TbfrgM.js → DevicePage-BmhKFgDo.js} +1 -1
- package/dashboard/dist/assets/{DialogTitle-BN06NuBQ.js → DialogTitle-Dcuz0ACc.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-BGQdmOIT.js → FadeIn-B-oMQCv_.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-DF-zHHNK.js → HeaderGithubStar-C6x-l5U0.js} +1 -1
- package/dashboard/dist/assets/IpCheckPage-D1tltH7T.js +20 -0
- package/dashboard/dist/assets/{LandingPage-B9OiRAGX.js → LandingPage-DnjQLNxt.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardAvatar-CrulAkZy.js → LeaderboardAvatar-PNgWQxaR.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-pLMt5N8v.js → LeaderboardPage-DRCSwReV.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfileModal-trFYROeI.js → LeaderboardProfileModal-DvbGnWDm.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfilePage-XQ5aGbP1.js → LeaderboardProfilePage-Bmk16GFd.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-DsdyKs5Z.js +2 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-BGFFlM7g.js → LocalOnlyNotice-CIa2X9wJ.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-0FLg6Gtf.js → LoginPage-CnDLx50g.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-ClduWm0G.js → PopoverPopup-ZsSFlvKU.js} +1 -1
- package/dashboard/dist/assets/{Select-yOe-loKw.js → Select-CY2FgOFv.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-_4uFhB_8.js → SelectItemText-DI_GTNc2.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-mndeQq_K.js → SettingsPage-D1DZkCMo.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-C0NHenLh.js → SkillsPage-DorZCQ0W.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-X0PwMFCK.js → WidgetsPage-BDLa_UaU.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-Cytzx6OI.js → WrappedPage-BKn5Q7iM.js} +1 -1
- package/dashboard/dist/assets/{agent-logos-CzYpdOy_.js → agent-logos-CM4Rt9bu.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-CE98MOft.js → arrow-up-right-jP0XdZdv.js} +1 -1
- package/dashboard/dist/assets/{download-DwKe306W.js → download-D8Hvx1kr.js} +1 -1
- package/dashboard/dist/assets/{info-_hRvsmjv.js → info-vv2jU1_C.js} +1 -1
- package/dashboard/dist/assets/{main-BO2Z3JNm.js → main-9P4Ny5Xr.js} +15 -15
- package/dashboard/dist/assets/main-Br5SsufY.css +1 -0
- package/dashboard/dist/assets/use-limits-display-prefs-BZVYlv9P.js +1 -0
- package/dashboard/dist/assets/{use-native-settings-D40wCPJV.js → use-native-settings-CR_f7uLH.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-CvgpaBd_.js +1 -0
- package/dashboard/dist/assets/{useCurrency-CZqPmMxC.js → useCurrency-Cq0FIlJZ.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-D-_sDVky.js → useScrollLock-B5U1SJQV.js} +1 -1
- package/dashboard/dist/brand-logos/hermes.svg +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +1 -1
- package/src/commands/device-login.js +19 -3
- package/src/commands/sync.js +538 -348
- package/src/lib/local-api.js +11 -2
- package/src/lib/pricing/curated-overrides.json +2 -1
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +129 -17
- package/src/lib/usage-limits.js +118 -25
- package/src/lib/wrapped-aggregator.js +12 -1
- package/dashboard/dist/assets/IpCheckPage-CgPFKRRL.js +0 -15
- package/dashboard/dist/assets/LimitsPage-CWLEVM54.js +0 -2
- package/dashboard/dist/assets/main-BfK9LoKV.css +0 -1
- package/dashboard/dist/assets/use-limits-display-prefs-ur9_stXM.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-BhXhFRP0.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -746,6 +746,9 @@ async function parseRolloutFile({
|
|
|
746
746
|
let model = typeof lastModel === "string" ? lastModel : null;
|
|
747
747
|
let totals = lastTotal && typeof lastTotal === "object" ? lastTotal : null;
|
|
748
748
|
let currentCwd = null;
|
|
749
|
+
let currentDate = null;
|
|
750
|
+
let isForkedRollout = false;
|
|
751
|
+
const rolloutDate = rolloutDateFromPath(filePath);
|
|
749
752
|
let currentProjectRef = projectRef || null;
|
|
750
753
|
let currentProjectKey = projectKey || null;
|
|
751
754
|
let eventsAggregated = 0;
|
|
@@ -756,7 +759,10 @@ async function parseRolloutFile({
|
|
|
756
759
|
const maybeTurnContext =
|
|
757
760
|
!maybeTokenCount &&
|
|
758
761
|
(line.includes('"turn_context"') || line.includes('"session_meta"')) &&
|
|
759
|
-
(line.includes('"model"') ||
|
|
762
|
+
(line.includes('"model"') ||
|
|
763
|
+
line.includes('"cwd"') ||
|
|
764
|
+
line.includes('"current_date"') ||
|
|
765
|
+
line.includes('"forked_from_id"'));
|
|
760
766
|
if (!maybeTokenCount && !maybeTurnContext) continue;
|
|
761
767
|
|
|
762
768
|
let obj;
|
|
@@ -771,6 +777,12 @@ async function parseRolloutFile({
|
|
|
771
777
|
obj?.payload &&
|
|
772
778
|
typeof obj.payload === "object"
|
|
773
779
|
) {
|
|
780
|
+
if (obj.type === "session_meta" && typeof obj.payload.forked_from_id === "string") {
|
|
781
|
+
isForkedRollout = obj.payload.forked_from_id.trim().length > 0;
|
|
782
|
+
}
|
|
783
|
+
if (obj.type === "turn_context" && typeof obj.payload.current_date === "string") {
|
|
784
|
+
currentDate = normalizeIsoDate(obj.payload.current_date);
|
|
785
|
+
}
|
|
774
786
|
if (typeof obj.payload.model === "string") {
|
|
775
787
|
model = obj.payload.model;
|
|
776
788
|
}
|
|
@@ -812,6 +824,9 @@ async function parseRolloutFile({
|
|
|
812
824
|
totals = totalUsage;
|
|
813
825
|
}
|
|
814
826
|
|
|
827
|
+
// date matching is conservative; same-day fork replays are still counted.
|
|
828
|
+
if (isForkedReplayToken({ isForkedRollout, rolloutDate, currentDate })) continue;
|
|
829
|
+
|
|
815
830
|
const bucketStart = toUtcHalfHourStart(tokenTimestamp);
|
|
816
831
|
if (!bucketStart) continue;
|
|
817
832
|
|
|
@@ -1772,6 +1787,21 @@ function toUtcHalfHourStart(ts) {
|
|
|
1772
1787
|
return bucketStart.toISOString();
|
|
1773
1788
|
}
|
|
1774
1789
|
|
|
1790
|
+
function rolloutDateFromPath(filePath) {
|
|
1791
|
+
const match = path.basename(String(filePath || "")).match(/^rollout-(\d{4}-\d{2}-\d{2})T/);
|
|
1792
|
+
return match ? match[1] : null;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function normalizeIsoDate(value) {
|
|
1796
|
+
const raw = typeof value === "string" ? value.trim() : "";
|
|
1797
|
+
const match = raw.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
1798
|
+
return match ? match[1] : null;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
function isForkedReplayToken({ isForkedRollout, rolloutDate, currentDate }) {
|
|
1802
|
+
return Boolean(isForkedRollout && rolloutDate && currentDate && currentDate < rolloutDate);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1775
1805
|
function normalizeNonNegativeNumber(value) {
|
|
1776
1806
|
const n = Number(value || 0);
|
|
1777
1807
|
if (!Number.isFinite(n) || n <= 0) return 0;
|
|
@@ -2619,13 +2649,32 @@ async function parseCursorApiIncremental({
|
|
|
2619
2649
|
const total = records.length;
|
|
2620
2650
|
|
|
2621
2651
|
if (records.length > 0) {
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2652
|
+
// Guard (2026-06 audit): only wipe buckets the fetched export can
|
|
2653
|
+
// actually rebuild. The wipe-then-refill design assumes the CSV is a
|
|
2654
|
+
// FULL-history export; if Cursor ever windows or truncates the export,
|
|
2655
|
+
// unconditionally zeroing every bucket would erase (and upload zeros
|
|
2656
|
+
// over) all history older than the response. Wiping from the earliest
|
|
2657
|
+
// record onward is identical for full exports and fail-safe for
|
|
2658
|
+
// partial ones.
|
|
2659
|
+
let earliestBucketStart = null;
|
|
2660
|
+
for (const record of records) {
|
|
2661
|
+
if (!record?.date) continue;
|
|
2662
|
+
const b = toUtcHalfHourStart(record.date);
|
|
2663
|
+
if (b && (!earliestBucketStart || b < earliestBucketStart)) earliestBucketStart = b;
|
|
2664
|
+
}
|
|
2665
|
+
// No parseable record date at all means the export is malformed —
|
|
2666
|
+
// refilling would add nothing, so wiping would zero out (and upload
|
|
2667
|
+
// zeros over) the entire history. Skip the wipe entirely.
|
|
2668
|
+
if (earliestBucketStart) {
|
|
2669
|
+
for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
|
|
2670
|
+
const parsed = parseBucketKey(key);
|
|
2671
|
+
const sourceKey = normalizeSourceInput(parsed.source) || DEFAULT_SOURCE;
|
|
2672
|
+
if (sourceKey !== defaultSource) continue;
|
|
2673
|
+
if (!bucket?.totals) continue;
|
|
2674
|
+
if (parsed.hourStart && parsed.hourStart < earliestBucketStart) continue;
|
|
2675
|
+
bucket.totals = initTotals();
|
|
2676
|
+
touchedBuckets.add(key);
|
|
2677
|
+
}
|
|
2629
2678
|
}
|
|
2630
2679
|
}
|
|
2631
2680
|
|
|
@@ -3076,7 +3125,7 @@ function readHermesSessions(dbPath, lastCompletedEpoch, unfinishedSessionIds = [
|
|
|
3076
3125
|
// in-progress (ended_at IS NULL), OR sessions that were previously observed
|
|
3077
3126
|
// unfinished. Hermes updates token counts in real-time, including a final
|
|
3078
3127
|
// delta when an active session later gets ended_at set.
|
|
3079
|
-
const sql = `SELECT id, model, started_at, ended_at, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens, message_count FROM sessions WHERE (started_at >= ${since} OR ended_at IS NULL${forceIncludeSql}) AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR reasoning_tokens > 0) ORDER BY started_at ASC`;
|
|
3128
|
+
const sql = `SELECT id, model, started_at, ended_at, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens, message_count FROM sessions WHERE (started_at >= ${since} OR ended_at IS NULL${forceIncludeSql}) AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR cache_write_tokens > 0 OR reasoning_tokens > 0) ORDER BY started_at ASC`;
|
|
3080
3129
|
|
|
3081
3130
|
let snapshot = null;
|
|
3082
3131
|
let effectiveDbPath = dbPath;
|
|
@@ -3166,7 +3215,13 @@ async function parseHermesIncremental({ hermesPath, dbPath, cursors, queuePath,
|
|
|
3166
3215
|
const cacheWrite = toNonNegativeInt(row.cache_write_tokens);
|
|
3167
3216
|
const reasoning = toNonNegativeInt(row.reasoning_tokens);
|
|
3168
3217
|
const messageCount = toNonNegativeInt(row.message_count);
|
|
3169
|
-
if (
|
|
3218
|
+
if (
|
|
3219
|
+
inputTokens === 0 &&
|
|
3220
|
+
outputTokens === 0 &&
|
|
3221
|
+
cacheRead === 0 &&
|
|
3222
|
+
cacheWrite === 0 &&
|
|
3223
|
+
reasoning === 0
|
|
3224
|
+
) continue;
|
|
3170
3225
|
|
|
3171
3226
|
// Save current snapshot for next sync
|
|
3172
3227
|
nextSnapshots[row.id] = { in: inputTokens, out: outputTokens, cacheRead, cacheWrite, reasoning, message_count: messageCount };
|
|
@@ -3850,13 +3905,26 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
|
|
|
3850
3905
|
const updatedAt = new Date().toISOString();
|
|
3851
3906
|
hourlyState.updatedAt = updatedAt;
|
|
3852
3907
|
cursors.hourly = hourlyState;
|
|
3853
|
-
cursors.kiroCli = {
|
|
3908
|
+
cursors.kiroCli = {
|
|
3909
|
+
...kiroCliState,
|
|
3910
|
+
requests: cappedEarly.requests,
|
|
3911
|
+
watermarkMs: Math.max(Number(kiroCliState.watermarkMs) || 0, cappedEarly.watermarkMs),
|
|
3912
|
+
updatedAt,
|
|
3913
|
+
};
|
|
3854
3914
|
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued };
|
|
3855
3915
|
}
|
|
3856
3916
|
const cb = typeof onProgress === "function" ? onProgress : null;
|
|
3857
3917
|
let recordsProcessed = 0;
|
|
3858
3918
|
let eventsAggregated = 0;
|
|
3859
3919
|
|
|
3920
|
+
// 2026-06 audit fix: requests older than the persisted prune watermark were
|
|
3921
|
+
// already counted once and had their cursor entry pruned by
|
|
3922
|
+
// clampAndCapKiroCliState — re-processing them (prev === undefined) re-ADDED
|
|
3923
|
+
// their tokens to the same bucket on every sync, inflating old buckets
|
|
3924
|
+
// without bound. Skip them; the watermark only ever advances, and starts at
|
|
3925
|
+
// 0 so a first-ever parse still ingests the full DB history.
|
|
3926
|
+
const kiroCliWatermarkMs = Number(kiroCliState.watermarkMs) || 0;
|
|
3927
|
+
|
|
3860
3928
|
for (let i = 0; i < flat.length; i++) {
|
|
3861
3929
|
const r = flat[i];
|
|
3862
3930
|
recordsProcessed++;
|
|
@@ -3871,6 +3939,7 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
|
|
|
3871
3939
|
|
|
3872
3940
|
const tsMs = Number(r.request_start_timestamp_ms);
|
|
3873
3941
|
if (!Number.isFinite(tsMs) || tsMs <= 0) continue;
|
|
3942
|
+
if (tsMs < kiroCliWatermarkMs) continue;
|
|
3874
3943
|
const bucketStart = toUtcHalfHourStart(new Date(tsMs).toISOString());
|
|
3875
3944
|
if (!bucketStart) continue;
|
|
3876
3945
|
|
|
@@ -3952,7 +4021,12 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
|
|
|
3952
4021
|
const updatedAt = new Date().toISOString();
|
|
3953
4022
|
hourlyState.updatedAt = updatedAt;
|
|
3954
4023
|
cursors.hourly = hourlyState;
|
|
3955
|
-
cursors.kiroCli = {
|
|
4024
|
+
cursors.kiroCli = {
|
|
4025
|
+
...kiroCliState,
|
|
4026
|
+
requests: cappedState.requests,
|
|
4027
|
+
watermarkMs: Math.max(kiroCliWatermarkMs, cappedState.watermarkMs),
|
|
4028
|
+
updatedAt,
|
|
4029
|
+
};
|
|
3956
4030
|
|
|
3957
4031
|
return { recordsProcessed, eventsAggregated, bucketsQueued };
|
|
3958
4032
|
}
|
|
@@ -3978,6 +4052,14 @@ function clampAndCapKiroCliState({ requestState, hourlyState, touchedBuckets })
|
|
|
3978
4052
|
}
|
|
3979
4053
|
// TASK-004: cap cursors.kiroCli.requests by age + count. Runs LAST so
|
|
3980
4054
|
// nothing active or just-retracted is pruned mid-flight.
|
|
4055
|
+
//
|
|
4056
|
+
// 2026-06 audit fix: pruning an entry while readKiroCliRequests has no
|
|
4057
|
+
// time floor meant the same request came back next sync with
|
|
4058
|
+
// `prev === undefined` and was re-ADDED to its (old) bucket — every sync,
|
|
4059
|
+
// forever. The returned watermarkMs records how far this prune reached;
|
|
4060
|
+
// the parse loop skips any request older than the persisted watermark, so
|
|
4061
|
+
// a pruned request can never be re-counted. First-ever parse still counts
|
|
4062
|
+
// arbitrarily old history (watermark starts at 0).
|
|
3981
4063
|
const ageCutoffMs = Date.now() - KIRO_CLI_CURSOR_MAX_AGE_MS;
|
|
3982
4064
|
const cappedEntries = [];
|
|
3983
4065
|
for (const [reqId, entry] of Object.entries(requestState)) {
|
|
@@ -3986,13 +4068,22 @@ function clampAndCapKiroCliState({ requestState, hourlyState, touchedBuckets })
|
|
|
3986
4068
|
if (!Number.isFinite(ts) || ts < ageCutoffMs) continue;
|
|
3987
4069
|
cappedEntries.push([reqId, entry, ts]);
|
|
3988
4070
|
}
|
|
4071
|
+
// +30min margin: entries are pruned by bucketStart (half-hour floor) while
|
|
4072
|
+
// the parse loop skips by raw request ts, which can sit up to 30 minutes
|
|
4073
|
+
// after its bucketStart. Without the margin a request whose bucket just
|
|
4074
|
+
// crossed the cutoff would be pruned yet still pass the skip, re-adding for
|
|
4075
|
+
// a few syncs until the watermark catches up.
|
|
4076
|
+
let watermarkMs = ageCutoffMs + 30 * 60 * 1000;
|
|
3989
4077
|
if (cappedEntries.length > KIRO_CLI_CURSOR_MAX_ENTRIES) {
|
|
3990
4078
|
cappedEntries.sort((a, b) => b[2] - a[2]); // newest first
|
|
4079
|
+
// Newest EVICTED entry sits at index MAX_ENTRIES after the sort; the
|
|
4080
|
+
// watermark must clear it so count-capped evictions can't re-add either.
|
|
4081
|
+
watermarkMs = Math.max(watermarkMs, cappedEntries[KIRO_CLI_CURSOR_MAX_ENTRIES][2] + 1);
|
|
3991
4082
|
cappedEntries.length = KIRO_CLI_CURSOR_MAX_ENTRIES;
|
|
3992
4083
|
}
|
|
3993
4084
|
const capped = {};
|
|
3994
4085
|
for (const [reqId, entry] of cappedEntries) capped[reqId] = entry;
|
|
3995
|
-
return capped;
|
|
4086
|
+
return { requests: capped, watermarkMs };
|
|
3996
4087
|
}
|
|
3997
4088
|
|
|
3998
4089
|
// Back-compat path: per-session .json files (the old fixture shape). Emits
|
|
@@ -5174,7 +5265,13 @@ async function parseRoocodeIncremental({
|
|
|
5174
5265
|
const cacheReads = toNonNegativeInt(payload.cacheReads);
|
|
5175
5266
|
const cacheWrites = toNonNegativeInt(payload.cacheWrites);
|
|
5176
5267
|
if (tokensIn === 0 && tokensOut === 0 && cacheReads === 0 && cacheWrites === 0) {
|
|
5177
|
-
|
|
5268
|
+
// Cline-family extensions write `api_req_started` at request START
|
|
5269
|
+
// (zero tokens) and back-fill the SAME message in place (same ts)
|
|
5270
|
+
// once the request completes. Marking the zero placeholder as seen
|
|
5271
|
+
// would skip the back-filled tokens forever — a sync racing an
|
|
5272
|
+
// in-flight request silently under-counted that turn. Leave it
|
|
5273
|
+
// unseen; the file-level mtime gate re-evaluates it when the task
|
|
5274
|
+
// file is rewritten.
|
|
5178
5275
|
continue;
|
|
5179
5276
|
}
|
|
5180
5277
|
|
|
@@ -6443,7 +6540,10 @@ async function parseKilocodeIncremental({
|
|
|
6443
6540
|
const cacheReads = toNonNegativeInt(payload.cacheReads);
|
|
6444
6541
|
const cacheWrites = toNonNegativeInt(payload.cacheWrites);
|
|
6445
6542
|
if (tokensIn === 0 && tokensOut === 0 && cacheReads === 0 && cacheWrites === 0) {
|
|
6446
|
-
|
|
6543
|
+
// See the roocode parser: `api_req_started` is written at request
|
|
6544
|
+
// START with zero tokens and back-filled in place (same ts) on
|
|
6545
|
+
// completion. Marking the placeholder seen would drop the
|
|
6546
|
+
// back-filled tokens forever when a sync races an in-flight request.
|
|
6447
6547
|
continue;
|
|
6448
6548
|
}
|
|
6449
6549
|
|
|
@@ -6583,7 +6683,13 @@ async function parseOmpIncremental({
|
|
|
6583
6683
|
const cacheWrite = toNonNegativeInt(usage.cacheWrite);
|
|
6584
6684
|
const reasoningTokens = toNonNegativeInt(usage.reasoningTokens);
|
|
6585
6685
|
|
|
6586
|
-
if (
|
|
6686
|
+
if (
|
|
6687
|
+
input === 0 &&
|
|
6688
|
+
output === 0 &&
|
|
6689
|
+
cacheRead === 0 &&
|
|
6690
|
+
cacheWrite === 0 &&
|
|
6691
|
+
reasoningTokens === 0
|
|
6692
|
+
) {
|
|
6587
6693
|
seenIds.add(entryId);
|
|
6588
6694
|
continue;
|
|
6589
6695
|
}
|
|
@@ -6826,7 +6932,13 @@ async function parsePiIncremental({
|
|
|
6826
6932
|
const cacheWrite = toNonNegativeInt(usage.cacheWrite);
|
|
6827
6933
|
const reasoningTokens = toNonNegativeInt(usage.reasoningTokens);
|
|
6828
6934
|
|
|
6829
|
-
if (
|
|
6935
|
+
if (
|
|
6936
|
+
input === 0 &&
|
|
6937
|
+
output === 0 &&
|
|
6938
|
+
cacheRead === 0 &&
|
|
6939
|
+
cacheWrite === 0 &&
|
|
6940
|
+
reasoningTokens === 0
|
|
6941
|
+
) {
|
|
6830
6942
|
seenIds.add(entryId);
|
|
6831
6943
|
continue;
|
|
6832
6944
|
}
|
package/src/lib/usage-limits.js
CHANGED
|
@@ -677,8 +677,8 @@ function expandGeminiExecutableCandidates({ home } = {}) {
|
|
|
677
677
|
return candidates;
|
|
678
678
|
}
|
|
679
679
|
|
|
680
|
-
function extractGeminiOauthClientCredentials({ commandRunner, home } = {}) {
|
|
681
|
-
const result = runCommand(commandRunner, "which", ["gemini"], { timeout: 2000 });
|
|
680
|
+
async function extractGeminiOauthClientCredentials({ commandRunner, home } = {}) {
|
|
681
|
+
const result = await runCommand(commandRunner, "which", ["gemini"], { timeout: 2000 });
|
|
682
682
|
const geminiPath = typeof result?.stdout === "string" ? result.stdout.trim() : "";
|
|
683
683
|
|
|
684
684
|
const geminiPaths = [
|
|
@@ -732,7 +732,7 @@ async function refreshGeminiAccessToken({
|
|
|
732
732
|
fetchImpl = fetch,
|
|
733
733
|
commandRunner,
|
|
734
734
|
}) {
|
|
735
|
-
const oauthClient = extractGeminiOauthClientCredentials({ commandRunner, home });
|
|
735
|
+
const oauthClient = await extractGeminiOauthClientCredentials({ commandRunner, home });
|
|
736
736
|
if (!oauthClient?.clientId || !oauthClient?.clientSecret) {
|
|
737
737
|
throw new Error("Gemini API error: Could not find Gemini CLI OAuth configuration");
|
|
738
738
|
}
|
|
@@ -940,24 +940,97 @@ async function fetchGeminiLimits({ home, env, fetchImpl = fetch, commandRunner }
|
|
|
940
940
|
}
|
|
941
941
|
}
|
|
942
942
|
|
|
943
|
+
// Async command runner. Previously this wrapped `cp.spawnSync`, which blocked the
|
|
944
|
+
// Node event loop for the full command duration (up to 20s for Kiro) and froze every
|
|
945
|
+
// other local-api endpoint plus the other providers' withProviderTimeout races.
|
|
946
|
+
// Returns a promise for a spawnSync-shaped result: { status, stdout, stderr, error? }.
|
|
947
|
+
// Injected runners (tests) may stay synchronous — their return value is wrapped in
|
|
948
|
+
// Promise.resolve so both sync and async runners work.
|
|
943
949
|
function runCommand(commandRunner, command, args, options = {}) {
|
|
944
|
-
const
|
|
945
|
-
return runner(command, args, {
|
|
950
|
+
const merged = {
|
|
946
951
|
encoding: "utf8",
|
|
947
952
|
maxBuffer: 10 * 1024 * 1024,
|
|
948
953
|
...options,
|
|
954
|
+
};
|
|
955
|
+
if (typeof commandRunner === "function") {
|
|
956
|
+
return Promise.resolve(commandRunner(command, args, merged));
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const { timeout, maxBuffer, ...spawnOptions } = merged;
|
|
960
|
+
return new Promise((resolve) => {
|
|
961
|
+
let child;
|
|
962
|
+
try {
|
|
963
|
+
child = cp.spawn(command, args, { ...spawnOptions, stdio: ["ignore", "pipe", "pipe"] });
|
|
964
|
+
} catch (error) {
|
|
965
|
+
resolve({ status: null, stdout: "", stderr: "", error });
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
let stdout = "";
|
|
970
|
+
let stderr = "";
|
|
971
|
+
let settled = false;
|
|
972
|
+
let timedOut = false;
|
|
973
|
+
let timer = null;
|
|
974
|
+
let hardTimer = null;
|
|
975
|
+
|
|
976
|
+
const settle = ({ status = null, error = null } = {}) => {
|
|
977
|
+
if (settled) return;
|
|
978
|
+
settled = true;
|
|
979
|
+
if (timer) clearTimeout(timer);
|
|
980
|
+
if (hardTimer) clearTimeout(hardTimer);
|
|
981
|
+
let finalError = error;
|
|
982
|
+
if (!finalError && timedOut) {
|
|
983
|
+
finalError = new Error(`spawn ${command} ETIMEDOUT`);
|
|
984
|
+
finalError.code = "ETIMEDOUT";
|
|
985
|
+
}
|
|
986
|
+
const result = { status, stdout, stderr };
|
|
987
|
+
if (finalError) result.error = finalError;
|
|
988
|
+
resolve(result);
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
if (Number.isFinite(timeout) && timeout > 0) {
|
|
992
|
+
timer = setTimeout(() => {
|
|
993
|
+
timedOut = true;
|
|
994
|
+
try { child.kill("SIGTERM"); } catch (_error) {}
|
|
995
|
+
// Guarantee settlement even if the child ignores SIGTERM or keeps stdio open.
|
|
996
|
+
hardTimer = setTimeout(() => {
|
|
997
|
+
try { child.kill("SIGKILL"); } catch (_error) {}
|
|
998
|
+
settle({ status: null });
|
|
999
|
+
}, 1000);
|
|
1000
|
+
if (typeof hardTimer.unref === "function") hardTimer.unref();
|
|
1001
|
+
}, timeout);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const collect = (stream, append) => {
|
|
1005
|
+
if (!stream) return;
|
|
1006
|
+
stream.setEncoding("utf8");
|
|
1007
|
+
stream.on("data", (chunk) => {
|
|
1008
|
+
append(chunk);
|
|
1009
|
+
if (stdout.length + stderr.length > maxBuffer) {
|
|
1010
|
+
const error = new Error(`spawn ${command} maxBuffer length exceeded`);
|
|
1011
|
+
error.code = "ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
|
|
1012
|
+
try { child.kill("SIGKILL"); } catch (_error) {}
|
|
1013
|
+
settle({ status: null, error });
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
};
|
|
1017
|
+
collect(child.stdout, (chunk) => { stdout += chunk; });
|
|
1018
|
+
collect(child.stderr, (chunk) => { stderr += chunk; });
|
|
1019
|
+
|
|
1020
|
+
child.on("error", (error) => settle({ status: null, error }));
|
|
1021
|
+
child.on("close", (code) => settle({ status: timedOut ? null : code }));
|
|
949
1022
|
});
|
|
950
1023
|
}
|
|
951
1024
|
|
|
952
|
-
function whichBinary(binary, { commandRunner } = {}) {
|
|
953
|
-
const result = runCommand(commandRunner, "which", [binary], { timeout: 2000 });
|
|
1025
|
+
async function whichBinary(binary, { commandRunner } = {}) {
|
|
1026
|
+
const result = await runCommand(commandRunner, "which", [binary], { timeout: 2000 });
|
|
954
1027
|
if (result?.error || result?.status !== 0) return null;
|
|
955
1028
|
const stdout = typeof result?.stdout === "string" ? result.stdout.trim() : "";
|
|
956
1029
|
return stdout ? stdout.split("\n")[0] : null;
|
|
957
1030
|
}
|
|
958
1031
|
|
|
959
|
-
function isBinaryAvailable(binary, { commandRunner } = {}) {
|
|
960
|
-
return whichBinary(binary, { commandRunner }) !== null;
|
|
1032
|
+
async function isBinaryAvailable(binary, { commandRunner } = {}) {
|
|
1033
|
+
return (await whichBinary(binary, { commandRunner })) !== null;
|
|
961
1034
|
}
|
|
962
1035
|
|
|
963
1036
|
function stripAnsi(text) {
|
|
@@ -1223,12 +1296,12 @@ async function fetchCopilotLimits({ home, env = process.env, fetchImpl = fetch }
|
|
|
1223
1296
|
}
|
|
1224
1297
|
}
|
|
1225
1298
|
|
|
1226
|
-
function fetchKiroLimits({ commandRunner, now = new Date() } = {}) {
|
|
1227
|
-
if (!isBinaryAvailable("kiro-cli", { commandRunner })) {
|
|
1299
|
+
async function fetchKiroLimits({ commandRunner, now = new Date() } = {}) {
|
|
1300
|
+
if (!(await isBinaryAvailable("kiro-cli", { commandRunner }))) {
|
|
1228
1301
|
return { configured: false };
|
|
1229
1302
|
}
|
|
1230
1303
|
|
|
1231
|
-
const result = runCommand(
|
|
1304
|
+
const result = await runCommand(
|
|
1232
1305
|
commandRunner,
|
|
1233
1306
|
"kiro-cli",
|
|
1234
1307
|
["chat", "--no-interactive", "/usage"],
|
|
@@ -1291,8 +1364,8 @@ function extractCommandFlag(command, flag) {
|
|
|
1291
1364
|
return match?.[1] || null;
|
|
1292
1365
|
}
|
|
1293
1366
|
|
|
1294
|
-
function detectAntigravityProcess({ commandRunner } = {}) {
|
|
1295
|
-
const result = runCommand(commandRunner, "/bin/ps", ["-ax", "-o", "pid=,command="], {
|
|
1367
|
+
async function detectAntigravityProcess({ commandRunner } = {}) {
|
|
1368
|
+
const result = await runCommand(commandRunner, "/bin/ps", ["-ax", "-o", "pid=,command="], {
|
|
1296
1369
|
timeout: 4000,
|
|
1297
1370
|
});
|
|
1298
1371
|
const lines = String(result?.stdout || "").split("\n");
|
|
@@ -1494,7 +1567,7 @@ function clearClaudeRateLimitCooldown({ home } = {}) {
|
|
|
1494
1567
|
} catch (_error) {}
|
|
1495
1568
|
}
|
|
1496
1569
|
|
|
1497
|
-
function resolveLsofBinary({ commandRunner } = {}) {
|
|
1570
|
+
async function resolveLsofBinary({ commandRunner } = {}) {
|
|
1498
1571
|
for (const candidate of ["/usr/sbin/lsof", "/usr/bin/lsof"]) {
|
|
1499
1572
|
if (fs.existsSync(candidate)) return candidate;
|
|
1500
1573
|
}
|
|
@@ -1513,12 +1586,12 @@ function parseListeningPorts(output) {
|
|
|
1513
1586
|
return Array.from(ports).sort((a, b) => a - b);
|
|
1514
1587
|
}
|
|
1515
1588
|
|
|
1516
|
-
function listAntigravityPorts(pid, { commandRunner } = {}) {
|
|
1517
|
-
const lsof = resolveLsofBinary({ commandRunner });
|
|
1589
|
+
async function listAntigravityPorts(pid, { commandRunner } = {}) {
|
|
1590
|
+
const lsof = await resolveLsofBinary({ commandRunner });
|
|
1518
1591
|
if (!lsof) {
|
|
1519
1592
|
throw new Error("Antigravity port detection needs lsof. Install it, then retry.");
|
|
1520
1593
|
}
|
|
1521
|
-
const result = runCommand(
|
|
1594
|
+
const result = await runCommand(
|
|
1522
1595
|
commandRunner,
|
|
1523
1596
|
lsof,
|
|
1524
1597
|
["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", String(pid)],
|
|
@@ -1768,7 +1841,7 @@ async function probeAntigravityPort(port, csrfToken, { timeoutMs, requestFn } =
|
|
|
1768
1841
|
}
|
|
1769
1842
|
|
|
1770
1843
|
async function fetchAntigravityLimits({ home, commandRunner, requestFn, timeoutMs = 8000, nowMs = Date.now() } = {}) {
|
|
1771
|
-
const processInfo = detectAntigravityProcess({ commandRunner });
|
|
1844
|
+
const processInfo = await detectAntigravityProcess({ commandRunner });
|
|
1772
1845
|
if (!processInfo.configured) {
|
|
1773
1846
|
return readAntigravityLimitsCache({ home, nowMs }) || { configured: false };
|
|
1774
1847
|
}
|
|
@@ -1787,7 +1860,7 @@ async function fetchAntigravityLimits({ home, commandRunner, requestFn, timeoutM
|
|
|
1787
1860
|
};
|
|
1788
1861
|
|
|
1789
1862
|
try {
|
|
1790
|
-
const ports = listAntigravityPorts(processInfo.pid, { commandRunner });
|
|
1863
|
+
const ports = await listAntigravityPorts(processInfo.pid, { commandRunner });
|
|
1791
1864
|
let workingPort = null;
|
|
1792
1865
|
for (const port of ports) {
|
|
1793
1866
|
if (await probeAntigravityPort(port, processInfo.csrfToken, { timeoutMs, requestFn })) {
|
|
@@ -1864,7 +1937,29 @@ function withPlanLabel(obj, raw, brand) {
|
|
|
1864
1937
|
return { ...obj, plan_label: normalizePlanLabel(raw, brand) };
|
|
1865
1938
|
}
|
|
1866
1939
|
|
|
1867
|
-
|
|
1940
|
+
// Single-flight guard: concurrent cache misses share one upstream fetch instead of
|
|
1941
|
+
// each triggering the full 9-provider round (Claude's OAuth usage endpoint 429s when
|
|
1942
|
+
// hammered). Survives an external resetUsageLimitsCache() (refresh=1 path in
|
|
1943
|
+
// local-api.js): a refresh arriving while a fetch is already running reuses that
|
|
1944
|
+
// in-flight fetch and returns its result.
|
|
1945
|
+
let inFlightFetch = null;
|
|
1946
|
+
|
|
1947
|
+
async function getUsageLimits(options = {}) {
|
|
1948
|
+
const nowMs = Date.now();
|
|
1949
|
+
if (cache.data && nowMs - cache.fetchedAt < CACHE_TTL_MS) {
|
|
1950
|
+
return cache.data;
|
|
1951
|
+
}
|
|
1952
|
+
if (inFlightFetch) {
|
|
1953
|
+
return inFlightFetch;
|
|
1954
|
+
}
|
|
1955
|
+
const promise = fetchUsageLimitsUncached(options).finally(() => {
|
|
1956
|
+
if (inFlightFetch === promise) inFlightFetch = null;
|
|
1957
|
+
});
|
|
1958
|
+
inFlightFetch = promise;
|
|
1959
|
+
return promise;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
async function fetchUsageLimitsUncached({
|
|
1868
1963
|
home,
|
|
1869
1964
|
env,
|
|
1870
1965
|
platform,
|
|
@@ -1876,9 +1971,6 @@ async function getUsageLimits({
|
|
|
1876
1971
|
providerTimeoutMs = DEFAULT_PROVIDER_TIMEOUT_MS,
|
|
1877
1972
|
} = {}) {
|
|
1878
1973
|
const nowMs = Date.now();
|
|
1879
|
-
if (cache.data && nowMs - cache.fetchedAt < CACHE_TTL_MS) {
|
|
1880
|
-
return cache.data;
|
|
1881
|
-
}
|
|
1882
1974
|
|
|
1883
1975
|
const [claudeToken, claudeSubscription, codexAuth] = await Promise.all([
|
|
1884
1976
|
Promise.resolve().then(() => readClaudeCodeAccessToken({ platform, securityRunner, home })),
|
|
@@ -1949,7 +2041,7 @@ async function getUsageLimits({
|
|
|
1949
2041
|
.catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
|
|
1950
2042
|
withProviderTimeout(fetchGeminiLimits({ home, env, fetchImpl: providerFetch, commandRunner }), "Gemini", providerTimeoutMs)
|
|
1951
2043
|
.catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
|
|
1952
|
-
|
|
2044
|
+
fetchKiroLimits({ commandRunner, now }),
|
|
1953
2045
|
fetchAntigravityLimits({ home, commandRunner, requestFn, nowMs }),
|
|
1954
2046
|
withProviderTimeout(fetchCopilotLimits({ home, env, fetchImpl: providerFetch }), "GitHub Copilot", providerTimeoutMs)
|
|
1955
2047
|
.catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
|
|
@@ -2043,6 +2135,7 @@ module.exports = {
|
|
|
2043
2135
|
getUsageLimits,
|
|
2044
2136
|
normalizePlanLabel,
|
|
2045
2137
|
resetUsageLimitsCache,
|
|
2138
|
+
runCommand,
|
|
2046
2139
|
extractGeminiOauthClientCredentials,
|
|
2047
2140
|
loadKimiCredentials,
|
|
2048
2141
|
normalizeCursorUsageSummary,
|
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
* React Wrapped page.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
// Lazy require to avoid loading the full local-api module (and its pricing
|
|
18
|
+
// tables) until rows are actually aggregated. local-api.js itself requires
|
|
19
|
+
// this module lazily inside a handler, so there is no top-level cycle either
|
|
20
|
+
// way — but keeping this lazy preserves wrapped-aggregator as a light import.
|
|
21
|
+
function normalizeQueueRow(row) {
|
|
22
|
+
return require("./local-api").normalizeQueueRow(row);
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
function isFiniteNumber(n) {
|
|
18
26
|
return typeof n === "number" && Number.isFinite(n);
|
|
19
27
|
}
|
|
@@ -70,7 +78,10 @@ function aggregateWrapped(rows, opts = {}) {
|
|
|
70
78
|
for (const row of Array.isArray(rows) ? rows : []) {
|
|
71
79
|
if (!row || typeof row !== "object") continue;
|
|
72
80
|
const key = `${row.source || ""}|${row.model || ""}|${row.hour_start || ""}`;
|
|
73
|
-
|
|
81
|
+
// Apply the same legacy-row corrections (codex inclusive-input, cursor
|
|
82
|
+
// billable=0) as local-api.js readQueueData so `tracker wrapped` matches
|
|
83
|
+
// the dashboard for identical data.
|
|
84
|
+
dedup.set(key, normalizeQueueRow(row));
|
|
74
85
|
}
|
|
75
86
|
const all = Array.from(dedup.values());
|
|
76
87
|
|