tokentracker-cli 0.96.2 → 0.96.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 (141) 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-CgEitoX1.js → AchievementBadge-CCcvmz6R.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-dnnea0JH.js → AchievementsPage-C5jdktOK.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-CJqG0UP7.js → AchievementsSection-BFMtUzn1.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-BLV6SJoI.js → BadgeDetailModal-Be8gM7tM.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-Cm78yEbz.js → BotAnimated-CXTGR71z.js} +1 -1
  11. package/dashboard/dist/assets/{Card-IPGSYT6O.js → Card-CsUsp3CV.js} +1 -1
  12. package/dashboard/dist/assets/{ClawdAnimated-04w1U1sm.js → ClawdAnimated-BEjpZjEE.js} +2 -2
  13. package/dashboard/dist/assets/{CommandPalette-gSSa1V_s.js → CommandPalette-CHc_Z1r8.js} +1 -1
  14. package/dashboard/dist/assets/{CommunityStatsModal-CICwUz14.js → CommunityStatsModal-Gs6kZ_Mp.js} +1 -1
  15. package/dashboard/dist/assets/{ConfirmModal-DaWC0cY7.js → ConfirmModal-niHkpv2O.js} +1 -1
  16. package/dashboard/dist/assets/{Controls-BDihA03F.js → Controls-X2Z1UNUx.js} +1 -1
  17. package/dashboard/dist/assets/DashboardPage-B66s3Exi.js +1 -0
  18. package/dashboard/dist/assets/{DevicePage-BPpo_eiY.js → DevicePage-DQdLdEI8.js} +1 -1
  19. package/dashboard/dist/assets/DialogClose-CCUjTxl8.js +1 -0
  20. package/dashboard/dist/assets/DialogDescription-CjMjYG3N.js +1 -0
  21. package/dashboard/dist/assets/DialogTitle-CgkUGAag.js +1 -0
  22. package/dashboard/dist/assets/{FadeIn-DmI9PRLa.js → FadeIn-D4QD1bPo.js} +1 -1
  23. package/dashboard/dist/assets/{HeaderGithubStar-CBO4cY7i.js → HeaderGithubStar-D0my-Z3a.js} +1 -1
  24. package/dashboard/dist/assets/{HoverTooltip-CPFGhi9K.js → HoverTooltip-nSWgrKlK.js} +1 -1
  25. package/dashboard/dist/assets/{IpCheckPage-Do7w_6z6.js → IpCheckPage-4hfqHAgi.js} +1 -1
  26. package/dashboard/dist/assets/{LandingPage-at_Ti1ZE.js → LandingPage-wIc4tVRI.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardAvatar-BH1cnKPp.js → LeaderboardAvatar-DGlN8Vy8.js} +1 -1
  28. package/dashboard/dist/assets/{LeaderboardPage-Cd30aXwL.js → LeaderboardPage-CybuzE87.js} +2 -2
  29. package/dashboard/dist/assets/{LeaderboardProfileModal-CVsti_HN.js → LeaderboardProfileModal-D9la3tNd.js} +4 -4
  30. package/dashboard/dist/assets/{LeaderboardProfilePage-BfSpDM9e.js → LeaderboardProfilePage-gxXSeJYb.js} +1 -1
  31. package/dashboard/dist/assets/LimitsPage-CZH3G54Y.js +8 -0
  32. package/dashboard/dist/assets/{LocalOnlyNotice-9-vRgsdn.js → LocalOnlyNotice-Ckc7zohC.js} +1 -1
  33. package/dashboard/dist/assets/{LoginCard-DM_ZdFNl.js → LoginCard-BqeRHqWK.js} +1 -1
  34. package/dashboard/dist/assets/{LoginModal-BaNMznwh.js → LoginModal-BjH6aIBG.js} +1 -1
  35. package/dashboard/dist/assets/{LoginPage-BMMthUf4.js → LoginPage-BOyPLNU3.js} +1 -1
  36. package/dashboard/dist/assets/PetPage-BXRXG40D.js +1 -0
  37. package/dashboard/dist/assets/PopoverPopup-Dad1ZYVd.js +1 -0
  38. package/dashboard/dist/assets/ProviderIcon-BDmrNJSl.js +1 -0
  39. package/dashboard/dist/assets/{ResetPasswordPage-Ca4SULSV.js → ResetPasswordPage-CZpE6qfa.js} +1 -1
  40. package/dashboard/dist/assets/{SegmentedControl-Bbsn-qvZ.js → SegmentedControl-CE1hrQFi.js} +1 -1
  41. package/dashboard/dist/assets/Select-YVj7u6Nv.js +1 -0
  42. package/dashboard/dist/assets/SelectItemText-BMYGgQ3o.js +1 -0
  43. package/dashboard/dist/assets/{ServiceStatusPage-BPS8KJ3a.js → ServiceStatusPage-HehjDbZg.js} +1 -1
  44. package/dashboard/dist/assets/{SessionsPage-C1yt3TMZ.js → SessionsPage-Chvu4YqZ.js} +2 -2
  45. package/dashboard/dist/assets/SettingsPage-BXjS7u9_.js +2 -0
  46. package/dashboard/dist/assets/SkillsPage-q3Rjlj9h.js +1 -0
  47. package/dashboard/dist/assets/{TokenGalaxy-DKvbJbnv.js → TokenGalaxy-BqZyj82r.js} +1 -1
  48. package/dashboard/dist/assets/ToolbarRootContext-Ch8ueI_C.js +1 -0
  49. package/dashboard/dist/assets/{TrendMonitor-YF5C_-L1.js → TrendMonitor-C2aTL9p1.js} +1 -1
  50. package/dashboard/dist/assets/{WidgetsPage-BKV3DgXg.js → WidgetsPage-DYaIai8j.js} +1 -1
  51. package/dashboard/dist/assets/{WrappedPage-s4IRoLf7.js → WrappedPage-51B0v-Sr.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-Cp7dNuCm.js +45 -0
  54. package/dashboard/dist/assets/{agent-logos-Hbt-wyl2.js → agent-logos-DXrPSAdX.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-KeIpKlw3.js → arrow-up-right-BFkf7l0U.js} +1 -1
  56. package/dashboard/dist/assets/{check-_QKyY1v_.js → check--N8wmgIJ.js} +1 -1
  57. package/dashboard/dist/assets/composite-zF5Pj94e.js +1 -0
  58. package/dashboard/dist/assets/{copy-DpLM3QMD.js → copy-BG0JMr6A.js} +1 -1
  59. package/dashboard/dist/assets/{download-C4NX-JnW.js → download-CPFJf-Sf.js} +1 -1
  60. package/dashboard/dist/assets/{external-link-CG-7FcOF.js → external-link-D2yF0kgP.js} +1 -1
  61. package/dashboard/dist/assets/{flame-CkWid7Ji.js → flame-l460AFx9.js} +1 -1
  62. package/dashboard/dist/assets/{format-tokens-CbwWhioZ.js → format-tokens-DKO4ljuH.js} +1 -1
  63. package/dashboard/dist/assets/{icons-BZq17rU3.js → icons-RoxRGkz4.js} +1 -1
  64. package/dashboard/dist/assets/{index-CR9tDiiC.js → index--ouT5L4N.js} +1 -1
  65. package/dashboard/dist/assets/{index-DDGLE0dz.js → index-DhyLCvvv.js} +1 -1
  66. package/dashboard/dist/assets/{index-CSwEiUn4.js → index-PTJ4368-.js} +1 -1
  67. package/dashboard/dist/assets/{info-mkeRjzqc.js → info-DNWMO4Ca.js} +1 -1
  68. package/dashboard/dist/assets/{link-2-BjMw8csC.js → link-2-DzWXAz1h.js} +1 -1
  69. package/dashboard/dist/assets/{loader-circle-cGWOJnWn.js → loader-circle-BzGMqXNu.js} +1 -1
  70. package/dashboard/dist/assets/{maximize-2-Cg_S73tD.js → maximize-2-Dkpyk4x3.js} +1 -1
  71. package/dashboard/dist/assets/{modifiers.esm-DNxTWxId.js → modifiers.esm-lggHUQpW.js} +1 -1
  72. package/dashboard/dist/assets/module-DUSXqJtA.js +3 -0
  73. package/dashboard/dist/assets/{monitor-smartphone-BvE9Tt2U.js → monitor-smartphone-Ck0-gz9l.js} +1 -1
  74. package/dashboard/dist/assets/{plus-BC2wdPRr.js → plus-qTJpnn43.js} +1 -1
  75. package/dashboard/dist/assets/provider-display-psY26qmp.js +42 -0
  76. package/dashboard/dist/assets/{react-D0kcslTz.js → react-CYS24KmX.js} +1 -1
  77. package/dashboard/dist/assets/{search-BuBNF3E3.js → search-CUzE6zxr.js} +1 -1
  78. package/dashboard/dist/assets/{skills-api-DxAN5pNx.js → skills-api-D9SzxPFG.js} +1 -1
  79. package/dashboard/dist/assets/{sortable.esm-DoqMcEX-.js → sortable.esm-Bl2hAUyR.js} +1 -1
  80. package/dashboard/dist/assets/store-mHSJAJtE.js +1 -0
  81. package/dashboard/dist/assets/{terminal-COWFUJdU.js → terminal-BlQNEvn1.js} +1 -1
  82. package/dashboard/dist/assets/{trash-2-Cqjki3RI.js → trash-CHdgmOyk.js} +1 -1
  83. package/dashboard/dist/assets/{use-community-stats-DsrlOPDM.js → use-community-stats-DUiCs91N.js} +1 -1
  84. package/dashboard/dist/assets/{use-latest-request-guard-ryRFL2gx.js → use-latest-request-guard-ehXfkvuU.js} +1 -1
  85. package/dashboard/dist/assets/use-limits-display-prefs-CcwwQ0Fq.js +1 -0
  86. package/dashboard/dist/assets/{use-ordered-list-D-Q86R1b.js → use-ordered-list-CCOtI3Wa.js} +1 -1
  87. package/dashboard/dist/assets/use-reduced-motion-CDlahPqe.js +1 -0
  88. package/dashboard/dist/assets/{use-session-efficiency-pref-CrNz45y7.js → use-session-efficiency-pref-CYRDpcc_.js} +1 -1
  89. package/dashboard/dist/assets/{use-transform-CoGM21px.js → use-transform-btLlTJpj.js} +1 -1
  90. package/dashboard/dist/assets/use-usage-limits-BGr5sR-c.js +1 -0
  91. package/dashboard/dist/assets/{useCurrency-CgCfDktb.js → useCurrency-CNutuEKd.js} +1 -1
  92. package/dashboard/dist/assets/{useTokenFormat-QKtzCKTL.js → useTokenFormat-C0bAJFMC.js} +1 -1
  93. package/dashboard/dist/assets/{zap-CTIobzgU.js → zap-B2fbyjbc.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/devin.svg +6 -0
  97. package/dashboard/dist/index.html +20 -16
  98. package/dashboard/dist/ip-check.html +2 -2
  99. package/dashboard/dist/leaderboard.html +2 -2
  100. package/dashboard/dist/llms.txt +4 -2
  101. package/dashboard/dist/share.html +2 -2
  102. package/package.json +2 -2
  103. package/src/commands/init.js +114 -4
  104. package/src/commands/status.js +37 -0
  105. package/src/commands/sync.js +92 -10
  106. package/src/commands/uninstall.js +23 -0
  107. package/src/lib/codex-config.js +42 -0
  108. package/src/lib/devin-limits.js +249 -0
  109. package/src/lib/devin-usage.js +212 -0
  110. package/src/lib/local-api.js +19 -0
  111. package/src/lib/passive-mode.js +13 -1
  112. package/src/lib/pricing/curated-overrides.json +67 -1
  113. package/src/lib/pricing/index.js +5 -2
  114. package/src/lib/pricing/matcher.js +28 -0
  115. package/src/lib/pricing/seed-snapshot.json +1 -1
  116. package/src/lib/rollout.js +1310 -144
  117. package/src/lib/skills-manager.js +231 -30
  118. package/src/lib/subscriptions.js +70 -16
  119. package/src/lib/usage-limits.js +199 -49
  120. package/dashboard/dist/assets/DashboardPage-hV7wncKF.js +0 -1
  121. package/dashboard/dist/assets/DialogClose-C_dF-r3s.js +0 -1
  122. package/dashboard/dist/assets/DialogDescription-GClOSCaE.js +0 -1
  123. package/dashboard/dist/assets/DialogTitle-Buh_5688.js +0 -1
  124. package/dashboard/dist/assets/LimitsPage-_-BadDAj.js +0 -8
  125. package/dashboard/dist/assets/PetPage-Iq8Hswai.js +0 -1
  126. package/dashboard/dist/assets/PopoverPopup-DXr3JxVV.js +0 -1
  127. package/dashboard/dist/assets/ProviderIcon-sb2keSY2.js +0 -1
  128. package/dashboard/dist/assets/Select-vD-yj-qa.js +0 -1
  129. package/dashboard/dist/assets/SelectItemText-CRtFEuvg.js +0 -1
  130. package/dashboard/dist/assets/SettingsPage-CWVHrMer.js +0 -2
  131. package/dashboard/dist/assets/SkillsPage-DGWEH1xm.js +0 -1
  132. package/dashboard/dist/assets/ToolbarRootContext-D8yov1dW.js +0 -1
  133. package/dashboard/dist/assets/__vite-browser-external-_LUatzwU.js +0 -45
  134. package/dashboard/dist/assets/composite-BidfcMeb.js +0 -1
  135. package/dashboard/dist/assets/limits-providers-DRkcWQup.js +0 -1
  136. package/dashboard/dist/assets/module-xpSFyNBK.js +0 -3
  137. package/dashboard/dist/assets/provider-display-B7GggHQy.js +0 -42
  138. package/dashboard/dist/assets/store-Cg1HeUFM.js +0 -1
  139. package/dashboard/dist/assets/use-limits-display-prefs-DkSuu12m.js +0 -1
  140. package/dashboard/dist/assets/use-reduced-motion-D7NNqavo.js +0 -1
  141. package/dashboard/dist/assets/use-usage-limits-C_6L1sO5.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");
@@ -31,6 +31,7 @@ const { fetchGrokLimits } = 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");
34
+ const { fetchDevinLimits } = require("./devin-limits");
34
35
  const { fetchQoderLimits, fetchQoderCnLimits } = require("./qoder-limits");
35
36
  const { fetchArkCodingPlanLimits } = require("./ark-coding-plan-limits");
36
37
  const { fetchArkAgentPlanLimits } = require("./ark-agent-plan-limits");
@@ -47,7 +48,15 @@ const execFileAsync = promisify(cp.execFile);
47
48
  // 2-minute in-memory cache. It also expires early at the earliest upcoming window
48
49
  // reset in the cached data (see cacheExpiresAtMs), floored so a provider reporting
49
50
  // a reset "right now" can't turn every poll into a full upstream round.
50
- 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
+ }
51
60
  const CACHE_TTL_MS = 2 * 60 * 1000;
52
61
  // Must stay below the macOS app's post-reset re-fetch grace (10s in
53
62
  // DashboardViewModel.resetBoundaryGrace), or that targeted refresh would be
@@ -57,10 +66,10 @@ const DEFAULT_PROVIDER_TIMEOUT_MS = 15_000;
57
66
  const ANTIGRAVITY_LIMITS_CACHE_FILE = "usage-limits-cache.json";
58
67
  const ANTIGRAVITY_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
59
68
  const ANTIGRAVITY_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
60
- // Public installed-app OAuth client used by Antigravity / agy (same id PokeTokenBar
61
- // and the agy binary embed). Not confidential — installed-app clients cannot keep a
62
- // secret. Used to refresh the on-disk Google token so quota can be read like
63
- // 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.
64
73
  const ANTIGRAVITY_OAUTH_CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
65
74
  const ANTIGRAVITY_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
66
75
  const ANTIGRAVITY_LOAD_CODE_ASSIST_URL = "https://daily-cloudcode-pa.googleapis.com/v1internal:loadCodeAssist";
@@ -2574,22 +2583,44 @@ function resolveClaudeRateLimitPath({ home } = {}) {
2574
2583
  return path.join(home || os.homedir(), ".tokentracker", "tracker", CLAUDE_RATE_LIMIT_FILE);
2575
2584
  }
2576
2585
 
2586
+ function claudeTokenExpiryStamp(tokenExpiresAtMs) {
2587
+ return Number.isFinite(tokenExpiresAtMs)
2588
+ ? new Date(tokenExpiresAtMs).toISOString()
2589
+ : null;
2590
+ }
2591
+
2577
2592
  // Returns the cooldown expiry in ms if a 429 cooldown is still active, else null.
2578
- 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 });
2579
2600
  try {
2580
- const parsed = JSON.parse(fs.readFileSync(resolveClaudeRateLimitPath({ home }), "utf8"));
2601
+ const parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
2581
2602
  const retryAtMs = parseTimeMs(parsed?.retry_at);
2582
- 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;
2583
2612
  } catch (_error) {}
2584
2613
  return null;
2585
2614
  }
2586
2615
 
2587
- function writeClaudeRateLimitCooldown(retryAfterSec, { home, nowMs = Date.now() } = {}) {
2616
+ function writeClaudeRateLimitCooldown(retryAfterSec, { home, nowMs = Date.now(), tokenExpiresAtMs } = {}) {
2588
2617
  const sec = Number.isFinite(retryAfterSec) && retryAfterSec > 0
2589
2618
  ? Math.min(retryAfterSec, CLAUDE_RATE_LIMIT_MAX_COOLDOWN_SEC)
2590
2619
  : CLAUDE_RATE_LIMIT_DEFAULT_COOLDOWN_SEC;
2591
2620
  const cachePath = resolveClaudeRateLimitPath({ home });
2592
2621
  const payload = { retry_at: new Date(nowMs + sec * 1000).toISOString() };
2622
+ const expiryStamp = claudeTokenExpiryStamp(tokenExpiresAtMs);
2623
+ if (expiryStamp) payload.token_expires_at = expiryStamp;
2593
2624
  try {
2594
2625
  fs.mkdirSync(path.dirname(cachePath), { recursive: true });
2595
2626
  const tmpPath = `${cachePath}.${process.pid}.tmp`;
@@ -3128,18 +3159,41 @@ function parseAntigravityCredentialPayload(raw) {
3128
3159
  };
3129
3160
  }
3130
3161
 
3131
- 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 = [];
3132
3188
  for (const credPath of listAntigravityCredentialPaths(home)) {
3133
3189
  try {
3134
3190
  const parsed = parseAntigravityCredentialPayload(fs.readFileSync(credPath, "utf8"));
3135
- if (parsed) {
3136
- return { ...parsed, source: "file", path: credPath };
3137
- }
3191
+ if (parsed) candidates.push({ ...parsed, source: "file", path: credPath });
3138
3192
  } catch {
3139
3193
  // missing or unreadable
3140
3194
  }
3141
3195
  }
3142
- return null;
3196
+ return candidates;
3143
3197
  }
3144
3198
 
3145
3199
  function readAntigravityKeychainRaw({ securityRunner, timeoutMs = 2000 } = {}) {
@@ -3170,14 +3224,18 @@ function readAntigravityKeychainRaw({ securityRunner, timeoutMs = 2000 } = {}) {
3170
3224
  }
3171
3225
  }
3172
3226
 
3173
- function loadAntigravityCredentials({ home, platform = process.platform, securityRunner } = {}) {
3174
- const fromFile = loadAntigravityCredentialsFromFiles({ home });
3175
- if (fromFile) return fromFile;
3176
- if (platform !== "darwin" && typeof securityRunner !== "function") return null;
3177
- const raw = readAntigravityKeychainRaw({ securityRunner });
3178
- const parsed = parseAntigravityCredentialPayload(raw);
3179
- if (!parsed) return null;
3180
- 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);
3181
3239
  }
3182
3240
 
3183
3241
  function persistAntigravityCredentials(creds, next, { nowMs = Date.now() } = {}) {
@@ -3317,9 +3375,12 @@ async function fetchAntigravityRemoteLimits({
3317
3375
  fetchImpl = fetch,
3318
3376
  nowMs = Date.now(),
3319
3377
  signal,
3378
+ creds,
3320
3379
  } = {}) {
3321
- const creds = loadAntigravityCredentials({ home, platform, securityRunner });
3322
- if (!creds) return null;
3380
+ const resolvedCreds = creds !== undefined
3381
+ ? creds
3382
+ : loadAntigravityCredentials({ home, platform, securityRunner, nowMs });
3383
+ if (!resolvedCreds) return null;
3323
3384
 
3324
3385
  const loadWithToken = async (accessToken) => {
3325
3386
  const payload = await fetchAntigravityQuotaSummaryJson(fetchImpl, accessToken, signal);
@@ -3337,21 +3398,41 @@ async function fetchAntigravityRemoteLimits({
3337
3398
  };
3338
3399
  };
3339
3400
 
3340
- let accessToken = await resolveAntigravityAccessToken(creds, { fetchImpl, nowMs, signal });
3401
+ let accessToken = await resolveAntigravityAccessToken(resolvedCreds, { fetchImpl, nowMs, signal });
3341
3402
  try {
3342
3403
  return await loadWithToken(accessToken);
3343
3404
  } catch (error) {
3344
- if (error?.code !== "AUTH_EXPIRED" || !creds.refreshToken) throw error;
3345
- 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
+ });
3346
3412
  return await loadWithToken(accessToken);
3347
3413
  }
3348
3414
  }
3349
3415
 
3350
- 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 } = {}) {
3351
3426
  const cached = readAntigravityLimitsCache({ home, nowMs });
3352
- if (cached) return cached;
3353
- const creds = loadAntigravityCredentials({ home, platform, securityRunner });
3354
- 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) {
3355
3436
  return { configured: false };
3356
3437
  }
3357
3438
  if (remoteError) {
@@ -3361,7 +3442,7 @@ function antigravityUnavailableResult({ home, nowMs, platform, securityRunner, r
3361
3442
  : raw;
3362
3443
  return { configured: true, error: message };
3363
3444
  }
3364
- if (creds) {
3445
+ if (resolvedCreds) {
3365
3446
  return { configured: true, error: ANTIGRAVITY_AUTH_EXPIRED_MESSAGE };
3366
3447
  }
3367
3448
  return { configured: true, error: ANTIGRAVITY_NOT_RUNNING_MESSAGE };
@@ -3390,6 +3471,7 @@ async function fetchAntigravityLimits({
3390
3471
  securityRunner,
3391
3472
  signal,
3392
3473
  } = {}) {
3474
+ const creds = loadAntigravityCredentials({ home, platform, securityRunner, nowMs });
3393
3475
  const startedAtMs = performance.now();
3394
3476
  // min(this step's ceiling, budget left after reserving the fallback guard).
3395
3477
  // 0 means "no time left" — the caller must skip the call, not issue it.
@@ -3430,7 +3512,15 @@ async function fetchAntigravityLimits({
3430
3512
  if (remoteTimeoutMs > 0) {
3431
3513
  try {
3432
3514
  const remote = await withProviderTimeout(
3433
- fetchAntigravityRemoteLimits({ home, platform, securityRunner, fetchImpl, nowMs, signal }),
3515
+ fetchAntigravityRemoteLimits({
3516
+ home,
3517
+ platform,
3518
+ securityRunner,
3519
+ fetchImpl,
3520
+ nowMs,
3521
+ signal,
3522
+ creds,
3523
+ }),
3434
3524
  "Antigravity",
3435
3525
  remoteTimeoutMs,
3436
3526
  );
@@ -3455,7 +3545,14 @@ async function fetchAntigravityLimits({
3455
3545
  signal,
3456
3546
  });
3457
3547
  if (!processInfo.configured) {
3458
- return antigravityUnavailableResult({ home, nowMs, platform, securityRunner, remoteError });
3548
+ return antigravityUnavailableResult({
3549
+ home,
3550
+ nowMs,
3551
+ platform,
3552
+ securityRunner,
3553
+ remoteError,
3554
+ creds,
3555
+ });
3459
3556
  }
3460
3557
  if (processInfo.error) {
3461
3558
  return { configured: true, error: processInfo.error };
@@ -3561,6 +3658,7 @@ async function fetchAntigravityLimits({
3561
3658
  platform,
3562
3659
  securityRunner,
3563
3660
  remoteError: remoteError || error,
3661
+ creds,
3564
3662
  });
3565
3663
  }
3566
3664
  }
@@ -3597,7 +3695,7 @@ function withPlanLabel(obj, raw, brand) {
3597
3695
  // hammered). Survives an external resetUsageLimitsCache() (refresh=1 path in
3598
3696
  // local-api.js): a refresh arriving while a fetch is already running reuses that
3599
3697
  // in-flight fetch and returns its result.
3600
- let inFlightFetch = null;
3698
+ const inFlightByDevinSelection = { off: null, on: null };
3601
3699
 
3602
3700
  // Codex stamps reset_at as unix seconds; every other provider (and Claude's
3603
3701
  // resets_at) uses ISO strings. Numbers that look like epoch milliseconds are
@@ -3635,17 +3733,19 @@ function cacheExpiresAtMs(data, fetchedAtMs) {
3635
3733
  }
3636
3734
 
3637
3735
  async function getUsageLimits(options = {}) {
3736
+ const selection = devinSelectionKey(options);
3737
+ const cache = cacheByDevinSelection[selection];
3638
3738
  const nowMs = Date.now();
3639
3739
  if (cache.data && nowMs < cache.expiresAtMs) {
3640
3740
  return cache.data;
3641
3741
  }
3642
- if (inFlightFetch) {
3643
- return inFlightFetch;
3742
+ if (inFlightByDevinSelection[selection]) {
3743
+ return inFlightByDevinSelection[selection];
3644
3744
  }
3645
3745
  const promise = fetchUsageLimitsUncached(options).finally(() => {
3646
- if (inFlightFetch === promise) inFlightFetch = null;
3746
+ if (inFlightByDevinSelection[selection] === promise) inFlightByDevinSelection[selection] = null;
3647
3747
  });
3648
- inFlightFetch = promise;
3748
+ inFlightByDevinSelection[selection] = promise;
3649
3749
  return promise;
3650
3750
  }
3651
3751
 
@@ -3660,14 +3760,17 @@ async function fetchUsageLimitsUncached({
3660
3760
  now = new Date(),
3661
3761
  providerTimeoutMs = DEFAULT_PROVIDER_TIMEOUT_MS,
3662
3762
  forceRefresh = false,
3763
+ devinEnabled = false,
3663
3764
  } = {}) {
3664
3765
  const nowMs = Date.now();
3665
3766
 
3666
- const [claudeToken, claudeSubscription, codexAuth] = await Promise.all([
3667
- Promise.resolve().then(() => readClaudeCodeAccessToken({ platform, securityRunner, home })),
3767
+ const [claudeOauth, claudeSubscription, codexAuth] = await Promise.all([
3768
+ Promise.resolve().then(() => readClaudeCodeOauthToken({ platform, securityRunner, home, nowMs })),
3668
3769
  Promise.resolve().then(() => detectClaudeCodeSubscriptionDetails({ platform, securityRunner, home })),
3669
3770
  readCodexAuthBundle({ home, env }),
3670
3771
  ]);
3772
+ const claudeToken = claudeOauth?.accessToken || null;
3773
+ const claudeTokenExpiresAtMs = claudeOauth?.expiresAtMs ?? null;
3671
3774
  const claudePlanType = claudeSubscription?.planType || null;
3672
3775
 
3673
3776
  // Match the official Codex CLI: prefer the access token's JWT expiry and refresh only
@@ -3706,7 +3809,9 @@ async function fetchUsageLimitsUncached({
3706
3809
 
3707
3810
  // Skip the upstream Claude call entirely while a 429 cooldown is active — calling again
3708
3811
  // just renews the penalty. The result handling below serves cache or a cooldown message.
3709
- const claudeRetryAtMs = claudeToken ? readClaudeRateLimitRetryAtMs({ home, nowMs }) : null;
3812
+ const claudeRetryAtMs = claudeToken
3813
+ ? readClaudeRateLimitRetryAtMs({ home, nowMs, tokenExpiresAtMs: claudeTokenExpiresAtMs })
3814
+ : null;
3710
3815
  // Also avoid cross-process hammering after a recent successful read: embedded-server
3711
3816
  // restarts and background polls read the disk cache instead of spending another Claude
3712
3817
  // OAuth usage request. An explicit user refresh (refresh=1 → forceRefresh) punches
@@ -3717,7 +3822,7 @@ async function fetchUsageLimitsUncached({
3717
3822
  : null;
3718
3823
 
3719
3824
  const providerFetch = withFetchTimeout(fetchImpl, providerTimeoutMs);
3720
- const [claudeResult, codexResult, cursor, kimi, gemini, kiro, antigravity, copilot, grok, zcode, opencodeGoRaw, qoder, qoderCn, codingPlan, agentPlan, commandCodeRaw, 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([
3721
3826
  claudeToken && !freshClaudeCache && !claudeRetryAtMs
3722
3827
  ? withProviderTimeout(fetchClaudeUsageLimits(claudeToken, { fetchImpl: providerFetch, maxAttempts: 1 }), "Claude", providerTimeoutMs).then(
3723
3828
  (value) => ({ status: "fulfilled", value }),
@@ -3831,6 +3936,16 @@ async function fetchUsageLimitsUncached({
3831
3936
  (value) => ({ status: "fulfilled", value }),
3832
3937
  (reason) => ({ status: "rejected", reason }),
3833
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
+ ),
3834
3949
  // Public status-page probe (fail-soft, own 5-min cache in provider-status.js).
3835
3950
  // Only probed for configured accounts — without a token the Claude section
3836
3951
  // never renders, so the reading would have nowhere to go.
@@ -3878,13 +3993,21 @@ async function fetchUsageLimitsUncached({
3878
3993
  // surface an accurate "retry in ~Nm" message rather than the misleading hardcoded one.
3879
3994
  const reason = claudeResult?.reason;
3880
3995
  if (reason?.code === "RATE_LIMITED") {
3881
- writeClaudeRateLimitCooldown(reason.retryAfterSec, { home, nowMs });
3996
+ writeClaudeRateLimitCooldown(reason.retryAfterSec, {
3997
+ home,
3998
+ nowMs,
3999
+ tokenExpiresAtMs: claudeTokenExpiresAtMs,
4000
+ });
3882
4001
  }
3883
4002
  const cached = readClaudeLimitsCache({ home, nowMs });
3884
4003
  if (cached) {
3885
4004
  claude = cached;
3886
4005
  } else {
3887
- const retryAtMs = readClaudeRateLimitRetryAtMs({ home, nowMs }) || claudeRetryAtMs;
4006
+ const retryAtMs = readClaudeRateLimitRetryAtMs({
4007
+ home,
4008
+ nowMs,
4009
+ tokenExpiresAtMs: claudeTokenExpiresAtMs,
4010
+ }) || claudeRetryAtMs;
3888
4011
  claude = {
3889
4012
  configured: true,
3890
4013
  error: retryAtMs
@@ -3907,7 +4030,11 @@ async function fetchUsageLimitsUncached({
3907
4030
  // cool-down just armed by this cycle's 429 is included; a successful read above
3908
4031
  // cleared the file, so this is null in the happy path.
3909
4032
  if (claude.configured) {
3910
- const claudeCooldownMs = readClaudeRateLimitRetryAtMs({ home, nowMs });
4033
+ const claudeCooldownMs = readClaudeRateLimitRetryAtMs({
4034
+ home,
4035
+ nowMs,
4036
+ tokenExpiresAtMs: claudeTokenExpiresAtMs,
4037
+ });
3911
4038
  if (claudeCooldownMs) {
3912
4039
  claude.retry_at = new Date(claudeCooldownMs).toISOString();
3913
4040
  }
@@ -4029,6 +4156,22 @@ async function fetchUsageLimitsUncached({
4029
4156
  : { configured: true, error: reason?.message || "Unknown error" };
4030
4157
  }
4031
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
+
4032
4175
  const data = {
4033
4176
  fetched_at: new Date(nowMs).toISOString(),
4034
4177
  claude: withPlanLabel(claude, claudePlanType, "Claude"),
@@ -4050,6 +4193,9 @@ async function fetchUsageLimitsUncached({
4050
4193
  // maps plan ids to the CLI's exact display strings, so skip the shared
4051
4194
  // Title-Case normalization ("Goat") and surface them as-is.
4052
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"),
4053
4199
  qoder: withPlanLabel(qoder, qoder?.plan_label, "Qoder"),
4054
4200
  qoderCn: withPlanLabel(qoderCn, qoderCn?.plan_label, "Qoder CN"),
4055
4201
  codingPlan: withPlanLabel(codingPlan, codingPlan?.plan_label, "Ark Coding Plan"),
@@ -4077,12 +4223,16 @@ async function fetchUsageLimitsUncached({
4077
4223
  };
4078
4224
  }
4079
4225
 
4080
- cache = { data, expiresAtMs: cacheExpiresAtMs(data, nowMs) };
4226
+ cacheByDevinSelection[devinSelectionKey({ devinEnabled })] = {
4227
+ data,
4228
+ expiresAtMs: cacheExpiresAtMs(data, nowMs),
4229
+ };
4081
4230
  return data;
4082
4231
  }
4083
4232
 
4084
4233
  function resetUsageLimitsCache() {
4085
- cache = { data: null, expiresAtMs: 0 };
4234
+ cacheByDevinSelection.off = { data: null, expiresAtMs: 0 };
4235
+ cacheByDevinSelection.on = { data: null, expiresAtMs: 0 };
4086
4236
  }
4087
4237
 
4088
4238
  module.exports = {