tokentracker-cli 1.5.3 → 1.5.4

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 (131) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-uuDGO9Mq.js → AchievementBadge-CKNb_cwg.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-BCTVT7Jo.js → AchievementsPage-u9P8DXJI.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-B6fJSJSr.js → AchievementsSection-BSCUJ4DR.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-FjUN__9G.js → BadgeDetailModal-Bg7cE3Zq.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-B-KqY_Wr.js → BotAnimated-C7D2WbQA.js} +1 -1
  6. package/dashboard/dist/assets/{Card-DS8T8piP.js → Card-B4IqAhai.js} +1 -1
  7. package/dashboard/dist/assets/{CloudCheckoutPage-_0UjsFPu.js → CloudCheckoutPage-P75pH9Ym.js} +1 -1
  8. package/dashboard/dist/assets/{CloudPage-BstgVBaV.js → CloudPage-DDEZfmZg.js} +2 -2
  9. package/dashboard/dist/assets/{CommandPalette-DtVsYZgc.js → CommandPalette-CER_AklA.js} +1 -1
  10. package/dashboard/dist/assets/{CommunityStatsModal-DTpEJqV3.js → CommunityStatsModal-Cbb74eib.js} +1 -1
  11. package/dashboard/dist/assets/{ConfirmModal-Lfdh2vr7.js → ConfirmModal-Q1ZQdZHt.js} +1 -1
  12. package/dashboard/dist/assets/{Controls-DpwQ7pkq.js → Controls-DaGOfcoz.js} +1 -1
  13. package/dashboard/dist/assets/DashboardPage-CXI7RmxO.js +1 -0
  14. package/dashboard/dist/assets/{DateRangePopover-CrWgrzT2.js → DateRangePopover-DSS1Ira_.js} +1 -1
  15. package/dashboard/dist/assets/{DevicePage-DbK5KWJS.js → DevicePage-6zPLYydh.js} +1 -1
  16. package/dashboard/dist/assets/{DialogClose-Dd2B_o03.js → DialogClose-DdCQ36LK.js} +1 -1
  17. package/dashboard/dist/assets/DialogDescription-DztHQvj-.js +1 -0
  18. package/dashboard/dist/assets/{DialogTitle-3JQBVha3.js → DialogTitle-BjP217Y1.js} +1 -1
  19. package/dashboard/dist/assets/{ElectricLogo-LYAqG8pG.js → ElectricLogo-Dhq8PM4h.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-DhDvUor7.js → FadeIn-CGNbED4E.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-9f1XmgYC.js → HeaderGithubStar-DwqEFzb6.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-BXhHAkP7.js → HoverTooltip-CQZTgyQy.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-ChPdDeMl.js → IpCheckPage-Bz09HXLn.js} +1 -1
  24. package/dashboard/dist/assets/{LandingPage-DnJ7Y9zw.js → LandingPage-DC-lZsYR.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardPage-BF9isLzu.js → LeaderboardPage-D8MpLM_t.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardProBadge-BM4JKgkC.js → LeaderboardProBadge-pdXgokhE.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-DfZABOpN.js → LeaderboardProfileModal-nHZLiE0m.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-BaZ38iT5.js → LeaderboardProfilePage-BKv3oIAW.js} +1 -1
  29. package/dashboard/dist/assets/LimitsPage-TmRTgSCb.js +6 -0
  30. package/dashboard/dist/assets/{LocalOnlyNotice-DvhlYoqQ.js → LocalOnlyNotice-aF01udyR.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-g_Gbnzhs.js → LoginCard-BaroD0Ct.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-DkJrI6b7.js → LoginModal-0jgyMGVM.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-CqCKSo0n.js → LoginPage-BUx8U58i.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-DbF7ROsj.js → PetPage-CpOjtJqg.js} +2 -2
  35. package/dashboard/dist/assets/{PopoverPopup-4WuH41ZT.js → PopoverPopup-BYjfer-M.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-6j0A2KD9.js → ProviderIcon-NtYa2h0u.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-DBd-iwuM.js → ResetPasswordPage-CYUg7TR1.js} +1 -1
  38. package/dashboard/dist/assets/{Select-BBEpLeiG.js → Select-CYP95o5y.js} +1 -1
  39. package/dashboard/dist/assets/{SelectItemText-Cn_U_rrO.js → SelectItemText-BCDfusOM.js} +1 -1
  40. package/dashboard/dist/assets/{SelfHostPage-2WZc6Rml.js → SelfHostPage-BmVc2rIT.js} +1 -1
  41. package/dashboard/dist/assets/ServiceStatusPage-Dyp8NwIn.js +1 -0
  42. package/dashboard/dist/assets/SessionsPage-tGEj6Ztz.js +3 -0
  43. package/dashboard/dist/assets/{SettingsPage-rXNr96i-.js → SettingsPage-CCMbq5wc.js} +3 -3
  44. package/dashboard/dist/assets/{SkillsPage-CudTvbZL.js → SkillsPage-C4O4fbaQ.js} +1 -1
  45. package/dashboard/dist/assets/{TokenGalaxy-BRKrbTsJ.js → TokenGalaxy-D2d738xr.js} +1 -1
  46. package/dashboard/dist/assets/{TooltipPopup-CsLKXhWC.js → TooltipPopup-BJfFYesG.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-8onUTbkk.js → TrendMonitor-CElakFSU.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-CcwE0lz8.js → WidgetsPage-Cvd1LbJm.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-Cj9LLYqP.js → WrappedPage-wRC1tE_a.js} +1 -1
  50. package/dashboard/dist/assets/__vite-browser-external-CMBWGkOq.css +1 -0
  51. package/dashboard/dist/assets/__vite-browser-external-V4TPLCJG.js +45 -0
  52. package/dashboard/dist/assets/{agent-logos-ChBA1nAH.js → agent-logos-Ba57HF3w.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-left-Bb5mfoPc.js → arrow-left-B1bKnxXL.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-right-D1Ti2XUI.js → arrow-right-DrOlKzX5.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-B_CjA_y8.js → arrow-up-right-hlYpo3yw.js} +1 -1
  56. package/dashboard/dist/assets/{calendar-days-DdfrAqie.js → calendar-days-CSY6SEsI.js} +1 -1
  57. package/dashboard/dist/assets/{check-29zGw1MK.js → check-xrP1gLw5.js} +1 -1
  58. package/dashboard/dist/assets/{chevron-down-DAzlFX7g.js → chevron-down-6-zT5n9d.js} +1 -1
  59. package/dashboard/dist/assets/{cloud-checkout-C2SeCjFM.js → cloud-checkout-BZymZD4K.js} +1 -1
  60. package/dashboard/dist/assets/{composite-DIf735Hq.js → composite-C9izWafa.js} +1 -1
  61. package/dashboard/dist/assets/{copy-BOuOM0HQ.js → copy-Dkas5wZX.js} +1 -1
  62. package/dashboard/dist/assets/{download-50tQRpGe.js → download-CagGRs0_.js} +1 -1
  63. package/dashboard/dist/assets/{external-link-BhlQClVe.js → external-link-DRl10hxd.js} +1 -1
  64. package/dashboard/dist/assets/{flame-CeGfA-0L.js → flame-CkvmsTFB.js} +1 -1
  65. package/dashboard/dist/assets/{format-tokens-Cq7YiAkJ.js → format-tokens-BtwhO3mV.js} +1 -1
  66. package/dashboard/dist/assets/{gift-Do-UFhiT.js → gift-YzjXkMQx.js} +1 -1
  67. package/dashboard/dist/assets/{icons-C6EZ_HmP.js → icons-CHhKYs0u.js} +1 -1
  68. package/dashboard/dist/assets/{index-BWhbBAd9.js → index-B0MQZTrM.js} +1 -1
  69. package/dashboard/dist/assets/{index-D9kwYWLX.js → index-C2DK8av-.js} +1 -1
  70. package/dashboard/dist/assets/{index-BFrbVqsm.js → index-CJknN9Jd.js} +1 -1
  71. package/dashboard/dist/assets/{index-c0crpe9Z.js → index-DojzoMqd.js} +1 -1
  72. package/dashboard/dist/assets/{info-B4MXmsNE.js → info-Torep-Ti.js} +1 -1
  73. package/dashboard/dist/assets/{link-2-DuWCslzl.js → link-2-DE5Yu-z6.js} +1 -1
  74. package/dashboard/dist/assets/{loader-circle-C2I3NZAO.js → loader-circle-BL8LxD9n.js} +1 -1
  75. package/dashboard/dist/assets/{local-usage-transfer-BQasvsUF.js → local-usage-transfer-8X1InWix.js} +1 -1
  76. package/dashboard/dist/assets/{modifiers.esm-DbeNS6FT.js → modifiers.esm-D60-6UQa.js} +1 -1
  77. package/dashboard/dist/assets/{monitor-smartphone-C6Y4ktsc.js → monitor-smartphone-C-IFPjqk.js} +1 -1
  78. package/dashboard/dist/assets/{play-CwgcDJC2.js → play-n6uWrdH9.js} +1 -1
  79. package/dashboard/dist/assets/{plus-B796wsUO.js → plus-B2f6TaNj.js} +1 -1
  80. package/dashboard/dist/assets/{provider-display-Cw26RFDm.js → provider-display-DWqCEcsP.js} +1 -1
  81. package/dashboard/dist/assets/{react-d7Bk1-_K.js → react-B-zfi_j6.js} +1 -1
  82. package/dashboard/dist/assets/{safePolygon-CDE0dNv_.js → safePolygon-C2PsvI-v.js} +1 -1
  83. package/dashboard/dist/assets/{search-DtKvsdRs.js → search-DZOaTLF9.js} +1 -1
  84. package/dashboard/dist/assets/{server-gAURHUyz.js → server-DIp4Qfsv.js} +1 -1
  85. package/dashboard/dist/assets/{shield-check-DFVdDTwh.js → shield-check-Cs2M8-iT.js} +1 -1
  86. package/dashboard/dist/assets/{skills-api-2rdPUrJI.js → skills-api-CoYy6Tov.js} +1 -1
  87. package/dashboard/dist/assets/{sortable.esm-DFlfwnJr.js → sortable.esm-B5_hjIfs.js} +1 -1
  88. package/dashboard/dist/assets/{store-BKPCk_de.js → store-BLXqtJzb.js} +1 -1
  89. package/dashboard/dist/assets/{terminal-BtWFakvS.js → terminal-CEMKX3ba.js} +1 -1
  90. package/dashboard/dist/assets/{trash-bpr9zTAM.js → trash-DOWYaXjK.js} +1 -1
  91. package/dashboard/dist/assets/{use-community-stats-DMnGgLne.js → use-community-stats-BjRI1Bzz.js} +1 -1
  92. package/dashboard/dist/assets/{use-latest-request-guard-BSBlJJHe.js → use-latest-request-guard-T7L5qBU6.js} +1 -1
  93. package/dashboard/dist/assets/use-limits-display-prefs-vNIrCPZA.js +1 -0
  94. package/dashboard/dist/assets/{use-native-settings-OthqiF6g.js → use-native-settings-Cg5Zb4q_.js} +1 -1
  95. package/dashboard/dist/assets/{use-ordered-list-DpAtr0R-.js → use-ordered-list-BF-wIvmG.js} +1 -1
  96. package/dashboard/dist/assets/use-reduced-motion-B9K5mIx4.js +1 -0
  97. package/dashboard/dist/assets/{use-transform-BRz9dz_2.js → use-transform-CbJ1IcNc.js} +1 -1
  98. package/dashboard/dist/assets/{use-usage-limits-CwmhudAX.js → use-usage-limits-Dgg3jpC4.js} +1 -1
  99. package/dashboard/dist/assets/{useCurrency-CAA-tQ27.js → useCurrency-3oBHWfSe.js} +1 -1
  100. package/dashboard/dist/assets/{usePositioner-Cj2DPmwM.js → usePositioner-D0YrIW0A.js} +1 -1
  101. package/dashboard/dist/assets/{useTokenFormat-Bf5-f7Hx.js → useTokenFormat-DZAHWE_R.js} +1 -1
  102. package/dashboard/dist/assets/{zap-B8Fg6PaC.js → zap-kLub6Jiv.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 +3 -2
  109. package/src/lib/accept-encoding.js +59 -0
  110. package/src/lib/cursor-config.js +3 -1
  111. package/src/lib/deepseek-limits.js +280 -0
  112. package/src/lib/grok-limits.js +40 -1
  113. package/src/lib/local-api.js +229 -31
  114. package/src/lib/opencode-status.js +88 -0
  115. package/src/lib/pricing/index.js +182 -79
  116. package/src/lib/pricing/seed-snapshot.json +1 -1
  117. package/src/lib/provider-api-key-store.js +2 -0
  118. package/src/lib/provider-status.js +17 -5
  119. package/src/lib/session-analytics.js +194 -29
  120. package/src/lib/static-server.js +74 -2
  121. package/src/lib/usage-limits.js +535 -16
  122. package/src/lib/workbuddy-limits.js +106 -14
  123. package/dashboard/dist/assets/DashboardPage-DUFWejkE.js +0 -1
  124. package/dashboard/dist/assets/DialogDescription-CLy0arbt.js +0 -1
  125. package/dashboard/dist/assets/LimitsPage-C7LbG3LX.js +0 -6
  126. package/dashboard/dist/assets/ServiceStatusPage-B-g4MDl0.js +0 -1
  127. package/dashboard/dist/assets/SessionsPage-CWS0iQ1L.js +0 -3
  128. package/dashboard/dist/assets/__vite-browser-external-07djSGmy.js +0 -45
  129. package/dashboard/dist/assets/__vite-browser-external-BDldtsD2.css +0 -1
  130. package/dashboard/dist/assets/use-limits-display-prefs-eYlvnUKn.js +0 -1
  131. package/dashboard/dist/assets/use-reduced-motion-DF0grwHs.js +0 -1
@@ -27,7 +27,7 @@ const {
27
27
  fetchCursorSandAccessStatus,
28
28
  fetchCursorSandUsageStatus,
29
29
  } = require("./cursor-config");
30
- const { fetchGrokLimits } = require("./grok-limits");
30
+ const { fetchGrokLimits, grokTokenSubject, loadGrokAuthEntry } = require("./grok-limits");
31
31
  const { fetchZcodeLimits } = require("./zcode-limits");
32
32
  const { fetchOpencodeGoLimits } = require("./opencode-go-limits");
33
33
  const { fetchCommandcodeLimits } = require("./commandcode-limits");
@@ -36,6 +36,7 @@ const { fetchQoderLimits, fetchQoderCnLimits } = require("./qoder-limits");
36
36
  const { fetchArkCodingPlanLimits } = require("./ark-coding-plan-limits");
37
37
  const { fetchArkAgentPlanLimits } = require("./ark-agent-plan-limits");
38
38
  const { fetchWorkbuddyLimits, fetchWorkbuddyAiLimits } = require("./workbuddy-limits");
39
+ const { fetchDeepseekLimits } = require("./deepseek-limits");
39
40
  const { fetchProviderServiceStatus } = require("./provider-status");
40
41
  const { resolveKimiProfile } = require("./kimi-profile");
41
42
  const { readSqliteJsonRows, readSqliteJsonRowsAsync } = require("./sqlite-reader");
@@ -70,7 +71,7 @@ const USAGE_LIMITS_PROVIDER_IDS = Object.freeze([
70
71
  "claude", "codex", "cursor", "gemini", "kimi", "kiro", "grok",
71
72
  "copilot", "antigravity", "zcode", "opencodeGo", "commandCode",
72
73
  "qoder", "qoderCn", "codingPlan", "agentPlan", "devin",
73
- "workbuddy", "workbuddyAi",
74
+ "workbuddy", "workbuddyAi", "deepseek",
74
75
  ]);
75
76
 
76
77
  // Normalize a caller-supplied hidden-provider list (array, Set, or
@@ -172,6 +173,24 @@ const CODEX_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
172
173
  const OPENCODE_GO_LIMITS_CACHE_FILE = "opencode-go-usage-limits-cache.json";
173
174
  const OPENCODE_GO_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
174
175
  const OPENCODE_GO_LOCAL_ESTIMATE_CACHE_MAX_AGE_MS = 60 * 60 * 1000;
176
+ // Cursor's web summary (cursor.com) and Grok Bot RPCs (api2.cursor.sh) both traverse
177
+ // proxied routes where a timeout or 5xx previously blanked the card until the next
178
+ // poll. Persist the last successful windows so a blip serves stale bars instead
179
+ // (mirrors Claude / Codex / OpenCode Go).
180
+ const CURSOR_LIMITS_CACHE_FILE = "cursor-usage-limits-cache.json";
181
+ const CURSOR_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
182
+ // The Kimi coding API (api.kimi.com / api.kimi.ai) is another proxied route where
183
+ // a timeout or 5xx previously blanked the card until the next poll. Persist the
184
+ // last successful windows so a blip serves stale bars instead
185
+ // (mirrors Cursor / Codex / OpenCode Go).
186
+ const KIMI_LIMITS_CACHE_FILE = "kimi-usage-limits-cache.json";
187
+ const KIMI_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
188
+ // Grok Build's billing proxy (cli-chat-proxy.grok.com) sits behind the same kind of
189
+ // proxied routes where a timeout or 5xx previously blanked the card until the next
190
+ // poll. Persist the last successful windows so a blip serves stale bars instead
191
+ // (mirrors Claude / Codex / Cursor).
192
+ const GROK_LIMITS_CACHE_FILE = "grok-usage-limits-cache.json";
193
+ const GROK_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
175
194
  // A 429 from the usage endpoint carries a long `retry-after` (often 20+ minutes). Persist
176
195
  // the cooldown so every surface — this process, the menu bar app's embedded server, a later
177
196
  // restart — stops calling until it expires. Hammering during the cooldown just renews the
@@ -860,11 +879,11 @@ function normalizeCursorSandUsageStatus(body, { eligible = true } = {}) {
860
879
  return buildWindow({ usedPercent, resetAt, windowSeconds });
861
880
  }
862
881
 
863
- async function fetchCursorLimits({ home, fetchImpl = fetch } = {}) {
864
- if (!isCursorInstalled({ home })) {
882
+ async function fetchCursorLimits({ home, env, fetchImpl = fetch } = {}) {
883
+ if (!isCursorInstalled({ home, env })) {
865
884
  return { configured: false };
866
885
  }
867
- const auth = extractCursorAuth({ home });
886
+ const auth = extractCursorAuth({ home, env });
868
887
  if (!auth?.cookie || !auth?.accessToken) {
869
888
  return { configured: false };
870
889
  }
@@ -887,11 +906,18 @@ async function fetchCursorLimits({ home, fetchImpl = fetch } = {}) {
887
906
  error: null,
888
907
  ...normalizeCursorUsageSummary(body),
889
908
  quaternary_window: grokBotWindow,
909
+ // Internal cache-partition key (stripped before the payload is served).
910
+ cache_identity: auth.userId,
890
911
  };
891
912
  } catch (error) {
892
913
  return {
893
914
  configured: true,
894
915
  error: error?.message || "Unknown error",
916
+ // A session-expiry error is actionable (re-login) and must not be hidden
917
+ // behind the stale disk-cache fallback (opencode-go auth_error precedent).
918
+ // Internal marker — stripped before the payload is served.
919
+ ...(error?.code === "AUTH_EXPIRED" ? { auth_error: true } : {}),
920
+ cache_identity: auth.userId,
895
921
  };
896
922
  }
897
923
  }
@@ -964,7 +990,9 @@ function kimiCredentialsExpired(creds, nowMs = Date.now()) {
964
990
 
965
991
  async function refreshKimiAccessToken({ refreshToken, profile, fetchImpl = fetch } = {}) {
966
992
  if (typeof refreshToken !== "string" || !refreshToken.trim()) {
967
- throw new Error("Not logged in to Kimi. Run 'kimi' in Terminal to authenticate.");
993
+ const error = new Error("Not logged in to Kimi. Run 'kimi' in Terminal to authenticate.");
994
+ error.code = "AUTH_EXPIRED";
995
+ throw error;
968
996
  }
969
997
 
970
998
  const body = new URLSearchParams({
@@ -983,7 +1011,9 @@ async function refreshKimiAccessToken({ refreshToken, profile, fetchImpl = fetch
983
1011
  body,
984
1012
  });
985
1013
  if (res.status === 401 || res.status === 403) {
986
- throw new Error("Not logged in to Kimi. Run 'kimi' in Terminal to authenticate.");
1014
+ const error = new Error("Not logged in to Kimi. Run 'kimi' in Terminal to authenticate.");
1015
+ error.code = "AUTH_EXPIRED";
1016
+ throw error;
987
1017
  }
988
1018
  if (!res.ok) {
989
1019
  throw new Error(`Kimi token refresh failed (HTTP ${res.status})`);
@@ -1017,7 +1047,11 @@ async function fetchKimiUsage(accessToken, { profile, fetchImpl = fetch } = {})
1017
1047
  },
1018
1048
  });
1019
1049
  if (res.status === 401) {
1020
- throw new Error("token_expired");
1050
+ // The retry-with-refresh path keys off this message; the code marks the
1051
+ // error actionable (re-login) so the cache fallback never hides it.
1052
+ const error = new Error("token_expired");
1053
+ error.code = "AUTH_EXPIRED";
1054
+ throw error;
1021
1055
  }
1022
1056
  if (!res.ok) {
1023
1057
  throw new Error(`Kimi API returned ${res.status}`);
@@ -1030,12 +1064,17 @@ async function fetchKimiLimits({ home, env, fetchImpl = fetch } = {}) {
1030
1064
  if (!profile.configured) {
1031
1065
  return { configured: false };
1032
1066
  }
1033
- if (profile.error) return { configured: true, error: profile.error };
1067
+ // A profile misconfiguration (unsupported endpoints, non-file credential
1068
+ // storage) is actionable — the user must fix their config — so it must not
1069
+ // be hidden behind the stale disk-cache fallback. Internal marker —
1070
+ // stripped before the payload is served.
1071
+ if (profile.error) return { configured: true, error: profile.error, config_error: true };
1034
1072
  const creds = loadKimiCredentials({ profile });
1035
1073
  let accessToken = typeof creds?.access_token === "string" ? creds.access_token.trim() : "";
1036
1074
  if (!accessToken) {
1037
1075
  return { configured: false };
1038
1076
  }
1077
+ const cacheIdentity = kimiProfileIdentity(profile);
1039
1078
  try {
1040
1079
  if (kimiCredentialsExpired(creds) && creds?.refresh_token) {
1041
1080
  accessToken = await refreshKimiAccessToken({
@@ -1063,11 +1102,18 @@ async function fetchKimiLimits({ home, env, fetchImpl = fetch } = {}) {
1063
1102
  configured: true,
1064
1103
  error: null,
1065
1104
  ...normalizeKimiUsageResponse(body),
1105
+ // Internal cache-partition key (stripped before the payload is served).
1106
+ cache_identity: cacheIdentity,
1066
1107
  };
1067
1108
  } catch (error) {
1068
1109
  return {
1069
1110
  configured: true,
1070
1111
  error: error?.message || "Unknown error",
1112
+ // A session-expiry error is actionable (re-login) and must not be hidden
1113
+ // behind the stale disk-cache fallback (opencode-go auth_error precedent).
1114
+ // Internal marker — stripped before the payload is served.
1115
+ ...(error?.code === "AUTH_EXPIRED" ? { auth_error: true } : {}),
1116
+ cache_identity: cacheIdentity,
1071
1117
  };
1072
1118
  }
1073
1119
  }
@@ -2767,6 +2813,323 @@ function writeOpencodeGoLimitsCache(limits, { trackerDir, home, nowMs = Date.now
2767
2813
  } catch (_error) {}
2768
2814
  }
2769
2815
 
2816
+ function resolveCursorLimitsCachePath({ home } = {}) {
2817
+ return path.join(home || os.homedir(), ".tokentracker", "tracker", CURSOR_LIMITS_CACHE_FILE);
2818
+ }
2819
+
2820
+ function hasCursorWindow(limits) {
2821
+ return Boolean(
2822
+ limits?.primary_window
2823
+ || limits?.secondary_window
2824
+ || limits?.tertiary_window
2825
+ || limits?.quaternary_window,
2826
+ );
2827
+ }
2828
+
2829
+ // Cursor windows carry `reset_at` as an ISO string (billing-cycle end for the three
2830
+ // summary windows, the RPC's own expiry for the Grok Bot window). A window whose reset
2831
+ // has already passed is stale data, not a usable fallback, so drop it. Windows without
2832
+ // a reset stamp are kept — the overall cached_at max-age gate already bounds them.
2833
+ function isCursorCacheWindowUsable(window, { nowMs } = {}) {
2834
+ if (!window || typeof window !== "object") return false;
2835
+ const resetAtMs = parseTimeMs(window.reset_at);
2836
+ if (resetAtMs === null) return true;
2837
+ return resetAtMs > nowMs;
2838
+ }
2839
+
2840
+ function normalizeCursorCachedLimits(
2841
+ raw,
2842
+ { nowMs = Date.now(), maxAgeMs = CURSOR_LIMITS_CACHE_MAX_AGE_MS } = {},
2843
+ ) {
2844
+ const cachedAtMs = parseTimeMs(raw?.cached_at);
2845
+ if (!Number.isFinite(cachedAtMs)) return null;
2846
+ if (cachedAtMs > nowMs + 60_000) return null;
2847
+ if (nowMs - cachedAtMs > maxAgeMs) return null;
2848
+
2849
+ const cached = {
2850
+ configured: true,
2851
+ error: null,
2852
+ membership_type: typeof raw?.membership_type === "string" ? raw.membership_type : null,
2853
+ primary_window: isCursorCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
2854
+ secondary_window: isCursorCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
2855
+ tertiary_window: isCursorCacheWindowUsable(raw?.tertiary_window, { nowMs }) ? raw.tertiary_window : null,
2856
+ quaternary_window: isCursorCacheWindowUsable(raw?.quaternary_window, { nowMs }) ? raw.quaternary_window : null,
2857
+ stale: true,
2858
+ cached_at: raw.cached_at,
2859
+ };
2860
+ return hasCursorWindow(cached) ? cached : null;
2861
+ }
2862
+
2863
+ // The signed-in Cursor account partitions the cache (zcode login-identity precedent):
2864
+ // the id is the non-secret normalized subject Cursor itself puts in the session cookie.
2865
+ // A single home holds one active login, so one stamped entry suffices; a read requires
2866
+ // the stored identity to match the current one, and a write without an identity is
2867
+ // refused — an unreadable login must never pin another account's bars in place.
2868
+ function readCursorLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
2869
+ if (!identity) return null;
2870
+ try {
2871
+ const parsed = JSON.parse(fs.readFileSync(resolveCursorLimitsCachePath({ home }), "utf8"));
2872
+ const entry = parsed?.cursor;
2873
+ if (entry?.identity !== identity) return null;
2874
+ return normalizeCursorCachedLimits(entry, { nowMs });
2875
+ } catch (_error) {
2876
+ return null;
2877
+ }
2878
+ }
2879
+
2880
+ function writeCursorLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
2881
+ if (!identity) return;
2882
+ if (!limits?.configured || limits.error || !hasCursorWindow(limits)) return;
2883
+ const cachePath = resolveCursorLimitsCachePath({ home });
2884
+ const payload = {
2885
+ cursor: {
2886
+ identity,
2887
+ membership_type: limits.membership_type || null,
2888
+ primary_window: limits.primary_window || null,
2889
+ secondary_window: limits.secondary_window || null,
2890
+ tertiary_window: limits.tertiary_window || null,
2891
+ quaternary_window: limits.quaternary_window || null,
2892
+ cached_at: new Date(nowMs).toISOString(),
2893
+ },
2894
+ };
2895
+ try {
2896
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
2897
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
2898
+ fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
2899
+ fs.renameSync(tmpPath, cachePath);
2900
+ } catch (_error) {}
2901
+ }
2902
+
2903
+ // Failure-only identity re-derivation for the cache fallback: when the provider slot's
2904
+ // result was produced by the outer timeout race, the fetcher's own identity field is
2905
+ // lost, so read the (still-local) state DB once to learn who we are serving for.
2906
+ function resolveCursorCacheIdentity({ home, env } = {}) {
2907
+ try {
2908
+ return extractCursorAuth({ home, env })?.userId || null;
2909
+ } catch (_error) {
2910
+ return null;
2911
+ }
2912
+ }
2913
+
2914
+ // The active Kimi profile partitions the cache (ark profile_identity precedent):
2915
+ // mainland and global logins — and any KIMI_HOME override — resolve to different
2916
+ // endpoint-scoped credential slots, and one slot must never serve another's
2917
+ // windows. The profile home plus OAuth host is non-secret and derivable from
2918
+ // local files alone (the credential file holds no account id).
2919
+ function kimiProfileIdentity(profile) {
2920
+ if (!profile?.home || !profile?.oauthHost) return null;
2921
+ return `${profile.home}::${profile.oauthHost}`;
2922
+ }
2923
+
2924
+ // Failure-only identity re-derivation for the cache fallback: when the provider
2925
+ // slot's result was produced by the outer timeout race, the fetcher's own
2926
+ // identity field is lost, so re-resolve the (still-local) Kimi profile.
2927
+ function resolveKimiCacheIdentity({ home, env } = {}) {
2928
+ try {
2929
+ return kimiProfileIdentity(resolveKimiProfile({ home, env }));
2930
+ } catch (_error) {
2931
+ return null;
2932
+ }
2933
+ }
2934
+
2935
+ function resolveKimiLimitsCachePath({ home } = {}) {
2936
+ return path.join(home || os.homedir(), ".tokentracker", "tracker", KIMI_LIMITS_CACHE_FILE);
2937
+ }
2938
+
2939
+ function hasKimiWindow(limits) {
2940
+ return Boolean(
2941
+ limits?.primary_window
2942
+ || limits?.secondary_window
2943
+ || limits?.tertiary_window,
2944
+ );
2945
+ }
2946
+
2947
+ // Kimi windows carry `reset_at` (ISO) when the API reports a cycle end; a window
2948
+ // whose reset has already passed is stale data, not a usable fallback, so drop
2949
+ // it. Windows without a reset stamp are kept — the overall cached_at max-age
2950
+ // gate already bounds them.
2951
+ function isKimiCacheWindowUsable(window, { nowMs } = {}) {
2952
+ if (!window || typeof window !== "object") return false;
2953
+ const resetAtMs = parseTimeMs(window.reset_at);
2954
+ if (resetAtMs === null) return true;
2955
+ return resetAtMs > nowMs;
2956
+ }
2957
+
2958
+ function normalizeKimiCachedLimits(
2959
+ raw,
2960
+ { nowMs = Date.now(), maxAgeMs = KIMI_LIMITS_CACHE_MAX_AGE_MS } = {},
2961
+ ) {
2962
+ const cachedAtMs = parseTimeMs(raw?.cached_at);
2963
+ if (!Number.isFinite(cachedAtMs)) return null;
2964
+ if (cachedAtMs > nowMs + 60_000) return null;
2965
+ if (nowMs - cachedAtMs > maxAgeMs) return null;
2966
+
2967
+ const cached = {
2968
+ configured: true,
2969
+ error: null,
2970
+ membership_level: typeof raw?.membership_level === "string" ? raw.membership_level : null,
2971
+ subscription_type: typeof raw?.subscription_type === "string" ? raw.subscription_type : null,
2972
+ parallel_limit: Number.isFinite(raw?.parallel_limit) ? raw.parallel_limit : null,
2973
+ primary_window: isKimiCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
2974
+ secondary_window: isKimiCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
2975
+ tertiary_window: isKimiCacheWindowUsable(raw?.tertiary_window, { nowMs }) ? raw.tertiary_window : null,
2976
+ stale: true,
2977
+ cached_at: raw.cached_at,
2978
+ };
2979
+ return hasKimiWindow(cached) ? cached : null;
2980
+ }
2981
+
2982
+ function readKimiLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
2983
+ if (!identity) return null;
2984
+ try {
2985
+ const parsed = JSON.parse(fs.readFileSync(resolveKimiLimitsCachePath({ home }), "utf8"));
2986
+ const entry = parsed?.kimi;
2987
+ if (entry?.identity !== identity) return null;
2988
+ return normalizeKimiCachedLimits(entry, { nowMs });
2989
+ } catch (_error) {
2990
+ return null;
2991
+ }
2992
+ }
2993
+
2994
+ function writeKimiLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
2995
+ if (!identity) return;
2996
+ if (!limits?.configured || limits.error || !hasKimiWindow(limits)) return;
2997
+ const cachePath = resolveKimiLimitsCachePath({ home });
2998
+ const payload = {
2999
+ kimi: {
3000
+ identity,
3001
+ membership_level: limits.membership_level || null,
3002
+ subscription_type: limits.subscription_type || null,
3003
+ parallel_limit: Number.isFinite(limits.parallel_limit) ? limits.parallel_limit : null,
3004
+ primary_window: limits.primary_window || null,
3005
+ secondary_window: limits.secondary_window || null,
3006
+ tertiary_window: limits.tertiary_window || null,
3007
+ cached_at: new Date(nowMs).toISOString(),
3008
+ },
3009
+ };
3010
+ try {
3011
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
3012
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
3013
+ fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
3014
+ fs.renameSync(tmpPath, cachePath);
3015
+ } catch (_error) {}
3016
+ }
3017
+
3018
+ function resolveGrokLimitsCachePath({ home } = {}) {
3019
+ return path.join(home || os.homedir(), ".tokentracker", "tracker", GROK_LIMITS_CACHE_FILE);
3020
+ }
3021
+
3022
+ function hasGrokWindow(limits) {
3023
+ return Boolean(limits?.primary_window || limits?.secondary_window);
3024
+ }
3025
+
3026
+ // Grok windows carry `reset_at` as an ISO string — the billing-period end shared
3027
+ // by the pool, on-demand, and per-product windows. A window whose reset has
3028
+ // already passed is stale data, not a usable fallback, so drop it. Windows
3029
+ // without a reset stamp are kept — the overall cached_at max-age gate already
3030
+ // bounds them (mirrors Cursor).
3031
+ function isGrokCacheWindowUsable(window, { nowMs } = {}) {
3032
+ if (!window || typeof window !== "object") return false;
3033
+ const resetAtMs = parseTimeMs(window.reset_at);
3034
+ if (resetAtMs === null) return true;
3035
+ return resetAtMs > nowMs;
3036
+ }
3037
+
3038
+ function normalizeGrokCachedLimits(
3039
+ raw,
3040
+ { nowMs = Date.now(), maxAgeMs = GROK_LIMITS_CACHE_MAX_AGE_MS } = {},
3041
+ ) {
3042
+ const cachedAtMs = parseTimeMs(raw?.cached_at);
3043
+ if (!Number.isFinite(cachedAtMs)) return null;
3044
+ if (cachedAtMs > nowMs + 60_000) return null;
3045
+ if (nowMs - cachedAtMs > maxAgeMs) return null;
3046
+
3047
+ const cached = {
3048
+ configured: true,
3049
+ error: null,
3050
+ plan_label: typeof raw?.plan_label === "string" ? raw.plan_label : null,
3051
+ period_type: typeof raw?.period_type === "string" ? raw.period_type : null,
3052
+ monthly_credits_limit: raw?.monthly_credits_limit ?? null,
3053
+ monthly_credits_used: raw?.monthly_credits_used ?? null,
3054
+ credit_usage_percent: raw?.credit_usage_percent ?? null,
3055
+ on_demand_cap: raw?.on_demand_cap ?? null,
3056
+ on_demand_used: raw?.on_demand_used ?? null,
3057
+ billing_period_start: typeof raw?.billing_period_start === "string" ? raw.billing_period_start : null,
3058
+ primary_window: isGrokCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
3059
+ secondary_window: isGrokCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
3060
+ // Per-product shares of the pool, not independent windows — each is gated
3061
+ // by the same reset stamp as the pool window it attributes.
3062
+ products: Array.isArray(raw?.products)
3063
+ ? raw.products.filter((entry) => isGrokCacheWindowUsable(entry?.window, { nowMs }))
3064
+ : [],
3065
+ stale: true,
3066
+ cached_at: raw.cached_at,
3067
+ };
3068
+ return hasGrokWindow(cached) ? cached : null;
3069
+ }
3070
+
3071
+ // The signed-in Grok account partitions the cache (zcode login-identity precedent):
3072
+ // the id is the non-secret OIDC `sub` claim inside the access token — the
3073
+ // credential itself never enters the id (no token fingerprint fallback: CodeQL
3074
+ // js/insufficient-password-hash precedent). auth.x.ai issues JWT access tokens
3075
+ // (RFC 9068), so the subject is present in practice; an opaque token yields no
3076
+ // identity and the cache is simply refused on both the write and the read path
3077
+ // (fail-closed) — an unattributable login must never pin another account's bars,
3078
+ // which a scope-key fallback cannot guarantee because `issuer::client_id` stays
3079
+ // constant across account switches.
3080
+ function readGrokLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
3081
+ if (!identity) return null;
3082
+ try {
3083
+ const parsed = JSON.parse(fs.readFileSync(resolveGrokLimitsCachePath({ home }), "utf8"));
3084
+ const entry = parsed?.grok;
3085
+ if (entry?.identity !== identity) return null;
3086
+ return normalizeGrokCachedLimits(entry, { nowMs });
3087
+ } catch (_error) {
3088
+ return null;
3089
+ }
3090
+ }
3091
+
3092
+ function writeGrokLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
3093
+ if (!identity) return;
3094
+ if (!limits?.configured || limits.error || !hasGrokWindow(limits)) return;
3095
+ const cachePath = resolveGrokLimitsCachePath({ home });
3096
+ const payload = {
3097
+ grok: {
3098
+ identity,
3099
+ plan_label: limits.plan_label || null,
3100
+ period_type: limits.period_type || null,
3101
+ monthly_credits_limit: limits.monthly_credits_limit ?? null,
3102
+ monthly_credits_used: limits.monthly_credits_used ?? null,
3103
+ credit_usage_percent: limits.credit_usage_percent ?? null,
3104
+ on_demand_cap: limits.on_demand_cap ?? null,
3105
+ on_demand_used: limits.on_demand_used ?? null,
3106
+ billing_period_start: limits.billing_period_start || null,
3107
+ primary_window: limits.primary_window || null,
3108
+ secondary_window: limits.secondary_window || null,
3109
+ products: Array.isArray(limits.products) ? limits.products : [],
3110
+ cached_at: new Date(nowMs).toISOString(),
3111
+ },
3112
+ };
3113
+ try {
3114
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
3115
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
3116
+ fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
3117
+ fs.renameSync(tmpPath, cachePath);
3118
+ } catch (_error) {}
3119
+ }
3120
+
3121
+ // Failure-only identity re-derivation for the cache fallback: when the provider
3122
+ // slot's result was produced by the outer timeout race, the fetcher's own
3123
+ // identity field is lost, so read auth.json once to learn who we are serving for.
3124
+ // Opaque tokens yield null → the read is refused (fail-closed, see above).
3125
+ function resolveGrokCacheIdentity({ home, env } = {}) {
3126
+ try {
3127
+ return grokTokenSubject(loadGrokAuthEntry({ home, env })?.entry?.key);
3128
+ } catch (_error) {
3129
+ return null;
3130
+ }
3131
+ }
3132
+
2770
3133
  function resolveClaudeRateLimitPath({ home } = {}) {
2771
3134
  return path.join(home || os.homedir(), ".tokentracker", "tracker", CLAUDE_RATE_LIMIT_FILE);
2772
3135
  }
@@ -4209,7 +4572,7 @@ async function fetchUsageLimitsUncached({
4209
4572
  // (#956).
4210
4573
  const hiddenSentinel = Promise.resolve({ configured: false });
4211
4574
  const hiddenSettled = Promise.resolve({ status: "fulfilled", value: { configured: false } });
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([
4575
+ const [claudeResult, codexResult, cursor, kimi, gemini, kiro, antigravity, copilot, grok, zcode, opencodeGoRaw, qoder, qoderCn, codingPlan, agentPlan, commandCodeRaw, devinRaw, workbuddy, workbuddyAi, deepseek, claudeServiceStatus] = await Promise.all([
4213
4576
  claudeToken && !freshClaudeCache && !claudeRetryAtMs
4214
4577
  ? withProviderTimeout(fetchClaudeUsageLimits(claudeToken, { fetchImpl: providerFetch, maxAttempts: 1 }), "Claude", providerTimeoutMs).then(
4215
4578
  (value) => ({ status: "fulfilled", value }),
@@ -4226,7 +4589,7 @@ async function fetchUsageLimitsUncached({
4226
4589
  (reason) => ({ status: "rejected", reason }),
4227
4590
  )
4228
4591
  : Promise.resolve(null),
4229
- hiddenSet.has("cursor") ? hiddenSentinel : withProviderTimeout(fetchCursorLimits({ home, fetchImpl: providerFetch }), "Cursor", providerTimeoutMs)
4592
+ hiddenSet.has("cursor") ? hiddenSentinel : withProviderTimeout(fetchCursorLimits({ home, env, fetchImpl: providerFetch }), "Cursor", providerTimeoutMs)
4230
4593
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4231
4594
  hiddenSet.has("kimi") ? hiddenSentinel : withProviderTimeout(fetchKimiLimits({ home, env, fetchImpl: providerFetch }), "Kimi", providerTimeoutMs)
4232
4595
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
@@ -4343,6 +4706,25 @@ async function fetchUsageLimitsUncached({
4343
4706
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4344
4707
  hiddenSet.has("workbuddyAi") ? hiddenSentinel : withWorkbuddyProviderTimeout((signal) => fetchWorkbuddyAiLimits({ home, env, fetchImpl: providerFetch, signal, nowMs: injectedNowMs }), "WorkBuddy AI", providerTimeoutMs)
4345
4708
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4709
+ // DeepSeek (platform.deepseek.com, pay-as-you-go): the balance endpoint is
4710
+ // the only documented programmatic source, so this slot yields a balance
4711
+ // card, not quota windows (#960). The last-good disk-cache fallback with a
4712
+ // pure TTL and a key fingerprint binding lives inside deepseek-limits.js;
4713
+ // auth errors bypass it so a rejected key stays actionable. persistCache
4714
+ // is sampled at slot creation — a mid-fetch key save can only leave a
4715
+ // cache entry bound to the OLD key's fingerprint, which a fetch under the
4716
+ // new key will never read.
4717
+ hiddenSet.has("deepseek") ? hiddenSentinel : withProviderTimeout(
4718
+ fetchDeepseekLimits({
4719
+ home,
4720
+ trackerDir,
4721
+ env,
4722
+ fetchImpl: providerFetch,
4723
+ persistCache: cacheGeneration === usageLimitsCacheGeneration,
4724
+ }),
4725
+ "DeepSeek",
4726
+ providerTimeoutMs,
4727
+ ).catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4346
4728
  // Public status-page probe (fail-soft, own 5-min cache in provider-status.js).
4347
4729
  // Only probed for configured accounts — without a token the Claude section
4348
4730
  // never renders, so the reading would have nowhere to go.
@@ -4573,9 +4955,142 @@ async function fetchUsageLimitsUncached({
4573
4955
  : { configured: true, error: reason?.message || "Unknown error" };
4574
4956
  }
4575
4957
 
4576
- const cursorLimits = cursorSandDuplicatesGrokPool(cursor, grok)
4577
- ? { ...cursor, quaternary_window: null }
4578
- : cursor;
4958
+ // Cursor: persist last successful windows so a transient timeout/5xx on the
4959
+ // cursor.com / api2.cursor.sh routes serves stale bars instead of a flashing
4960
+ // "fetch failed" (mirrors Codex / Claude / OpenCode Go). A session-expiry error
4961
+ // (401/403 — "re-login in Cursor") is actionable and must not be hidden behind
4962
+ // stale cache, so it passes through untouched.
4963
+ let cursorWithCache;
4964
+ if (!cursor || cursor.configured === false) {
4965
+ cursorWithCache = cursor;
4966
+ } else if (cursor.auth_error === true) {
4967
+ cursorWithCache = cursor;
4968
+ } else if (!cursor.error && hasCursorWindow(cursor)) {
4969
+ cursorWithCache = {
4970
+ ...cursor,
4971
+ stale: false,
4972
+ cached_at: new Date(nowMs).toISOString(),
4973
+ };
4974
+ if (cacheGeneration === usageLimitsCacheGeneration) {
4975
+ writeCursorLimitsCache(cursorWithCache, {
4976
+ home,
4977
+ nowMs,
4978
+ identity: typeof cursor.cache_identity === "string"
4979
+ ? cursor.cache_identity
4980
+ : resolveCursorCacheIdentity({ home, env }),
4981
+ });
4982
+ }
4983
+ } else {
4984
+ // The slot's error object loses the fetcher's identity field when the outer
4985
+ // timeout race wins (the common failure mode this cache exists for), so
4986
+ // re-derive it from the local state DB on this failure-only path.
4987
+ const identity = typeof cursor.cache_identity === "string"
4988
+ ? cursor.cache_identity
4989
+ : resolveCursorCacheIdentity({ home, env });
4990
+ const cached = readCursorLimitsCache({ home, nowMs, identity });
4991
+ cursorWithCache = cached || cursor;
4992
+ }
4993
+ // cache_identity / auth_error are internal cache-plumbing markers, not payload.
4994
+ if (cursorWithCache && ("cache_identity" in cursorWithCache || "auth_error" in cursorWithCache)) {
4995
+ const { cache_identity: _identity, auth_error: _authError, ...cursorPayload } = cursorWithCache;
4996
+ cursorWithCache = cursorPayload;
4997
+ }
4998
+
4999
+ // Grok: persist last successful windows so a transient timeout/5xx on the
5000
+ // cli-chat-proxy.grok.com route serves stale bars instead of a flashing
5001
+ // "fetch failed" (mirrors Codex / Claude / Cursor). A re-login error
5002
+ // (GROK_AUTH_REQUIRED / GROK_REAUTH_REQUIRED — "run `grok login`") is
5003
+ // actionable and must not be hidden behind stale cache, so it passes
5004
+ // through untouched.
5005
+ let grokWithCache;
5006
+ if (!grok || grok.configured === false) {
5007
+ grokWithCache = grok;
5008
+ } else if (grok.auth_error === true) {
5009
+ grokWithCache = grok;
5010
+ } else if (!grok.error && hasGrokWindow(grok)) {
5011
+ grokWithCache = {
5012
+ ...grok,
5013
+ stale: false,
5014
+ cached_at: new Date(nowMs).toISOString(),
5015
+ };
5016
+ // Write only under the identity the fetch itself carried: re-reading
5017
+ // auth.json here could attribute a finished fetch's windows to an account
5018
+ // that logged in while the request was in flight (opaque → JWT switch).
5019
+ // No fetch-side identity (opaque token) → refuse the write (fail-closed).
5020
+ if (cacheGeneration === usageLimitsCacheGeneration && typeof grok.cache_identity === "string") {
5021
+ writeGrokLimitsCache(grokWithCache, {
5022
+ home,
5023
+ nowMs,
5024
+ identity: grok.cache_identity,
5025
+ });
5026
+ }
5027
+ } else {
5028
+ // The slot's error object loses the fetcher's identity field when the outer
5029
+ // timeout race wins (the common failure mode this cache exists for), so
5030
+ // re-derive it from auth.json on this failure-only path.
5031
+ const identity = typeof grok.cache_identity === "string"
5032
+ ? grok.cache_identity
5033
+ : resolveGrokCacheIdentity({ home, env });
5034
+ const cached = readGrokLimitsCache({ home, nowMs, identity });
5035
+ grokWithCache = cached || grok;
5036
+ }
5037
+ // cache_identity / auth_error are internal cache-plumbing markers, not payload.
5038
+ if (grokWithCache && ("cache_identity" in grokWithCache || "auth_error" in grokWithCache)) {
5039
+ const { cache_identity: _identity, auth_error: _authError, ...grokPayload } = grokWithCache;
5040
+ grokWithCache = grokPayload;
5041
+ }
5042
+
5043
+ const cursorLimits = cursorSandDuplicatesGrokPool(cursorWithCache, grokWithCache)
5044
+ ? { ...cursorWithCache, quaternary_window: null }
5045
+ : cursorWithCache;
5046
+
5047
+ // Kimi: persist last successful windows so a transient timeout/5xx on the
5048
+ // api.kimi.com / api.kimi.ai coding routes serves stale bars instead of a
5049
+ // flashing "fetch failed" (mirrors Cursor / Codex / OpenCode Go). Re-login
5050
+ // errors ("Not logged in to Kimi" / "token_expired") and actionable profile
5051
+ // misconfigurations must not be hidden behind stale cache, so they pass
5052
+ // through untouched.
5053
+ let kimiWithCache;
5054
+ if (!kimi || kimi.configured === false) {
5055
+ kimiWithCache = kimi;
5056
+ } else if (kimi.auth_error === true || kimi.config_error === true) {
5057
+ kimiWithCache = kimi;
5058
+ } else if (!kimi.error && hasKimiWindow(kimi)) {
5059
+ kimiWithCache = {
5060
+ ...kimi,
5061
+ stale: false,
5062
+ cached_at: new Date(nowMs).toISOString(),
5063
+ };
5064
+ if (cacheGeneration === usageLimitsCacheGeneration) {
5065
+ writeKimiLimitsCache(kimiWithCache, {
5066
+ home,
5067
+ nowMs,
5068
+ identity: typeof kimi.cache_identity === "string"
5069
+ ? kimi.cache_identity
5070
+ : resolveKimiCacheIdentity({ home, env }),
5071
+ });
5072
+ }
5073
+ } else {
5074
+ // The slot's error object loses the fetcher's identity field when the outer
5075
+ // timeout race wins (the common failure mode this cache exists for), so
5076
+ // re-derive it from the local Kimi profile on this failure-only path.
5077
+ const identity = typeof kimi.cache_identity === "string"
5078
+ ? kimi.cache_identity
5079
+ : resolveKimiCacheIdentity({ home, env });
5080
+ const cached = readKimiLimitsCache({ home, nowMs, identity });
5081
+ kimiWithCache = cached || kimi;
5082
+ }
5083
+ // cache_identity / auth_error / config_error are internal cache-plumbing
5084
+ // markers, not payload.
5085
+ if (kimiWithCache && ("cache_identity" in kimiWithCache || "auth_error" in kimiWithCache || "config_error" in kimiWithCache)) {
5086
+ const {
5087
+ cache_identity: _kimiIdentity,
5088
+ auth_error: _kimiAuthError,
5089
+ config_error: _kimiConfigError,
5090
+ ...kimiPayload
5091
+ } = kimiWithCache;
5092
+ kimiWithCache = kimiPayload;
5093
+ }
4579
5094
 
4580
5095
  const data = {
4581
5096
  fetched_at: new Date(nowMs).toISOString(),
@@ -4586,7 +5101,7 @@ async function fetchUsageLimitsUncached({
4586
5101
  // tier, and membership.level is an opaque enum (LEVEL_INTERMEDIATE) with no
4587
5102
  // authoritative human-readable plan mapping. Both rendered as garbage like
4588
5103
  // "Kimi Type_event" (issue #130), so show the bare brand instead.
4589
- kimi: withPlanLabel(kimi, null, "Kimi"),
5104
+ kimi: withPlanLabel(kimiWithCache, null, "Kimi"),
4590
5105
  gemini: withPlanLabel(gemini, gemini.account_plan, "Gemini"),
4591
5106
  kiro: withPlanLabel(kiro, kiro.plan_name, "Kiro"),
4592
5107
  antigravity: withPlanLabel(antigravity, antigravity.account_plan, "Antigravity"),
@@ -4597,7 +5112,7 @@ async function fetchUsageLimitsUncached({
4597
5112
  // outright, so surface the label as-is like CommandCode below. Error and
4598
5113
  // unconfigured branches never carry plan_label, so the panel still falls
4599
5114
  // back to the bare brand name.
4600
- grok,
5115
+ grok: grokWithCache,
4601
5116
  zcode: withPlanLabel(zcode, zcode.plan_label, "ZCode"),
4602
5117
  opencodeGo: withPlanLabel(opencodeGo, opencodeGo?.plan_label, "OpenCode Go"),
4603
5118
  // CommandCode tiers are acronym brands (GOAT/Pro/Max) — the fetcher already
@@ -4616,6 +5131,10 @@ async function fetchUsageLimitsUncached({
4616
5131
  // as separate providers — one row per signed-in edition, never merged.
4617
5132
  workbuddy,
4618
5133
  workbuddyAi,
5134
+ // DeepSeek carries a balance card (no windows, no plan label): the fetcher
5135
+ // stamps cached_at/stale on both its live and last-good paths, so pass it
5136
+ // through verbatim like grok.
5137
+ deepseek,
4619
5138
  };
4620
5139
 
4621
5140
  for (const [providerName, provider] of Object.entries(data)) {