tokentracker-cli 1.5.2 → 1.5.3

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 (125) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-B5kmt0VD.js → AchievementBadge-uuDGO9Mq.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-C0FlkOo0.js → AchievementsPage-BCTVT7Jo.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-CRcp80Pp.js → AchievementsSection-B6fJSJSr.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-DAFjGbKW.js → BadgeDetailModal-FjUN__9G.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-CO0bbimB.js → BotAnimated-B-KqY_Wr.js} +1 -1
  6. package/dashboard/dist/assets/{Card-Xhr5exz8.js → Card-DS8T8piP.js} +1 -1
  7. package/dashboard/dist/assets/{CloudCheckoutPage-DsQ7FmsI.js → CloudCheckoutPage-_0UjsFPu.js} +1 -1
  8. package/dashboard/dist/assets/{CloudPage-DkSexu5V.js → CloudPage-BstgVBaV.js} +2 -2
  9. package/dashboard/dist/assets/{CommandPalette-Bm0huQdj.js → CommandPalette-DtVsYZgc.js} +1 -1
  10. package/dashboard/dist/assets/{CommunityStatsModal-BsL8GN8Q.js → CommunityStatsModal-DTpEJqV3.js} +1 -1
  11. package/dashboard/dist/assets/{ConfirmModal-_WPp292g.js → ConfirmModal-Lfdh2vr7.js} +1 -1
  12. package/dashboard/dist/assets/{Controls-ktzn68KM.js → Controls-DpwQ7pkq.js} +1 -1
  13. package/dashboard/dist/assets/DashboardPage-DUFWejkE.js +1 -0
  14. package/dashboard/dist/assets/{DateRangePopover-DWr4zIzF.js → DateRangePopover-CrWgrzT2.js} +1 -1
  15. package/dashboard/dist/assets/{DevicePage-CBa8Mk47.js → DevicePage-DbK5KWJS.js} +1 -1
  16. package/dashboard/dist/assets/{DialogClose-CZ5jzgtb.js → DialogClose-Dd2B_o03.js} +1 -1
  17. package/dashboard/dist/assets/DialogDescription-CLy0arbt.js +1 -0
  18. package/dashboard/dist/assets/{DialogTitle-Dr0N6q15.js → DialogTitle-3JQBVha3.js} +1 -1
  19. package/dashboard/dist/assets/{ElectricLogo-sYqgrO6-.js → ElectricLogo-LYAqG8pG.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-Qq-hcrZW.js → FadeIn-DhDvUor7.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-B77zSv7i.js → HeaderGithubStar-9f1XmgYC.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-lRDaUmoW.js → HoverTooltip-BXhHAkP7.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-B6AvHZjA.js → IpCheckPage-ChPdDeMl.js} +1 -1
  24. package/dashboard/dist/assets/{LandingPage-DgzOjf3x.js → LandingPage-DnJ7Y9zw.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardPage-BtjjtDMC.js → LeaderboardPage-BF9isLzu.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardProBadge-B2wNTUTU.js → LeaderboardProBadge-BM4JKgkC.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-DJJckwHh.js → LeaderboardProfileModal-DfZABOpN.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-DJdxByl2.js → LeaderboardProfilePage-BaZ38iT5.js} +1 -1
  29. package/dashboard/dist/assets/LimitsPage-C7LbG3LX.js +6 -0
  30. package/dashboard/dist/assets/{LocalOnlyNotice-B2lVqf_H.js → LocalOnlyNotice-DvhlYoqQ.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-i0gg1yby.js → LoginCard-g_Gbnzhs.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-B-loLNZM.js → LoginModal-DkJrI6b7.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-D_swI75j.js → LoginPage-CqCKSo0n.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-DDpKvpjG.js → PetPage-DbF7ROsj.js} +2 -2
  35. package/dashboard/dist/assets/{PopoverPopup-Gja4MyEK.js → PopoverPopup-4WuH41ZT.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-DOPj7tAZ.js → ProviderIcon-6j0A2KD9.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-BVADLk89.js → ResetPasswordPage-DBd-iwuM.js} +1 -1
  38. package/dashboard/dist/assets/{Select-D-38tN-E.js → Select-BBEpLeiG.js} +1 -1
  39. package/dashboard/dist/assets/{SelectItemText-jN61ukxk.js → SelectItemText-Cn_U_rrO.js} +1 -1
  40. package/dashboard/dist/assets/{SelfHostPage-k7AkGpl1.js → SelfHostPage-2WZc6Rml.js} +1 -1
  41. package/dashboard/dist/assets/ServiceStatusPage-B-g4MDl0.js +1 -0
  42. package/dashboard/dist/assets/{SessionsPage-BymccEEz.js → SessionsPage-CWS0iQ1L.js} +1 -1
  43. package/dashboard/dist/assets/{SettingsPage-DfnMCAuA.js → SettingsPage-rXNr96i-.js} +3 -3
  44. package/dashboard/dist/assets/{SkillsPage-YyIHIaHN.js → SkillsPage-CudTvbZL.js} +1 -1
  45. package/dashboard/dist/assets/{TokenGalaxy-BLcvZuQ9.js → TokenGalaxy-BRKrbTsJ.js} +1 -1
  46. package/dashboard/dist/assets/{TooltipPopup-3E8kyVzz.js → TooltipPopup-CsLKXhWC.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-ByKAdXSF.js → TrendMonitor-8onUTbkk.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-BgEVIIzL.js → WidgetsPage-CcwE0lz8.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-HjbX8QaA.js → WrappedPage-Cj9LLYqP.js} +1 -1
  50. package/dashboard/dist/assets/__vite-browser-external-07djSGmy.js +45 -0
  51. package/dashboard/dist/assets/__vite-browser-external-BDldtsD2.css +1 -0
  52. package/dashboard/dist/assets/{agent-logos-BVF2siPQ.js → agent-logos-ChBA1nAH.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-left-CYO24PGP.js → arrow-left-Bb5mfoPc.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-right-Xg9PZYhw.js → arrow-right-D1Ti2XUI.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-B7ah6Qv-.js → arrow-up-right-B_CjA_y8.js} +1 -1
  56. package/dashboard/dist/assets/{calendar-days-BV_iG938.js → calendar-days-DdfrAqie.js} +1 -1
  57. package/dashboard/dist/assets/{check--EZH8RTp.js → check-29zGw1MK.js} +1 -1
  58. package/dashboard/dist/assets/{chevron-down-BzGTlk0-.js → chevron-down-DAzlFX7g.js} +1 -1
  59. package/dashboard/dist/assets/{cloud-checkout-BgyxPHEh.js → cloud-checkout-C2SeCjFM.js} +1 -1
  60. package/dashboard/dist/assets/{composite-C0KrmLEK.js → composite-DIf735Hq.js} +1 -1
  61. package/dashboard/dist/assets/{copy-BtucSK2U.js → copy-BOuOM0HQ.js} +1 -1
  62. package/dashboard/dist/assets/{download-CWHq03Y2.js → download-50tQRpGe.js} +1 -1
  63. package/dashboard/dist/assets/{external-link-DCTXF5VZ.js → external-link-BhlQClVe.js} +1 -1
  64. package/dashboard/dist/assets/{flame-BE5TsqVF.js → flame-CeGfA-0L.js} +1 -1
  65. package/dashboard/dist/assets/{format-tokens-ThNIMWnZ.js → format-tokens-Cq7YiAkJ.js} +1 -1
  66. package/dashboard/dist/assets/{gift-DgaN9s4-.js → gift-Do-UFhiT.js} +1 -1
  67. package/dashboard/dist/assets/{icons-B4r5EiDC.js → icons-C6EZ_HmP.js} +1 -1
  68. package/dashboard/dist/assets/{index-CKhcCLNC.js → index-BFrbVqsm.js} +1 -1
  69. package/dashboard/dist/assets/{index-DqCtgU0G.js → index-BWhbBAd9.js} +1 -1
  70. package/dashboard/dist/assets/{index-fNKH4U7j.js → index-D9kwYWLX.js} +1 -1
  71. package/dashboard/dist/assets/{index-DFF9xo6f.js → index-c0crpe9Z.js} +1 -1
  72. package/dashboard/dist/assets/{info-B4YzRR4y.js → info-B4MXmsNE.js} +1 -1
  73. package/dashboard/dist/assets/{link-2-DzbUbaBw.js → link-2-DuWCslzl.js} +1 -1
  74. package/dashboard/dist/assets/{loader-circle-B9c7D-7v.js → loader-circle-C2I3NZAO.js} +1 -1
  75. package/dashboard/dist/assets/{local-usage-transfer-btuk_b0Y.js → local-usage-transfer-BQasvsUF.js} +1 -1
  76. package/dashboard/dist/assets/{modifiers.esm-BjrU-Xz7.js → modifiers.esm-DbeNS6FT.js} +1 -1
  77. package/dashboard/dist/assets/{monitor-smartphone-Bwn4tb4q.js → monitor-smartphone-C6Y4ktsc.js} +1 -1
  78. package/dashboard/dist/assets/{play-DIFOwGIQ.js → play-CwgcDJC2.js} +1 -1
  79. package/dashboard/dist/assets/{plus-BpBqnwVg.js → plus-B796wsUO.js} +1 -1
  80. package/dashboard/dist/assets/{provider-display-DSrs4u7T.js → provider-display-Cw26RFDm.js} +1 -1
  81. package/dashboard/dist/assets/{react-BgG7_MY0.js → react-d7Bk1-_K.js} +1 -1
  82. package/dashboard/dist/assets/{safePolygon-BUGWIDOM.js → safePolygon-CDE0dNv_.js} +1 -1
  83. package/dashboard/dist/assets/{search-aHrmBmtZ.js → search-DtKvsdRs.js} +1 -1
  84. package/dashboard/dist/assets/{server-DOEvwlEU.js → server-gAURHUyz.js} +1 -1
  85. package/dashboard/dist/assets/{shield-check-BPTnLhsT.js → shield-check-DFVdDTwh.js} +1 -1
  86. package/dashboard/dist/assets/{skills-api-C052Mwez.js → skills-api-2rdPUrJI.js} +1 -1
  87. package/dashboard/dist/assets/{sortable.esm-DaZKhjhu.js → sortable.esm-DFlfwnJr.js} +1 -1
  88. package/dashboard/dist/assets/{store-Cq30Pofw.js → store-BKPCk_de.js} +1 -1
  89. package/dashboard/dist/assets/{terminal-CoSDSkE7.js → terminal-BtWFakvS.js} +1 -1
  90. package/dashboard/dist/assets/{trash-BAmt-kPj.js → trash-bpr9zTAM.js} +1 -1
  91. package/dashboard/dist/assets/{use-community-stats-Bxq0kBhm.js → use-community-stats-DMnGgLne.js} +1 -1
  92. package/dashboard/dist/assets/{use-latest-request-guard-DmIQDlvk.js → use-latest-request-guard-BSBlJJHe.js} +1 -1
  93. package/dashboard/dist/assets/use-limits-display-prefs-eYlvnUKn.js +1 -0
  94. package/dashboard/dist/assets/{use-native-settings-s_CIC8Bb.js → use-native-settings-OthqiF6g.js} +1 -1
  95. package/dashboard/dist/assets/{use-ordered-list-BHhykAbt.js → use-ordered-list-DpAtr0R-.js} +1 -1
  96. package/dashboard/dist/assets/use-reduced-motion-DF0grwHs.js +1 -0
  97. package/dashboard/dist/assets/{use-transform-CVeMFlS6.js → use-transform-BRz9dz_2.js} +1 -1
  98. package/dashboard/dist/assets/use-usage-limits-CwmhudAX.js +1 -0
  99. package/dashboard/dist/assets/{useCurrency-DilqL2Iv.js → useCurrency-CAA-tQ27.js} +1 -1
  100. package/dashboard/dist/assets/{usePositioner-BcnySw8a.js → usePositioner-Cj2DPmwM.js} +1 -1
  101. package/dashboard/dist/assets/{useTokenFormat-CvKW46WP.js → useTokenFormat-Bf5-f7Hx.js} +1 -1
  102. package/dashboard/dist/assets/{zap-D7VRNTwk.js → zap-B8Fg6PaC.js} +1 -1
  103. package/dashboard/dist/index.html +2 -2
  104. package/dashboard/dist/ip-check.html +2 -2
  105. package/dashboard/dist/leaderboard.html +2 -2
  106. package/dashboard/dist/share.html +2 -2
  107. package/package.json +1 -1
  108. package/src/commands/serve.js +22 -13
  109. package/src/lib/deepseek-status.js +157 -0
  110. package/src/lib/local-api.js +44 -4
  111. package/src/lib/pricing/seed-snapshot.json +1 -1
  112. package/src/lib/provider-status.js +15 -2
  113. package/src/lib/qoder-limits.js +17 -2
  114. package/src/lib/usage-limits.js +181 -39
  115. package/src/lib/workbuddy-limits.js +191 -18
  116. package/src/lib/zcode-limits.js +143 -35
  117. package/dashboard/dist/assets/DashboardPage-CdNXi3Im.js +0 -1
  118. package/dashboard/dist/assets/DialogDescription-kCZ4gN09.js +0 -1
  119. package/dashboard/dist/assets/LimitsPage-gSBHSBi_.js +0 -6
  120. package/dashboard/dist/assets/ServiceStatusPage-D_Fa_pYz.js +0 -1
  121. package/dashboard/dist/assets/__vite-browser-external-Br-KAXh6.css +0 -1
  122. package/dashboard/dist/assets/__vite-browser-external-D_cMeuqI.js +0 -45
  123. package/dashboard/dist/assets/use-limits-display-prefs-CyAn1fme.js +0 -1
  124. package/dashboard/dist/assets/use-reduced-motion-BmFyFE3S.js +0 -1
  125. package/dashboard/dist/assets/use-usage-limits-DRZMI20u.js +0 -1
@@ -9,6 +9,8 @@
9
9
  // Statuspage.io contract (`/api/v2/status.json`):
10
10
  // { "page": { "updated_at": ISO }, "status": { "indicator": "none|minor|major|critical", "description": "..." } }
11
11
 
12
+ const { parseDeepSeekStatusPage } = require("./deepseek-status");
13
+
12
14
  const STATUS_PROBE_TIMEOUT_MS = 6_000;
13
15
  // Status pages change on incident timescales; 5 minutes keeps the limits
14
16
  // poll (2-min TTL upstream) from hammering statuspage.io every cycle.
@@ -23,6 +25,13 @@ const STATUS_PAGE_SPECS = {
23
25
  apiUrl: "https://status.claude.com/api/v2/status.json",
24
26
  pageUrl: "https://status.claude.com",
25
27
  },
28
+ deepseek: {
29
+ // Flashcat-hosted Next.js page: no public JSON API and no CORS. Relayed
30
+ // server-side here; `format` switches the fetch to HTML + RSC parsing.
31
+ apiUrl: "https://status.deepseek.com/",
32
+ pageUrl: "https://status.deepseek.com",
33
+ format: "deepseek-rsc",
34
+ },
26
35
  };
27
36
 
28
37
  const VALID_INDICATORS = new Set(["none", "minor", "major", "critical"]);
@@ -68,13 +77,17 @@ async function fetchProviderServiceStatus(providerId, { fetchImpl = fetch, nowMs
68
77
  const controller = new AbortController();
69
78
  const timer = setTimeout(() => controller.abort(), STATUS_PROBE_TIMEOUT_MS);
70
79
  try {
80
+ const isDeepSeek = spec.format === "deepseek-rsc";
71
81
  const res = await fetchImpl(spec.apiUrl, {
72
82
  method: "GET",
73
- headers: { Accept: "application/json" },
83
+ headers: { Accept: isDeepSeek ? "text/html" : "application/json" },
74
84
  signal: controller.signal,
75
85
  });
76
86
  if (res.ok) {
77
- data = normalizeStatusPayload(await res.json(), spec, nowMs);
87
+ const payload = isDeepSeek
88
+ ? parseDeepSeekStatusPage(await res.text())
89
+ : await res.json();
90
+ data = normalizeStatusPayload(payload, spec, nowMs);
78
91
  }
79
92
  } finally {
80
93
  clearTimeout(timer);
@@ -40,6 +40,9 @@ const QODER_ACTIVITY_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
40
40
  const QODER_LIMITS_CACHE_FILE = "qoder-usage-limits-cache.json";
41
41
  const QODER_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
42
42
  const QODER_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
43
+ // Align with usage-limits provenance aging (10 min): a cache hit fresher than
44
+ // this inherits its written source and is not flagged stale (#955 self-lock).
45
+ const QODER_LIMITS_CACHE_FRESH_TTL_MS = 10 * 60 * 1000;
43
46
  // Qoder Desktop 0.4+ writes its account usage summary to main.log, which can
44
47
  // accumulate a whole session before the quota is read. Scan a bounded window
45
48
  // backwards instead of assuming the summary is near EOF.
@@ -435,6 +438,15 @@ function readQoderLimitsCache({
435
438
  if (nowMs - cachedAtMs > QODER_LIMITS_CACHE_MAX_AGE_MS && !hasUnexpiredDatedWindow) {
436
439
  return null;
437
440
  }
441
+ // Provenance must describe the data's age, not the transport (#955): a
442
+ // successful refresh writes this cache and immediately floors later log
443
+ // scans, so the next refresh re-serves the same payload. Marking every
444
+ // hit stale/disk-cache made the Limits page permanently partial.
445
+ const ageMs = nowMs - cachedAtMs;
446
+ const fresh = ageMs <= QODER_LIMITS_CACHE_FRESH_TTL_MS;
447
+ const writtenSource = typeof raw.source === "string" && raw.source.trim()
448
+ ? raw.source.trim()
449
+ : null;
438
450
  return {
439
451
  configured: true,
440
452
  error: null,
@@ -444,9 +456,9 @@ function readQoderLimitsCache({
444
456
  secondary_window: secondaryWindow,
445
457
  ...(tertiaryWindow ? { tertiary_window: tertiaryWindow } : {}),
446
458
  cached: true,
447
- stale: true,
459
+ stale: !fresh,
448
460
  cached_at: raw.cached_at,
449
- source: "disk-cache",
461
+ source: fresh && writtenSource ? writtenSource : "disk-cache",
450
462
  };
451
463
  } catch (_error) {
452
464
  return null;
@@ -467,6 +479,9 @@ function writeQoderLimitsCache(limits, {
467
479
  primary_window: limits.primary_window || null,
468
480
  secondary_window: limits.secondary_window || null,
469
481
  ...(limits.tertiary_window ? { tertiary_window: limits.tertiary_window } : {}),
482
+ // Remember the winning source so a fresh re-serve keeps honest
483
+ // provenance instead of always claiming "disk-cache" (#955).
484
+ source: typeof limits.source === "string" ? limits.source : null,
470
485
  cached_at: new Date(nowMs).toISOString(),
471
486
  },
472
487
  };
@@ -50,21 +50,54 @@ const execFileAsync = promisify(cp.execFile);
50
50
  // 2-minute in-memory cache. It also expires early at the earliest upcoming window
51
51
  // reset in the cached data (see cacheExpiresAtMs), floored so a provider reporting
52
52
  // a reset "right now" can't turn every poll into a full upstream round.
53
- // Partitioned by the Devin opt-in selection AND the Antigravity quota opt-out, so
54
- // a request made while Devin is off is never served (or joined onto) a response
55
- // fetched while it was on, and an aggregate fetched while Antigravity was enabled
56
- // is never served after the opt-out is set. Key = `${devin}-${antigravity}`.
57
- const USAGE_LIMITS_SELECTION_KEYS = ["off-on", "off-off", "on-on", "on-off"];
53
+ // Partitioned by the Devin opt-in selection AND the Antigravity quota opt-out AND
54
+ // the per-request hidden-provider set (#956), so a request made while Devin is off
55
+ // is never served (or joined onto) a response fetched while it was on, an aggregate
56
+ // fetched while Antigravity was enabled is never served after the opt-out is set,
57
+ // and a response fetched for a visibility selection is never served to one made
58
+ // under a different one. Key = `${devin}-${antigravity}[-<hidden ids>]`.
59
+ const USAGE_LIMITS_BASE_SELECTION_KEYS = ["off-on", "off-off", "on-on", "on-off"];
58
60
  const cacheBySelection = {};
59
- for (const key of USAGE_LIMITS_SELECTION_KEYS) cacheBySelection[key] = { data: null, expiresAtMs: 0 };
61
+ for (const key of USAGE_LIMITS_BASE_SELECTION_KEYS) cacheBySelection[key] = { data: null, expiresAtMs: 0 };
60
62
  // Credential replacements (provider-key save) bump this generation so a fetch
61
63
  // that started under the old credential cannot repopulate the memory or disk
62
64
  // caches after invalidation.
63
65
  let usageLimitsCacheGeneration = 0;
66
+ // Provider row keys of the usage-limits aggregate — the id space `hidden`
67
+ // parameter values are validated against (dashboard rows, menu-bar
68
+ // LimitsSettingsStore.allProviders, and this payload all share it).
69
+ const USAGE_LIMITS_PROVIDER_IDS = Object.freeze([
70
+ "claude", "codex", "cursor", "gemini", "kimi", "kiro", "grok",
71
+ "copilot", "antigravity", "zcode", "opencodeGo", "commandCode",
72
+ "qoder", "qoderCn", "codingPlan", "agentPlan", "devin",
73
+ "workbuddy", "workbuddyAi",
74
+ ]);
75
+
76
+ // Normalize a caller-supplied hidden-provider list (array, Set, or
77
+ // comma-separated query value) into a sorted, de-duplicated id array, or null
78
+ // when nothing is hidden. Unknown ids are dropped: the parameter only ever
79
+ // narrows the fetch, so a silently ignored typo must not widen the cache-key
80
+ // space (and leniency matches the devin query param, which accepts any value
81
+ // that is not "1"/"true" as off).
82
+ function normalizeHiddenProviders(input) {
83
+ const raw = input instanceof Set
84
+ ? [...input]
85
+ : Array.isArray(input)
86
+ ? input
87
+ : typeof input === "string" && input.trim().length > 0
88
+ ? input.split(",")
89
+ : [];
90
+ const ids = [...new Set(
91
+ raw.map((id) => String(id).trim()).filter((id) => USAGE_LIMITS_PROVIDER_IDS.includes(id)),
92
+ )].sort();
93
+ return ids.length > 0 ? ids : null;
94
+ }
95
+
64
96
  function usageLimitsSelectionKey(options) {
65
97
  const devin = options?.devinEnabled === true ? "on" : "off";
66
98
  const antigravity = isAntigravityQuotaDisabled() ? "off" : "on";
67
- return `${devin}-${antigravity}`;
99
+ const hidden = normalizeHiddenProviders(options?.hiddenProviders);
100
+ return `${devin}-${antigravity}${hidden ? `-${hidden.join("+")}` : ""}`;
68
101
  }
69
102
  const CACHE_TTL_MS = 2 * 60 * 1000;
70
103
  // Must stay below the macOS app's post-reset re-fetch grace (10s in
@@ -237,6 +270,43 @@ function withAbortableProviderTimeout(start, label, timeoutMs) {
237
270
  .finally(() => controller.abort());
238
271
  }
239
272
 
273
+ // WorkBuddy bounds its own live chain (the IPC proxy timeout is internal), and
274
+ // its disk-cache fallback runs after the abort lands: the abort makes the
275
+ // socket fail within milliseconds and the cache read is synchronous. A plain
276
+ // provider-timeout race would settle on the timeout first and drop that
277
+ // fallback, so after the budget expires, abort and give the attempt a short
278
+ // grace to land its fallback — its outcome (cache hit or specific error)
279
+ // replaces the generic timeout. The hard bound is timeoutMs + graceMs.
280
+ function withWorkbuddyProviderTimeout(start, label, timeoutMs, graceMs = 2_000) {
281
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return start(undefined);
282
+ const controller = new AbortController();
283
+ const work = start(controller.signal);
284
+ let timeoutTimer;
285
+ let graceTimer;
286
+ let deadlineFired = false;
287
+ // Once the budget aborts the attempt, any rejection it produces is the
288
+ // deadline's doing — the IPC socket fails with its cancellation error, a
289
+ // fetch with an AbortError — so report the labeled timeout instead of the
290
+ // symptom. A post-abort resolution still wins the grace race (the
291
+ // disk-cache fallback).
292
+ const guardedWork = work.catch((error) => {
293
+ if (deadlineFired) throw new Error(`${label} usage request timed out.`);
294
+ throw error;
295
+ });
296
+ const timeoutPromise = new Promise((_, reject) => {
297
+ timeoutTimer = setTimeout(() => {
298
+ deadlineFired = true;
299
+ controller.abort();
300
+ graceTimer = setTimeout(() => reject(new Error(`${label} usage request timed out.`)), graceMs);
301
+ }, timeoutMs);
302
+ });
303
+ return Promise.race([guardedWork, timeoutPromise]).finally(() => {
304
+ clearTimeout(timeoutTimer);
305
+ clearTimeout(graceTimer);
306
+ controller.abort();
307
+ });
308
+ }
309
+
240
310
  function parseRetryAfterSeconds(headers) {
241
311
  const ra = headers?.get ? headers.get("retry-after") : null;
242
312
  const sec = ra ? Number.parseInt(ra, 10) : NaN;
@@ -3959,7 +4029,6 @@ function withPlanLabel(obj, raw, brand) {
3959
4029
  // local-api.js): a refresh arriving while a fetch is already running reuses that
3960
4030
  // in-flight fetch and returns its result.
3961
4031
  const inFlightBySelection = {};
3962
- for (const key of USAGE_LIMITS_SELECTION_KEYS) inFlightBySelection[key] = null;
3963
4032
 
3964
4033
  // Codex stamps reset_at as unix seconds; every other provider (and Claude's
3965
4034
  // resets_at) uses ISO strings. Numbers that look like epoch milliseconds are
@@ -4001,7 +4070,9 @@ async function getUsageLimits(options = {}) {
4001
4070
  const selection = usageLimitsSelectionKey(options);
4002
4071
  const cache = cacheBySelection[selection];
4003
4072
  const nowMs = Date.now();
4004
- if (cache.data && nowMs < cache.expiresAtMs) {
4073
+ // Unseen selections (a hidden set never requested before this process's
4074
+ // lifetime) simply miss — partitions are created lazily on first fetch.
4075
+ if (cache?.data && nowMs < cache.expiresAtMs) {
4005
4076
  return cache.data;
4006
4077
  }
4007
4078
  if (inFlightBySelection[selection]) {
@@ -4037,6 +4108,11 @@ async function fetchUsageLimitsUncached({
4037
4108
  providerTimeoutMs = DEFAULT_PROVIDER_TIMEOUT_MS,
4038
4109
  forceRefresh = false,
4039
4110
  devinEnabled = false,
4111
+ // Provider visibility gate (#956): ids to skip entirely — no upstream call,
4112
+ // no credential read, no subprocess, no cache read/write. Null/empty means
4113
+ // "fetch every configured provider" (the legacy behavior clients that don't
4114
+ // send the parameter keep getting).
4115
+ hiddenProviders = null,
4040
4116
  // Generation captured by getUsageLimits before the fetch began. A provider
4041
4117
  // key save bumps it, and this fetch must then stop at returning its own
4042
4118
  // result: publishing it would restore pre-save data for every later caller.
@@ -4045,13 +4121,30 @@ async function fetchUsageLimitsUncached({
4045
4121
  // here would let an opt-out flipped mid-fetch store a pre-opt-out result in the
4046
4122
  // post-opt-out slot.
4047
4123
  selectionKey,
4124
+ // Injectable clock for tests. Production leaves it unset so per-provider
4125
+ // cache gates sample the time when they actually run (the live attempt can
4126
+ // spend the whole budget before its fallback reads the cache) instead of
4127
+ // inheriting this fetch's start instant.
4128
+ nowMs,
4048
4129
  } = {}) {
4049
- const nowMs = Date.now();
4050
-
4130
+ const injectedNowMs = Number.isFinite(nowMs) ? nowMs : undefined;
4131
+ nowMs = injectedNowMs ?? Date.now();
4132
+ const hiddenSet = new Set(normalizeHiddenProviders(hiddenProviders) || []);
4133
+
4134
+ // A hidden provider must not have its local credentials read at all (#956):
4135
+ // these upfront token/subscription probes are the first file and keychain
4136
+ // touches of the Claude/Codex chains, so they are gated here and every
4137
+ // downstream step keys off the resulting null token.
4051
4138
  const [claudeOauth, claudeSubscription, codexAuth] = await Promise.all([
4052
- Promise.resolve().then(() => readClaudeCodeOauthToken({ platform, securityRunner, home, nowMs })),
4053
- Promise.resolve().then(() => detectClaudeCodeSubscriptionDetails({ platform, securityRunner, home })),
4054
- readCodexAuthBundle({ home, env }),
4139
+ hiddenSet.has("claude")
4140
+ ? Promise.resolve(null)
4141
+ : Promise.resolve().then(() => readClaudeCodeOauthToken({ platform, securityRunner, home, nowMs })),
4142
+ hiddenSet.has("claude")
4143
+ ? Promise.resolve(null)
4144
+ : Promise.resolve().then(() => detectClaudeCodeSubscriptionDetails({ platform, securityRunner, home })),
4145
+ hiddenSet.has("codex")
4146
+ ? Promise.resolve(null)
4147
+ : readCodexAuthBundle({ home, env }),
4055
4148
  ]);
4056
4149
  const claudeToken = claudeOauth?.accessToken || null;
4057
4150
  const claudeTokenExpiresAtMs = claudeOauth?.expiresAtMs ?? null;
@@ -4107,6 +4200,15 @@ async function fetchUsageLimitsUncached({
4107
4200
  : null;
4108
4201
 
4109
4202
  const providerFetch = withFetchTimeout(fetchImpl, providerTimeoutMs);
4203
+ // What a hidden provider's Promise.all slot resolves to. Both shapes flow
4204
+ // through the same assembly branches as a genuinely unconfigured provider —
4205
+ // the CommandCode/Devin fulfilled branches keep the value verbatim, the
4206
+ // OpenCode Go branch passes `configured === false` through, Claude/Codex
4207
+ // naturally produce `{ configured: false }` from their gated null tokens —
4208
+ // so no hidden error, cache read, or credential touch can reach the payload
4209
+ // (#956).
4210
+ const hiddenSentinel = Promise.resolve({ configured: false });
4211
+ const hiddenSettled = Promise.resolve({ status: "fulfilled", value: { configured: false } });
4110
4212
  const [claudeResult, codexResult, cursor, kimi, gemini, kiro, antigravity, copilot, grok, zcode, opencodeGoRaw, qoder, qoderCn, codingPlan, agentPlan, commandCodeRaw, devinRaw, workbuddy, workbuddyAi, claudeServiceStatus] = await Promise.all([
4111
4213
  claudeToken && !freshClaudeCache && !claudeRetryAtMs
4112
4214
  ? withProviderTimeout(fetchClaudeUsageLimits(claudeToken, { fetchImpl: providerFetch, maxAttempts: 1 }), "Claude", providerTimeoutMs).then(
@@ -4124,17 +4226,17 @@ async function fetchUsageLimitsUncached({
4124
4226
  (reason) => ({ status: "rejected", reason }),
4125
4227
  )
4126
4228
  : Promise.resolve(null),
4127
- withProviderTimeout(fetchCursorLimits({ home, fetchImpl: providerFetch }), "Cursor", providerTimeoutMs)
4229
+ hiddenSet.has("cursor") ? hiddenSentinel : withProviderTimeout(fetchCursorLimits({ home, fetchImpl: providerFetch }), "Cursor", providerTimeoutMs)
4128
4230
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4129
- withProviderTimeout(fetchKimiLimits({ home, env, fetchImpl: providerFetch }), "Kimi", providerTimeoutMs)
4231
+ hiddenSet.has("kimi") ? hiddenSentinel : withProviderTimeout(fetchKimiLimits({ home, env, fetchImpl: providerFetch }), "Kimi", providerTimeoutMs)
4130
4232
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4131
- withProviderTimeout(fetchGeminiLimits({ home, env, fetchImpl: providerFetch, commandRunner }), "Gemini", providerTimeoutMs)
4233
+ hiddenSet.has("gemini") ? hiddenSentinel : withProviderTimeout(fetchGeminiLimits({ home, env, fetchImpl: providerFetch, commandRunner }), "Gemini", providerTimeoutMs)
4132
4234
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4133
- fetchKiroLimits({ commandRunner, now, platform, home }),
4235
+ hiddenSet.has("kiro") ? hiddenSentinel : fetchKiroLimits({ commandRunner, now, platform, home }),
4134
4236
  // Antigravity's own budget keeps the serial chain inside providerTimeoutMs; this
4135
4237
  // outer race is the enforcing backstop every other provider already has, and the
4136
4238
  // signal makes a fired race actually kill the spawned scans and open sockets.
4137
- withAbortableProviderTimeout(
4239
+ hiddenSet.has("antigravity") ? hiddenSentinel : withAbortableProviderTimeout(
4138
4240
  (signal) => fetchAntigravityLimits({
4139
4241
  home,
4140
4242
  commandRunner,
@@ -4150,18 +4252,18 @@ async function fetchUsageLimitsUncached({
4150
4252
  "Antigravity",
4151
4253
  providerTimeoutMs,
4152
4254
  ).catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4153
- withProviderTimeout(fetchCopilotLimits({ home, env, fetchImpl: providerFetch, platform, securityRunner }), "GitHub Copilot", providerTimeoutMs)
4255
+ hiddenSet.has("copilot") ? hiddenSentinel : withProviderTimeout(fetchCopilotLimits({ home, env, fetchImpl: providerFetch, platform, securityRunner }), "GitHub Copilot", providerTimeoutMs)
4154
4256
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4155
- withProviderTimeout(fetchGrokLimits({ home, env, fetchImpl: providerFetch, timeoutMs: providerTimeoutMs }), "Grok Build", providerTimeoutMs)
4257
+ hiddenSet.has("grok") ? hiddenSentinel : withProviderTimeout(fetchGrokLimits({ home, env, fetchImpl: providerFetch, timeoutMs: providerTimeoutMs }), "Grok Build", providerTimeoutMs)
4156
4258
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4157
- withProviderTimeout(fetchZcodeLimits({ home, env, fetchImpl: providerFetch, providerTimeoutMs }), "ZCode", providerTimeoutMs)
4259
+ hiddenSet.has("zcode") ? hiddenSentinel : withProviderTimeout(fetchZcodeLimits({ home, env, fetchImpl: providerFetch, providerTimeoutMs }), "ZCode", providerTimeoutMs)
4158
4260
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4159
4261
  // OpenCode Go: authoritative subscription windows come from the dashboard
4160
4262
  // scrape; local opencode.db cost is available only as an explicit estimate.
4161
4263
  // See src/lib/opencode-go-limits.js.
4162
- withProviderTimeout(fetchOpencodeGoLimits({ home, trackerDir, env, fetchImpl: providerFetch }), "OpenCode Go", providerTimeoutMs)
4264
+ hiddenSet.has("opencodeGo") ? hiddenSentinel : withProviderTimeout(fetchOpencodeGoLimits({ home, trackerDir, env, fetchImpl: providerFetch }), "OpenCode Go", providerTimeoutMs)
4163
4265
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4164
- withProviderTimeout(
4266
+ hiddenSet.has("qoder") ? hiddenSentinel : withProviderTimeout(
4165
4267
  fetchQoderLimits({
4166
4268
  home,
4167
4269
  env,
@@ -4171,7 +4273,7 @@ async function fetchUsageLimitsUncached({
4171
4273
  "Qoder",
4172
4274
  providerTimeoutMs,
4173
4275
  ).catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4174
- withProviderTimeout(
4276
+ hiddenSet.has("qoderCn") ? hiddenSentinel : withProviderTimeout(
4175
4277
  fetchQoderCnLimits({
4176
4278
  home,
4177
4279
  env,
@@ -4185,7 +4287,7 @@ async function fetchUsageLimitsUncached({
4185
4287
  // products sharing the same arkcli binary. No token-consumption source —
4186
4288
  // consumption for the compatible CLIs is already counted from their
4187
4289
  // local files; these only surface the 5h/week/month quota percentages.
4188
- withAbortableProviderTimeout(
4290
+ hiddenSet.has("codingPlan") ? hiddenSentinel : withAbortableProviderTimeout(
4189
4291
  (signal) => fetchArkCodingPlanLimits({
4190
4292
  commandRunner,
4191
4293
  home,
@@ -4197,7 +4299,7 @@ async function fetchUsageLimitsUncached({
4197
4299
  "Ark Coding Plan",
4198
4300
  providerTimeoutMs,
4199
4301
  ).catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4200
- withAbortableProviderTimeout(
4302
+ hiddenSet.has("agentPlan") ? hiddenSentinel : withAbortableProviderTimeout(
4201
4303
  (signal) => fetchArkAgentPlanLimits({
4202
4304
  commandRunner,
4203
4305
  home,
@@ -4217,7 +4319,7 @@ async function fetchUsageLimitsUncached({
4217
4319
  // outer race bounds the whole slot: without it three sequential per-request
4218
4320
  // timeouts (whoami, then credits+subscriptions) could hold the aggregate
4219
4321
  // ~2x longer than any sibling provider.
4220
- withProviderTimeout(fetchCommandcodeLimits({ home, trackerDir, env, fetchImpl: providerFetch }), "CommandCode", providerTimeoutMs)
4322
+ hiddenSet.has("commandCode") ? hiddenSettled : withProviderTimeout(fetchCommandcodeLimits({ home, trackerDir, env, fetchImpl: providerFetch }), "CommandCode", providerTimeoutMs)
4221
4323
  .then(
4222
4324
  (value) => ({ status: "fulfilled", value }),
4223
4325
  (reason) => ({ status: "rejected", reason }),
@@ -4227,17 +4329,19 @@ async function fetchUsageLimitsUncached({
4227
4329
  // ~/.local/share/devin/credentials.toml. No local fallback — window state
4228
4330
  // lives server-side. fetchDevinLimits throws on auth expiry so the
4229
4331
  // assemble step below can flag auth_action_required.
4230
- withProviderTimeout(fetchDevinLimits({ home, env, enabled: devinEnabled === true, fetchImpl: providerFetch }), "Devin", providerTimeoutMs)
4332
+ hiddenSet.has("devin") ? hiddenSettled : withProviderTimeout(fetchDevinLimits({ home, env, enabled: devinEnabled === true, fetchImpl: providerFetch }), "Devin", providerTimeoutMs)
4231
4333
  .then(
4232
4334
  (value) => ({ status: "fulfilled", value }),
4233
4335
  (reason) => ({ status: "rejected", reason }),
4234
4336
  ),
4235
4337
  // WorkBuddy CN and WorkBuddy AI are separate editions. Each one calls only
4236
4338
  // its own local authenticated billing proxy. Abort closes the IPC socket
4237
- // when the provider budget expires. Reward credits are not a plan window.
4238
- withAbortableProviderTimeout((signal) => fetchWorkbuddyLimits({ home, env, fetchImpl: providerFetch, signal }), "WorkBuddy", providerTimeoutMs)
4339
+ // when the provider budget expires; the post-abort grace lets the edition's
4340
+ // disk-cache fallback land instead of the bare timeout. Reward credits are
4341
+ // not a plan window.
4342
+ hiddenSet.has("workbuddy") ? hiddenSentinel : withWorkbuddyProviderTimeout((signal) => fetchWorkbuddyLimits({ home, env, fetchImpl: providerFetch, signal, nowMs: injectedNowMs }), "WorkBuddy", providerTimeoutMs)
4239
4343
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4240
- withAbortableProviderTimeout((signal) => fetchWorkbuddyAiLimits({ home, env, fetchImpl: providerFetch, signal }), "WorkBuddy AI", providerTimeoutMs)
4344
+ hiddenSet.has("workbuddyAi") ? hiddenSentinel : withWorkbuddyProviderTimeout((signal) => fetchWorkbuddyAiLimits({ home, env, fetchImpl: providerFetch, signal, nowMs: injectedNowMs }), "WorkBuddy AI", providerTimeoutMs)
4241
4345
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4242
4346
  // Public status-page probe (fail-soft, own 5-min cache in provider-status.js).
4243
4347
  // Only probed for configured accounts — without a token the Claude section
@@ -4255,8 +4359,10 @@ async function fetchUsageLimitsUncached({
4255
4359
  // that still exists means the latter — reporting `configured: false` there hides the
4256
4360
  // whole Claude section while the usage bars (parsed from local logs, no auth needed)
4257
4361
  // keep updating, which reads as "TokenTracker doesn't support my plan" rather than
4258
- // "your CLI login expired". Existence-only probe: no secret is read here.
4259
- const credentialsPresent = detectClaudeCodeCredentialsPresence({ platform, securityRunner, home });
4362
+ // "your CLI login expired". Existence-only probe: no secret is read here — and a
4363
+ // hidden Claude skips even that (#956): its row is the plain sentinel either way.
4364
+ const credentialsPresent = !hiddenSet.has("claude")
4365
+ && detectClaudeCodeCredentialsPresence({ platform, securityRunner, home });
4260
4366
  claude = credentialsPresent
4261
4367
  ? { configured: true, error: CLAUDE_AUTH_EXPIRED_MESSAGE, auth_action_required: "reauth" }
4262
4368
  : { configured: false };
@@ -4514,6 +4620,10 @@ async function fetchUsageLimitsUncached({
4514
4620
 
4515
4621
  for (const [providerName, provider] of Object.entries(data)) {
4516
4622
  if (providerName === "fetched_at" || !provider || typeof provider !== "object") continue;
4623
+ // An exact not-configured sentinel (hidden provider #956, or an opted-out
4624
+ // Devin) is metadata-free by contract — it was never fetched, so it must
4625
+ // not be aged into a "disk-cache"/stale provenance.
4626
+ if (provider.configured === false && Object.keys(provider).length === 1) continue;
4517
4627
  const capturedAt = provider.cached_at || data.fetched_at;
4518
4628
  const ageMs = Math.max(0, nowMs - Date.parse(capturedAt || ""));
4519
4629
  const stale = provider.stale === true || (Number.isFinite(ageMs) && ageMs > 10 * 60 * 1000);
@@ -4534,7 +4644,8 @@ async function fetchUsageLimitsUncached({
4534
4644
  }
4535
4645
 
4536
4646
  if (cacheGeneration === usageLimitsCacheGeneration) {
4537
- cacheBySelection[selectionKey || usageLimitsSelectionKey({ devinEnabled })] = {
4647
+ pruneSelectionPartitions(nowMs);
4648
+ cacheBySelection[selectionKey || usageLimitsSelectionKey({ devinEnabled, hiddenProviders })] = {
4538
4649
  data,
4539
4650
  expiresAtMs: cacheExpiresAtMs(data, nowMs),
4540
4651
  };
@@ -4542,20 +4653,50 @@ async function fetchUsageLimitsUncached({
4542
4653
  return data;
4543
4654
  }
4544
4655
 
4656
+ // Hidden-set keys make the partition space user-shaped and unbounded in
4657
+ // principle, while realistic sessions only ever hold a handful of selections.
4658
+ // Keep it bounded: once past the soft cap, drop expired partitions first and
4659
+ // then the oldest still-unexpired ones — they are simply refetched on demand.
4660
+ const SELECTION_PARTITION_CAP = 48;
4661
+ function pruneSelectionPartitions(nowMs) {
4662
+ const keys = Object.keys(cacheBySelection);
4663
+ if (keys.length <= SELECTION_PARTITION_CAP) return;
4664
+ const expired = [];
4665
+ const live = [];
4666
+ for (const key of keys) {
4667
+ (cacheBySelection[key].expiresAtMs <= nowMs ? expired : live).push(key);
4668
+ }
4669
+ const dropCount = keys.length - SELECTION_PARTITION_CAP;
4670
+ const drop = expired.length >= dropCount
4671
+ ? expired.slice(0, dropCount)
4672
+ : [...expired, ...live.slice(0, dropCount - expired.length)];
4673
+ for (const key of drop) delete cacheBySelection[key];
4674
+ }
4675
+
4545
4676
  function resetUsageLimitsCache({ invalidateInFlight = false } = {}) {
4546
4677
  if (invalidateInFlight) usageLimitsCacheGeneration += 1;
4547
- for (const key of USAGE_LIMITS_SELECTION_KEYS) {
4678
+ // Selection partitions are created lazily (hidden sets extend the key
4679
+ // space), so reset whatever exists instead of a fixed key list.
4680
+ for (const key of Object.keys(cacheBySelection)) {
4548
4681
  cacheBySelection[key] = { data: null, expiresAtMs: 0 };
4549
- // Ordinary resets still let a running fetch serve refresh=1 callers; a
4550
- // credential replacement must not, so drop the old-key promises too.
4551
- if (invalidateInFlight) inFlightBySelection[key] = null;
4682
+ }
4683
+ // Ordinary resets still let a running fetch serve refresh=1 callers; a
4684
+ // credential replacement must not, so drop the old-key promises too. The
4685
+ // in-flight map is walked on its own: a selection can hold a promise before
4686
+ // its partition exists (stored only when the fetch completes).
4687
+ if (invalidateInFlight) {
4688
+ for (const key of Object.keys(inFlightBySelection)) {
4689
+ delete inFlightBySelection[key];
4690
+ }
4552
4691
  }
4553
4692
  }
4554
4693
 
4555
4694
  module.exports = {
4556
4695
  getUsageLimits,
4557
4696
  normalizePlanLabel,
4697
+ normalizeHiddenProviders,
4558
4698
  resetUsageLimitsCache,
4699
+ USAGE_LIMITS_PROVIDER_IDS,
4559
4700
  OPENCODE_GO_LIMITS_CACHE_FILE,
4560
4701
  resolveOpencodeGoLimitsCachePath,
4561
4702
  cacheExpiresAtMs,
@@ -4601,4 +4742,5 @@ module.exports = {
4601
4742
  fetchQoderCnLimits,
4602
4743
  fetchWorkbuddyLimits,
4603
4744
  fetchWorkbuddyAiLimits,
4745
+ withWorkbuddyProviderTimeout,
4604
4746
  };