tokentracker-cli 0.96.1 → 0.96.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 (145) hide show
  1. package/README.de.md +34 -25
  2. package/README.ja.md +38 -27
  3. package/README.ko.md +39 -28
  4. package/README.md +36 -28
  5. package/README.zh-CN.md +36 -25
  6. package/dashboard/dist/assets/{AchievementBadge-DKyElm0T.js → AchievementBadge-BX4iBBYM.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-BvCx3wND.js → AchievementsPage-DRYRuCbh.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-DN69LzXX.js → AchievementsSection-BM_JWWhJ.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-DGiKdZfz.js → BadgeDetailModal-DxoxXst2.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-zN8rH7_K.js → BotAnimated-BVpFxG8u.js} +1 -1
  11. package/dashboard/dist/assets/{Card-COWo-ClV.js → Card-BJJs701t.js} +1 -1
  12. package/dashboard/dist/assets/{ClawdAnimated-BWsB7Eat.js → ClawdAnimated-46wPZ_Tc.js} +2 -2
  13. package/dashboard/dist/assets/{CommandPalette-BfvIwuoJ.js → CommandPalette-B_zkc7IU.js} +1 -1
  14. package/dashboard/dist/assets/{CommunityStatsModal-BQWg7R_y.js → CommunityStatsModal-M2k5rIf1.js} +1 -1
  15. package/dashboard/dist/assets/{ConfirmModal-C7ABSI-O.js → ConfirmModal-DBvOTcAQ.js} +1 -1
  16. package/dashboard/dist/assets/{Controls-vMzEZzej.js → Controls-BCkjPMSH.js} +1 -1
  17. package/dashboard/dist/assets/DashboardPage-3bHhou09.js +1 -0
  18. package/dashboard/dist/assets/{DevicePage-3ejUBLWM.js → DevicePage-DFBUce8C.js} +1 -1
  19. package/dashboard/dist/assets/DialogClose-kvDd8h3V.js +1 -0
  20. package/dashboard/dist/assets/DialogDescription-BMyOicfz.js +1 -0
  21. package/dashboard/dist/assets/DialogTitle-DL5nvD31.js +1 -0
  22. package/dashboard/dist/assets/{FadeIn-D7Jcfz1N.js → FadeIn-B-Bg6SEy.js} +1 -1
  23. package/dashboard/dist/assets/{HeaderGithubStar-D--Rx5BS.js → HeaderGithubStar-BExz0hh4.js} +1 -1
  24. package/dashboard/dist/assets/{HoverTooltip-n1W0z3Wr.js → HoverTooltip-Dj5zemYT.js} +1 -1
  25. package/dashboard/dist/assets/{IpCheckPage-D202tl9B.js → IpCheckPage-DN7FuJ_v.js} +1 -1
  26. package/dashboard/dist/assets/{LandingPage-D6yL2EZo.js → LandingPage-9dKgSBRy.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardAvatar-ChFg2mtA.js → LeaderboardAvatar-00V95XIg.js} +1 -1
  28. package/dashboard/dist/assets/{LeaderboardPage-BXHwGs8r.js → LeaderboardPage-DkKfUt0Z.js} +2 -2
  29. package/dashboard/dist/assets/{LeaderboardProfileModal-B5LoOpCk.js → LeaderboardProfileModal-C9sIHBgN.js} +4 -4
  30. package/dashboard/dist/assets/{LeaderboardProfilePage-jsukhRz8.js → LeaderboardProfilePage-BAiLMsEb.js} +1 -1
  31. package/dashboard/dist/assets/LimitsPage-BuenTaHa.js +8 -0
  32. package/dashboard/dist/assets/{LocalOnlyNotice-Du0lz657.js → LocalOnlyNotice-CKhY27Lr.js} +1 -1
  33. package/dashboard/dist/assets/{LoginCard-BFEX9x7-.js → LoginCard-BbvoBx5N.js} +1 -1
  34. package/dashboard/dist/assets/{LoginModal-DqC7_d35.js → LoginModal-P7zIkBhO.js} +1 -1
  35. package/dashboard/dist/assets/{LoginPage-Dzx8N7HE.js → LoginPage-Bo0bR26x.js} +1 -1
  36. package/dashboard/dist/assets/PetPage-C6i9bSSl.js +1 -0
  37. package/dashboard/dist/assets/PopoverPopup-h0gf36vg.js +1 -0
  38. package/dashboard/dist/assets/ProviderIcon-Dm8fwPdj.js +1 -0
  39. package/dashboard/dist/assets/{ResetPasswordPage-x_GR79sR.js → ResetPasswordPage-DvSJ8kg0.js} +1 -1
  40. package/dashboard/dist/assets/{SegmentedControl-BxDiDV4R.js → SegmentedControl-CP7oiGZE.js} +1 -1
  41. package/dashboard/dist/assets/Select-iwP92x9C.js +1 -0
  42. package/dashboard/dist/assets/SelectItemText-DzljA15_.js +1 -0
  43. package/dashboard/dist/assets/{ServiceStatusPage-BVQuVdjg.js → ServiceStatusPage-uD4D0Tid.js} +1 -1
  44. package/dashboard/dist/assets/{SessionsPage-DE39wNIO.js → SessionsPage-Cmb1ssNP.js} +2 -2
  45. package/dashboard/dist/assets/SettingsPage-D8ryg1Pv.js +2 -0
  46. package/dashboard/dist/assets/SkillsPage-eXI09q03.js +1 -0
  47. package/dashboard/dist/assets/{TokenGalaxy-BPM779Wb.js → TokenGalaxy-f2djrlUv.js} +1 -1
  48. package/dashboard/dist/assets/ToolbarRootContext-CDxCPYLv.js +1 -0
  49. package/dashboard/dist/assets/{TrendMonitor-BkNy-o1D.js → TrendMonitor-C419X22G.js} +1 -1
  50. package/dashboard/dist/assets/{WidgetsPage-Bpg6H-ZB.js → WidgetsPage-AYq__pOO.js} +1 -1
  51. package/dashboard/dist/assets/{WrappedPage-C6-NM-RO.js → WrappedPage-Bsm9k5XI.js} +1 -1
  52. package/dashboard/dist/assets/{__vite-browser-external-CvMMIjhi.css → __vite-browser-external-5P7me0Qq.css} +1 -1
  53. package/dashboard/dist/assets/__vite-browser-external-BDkeh90N.js +45 -0
  54. package/dashboard/dist/assets/{agent-logos-DpbkRqXT.js → agent-logos-B6Vg-4_1.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-BjOtWUVs.js → arrow-up-right-DIQXwaOD.js} +1 -1
  56. package/dashboard/dist/assets/{check-BlxnX5jZ.js → check-CgD_bV39.js} +1 -1
  57. package/dashboard/dist/assets/composite-BIgEqWZD.js +1 -0
  58. package/dashboard/dist/assets/{copy-CIsWjLY5.js → copy-DlM1BHdp.js} +1 -1
  59. package/dashboard/dist/assets/{download-QDaQbQdd.js → download-CQFG-VIY.js} +1 -1
  60. package/dashboard/dist/assets/{external-link-DPV4VU3m.js → external-link-BQyDqGxz.js} +1 -1
  61. package/dashboard/dist/assets/{flame-B_X-7VNp.js → flame-BamzyXzF.js} +1 -1
  62. package/dashboard/dist/assets/{format-tokens-DEx2TJj8.js → format-tokens-C4G-_xYt.js} +1 -1
  63. package/dashboard/dist/assets/{icons-983RyGSl.js → icons-Ntscvl1a.js} +1 -1
  64. package/dashboard/dist/assets/{index-qtonw_x4.js → index-CUrcPqgp.js} +1 -1
  65. package/dashboard/dist/assets/{index-BONYA8HZ.js → index-DGxgsNoQ.js} +1 -1
  66. package/dashboard/dist/assets/{index-Cjs5stC0.js → index-FRIt5mlv.js} +1 -1
  67. package/dashboard/dist/assets/{info-DpVOhXZI.js → info-CXytWcZi.js} +1 -1
  68. package/dashboard/dist/assets/{link-2-CBro-upz.js → link-2-Dexoo_jO.js} +1 -1
  69. package/dashboard/dist/assets/{loader-circle-wYFdSyOd.js → loader-circle-Cn0JHc8t.js} +1 -1
  70. package/dashboard/dist/assets/{maximize-2-DhTyQ4ly.js → maximize-2-DLh7gYuP.js} +1 -1
  71. package/dashboard/dist/assets/{modifiers.esm-CPljwVhN.js → modifiers.esm-DOh_xtyt.js} +1 -1
  72. package/dashboard/dist/assets/module-DUSXqJtA.js +3 -0
  73. package/dashboard/dist/assets/{monitor-smartphone-D_wr-HOR.js → monitor-smartphone-CpqS9Ior.js} +1 -1
  74. package/dashboard/dist/assets/{plus-BNjfkBEF.js → plus-bfQV7nyv.js} +1 -1
  75. package/dashboard/dist/assets/provider-display-Bf9pW06U.js +42 -0
  76. package/dashboard/dist/assets/{react-yHv9MTUA.js → react-BmFZKA_b.js} +1 -1
  77. package/dashboard/dist/assets/{search-Dfak92ZA.js → search-l3jJ2fzC.js} +1 -1
  78. package/dashboard/dist/assets/{skills-api-By2PdNa1.js → skills-api-CWmLnaxn.js} +1 -1
  79. package/dashboard/dist/assets/{sortable.esm-B9xy66_v.js → sortable.esm-DLgyXLHM.js} +1 -1
  80. package/dashboard/dist/assets/store-JM-a8ElZ.js +1 -0
  81. package/dashboard/dist/assets/{terminal-pbXWBmZM.js → terminal-6aY2Sn8P.js} +1 -1
  82. package/dashboard/dist/assets/{trash-2-CboDPwbW.js → trash-D5oon5pT.js} +1 -1
  83. package/dashboard/dist/assets/{use-community-stats-D6HTdnHq.js → use-community-stats-ZWUMFwzc.js} +1 -1
  84. package/dashboard/dist/assets/{use-latest-request-guard-hwSr9Cam.js → use-latest-request-guard-BDaXr4gK.js} +1 -1
  85. package/dashboard/dist/assets/use-limits-display-prefs-yXXToNIc.js +1 -0
  86. package/dashboard/dist/assets/{use-ordered-list-OtfOqrNb.js → use-ordered-list-C6RG8N6c.js} +1 -1
  87. package/dashboard/dist/assets/use-reduced-motion-BqDKim_3.js +1 -0
  88. package/dashboard/dist/assets/{use-session-efficiency-pref-BScvtS8F.js → use-session-efficiency-pref-DpgZO_Lx.js} +1 -1
  89. package/dashboard/dist/assets/{use-transform-Cy1ykjEs.js → use-transform-D42P7jkd.js} +1 -1
  90. package/dashboard/dist/assets/use-usage-limits-CV6puFwq.js +1 -0
  91. package/dashboard/dist/assets/{useCurrency-VqF3cy1C.js → useCurrency-GWBmOrCR.js} +1 -1
  92. package/dashboard/dist/assets/{useTokenFormat-Cgub4MXu.js → useTokenFormat-Beybkb0v.js} +1 -1
  93. package/dashboard/dist/assets/{zap-DStzvQJI.js → zap-CEq2616W.js} +1 -1
  94. package/dashboard/dist/brand-logos/acode-dark.png +0 -0
  95. package/dashboard/dist/brand-logos/acode.png +0 -0
  96. package/dashboard/dist/brand-logos/commandcode.svg +1 -0
  97. package/dashboard/dist/brand-logos/devin.svg +6 -0
  98. package/dashboard/dist/index.html +20 -16
  99. package/dashboard/dist/ip-check.html +2 -2
  100. package/dashboard/dist/leaderboard.html +2 -2
  101. package/dashboard/dist/llms.txt +4 -2
  102. package/dashboard/dist/share.html +2 -2
  103. package/package.json +2 -2
  104. package/src/commands/init.js +114 -4
  105. package/src/commands/status.js +37 -0
  106. package/src/commands/sync.js +92 -10
  107. package/src/commands/uninstall.js +23 -0
  108. package/src/lib/codex-config.js +42 -0
  109. package/src/lib/codex-rollout-parser.js +2 -2
  110. package/src/lib/commandcode-limits.js +317 -0
  111. package/src/lib/devin-limits.js +249 -0
  112. package/src/lib/devin-usage.js +212 -0
  113. package/src/lib/local-api.js +14 -0
  114. package/src/lib/passive-mode.js +13 -1
  115. package/src/lib/pricing/curated-overrides.json +78 -1
  116. package/src/lib/pricing/index.js +11 -9
  117. package/src/lib/pricing/matcher.js +28 -0
  118. package/src/lib/pricing/seed-snapshot.json +1 -1
  119. package/src/lib/rollout.js +1473 -147
  120. package/src/lib/session-analytics.js +2 -2
  121. package/src/lib/skills-manager.js +5 -0
  122. package/src/lib/subscriptions.js +70 -16
  123. package/src/lib/usage-limits.js +240 -49
  124. package/dashboard/dist/assets/DashboardPage-QBhbCMZF.js +0 -1
  125. package/dashboard/dist/assets/DialogClose-D_dvtfGV.js +0 -1
  126. package/dashboard/dist/assets/DialogDescription-ryGYdQjX.js +0 -1
  127. package/dashboard/dist/assets/DialogTitle-_ahf0z8b.js +0 -1
  128. package/dashboard/dist/assets/LimitsPage-PvqkOOwp.js +0 -6
  129. package/dashboard/dist/assets/PetPage-CNlWHF4z.js +0 -1
  130. package/dashboard/dist/assets/PopoverPopup-D9jadD3j.js +0 -1
  131. package/dashboard/dist/assets/ProviderIcon-Caz5PuAm.js +0 -1
  132. package/dashboard/dist/assets/Select-B5o-1sEl.js +0 -1
  133. package/dashboard/dist/assets/SelectItemText-CiA6L7Wk.js +0 -1
  134. package/dashboard/dist/assets/SettingsPage-DJrb_fsK.js +0 -2
  135. package/dashboard/dist/assets/SkillsPage-COk-YQQ5.js +0 -1
  136. package/dashboard/dist/assets/ToolbarRootContext-CozJT6Vs.js +0 -1
  137. package/dashboard/dist/assets/__vite-browser-external-BGnq8Ebm.js +0 -45
  138. package/dashboard/dist/assets/composite-Dxmivlk4.js +0 -1
  139. package/dashboard/dist/assets/limits-providers-BUoquSA-.js +0 -1
  140. package/dashboard/dist/assets/module-xpSFyNBK.js +0 -3
  141. package/dashboard/dist/assets/provider-display-BOBtUyFi.js +0 -42
  142. package/dashboard/dist/assets/store-oQZR4d9M.js +0 -1
  143. package/dashboard/dist/assets/use-limits-display-prefs-B-pbwUS8.js +0 -1
  144. package/dashboard/dist/assets/use-reduced-motion-DGXcZBX3.js +0 -1
  145. package/dashboard/dist/assets/use-usage-limits-CSMb2zYW.js +0 -1
@@ -11,7 +11,7 @@ const { promisify } = require("node:util");
11
11
  const {
12
12
  detectClaudeCodeCredentialsPresence,
13
13
  detectClaudeCodeSubscriptionDetails,
14
- readClaudeCodeAccessToken,
14
+ readClaudeCodeOauthToken,
15
15
  readCodexAccessToken,
16
16
  readCodexAuthBundle,
17
17
  } = require("./subscriptions");
@@ -30,6 +30,8 @@ const {
30
30
  const { fetchGrokLimits } = require("./grok-limits");
31
31
  const { fetchZcodeLimits } = require("./zcode-limits");
32
32
  const { fetchOpencodeGoLimits } = require("./opencode-go-limits");
33
+ const { fetchCommandcodeLimits } = require("./commandcode-limits");
34
+ const { fetchDevinLimits } = require("./devin-limits");
33
35
  const { fetchQoderLimits, fetchQoderCnLimits } = require("./qoder-limits");
34
36
  const { fetchArkCodingPlanLimits } = require("./ark-coding-plan-limits");
35
37
  const { fetchArkAgentPlanLimits } = require("./ark-agent-plan-limits");
@@ -46,7 +48,15 @@ const execFileAsync = promisify(cp.execFile);
46
48
  // 2-minute in-memory cache. It also expires early at the earliest upcoming window
47
49
  // reset in the cached data (see cacheExpiresAtMs), floored so a provider reporting
48
50
  // a reset "right now" can't turn every poll into a full upstream round.
49
- let cache = { data: null, expiresAtMs: 0 };
51
+ // Partitioned by the Devin opt-in selection so a request made while Devin is
52
+ // off is never served (or joined onto) a response fetched while it was on.
53
+ const cacheByDevinSelection = {
54
+ off: { data: null, expiresAtMs: 0 },
55
+ on: { data: null, expiresAtMs: 0 },
56
+ };
57
+ function devinSelectionKey(options) {
58
+ return options?.devinEnabled === true ? "on" : "off";
59
+ }
50
60
  const CACHE_TTL_MS = 2 * 60 * 1000;
51
61
  // Must stay below the macOS app's post-reset re-fetch grace (10s in
52
62
  // DashboardViewModel.resetBoundaryGrace), or that targeted refresh would be
@@ -56,10 +66,10 @@ const DEFAULT_PROVIDER_TIMEOUT_MS = 15_000;
56
66
  const ANTIGRAVITY_LIMITS_CACHE_FILE = "usage-limits-cache.json";
57
67
  const ANTIGRAVITY_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
58
68
  const ANTIGRAVITY_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
59
- // Public installed-app OAuth client used by Antigravity / agy (same id PokeTokenBar
60
- // and the agy binary embed). Not confidential — installed-app clients cannot keep a
61
- // secret. Used to refresh the on-disk Google token so quota can be read like
62
- // Claude/Codex without the IDE process running.
69
+ // Same client id PokeTokenBar and the agy binary embed. This client requires a
70
+ // client_secret; without it a refresh is rejected as 400 invalid_request, so
71
+ // remote renewal is unavailable. After expiry, quota depends on a local
72
+ // Antigravity/agy process or the user signing in again.
63
73
  const ANTIGRAVITY_OAUTH_CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
64
74
  const ANTIGRAVITY_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
65
75
  const ANTIGRAVITY_LOAD_CODE_ASSIST_URL = "https://daily-cloudcode-pa.googleapis.com/v1internal:loadCodeAssist";
@@ -2573,22 +2583,44 @@ function resolveClaudeRateLimitPath({ home } = {}) {
2573
2583
  return path.join(home || os.homedir(), ".tokentracker", "tracker", CLAUDE_RATE_LIMIT_FILE);
2574
2584
  }
2575
2585
 
2586
+ function claudeTokenExpiryStamp(tokenExpiresAtMs) {
2587
+ return Number.isFinite(tokenExpiresAtMs)
2588
+ ? new Date(tokenExpiresAtMs).toISOString()
2589
+ : null;
2590
+ }
2591
+
2576
2592
  // Returns the cooldown expiry in ms if a 429 cooldown is still active, else null.
2577
- function readClaudeRateLimitRetryAtMs({ home, nowMs = Date.now() } = {}) {
2593
+ // A cooldown armed for a previous access token must not outlive that token: after
2594
+ // the user refreshes the Claude Code login, forceRefresh still cannot punch through
2595
+ // this file, so a cooldown stamped with a different token expiry is discarded.
2596
+ // The token's own expiry identifies the credential without persisting anything
2597
+ // derived from the secret.
2598
+ function readClaudeRateLimitRetryAtMs({ home, nowMs = Date.now(), tokenExpiresAtMs } = {}) {
2599
+ const cachePath = resolveClaudeRateLimitPath({ home });
2578
2600
  try {
2579
- const parsed = JSON.parse(fs.readFileSync(resolveClaudeRateLimitPath({ home }), "utf8"));
2601
+ const parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
2580
2602
  const retryAtMs = parseTimeMs(parsed?.retry_at);
2581
- if (retryAtMs !== null && retryAtMs > nowMs) return retryAtMs;
2603
+ if (retryAtMs === null || retryAtMs <= nowMs) return null;
2604
+ const stampedExpiry = typeof parsed.token_expires_at === "string"
2605
+ ? parsed.token_expires_at
2606
+ : null;
2607
+ if (stampedExpiry && stampedExpiry !== claudeTokenExpiryStamp(tokenExpiresAtMs)) {
2608
+ clearClaudeRateLimitCooldown({ home });
2609
+ return null;
2610
+ }
2611
+ return retryAtMs;
2582
2612
  } catch (_error) {}
2583
2613
  return null;
2584
2614
  }
2585
2615
 
2586
- function writeClaudeRateLimitCooldown(retryAfterSec, { home, nowMs = Date.now() } = {}) {
2616
+ function writeClaudeRateLimitCooldown(retryAfterSec, { home, nowMs = Date.now(), tokenExpiresAtMs } = {}) {
2587
2617
  const sec = Number.isFinite(retryAfterSec) && retryAfterSec > 0
2588
2618
  ? Math.min(retryAfterSec, CLAUDE_RATE_LIMIT_MAX_COOLDOWN_SEC)
2589
2619
  : CLAUDE_RATE_LIMIT_DEFAULT_COOLDOWN_SEC;
2590
2620
  const cachePath = resolveClaudeRateLimitPath({ home });
2591
2621
  const payload = { retry_at: new Date(nowMs + sec * 1000).toISOString() };
2622
+ const expiryStamp = claudeTokenExpiryStamp(tokenExpiresAtMs);
2623
+ if (expiryStamp) payload.token_expires_at = expiryStamp;
2592
2624
  try {
2593
2625
  fs.mkdirSync(path.dirname(cachePath), { recursive: true });
2594
2626
  const tmpPath = `${cachePath}.${process.pid}.tmp`;
@@ -3127,18 +3159,41 @@ function parseAntigravityCredentialPayload(raw) {
3127
3159
  };
3128
3160
  }
3129
3161
 
3130
- function loadAntigravityCredentialsFromFiles({ home } = {}) {
3162
+ function isAntigravityCredentialFresh(creds, nowMs) {
3163
+ return creds.expiryMs != null && creds.expiryMs > nowMs + ANTIGRAVITY_TOKEN_REFRESH_SKEW_MS;
3164
+ }
3165
+
3166
+ function pickLatestAntigravityExpiry(candidates) {
3167
+ let best = candidates[0];
3168
+ for (let i = 1; i < candidates.length; i += 1) {
3169
+ const expiry = candidates[i].expiryMs;
3170
+ if (expiry != null && (best.expiryMs == null || expiry > best.expiryMs)) {
3171
+ best = candidates[i];
3172
+ }
3173
+ }
3174
+ return best;
3175
+ }
3176
+
3177
+ function pickAntigravityCredentials(candidates, nowMs) {
3178
+ if (candidates.length === 0) return null;
3179
+ const fresh = candidates.filter((creds) => isAntigravityCredentialFresh(creds, nowMs));
3180
+ if (fresh.length > 0) return pickLatestAntigravityExpiry(fresh);
3181
+ const unknown = candidates.filter((creds) => creds.expiryMs == null);
3182
+ if (unknown.length > 0) return unknown[0];
3183
+ return pickLatestAntigravityExpiry(candidates);
3184
+ }
3185
+
3186
+ function collectAntigravityFileCredentials({ home } = {}) {
3187
+ const candidates = [];
3131
3188
  for (const credPath of listAntigravityCredentialPaths(home)) {
3132
3189
  try {
3133
3190
  const parsed = parseAntigravityCredentialPayload(fs.readFileSync(credPath, "utf8"));
3134
- if (parsed) {
3135
- return { ...parsed, source: "file", path: credPath };
3136
- }
3191
+ if (parsed) candidates.push({ ...parsed, source: "file", path: credPath });
3137
3192
  } catch {
3138
3193
  // missing or unreadable
3139
3194
  }
3140
3195
  }
3141
- return null;
3196
+ return candidates;
3142
3197
  }
3143
3198
 
3144
3199
  function readAntigravityKeychainRaw({ securityRunner, timeoutMs = 2000 } = {}) {
@@ -3169,14 +3224,18 @@ function readAntigravityKeychainRaw({ securityRunner, timeoutMs = 2000 } = {}) {
3169
3224
  }
3170
3225
  }
3171
3226
 
3172
- function loadAntigravityCredentials({ home, platform = process.platform, securityRunner } = {}) {
3173
- const fromFile = loadAntigravityCredentialsFromFiles({ home });
3174
- if (fromFile) return fromFile;
3175
- if (platform !== "darwin" && typeof securityRunner !== "function") return null;
3176
- const raw = readAntigravityKeychainRaw({ securityRunner });
3177
- const parsed = parseAntigravityCredentialPayload(raw);
3178
- if (!parsed) return null;
3179
- return { ...parsed, source: "keychain", path: null };
3227
+ function loadAntigravityCredentials({
3228
+ home,
3229
+ platform = process.platform,
3230
+ securityRunner,
3231
+ nowMs = Date.now(),
3232
+ } = {}) {
3233
+ const candidates = collectAntigravityFileCredentials({ home });
3234
+ if (platform === "darwin" || typeof securityRunner === "function") {
3235
+ const parsed = parseAntigravityCredentialPayload(readAntigravityKeychainRaw({ securityRunner }));
3236
+ if (parsed) candidates.push({ ...parsed, source: "keychain", path: null });
3237
+ }
3238
+ return pickAntigravityCredentials(candidates, nowMs);
3180
3239
  }
3181
3240
 
3182
3241
  function persistAntigravityCredentials(creds, next, { nowMs = Date.now() } = {}) {
@@ -3316,9 +3375,12 @@ async function fetchAntigravityRemoteLimits({
3316
3375
  fetchImpl = fetch,
3317
3376
  nowMs = Date.now(),
3318
3377
  signal,
3378
+ creds,
3319
3379
  } = {}) {
3320
- const creds = loadAntigravityCredentials({ home, platform, securityRunner });
3321
- if (!creds) return null;
3380
+ const resolvedCreds = creds !== undefined
3381
+ ? creds
3382
+ : loadAntigravityCredentials({ home, platform, securityRunner, nowMs });
3383
+ if (!resolvedCreds) return null;
3322
3384
 
3323
3385
  const loadWithToken = async (accessToken) => {
3324
3386
  const payload = await fetchAntigravityQuotaSummaryJson(fetchImpl, accessToken, signal);
@@ -3336,21 +3398,41 @@ async function fetchAntigravityRemoteLimits({
3336
3398
  };
3337
3399
  };
3338
3400
 
3339
- let accessToken = await resolveAntigravityAccessToken(creds, { fetchImpl, nowMs, signal });
3401
+ let accessToken = await resolveAntigravityAccessToken(resolvedCreds, { fetchImpl, nowMs, signal });
3340
3402
  try {
3341
3403
  return await loadWithToken(accessToken);
3342
3404
  } catch (error) {
3343
- if (error?.code !== "AUTH_EXPIRED" || !creds.refreshToken) throw error;
3344
- accessToken = await resolveAntigravityAccessToken(creds, { fetchImpl, nowMs, forceRefresh: true, signal });
3405
+ if (error?.code !== "AUTH_EXPIRED" || !resolvedCreds.refreshToken) throw error;
3406
+ accessToken = await resolveAntigravityAccessToken(resolvedCreds, {
3407
+ fetchImpl,
3408
+ nowMs,
3409
+ forceRefresh: true,
3410
+ signal,
3411
+ });
3345
3412
  return await loadWithToken(accessToken);
3346
3413
  }
3347
3414
  }
3348
3415
 
3349
- function antigravityUnavailableResult({ home, nowMs, platform, securityRunner, remoteError } = {}) {
3416
+ function antigravityCredentialsNeedReauth(creds, { nowMs, remoteError } = {}) {
3417
+ if (remoteError?.code === "AUTH_EXPIRED") return true;
3418
+ return Boolean(
3419
+ creds
3420
+ && creds.expiryMs != null
3421
+ && creds.expiryMs <= nowMs + ANTIGRAVITY_TOKEN_REFRESH_SKEW_MS,
3422
+ );
3423
+ }
3424
+
3425
+ function antigravityUnavailableResult({ home, nowMs, platform, securityRunner, remoteError, creds } = {}) {
3350
3426
  const cached = readAntigravityLimitsCache({ home, nowMs });
3351
- if (cached) return cached;
3352
- const creds = loadAntigravityCredentials({ home, platform, securityRunner });
3353
- if (!hasAntigravityInstallEvidence({ home }) && !creds) {
3427
+ const resolvedCreds = creds !== undefined
3428
+ ? creds
3429
+ : loadAntigravityCredentials({ home, platform, securityRunner, nowMs });
3430
+ if (cached) {
3431
+ return antigravityCredentialsNeedReauth(resolvedCreds, { nowMs, remoteError })
3432
+ ? { ...cached, auth_action_required: "reauth" }
3433
+ : cached;
3434
+ }
3435
+ if (!hasAntigravityInstallEvidence({ home }) && !resolvedCreds) {
3354
3436
  return { configured: false };
3355
3437
  }
3356
3438
  if (remoteError) {
@@ -3360,7 +3442,7 @@ function antigravityUnavailableResult({ home, nowMs, platform, securityRunner, r
3360
3442
  : raw;
3361
3443
  return { configured: true, error: message };
3362
3444
  }
3363
- if (creds) {
3445
+ if (resolvedCreds) {
3364
3446
  return { configured: true, error: ANTIGRAVITY_AUTH_EXPIRED_MESSAGE };
3365
3447
  }
3366
3448
  return { configured: true, error: ANTIGRAVITY_NOT_RUNNING_MESSAGE };
@@ -3389,6 +3471,7 @@ async function fetchAntigravityLimits({
3389
3471
  securityRunner,
3390
3472
  signal,
3391
3473
  } = {}) {
3474
+ const creds = loadAntigravityCredentials({ home, platform, securityRunner, nowMs });
3392
3475
  const startedAtMs = performance.now();
3393
3476
  // min(this step's ceiling, budget left after reserving the fallback guard).
3394
3477
  // 0 means "no time left" — the caller must skip the call, not issue it.
@@ -3429,7 +3512,15 @@ async function fetchAntigravityLimits({
3429
3512
  if (remoteTimeoutMs > 0) {
3430
3513
  try {
3431
3514
  const remote = await withProviderTimeout(
3432
- fetchAntigravityRemoteLimits({ home, platform, securityRunner, fetchImpl, nowMs, signal }),
3515
+ fetchAntigravityRemoteLimits({
3516
+ home,
3517
+ platform,
3518
+ securityRunner,
3519
+ fetchImpl,
3520
+ nowMs,
3521
+ signal,
3522
+ creds,
3523
+ }),
3433
3524
  "Antigravity",
3434
3525
  remoteTimeoutMs,
3435
3526
  );
@@ -3454,7 +3545,14 @@ async function fetchAntigravityLimits({
3454
3545
  signal,
3455
3546
  });
3456
3547
  if (!processInfo.configured) {
3457
- return antigravityUnavailableResult({ home, nowMs, platform, securityRunner, remoteError });
3548
+ return antigravityUnavailableResult({
3549
+ home,
3550
+ nowMs,
3551
+ platform,
3552
+ securityRunner,
3553
+ remoteError,
3554
+ creds,
3555
+ });
3458
3556
  }
3459
3557
  if (processInfo.error) {
3460
3558
  return { configured: true, error: processInfo.error };
@@ -3560,6 +3658,7 @@ async function fetchAntigravityLimits({
3560
3658
  platform,
3561
3659
  securityRunner,
3562
3660
  remoteError: remoteError || error,
3661
+ creds,
3563
3662
  });
3564
3663
  }
3565
3664
  }
@@ -3596,7 +3695,7 @@ function withPlanLabel(obj, raw, brand) {
3596
3695
  // hammered). Survives an external resetUsageLimitsCache() (refresh=1 path in
3597
3696
  // local-api.js): a refresh arriving while a fetch is already running reuses that
3598
3697
  // in-flight fetch and returns its result.
3599
- let inFlightFetch = null;
3698
+ const inFlightByDevinSelection = { off: null, on: null };
3600
3699
 
3601
3700
  // Codex stamps reset_at as unix seconds; every other provider (and Claude's
3602
3701
  // resets_at) uses ISO strings. Numbers that look like epoch milliseconds are
@@ -3634,17 +3733,19 @@ function cacheExpiresAtMs(data, fetchedAtMs) {
3634
3733
  }
3635
3734
 
3636
3735
  async function getUsageLimits(options = {}) {
3736
+ const selection = devinSelectionKey(options);
3737
+ const cache = cacheByDevinSelection[selection];
3637
3738
  const nowMs = Date.now();
3638
3739
  if (cache.data && nowMs < cache.expiresAtMs) {
3639
3740
  return cache.data;
3640
3741
  }
3641
- if (inFlightFetch) {
3642
- return inFlightFetch;
3742
+ if (inFlightByDevinSelection[selection]) {
3743
+ return inFlightByDevinSelection[selection];
3643
3744
  }
3644
3745
  const promise = fetchUsageLimitsUncached(options).finally(() => {
3645
- if (inFlightFetch === promise) inFlightFetch = null;
3746
+ if (inFlightByDevinSelection[selection] === promise) inFlightByDevinSelection[selection] = null;
3646
3747
  });
3647
- inFlightFetch = promise;
3748
+ inFlightByDevinSelection[selection] = promise;
3648
3749
  return promise;
3649
3750
  }
3650
3751
 
@@ -3659,14 +3760,17 @@ async function fetchUsageLimitsUncached({
3659
3760
  now = new Date(),
3660
3761
  providerTimeoutMs = DEFAULT_PROVIDER_TIMEOUT_MS,
3661
3762
  forceRefresh = false,
3763
+ devinEnabled = false,
3662
3764
  } = {}) {
3663
3765
  const nowMs = Date.now();
3664
3766
 
3665
- const [claudeToken, claudeSubscription, codexAuth] = await Promise.all([
3666
- Promise.resolve().then(() => readClaudeCodeAccessToken({ platform, securityRunner, home })),
3767
+ const [claudeOauth, claudeSubscription, codexAuth] = await Promise.all([
3768
+ Promise.resolve().then(() => readClaudeCodeOauthToken({ platform, securityRunner, home, nowMs })),
3667
3769
  Promise.resolve().then(() => detectClaudeCodeSubscriptionDetails({ platform, securityRunner, home })),
3668
3770
  readCodexAuthBundle({ home, env }),
3669
3771
  ]);
3772
+ const claudeToken = claudeOauth?.accessToken || null;
3773
+ const claudeTokenExpiresAtMs = claudeOauth?.expiresAtMs ?? null;
3670
3774
  const claudePlanType = claudeSubscription?.planType || null;
3671
3775
 
3672
3776
  // Match the official Codex CLI: prefer the access token's JWT expiry and refresh only
@@ -3705,7 +3809,9 @@ async function fetchUsageLimitsUncached({
3705
3809
 
3706
3810
  // Skip the upstream Claude call entirely while a 429 cooldown is active — calling again
3707
3811
  // just renews the penalty. The result handling below serves cache or a cooldown message.
3708
- const claudeRetryAtMs = claudeToken ? readClaudeRateLimitRetryAtMs({ home, nowMs }) : null;
3812
+ const claudeRetryAtMs = claudeToken
3813
+ ? readClaudeRateLimitRetryAtMs({ home, nowMs, tokenExpiresAtMs: claudeTokenExpiresAtMs })
3814
+ : null;
3709
3815
  // Also avoid cross-process hammering after a recent successful read: embedded-server
3710
3816
  // restarts and background polls read the disk cache instead of spending another Claude
3711
3817
  // OAuth usage request. An explicit user refresh (refresh=1 → forceRefresh) punches
@@ -3716,7 +3822,7 @@ async function fetchUsageLimitsUncached({
3716
3822
  : null;
3717
3823
 
3718
3824
  const providerFetch = withFetchTimeout(fetchImpl, providerTimeoutMs);
3719
- const [claudeResult, codexResult, cursor, kimi, gemini, kiro, antigravity, copilot, grok, zcode, opencodeGoRaw, qoder, qoderCn, codingPlan, agentPlan, claudeServiceStatus] = await Promise.all([
3825
+ const [claudeResult, codexResult, cursor, kimi, gemini, kiro, antigravity, copilot, grok, zcode, opencodeGoRaw, qoder, qoderCn, codingPlan, agentPlan, commandCodeRaw, devinRaw, claudeServiceStatus] = await Promise.all([
3720
3826
  claudeToken && !freshClaudeCache && !claudeRetryAtMs
3721
3827
  ? withProviderTimeout(fetchClaudeUsageLimits(claudeToken, { fetchImpl: providerFetch, maxAttempts: 1 }), "Claude", providerTimeoutMs).then(
3722
3828
  (value) => ({ status: "fulfilled", value }),
@@ -3817,6 +3923,29 @@ async function fetchUsageLimitsUncached({
3817
3923
  "Ark Agent Plan",
3818
3924
  providerTimeoutMs,
3819
3925
  ).catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
3926
+ // CommandCode (commandcode.ai): official subscription windows (5h + weekly)
3927
+ // from the CLI's own alpha endpoints, keyed by the apiKey the CLI stores in
3928
+ // ~/.commandcode/auth.json (or COMMAND_CODE_API_KEY). No local fallback —
3929
+ // window state lives server-side. fetchCommandcodeLimits throws on auth
3930
+ // expiry so the assemble step below can flag auth_action_required. The
3931
+ // outer race bounds the whole slot: without it three sequential per-request
3932
+ // timeouts (whoami, then credits+subscriptions) could hold the aggregate
3933
+ // ~2x longer than any sibling provider.
3934
+ withProviderTimeout(fetchCommandcodeLimits({ home, env, fetchImpl: providerFetch }), "CommandCode", providerTimeoutMs)
3935
+ .then(
3936
+ (value) => ({ status: "fulfilled", value }),
3937
+ (reason) => ({ status: "rejected", reason }),
3938
+ ),
3939
+ // Devin (devin.ai): daily/weekly subscription quota from the official
3940
+ // GetPlanStatus RPC, keyed by the session token the Devin CLI stores in
3941
+ // ~/.local/share/devin/credentials.toml. No local fallback — window state
3942
+ // lives server-side. fetchDevinLimits throws on auth expiry so the
3943
+ // assemble step below can flag auth_action_required.
3944
+ withProviderTimeout(fetchDevinLimits({ home, env, enabled: devinEnabled === true, fetchImpl: providerFetch }), "Devin", providerTimeoutMs)
3945
+ .then(
3946
+ (value) => ({ status: "fulfilled", value }),
3947
+ (reason) => ({ status: "rejected", reason }),
3948
+ ),
3820
3949
  // Public status-page probe (fail-soft, own 5-min cache in provider-status.js).
3821
3950
  // Only probed for configured accounts — without a token the Claude section
3822
3951
  // never renders, so the reading would have nowhere to go.
@@ -3864,13 +3993,21 @@ async function fetchUsageLimitsUncached({
3864
3993
  // surface an accurate "retry in ~Nm" message rather than the misleading hardcoded one.
3865
3994
  const reason = claudeResult?.reason;
3866
3995
  if (reason?.code === "RATE_LIMITED") {
3867
- writeClaudeRateLimitCooldown(reason.retryAfterSec, { home, nowMs });
3996
+ writeClaudeRateLimitCooldown(reason.retryAfterSec, {
3997
+ home,
3998
+ nowMs,
3999
+ tokenExpiresAtMs: claudeTokenExpiresAtMs,
4000
+ });
3868
4001
  }
3869
4002
  const cached = readClaudeLimitsCache({ home, nowMs });
3870
4003
  if (cached) {
3871
4004
  claude = cached;
3872
4005
  } else {
3873
- const retryAtMs = readClaudeRateLimitRetryAtMs({ home, nowMs }) || claudeRetryAtMs;
4006
+ const retryAtMs = readClaudeRateLimitRetryAtMs({
4007
+ home,
4008
+ nowMs,
4009
+ tokenExpiresAtMs: claudeTokenExpiresAtMs,
4010
+ }) || claudeRetryAtMs;
3874
4011
  claude = {
3875
4012
  configured: true,
3876
4013
  error: retryAtMs
@@ -3893,7 +4030,11 @@ async function fetchUsageLimitsUncached({
3893
4030
  // cool-down just armed by this cycle's 429 is included; a successful read above
3894
4031
  // cleared the file, so this is null in the happy path.
3895
4032
  if (claude.configured) {
3896
- const claudeCooldownMs = readClaudeRateLimitRetryAtMs({ home, nowMs });
4033
+ const claudeCooldownMs = readClaudeRateLimitRetryAtMs({
4034
+ home,
4035
+ nowMs,
4036
+ tokenExpiresAtMs: claudeTokenExpiresAtMs,
4037
+ });
3897
4038
  if (claudeCooldownMs) {
3898
4039
  claude.retry_at = new Date(claudeCooldownMs).toISOString();
3899
4040
  }
@@ -3993,6 +4134,44 @@ async function fetchUsageLimitsUncached({
3993
4134
  }
3994
4135
  }
3995
4136
 
4137
+ // CommandCode: official windows straight from the API — nothing to cache on
4138
+ // disk beyond the shared 2-minute in-memory cache, so keep the assembly
4139
+ // simple. A fulfilled `configured: false` means no apiKey found; a rejected
4140
+ // fetch surfaced an auth-expired error the panel should act on.
4141
+ let commandCodeObj;
4142
+ if (commandCodeRaw?.status === "fulfilled") {
4143
+ const value = commandCodeRaw.value;
4144
+ commandCodeObj = value && value.configured === false
4145
+ ? value
4146
+ : { ...value, stale: false, cached_at: new Date(nowMs).toISOString() };
4147
+ } else {
4148
+ const reason = commandCodeRaw?.reason || null;
4149
+ const authExpired = Boolean(
4150
+ reason &&
4151
+ (reason?.code === "AUTH_EXPIRED" ||
4152
+ /token expired|run `cmd login`|Not authenticated/i.test(reason?.message || "")),
4153
+ );
4154
+ commandCodeObj = authExpired
4155
+ ? { configured: true, error: reason?.message || "Unknown error", auth_action_required: "reauth" }
4156
+ : { configured: true, error: reason?.message || "Unknown error" };
4157
+ }
4158
+
4159
+ // Devin: server-owned quota windows like CommandCode — a fulfilled
4160
+ // `configured: false` means no CLI credentials; a rejected fetch with
4161
+ // AUTH_EXPIRED flags auth_action_required for the re-sign-in hint.
4162
+ let devinObj;
4163
+ if (devinRaw?.status === "fulfilled") {
4164
+ const value = devinRaw.value;
4165
+ devinObj = value && value.configured === false
4166
+ ? value
4167
+ : { ...value, stale: false, cached_at: new Date(nowMs).toISOString() };
4168
+ } else {
4169
+ const reason = devinRaw?.reason || null;
4170
+ devinObj = reason?.code === "AUTH_EXPIRED"
4171
+ ? { configured: true, error: reason?.message || "Unknown error", auth_action_required: "reauth" }
4172
+ : { configured: true, error: reason?.message || "Unknown error" };
4173
+ }
4174
+
3996
4175
  const data = {
3997
4176
  fetched_at: new Date(nowMs).toISOString(),
3998
4177
  claude: withPlanLabel(claude, claudePlanType, "Claude"),
@@ -4010,6 +4189,13 @@ async function fetchUsageLimitsUncached({
4010
4189
  grok: withPlanLabel(grok, null, "Grok"),
4011
4190
  zcode: withPlanLabel(zcode, zcode.plan_label, "ZCode"),
4012
4191
  opencodeGo: withPlanLabel(opencodeGo, opencodeGo?.plan_label, "OpenCode Go"),
4192
+ // CommandCode tiers are acronym brands (GOAT/Pro/Max) — the fetcher already
4193
+ // maps plan ids to the CLI's exact display strings, so skip the shared
4194
+ // Title-Case normalization ("Goat") and surface them as-is.
4195
+ commandCode: commandCodeObj,
4196
+ // Devin's planName ("Pro", "Max") is already the official display name —
4197
+ // Title-Case normalization is still harmless and strips any brand prefix.
4198
+ devin: withPlanLabel(devinObj, devinObj?.plan_label, "Devin"),
4013
4199
  qoder: withPlanLabel(qoder, qoder?.plan_label, "Qoder"),
4014
4200
  qoderCn: withPlanLabel(qoderCn, qoderCn?.plan_label, "Qoder CN"),
4015
4201
  codingPlan: withPlanLabel(codingPlan, codingPlan?.plan_label, "Ark Coding Plan"),
@@ -4037,12 +4223,16 @@ async function fetchUsageLimitsUncached({
4037
4223
  };
4038
4224
  }
4039
4225
 
4040
- cache = { data, expiresAtMs: cacheExpiresAtMs(data, nowMs) };
4226
+ cacheByDevinSelection[devinSelectionKey({ devinEnabled })] = {
4227
+ data,
4228
+ expiresAtMs: cacheExpiresAtMs(data, nowMs),
4229
+ };
4041
4230
  return data;
4042
4231
  }
4043
4232
 
4044
4233
  function resetUsageLimitsCache() {
4045
- cache = { data: null, expiresAtMs: 0 };
4234
+ cacheByDevinSelection.off = { data: null, expiresAtMs: 0 };
4235
+ cacheByDevinSelection.on = { data: null, expiresAtMs: 0 };
4046
4236
  }
4047
4237
 
4048
4238
  module.exports = {
@@ -4080,6 +4270,7 @@ module.exports = {
4080
4270
  fetchGrokLimits,
4081
4271
  fetchZcodeLimits,
4082
4272
  fetchOpencodeGoLimits,
4273
+ fetchCommandcodeLimits,
4083
4274
  fetchQoderLimits,
4084
4275
  fetchQoderCnLimits,
4085
4276
  };