tokentracker-cli 0.44.0 → 0.44.1

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 (39) hide show
  1. package/dashboard/dist/assets/{ActivityHeatmap-C2y1lgOo.js → ActivityHeatmap-Ba6Gf4TR.js} +1 -1
  2. package/dashboard/dist/assets/{Card-BJVoMVdB.js → Card-C91m34GQ.js} +1 -1
  3. package/dashboard/dist/assets/{DashboardPage-NcjsaIbt.js → DashboardPage-CNanPHSx.js} +1 -1
  4. package/dashboard/dist/assets/{DevicePage-EHZwN0AA.js → DevicePage-BEmsLIAp.js} +1 -1
  5. package/dashboard/dist/assets/{DialogTitle-DVWJF7Sh.js → DialogTitle-XRmWoBSR.js} +1 -1
  6. package/dashboard/dist/assets/{FadeIn-DtXdVuFa.js → FadeIn-q9JURSK-.js} +1 -1
  7. package/dashboard/dist/assets/{HeaderGithubStar-Cj_NuY82.js → HeaderGithubStar-DyqWyfr_.js} +1 -1
  8. package/dashboard/dist/assets/{IpCheckPage-DZ4te6Pi.js → IpCheckPage-B7SnyHbd.js} +1 -1
  9. package/dashboard/dist/assets/{LandingPage-CR9hpOA8.js → LandingPage-B6FNNQTm.js} +1 -1
  10. package/dashboard/dist/assets/{LeaderboardAvatar-DsQv0oYJ.js → LeaderboardAvatar-CKwYyDx3.js} +1 -1
  11. package/dashboard/dist/assets/{LeaderboardPage-C6pR-fls.js → LeaderboardPage-ppzIcAqO.js} +3 -3
  12. package/dashboard/dist/assets/{LeaderboardProfileModal-DrAB9Pof.js → LeaderboardProfileModal-CS9v3_P5.js} +1 -1
  13. package/dashboard/dist/assets/{LeaderboardProfilePage-CMBVCK_9.js → LeaderboardProfilePage-Bo_bgL58.js} +1 -1
  14. package/dashboard/dist/assets/{LimitsPage-CMAgr4sd.js → LimitsPage-DS-hOIBo.js} +1 -1
  15. package/dashboard/dist/assets/{LocalOnlyNotice-DysLhDFE.js → LocalOnlyNotice-DEvHdHfe.js} +1 -1
  16. package/dashboard/dist/assets/{LoginPage-jh8YcJtZ.js → LoginPage-B2PpoMz4.js} +1 -1
  17. package/dashboard/dist/assets/{PopoverPopup-DJt7HnL-.js → PopoverPopup-Biu6xF-0.js} +1 -1
  18. package/dashboard/dist/assets/{Select-16iq_C1U.js → Select-7ll8aOrD.js} +1 -1
  19. package/dashboard/dist/assets/{SelectItemText-Bwctpnf1.js → SelectItemText-LT3pggjS.js} +1 -1
  20. package/dashboard/dist/assets/{SettingsPage-BvA-0-6K.js → SettingsPage-DnUfjcf_.js} +1 -1
  21. package/dashboard/dist/assets/{SkillsPage-BCgd79rI.js → SkillsPage-BgOXQ-tc.js} +1 -1
  22. package/dashboard/dist/assets/{WidgetsPage-DecKPwRh.js → WidgetsPage-bb233cRe.js} +1 -1
  23. package/dashboard/dist/assets/{WrappedPage-CzyRJXb4.js → WrappedPage-zz5LMyBl.js} +1 -1
  24. package/dashboard/dist/assets/{agent-logos-3S7JvH_A.js → agent-logos-BCIWowJy.js} +1 -1
  25. package/dashboard/dist/assets/{arrow-up-right-SUDWxkx-.js → arrow-up-right-DVgPAXIn.js} +1 -1
  26. package/dashboard/dist/assets/{download-B06KpWLb.js → download-D_jlB4ad.js} +1 -1
  27. package/dashboard/dist/assets/{info-BYuw8wyW.js → info-B6z0r43K.js} +1 -1
  28. package/dashboard/dist/assets/{main-CZ7INQ5C.js → main-BKWG2HXF.js} +2 -2
  29. package/dashboard/dist/assets/{use-limits-display-prefs-5el6WphI.js → use-limits-display-prefs-BszlyiM-.js} +1 -1
  30. package/dashboard/dist/assets/{use-native-settings-BsOpSNnV.js → use-native-settings-CE-ofZ6y.js} +1 -1
  31. package/dashboard/dist/assets/use-usage-limits-C9JiBxl3.js +1 -0
  32. package/dashboard/dist/assets/{useCurrency-CjB3wfFx.js → useCurrency-BQcSptXu.js} +1 -1
  33. package/dashboard/dist/assets/{useScrollLock-Dyho5BJU.js → useScrollLock-Cbkcni3m.js} +1 -1
  34. package/dashboard/dist/index.html +1 -1
  35. package/dashboard/dist/share.html +1 -1
  36. package/package.json +1 -1
  37. package/src/lib/pricing/seed-snapshot.json +1 -1
  38. package/src/lib/usage-limits.js +161 -7
  39. package/dashboard/dist/assets/use-usage-limits-BgRs0aJ1.js +0 -1
@@ -30,6 +30,19 @@ const DEFAULT_PROVIDER_TIMEOUT_MS = 15_000;
30
30
  const ANTIGRAVITY_LIMITS_CACHE_FILE = "usage-limits-cache.json";
31
31
  const ANTIGRAVITY_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
32
32
  const ANTIGRAVITY_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
33
+ // Claude shares its OAuth usage endpoint budget with Claude Code itself, so a transient
34
+ // 429 is common. Persist the last successful read so the panel can keep showing it instead
35
+ // of flashing a red error. Separate file from Antigravity's (whose writer rewrites the whole
36
+ // file with only its own key, so a shared file would clobber).
37
+ const CLAUDE_LIMITS_CACHE_FILE = "claude-usage-limits-cache.json";
38
+ const CLAUDE_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
39
+ // A 429 from the usage endpoint carries a long `retry-after` (often 20+ minutes). Persist
40
+ // the cooldown so every surface — this process, the menu bar app's embedded server, a later
41
+ // restart — stops calling until it expires. Hammering during the cooldown just renews the
42
+ // penalty, which is what kept the panel stuck on the error.
43
+ const CLAUDE_RATE_LIMIT_FILE = "claude-usage-rate-limit.json";
44
+ const CLAUDE_RATE_LIMIT_DEFAULT_COOLDOWN_SEC = 5 * 60;
45
+ const CLAUDE_RATE_LIMIT_MAX_COOLDOWN_SEC = 60 * 60;
33
46
 
34
47
  function clampPercent(value) {
35
48
  if (value === null || value === undefined || value === "") return null;
@@ -99,6 +112,20 @@ function withProviderTimeout(promise, label, timeoutMs) {
99
112
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
100
113
  }
101
114
 
115
+ function parseRetryAfterSeconds(headers) {
116
+ const ra = headers?.get ? headers.get("retry-after") : null;
117
+ const sec = ra ? Number.parseInt(ra, 10) : NaN;
118
+ return Number.isFinite(sec) && sec > 0 ? sec : null;
119
+ }
120
+
121
+ function formatClaudeRateLimitMessage(retryAfterSec) {
122
+ if (Number.isFinite(retryAfterSec) && retryAfterSec > 0) {
123
+ const mins = Math.ceil(retryAfterSec / 60);
124
+ return `Claude API rate limited (429) — retry in ~${mins}m.`;
125
+ }
126
+ return "Claude API rate limited (429) — retry shortly.";
127
+ }
128
+
102
129
  async function fetchClaudeUsageLimits(accessToken, { fetchImpl = fetch, maxAttempts = 3 } = {}) {
103
130
  const url = "https://api.anthropic.com/api/oauth/usage";
104
131
  const headers = {
@@ -120,9 +147,11 @@ async function fetchClaudeUsageLimits(accessToken, { fetchImpl = fetch, maxAttem
120
147
  }
121
148
  if (!res.ok) {
122
149
  if (res.status === 429) {
123
- throw new Error(
124
- "Claude API rate limited (429) — wait ~1 minute and refresh.",
125
- );
150
+ const retryAfterSec = parseRetryAfterSeconds(res.headers);
151
+ const err = new Error(formatClaudeRateLimitMessage(retryAfterSec));
152
+ err.code = "RATE_LIMITED";
153
+ err.retryAfterSec = retryAfterSec;
154
+ throw err;
126
155
  }
127
156
  throw new Error(`Claude API returned ${res.status}`);
128
157
  }
@@ -1271,6 +1300,107 @@ function writeAntigravityLimitsCache(limits, { home, nowMs = Date.now() } = {})
1271
1300
  } catch (_error) {}
1272
1301
  }
1273
1302
 
1303
+ function resolveClaudeLimitsCachePath({ home } = {}) {
1304
+ return path.join(home || os.homedir(), ".tokentracker", "tracker", CLAUDE_LIMITS_CACHE_FILE);
1305
+ }
1306
+
1307
+ // Claude windows carry their own `resets_at`; a window whose reset has already passed is
1308
+ // stale data, not a usable fallback, so drop it. Windows without a reset stamp are kept
1309
+ // (the overall cached_at max-age gate already bounds them).
1310
+ function isClaudeCacheWindowUsable(window, { nowMs } = {}) {
1311
+ if (!window || typeof window !== "object") return false;
1312
+ const resetAtMs = parseTimeMs(window.resets_at);
1313
+ if (resetAtMs === null) return true;
1314
+ return resetAtMs > nowMs;
1315
+ }
1316
+
1317
+ function hasClaudeWindow(limits) {
1318
+ return Boolean(limits?.five_hour || limits?.seven_day || limits?.seven_day_opus);
1319
+ }
1320
+
1321
+ function normalizeClaudeCachedLimits(raw, { nowMs = Date.now() } = {}) {
1322
+ const cachedAtMs = parseTimeMs(raw?.cached_at);
1323
+ if (!Number.isFinite(cachedAtMs)) return null;
1324
+ if (cachedAtMs > nowMs + 60_000) return null;
1325
+ if (nowMs - cachedAtMs > CLAUDE_LIMITS_CACHE_MAX_AGE_MS) return null;
1326
+
1327
+ const cached = {
1328
+ configured: true,
1329
+ error: null,
1330
+ five_hour: isClaudeCacheWindowUsable(raw?.five_hour, { nowMs }) ? raw.five_hour : null,
1331
+ seven_day: isClaudeCacheWindowUsable(raw?.seven_day, { nowMs }) ? raw.seven_day : null,
1332
+ seven_day_opus: isClaudeCacheWindowUsable(raw?.seven_day_opus, { nowMs }) ? raw.seven_day_opus : null,
1333
+ extra_usage: raw?.extra_usage ?? null,
1334
+ stale: true,
1335
+ cached_at: raw.cached_at,
1336
+ };
1337
+ return hasClaudeWindow(cached) ? cached : null;
1338
+ }
1339
+
1340
+ function readClaudeLimitsCache({ home, nowMs = Date.now() } = {}) {
1341
+ const cachePath = resolveClaudeLimitsCachePath({ home });
1342
+ try {
1343
+ const parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
1344
+ return normalizeClaudeCachedLimits(parsed?.claude, { nowMs });
1345
+ } catch (_error) {
1346
+ return null;
1347
+ }
1348
+ }
1349
+
1350
+ function writeClaudeLimitsCache(limits, { home, nowMs = Date.now() } = {}) {
1351
+ if (!limits?.configured || limits.error || !hasClaudeWindow(limits)) return;
1352
+ const cachePath = resolveClaudeLimitsCachePath({ home });
1353
+ const payload = {
1354
+ claude: {
1355
+ five_hour: limits.five_hour || null,
1356
+ seven_day: limits.seven_day || null,
1357
+ seven_day_opus: limits.seven_day_opus || null,
1358
+ extra_usage: limits.extra_usage || null,
1359
+ cached_at: new Date(nowMs).toISOString(),
1360
+ },
1361
+ };
1362
+ try {
1363
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
1364
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
1365
+ fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
1366
+ fs.renameSync(tmpPath, cachePath);
1367
+ } catch (_error) {}
1368
+ }
1369
+
1370
+ function resolveClaudeRateLimitPath({ home } = {}) {
1371
+ return path.join(home || os.homedir(), ".tokentracker", "tracker", CLAUDE_RATE_LIMIT_FILE);
1372
+ }
1373
+
1374
+ // Returns the cooldown expiry in ms if a 429 cooldown is still active, else null.
1375
+ function readClaudeRateLimitRetryAtMs({ home, nowMs = Date.now() } = {}) {
1376
+ try {
1377
+ const parsed = JSON.parse(fs.readFileSync(resolveClaudeRateLimitPath({ home }), "utf8"));
1378
+ const retryAtMs = parseTimeMs(parsed?.retry_at);
1379
+ if (retryAtMs !== null && retryAtMs > nowMs) return retryAtMs;
1380
+ } catch (_error) {}
1381
+ return null;
1382
+ }
1383
+
1384
+ function writeClaudeRateLimitCooldown(retryAfterSec, { home, nowMs = Date.now() } = {}) {
1385
+ const sec = Number.isFinite(retryAfterSec) && retryAfterSec > 0
1386
+ ? Math.min(retryAfterSec, CLAUDE_RATE_LIMIT_MAX_COOLDOWN_SEC)
1387
+ : CLAUDE_RATE_LIMIT_DEFAULT_COOLDOWN_SEC;
1388
+ const cachePath = resolveClaudeRateLimitPath({ home });
1389
+ const payload = { retry_at: new Date(nowMs + sec * 1000).toISOString() };
1390
+ try {
1391
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
1392
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
1393
+ fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
1394
+ fs.renameSync(tmpPath, cachePath);
1395
+ } catch (_error) {}
1396
+ }
1397
+
1398
+ function clearClaudeRateLimitCooldown({ home } = {}) {
1399
+ try {
1400
+ fs.unlinkSync(resolveClaudeRateLimitPath({ home }));
1401
+ } catch (_error) {}
1402
+ }
1403
+
1274
1404
  function resolveLsofBinary({ commandRunner } = {}) {
1275
1405
  for (const candidate of ["/usr/sbin/lsof", "/usr/bin/lsof"]) {
1276
1406
  if (fs.existsSync(candidate)) return candidate;
@@ -1698,9 +1828,13 @@ async function getUsageLimits({
1698
1828
  const codexAccountId = codexAuthRefreshed?.accountId || null;
1699
1829
  const codexPlanType = codexAuthRefreshed?.planType || null;
1700
1830
 
1831
+ // Skip the upstream Claude call entirely while a 429 cooldown is active — calling again
1832
+ // just renews the penalty. The result handling below serves cache or a cooldown message.
1833
+ const claudeRetryAtMs = claudeToken ? readClaudeRateLimitRetryAtMs({ home, nowMs }) : null;
1834
+
1701
1835
  const providerFetch = withFetchTimeout(fetchImpl, providerTimeoutMs);
1702
1836
  const [claudeResult, codexResult, cursor, kimi, gemini, kiro, antigravity, copilot, grok] = await Promise.all([
1703
- claudeToken
1837
+ claudeToken && !claudeRetryAtMs
1704
1838
  ? withProviderTimeout(fetchClaudeUsageLimits(claudeToken, { fetchImpl: providerFetch, maxAttempts: 1 }), "Claude", providerTimeoutMs).then(
1705
1839
  (value) => ({ status: "fulfilled", value }),
1706
1840
  (reason) => ({ status: "rejected", reason }),
@@ -1733,9 +1867,7 @@ async function getUsageLimits({
1733
1867
  let claude;
1734
1868
  if (!claudeToken) {
1735
1869
  claude = { configured: false };
1736
- } else if (!claudeResult || claudeResult.status === "rejected") {
1737
- claude = { configured: true, error: claudeResult?.reason?.message || "Unknown error" };
1738
- } else {
1870
+ } else if (claudeResult && claudeResult.status === "fulfilled") {
1739
1871
  claude = {
1740
1872
  configured: true,
1741
1873
  error: null,
@@ -1744,6 +1876,28 @@ async function getUsageLimits({
1744
1876
  seven_day_opus: claudeResult.value.seven_day_opus,
1745
1877
  extra_usage: claudeResult.value.extra_usage,
1746
1878
  };
1879
+ writeClaudeLimitsCache(claude, { home, nowMs });
1880
+ clearClaudeRateLimitCooldown({ home });
1881
+ } else {
1882
+ // Either a fresh 429 (record its cooldown) or a call we skipped because a cooldown was
1883
+ // already active. Serve the last successful read so the bars stay visible; otherwise
1884
+ // surface an accurate "retry in ~Nm" message rather than the misleading hardcoded one.
1885
+ const reason = claudeResult?.reason;
1886
+ if (reason?.code === "RATE_LIMITED") {
1887
+ writeClaudeRateLimitCooldown(reason.retryAfterSec, { home, nowMs });
1888
+ }
1889
+ const cached = readClaudeLimitsCache({ home, nowMs });
1890
+ if (cached) {
1891
+ claude = cached;
1892
+ } else {
1893
+ const retryAtMs = readClaudeRateLimitRetryAtMs({ home, nowMs }) || claudeRetryAtMs;
1894
+ claude = {
1895
+ configured: true,
1896
+ error: retryAtMs
1897
+ ? formatClaudeRateLimitMessage(Math.round((retryAtMs - nowMs) / 1000))
1898
+ : reason?.message || "Unknown error",
1899
+ };
1900
+ }
1747
1901
  }
1748
1902
 
1749
1903
  let codex;
@@ -1 +0,0 @@
1
- import{r as a,ab as m,aI as f}from"./main-CZ7INQ5C.js";function p(s){const r=!!s?.initialState,[h,c]=a.useState(()=>r?s?.initialState?.data??null:null),[S,l]=a.useState(()=>r?s?.initialState?.error??null:null),[g,b]=a.useState(!r),i=!!s?.initialRefresh,o=!!s?.publishToPreloadCache,n=a.useCallback((e,t)=>{!o||!e||typeof e!="object"||m(e,{source:t})},[o]),d=a.useCallback(async()=>{try{const e=await f({refresh:!0}),t=e&&typeof e=="object"?e:null;c(t),l(null),n(t,"manual-refresh")}catch(e){l(e?.message||String(e))}},[n]);return a.useEffect(()=>{if(r&&!i)return;let e=!1;return(async()=>{try{const t=await f(i?{refresh:!0}:{});if(e)return;const u=t&&typeof t=="object"?t:null;c(u),l(null),n(u,"page-load")}catch(t){if(e)return;l(t?.message||String(t))}finally{e||b(!1)}})(),()=>{e=!0}},[r,i,n]),{data:h,error:S,isLoading:g,refresh:d}}export{p as u};