tokentracker-cli 0.43.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 (44) hide show
  1. package/README.md +1 -1
  2. package/dashboard/dist/assets/{ActivityHeatmap-0IDxIr7i.js → ActivityHeatmap-Ba6Gf4TR.js} +1 -1
  3. package/dashboard/dist/assets/{Card-hVCKEGdC.js → Card-C91m34GQ.js} +1 -1
  4. package/dashboard/dist/assets/{DashboardPage-C3QvJSI2.js → DashboardPage-CNanPHSx.js} +1 -1
  5. package/dashboard/dist/assets/{DevicePage-UVSutySl.js → DevicePage-BEmsLIAp.js} +1 -1
  6. package/dashboard/dist/assets/{DialogTitle-D0S8c1BO.js → DialogTitle-XRmWoBSR.js} +1 -1
  7. package/dashboard/dist/assets/{FadeIn-anio8NgI.js → FadeIn-q9JURSK-.js} +1 -1
  8. package/dashboard/dist/assets/{HeaderGithubStar-wRjA_Fvu.js → HeaderGithubStar-DyqWyfr_.js} +1 -1
  9. package/dashboard/dist/assets/{IpCheckPage-vl0aIVv0.js → IpCheckPage-B7SnyHbd.js} +1 -1
  10. package/dashboard/dist/assets/{LandingPage-CJO4q92s.js → LandingPage-B6FNNQTm.js} +1 -1
  11. package/dashboard/dist/assets/{LeaderboardAvatar-CjPVgiDo.js → LeaderboardAvatar-CKwYyDx3.js} +1 -1
  12. package/dashboard/dist/assets/{LeaderboardPage-DiQn0DqL.js → LeaderboardPage-ppzIcAqO.js} +3 -3
  13. package/dashboard/dist/assets/{LeaderboardProfileModal-yovVjgnB.js → LeaderboardProfileModal-CS9v3_P5.js} +1 -1
  14. package/dashboard/dist/assets/{LeaderboardProfilePage-C0vqt4m5.js → LeaderboardProfilePage-Bo_bgL58.js} +1 -1
  15. package/dashboard/dist/assets/{LimitsPage-8o294HKr.js → LimitsPage-DS-hOIBo.js} +1 -1
  16. package/dashboard/dist/assets/{LocalOnlyNotice-BQsPhRvE.js → LocalOnlyNotice-DEvHdHfe.js} +1 -1
  17. package/dashboard/dist/assets/{LoginPage-D8hQf3xT.js → LoginPage-B2PpoMz4.js} +1 -1
  18. package/dashboard/dist/assets/{PopoverPopup-rhSAh0x6.js → PopoverPopup-Biu6xF-0.js} +1 -1
  19. package/dashboard/dist/assets/{Select-ByLRQqzg.js → Select-7ll8aOrD.js} +1 -1
  20. package/dashboard/dist/assets/{SelectItemText-CHHy8UA1.js → SelectItemText-LT3pggjS.js} +1 -1
  21. package/dashboard/dist/assets/{SettingsPage-DGyuX3ua.js → SettingsPage-DnUfjcf_.js} +1 -1
  22. package/dashboard/dist/assets/{SkillsPage-CMsfSZar.js → SkillsPage-BgOXQ-tc.js} +1 -1
  23. package/dashboard/dist/assets/{WidgetsPage-DavgYHA1.js → WidgetsPage-bb233cRe.js} +1 -1
  24. package/dashboard/dist/assets/{WrappedPage-BARpWxxH.js → WrappedPage-zz5LMyBl.js} +1 -1
  25. package/dashboard/dist/assets/{agent-logos-BsEMlmFc.js → agent-logos-BCIWowJy.js} +1 -1
  26. package/dashboard/dist/assets/{arrow-up-right-Nh5NXsRd.js → arrow-up-right-DVgPAXIn.js} +1 -1
  27. package/dashboard/dist/assets/{download-Bz-ad2Zi.js → download-D_jlB4ad.js} +1 -1
  28. package/dashboard/dist/assets/{info-Edlzr0qR.js → info-B6z0r43K.js} +1 -1
  29. package/dashboard/dist/assets/{main-CGYVeoRd.js → main-BKWG2HXF.js} +2 -2
  30. package/dashboard/dist/assets/{use-limits-display-prefs-Cc82ZSkQ.js → use-limits-display-prefs-BszlyiM-.js} +1 -1
  31. package/dashboard/dist/assets/{use-native-settings-rTdpowec.js → use-native-settings-CE-ofZ6y.js} +1 -1
  32. package/dashboard/dist/assets/use-usage-limits-C9JiBxl3.js +1 -0
  33. package/dashboard/dist/assets/{useCurrency-BY5HnhWy.js → useCurrency-BQcSptXu.js} +1 -1
  34. package/dashboard/dist/assets/{useScrollLock-oNpe5Ufe.js → useScrollLock-Cbkcni3m.js} +1 -1
  35. package/dashboard/dist/index.html +1 -1
  36. package/dashboard/dist/share.html +1 -1
  37. package/package.json +1 -1
  38. package/src/commands/status.js +13 -2
  39. package/src/commands/sync.js +1 -1
  40. package/src/lib/pricing/matcher.js +33 -5
  41. package/src/lib/pricing/seed-snapshot.json +1 -1
  42. package/src/lib/rollout.js +19 -5
  43. package/src/lib/usage-limits.js +161 -7
  44. package/dashboard/dist/assets/use-usage-limits-DFjNciSe.js +0 -1
@@ -5229,7 +5229,7 @@ async function parseRoocodeIncremental({
5229
5229
  }
5230
5230
 
5231
5231
  // ─────────────────────────────────────────────────────────────────────────────
5232
- // Zed Agent (hosted models only, provider == "zed.dev")
5232
+ // Zed Agent (all model providers hosted "zed.dev" and bring-your-own alike)
5233
5233
  //
5234
5234
  // Data: SQLite at
5235
5235
  // macOS: ~/Library/Application Support/Zed/threads/threads.db
@@ -5249,11 +5249,21 @@ async function parseRoocodeIncremental({
5249
5249
  // antigravity cumulative-delta pattern: keep last-seen totals per thread in
5250
5250
  // `cursors.zed.threadTotals`, emit (current - previous) on each sync.
5251
5251
  //
5252
- // External ACP agents are skipped (their CLIs already report to us through
5253
- // their own parserscounting them via the Zed UI would double-count).
5252
+ // Providers already reported by a dedicated parser are skipped to avoid
5253
+ // double-counting (see ZED_DOUBLE_COUNTED_PROVIDERSempty today). Model names
5254
+ // are normalized for pricing in the matcher (normalizeZedModel), not here, so
5255
+ // the real Zed model name is preserved for display.
5254
5256
  // ─────────────────────────────────────────────────────────────────────────────
5255
5257
 
5256
- const ZED_HOSTED_PROVIDER = "zed.dev";
5258
+ // Providers whose usage is ALSO captured by a dedicated TokenTracker parser, so
5259
+ // counting them via the Zed thread store would double-count. Zed's native model
5260
+ // providers (zed.dev, copilot_chat, openai*, anthropic, google, ollama,
5261
+ // lmstudio, …) do NOT overlap: e.g. Zed's copilot_chat talks to the Copilot API
5262
+ // directly and never writes ~/.copilot/otel, which is what the Copilot parser
5263
+ // reads. The set is therefore empty today; it's the extension point if Zed ever
5264
+ // persists external-ACP-agent usage (Claude Code / Codex run inside Zed) into
5265
+ // threads.db with a recognizable provider id.
5266
+ const ZED_DOUBLE_COUNTED_PROVIDERS = new Set();
5257
5267
  const MAX_ZED_THREAD_JSON_BYTES = 32 * 1024 * 1024;
5258
5268
 
5259
5269
  function resolveZedDbPath(env = process.env) {
@@ -5347,7 +5357,11 @@ function extractZedTotals(thread) {
5347
5357
  const model = thread.model;
5348
5358
  if (!model || typeof model !== "object") return null;
5349
5359
  const provider = typeof model.provider === "string" ? model.provider.trim() : "";
5350
- if (provider.toLowerCase() !== ZED_HOSTED_PROVIDER) return null;
5360
+ // Count usage for ALL providers — Zed-hosted (zed.dev) and bring-your-own
5361
+ // (copilot_chat, openai-subscribed, anthropic, lmstudio, …) alike. Only skip
5362
+ // providers whose usage a dedicated parser already reports (see
5363
+ // ZED_DOUBLE_COUNTED_PROVIDERS).
5364
+ if (provider && ZED_DOUBLE_COUNTED_PROVIDERS.has(provider.toLowerCase())) return null;
5351
5365
  const modelId = typeof model.model === "string" ? model.model.trim() : "";
5352
5366
  if (!modelId) return null;
5353
5367
 
@@ -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-CGYVeoRd.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};