tokentracker-cli 1.1.10 → 1.1.12

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 (121) hide show
  1. package/README.md +2 -1
  2. package/dashboard/dist/assets/{AchievementBadge-CH97d6cg.js → AchievementBadge-bxsSwozZ.js} +1 -1
  3. package/dashboard/dist/assets/{AchievementsPage-Dwvj09sF.js → AchievementsPage-CCGsTNky.js} +2 -2
  4. package/dashboard/dist/assets/{AchievementsSection-D6HHK1f-.js → AchievementsSection-a-ucN3O9.js} +1 -1
  5. package/dashboard/dist/assets/{BadgeDetailModal-Bpr6rY9l.js → BadgeDetailModal-CNCCgDRL.js} +1 -1
  6. package/dashboard/dist/assets/BotAnimated-DYrmIQt7.js +1 -0
  7. package/dashboard/dist/assets/{Card-38v0-L6e.js → Card-615UteEP.js} +1 -1
  8. package/dashboard/dist/assets/{CommandPalette-Bj0e0ye0.js → CommandPalette-BzmRKASJ.js} +1 -1
  9. package/dashboard/dist/assets/{CommunityStatsModal-IoQLJCe2.js → CommunityStatsModal-DdRpM1bD.js} +1 -1
  10. package/dashboard/dist/assets/{ConfirmModal-B7uK0Wjr.js → ConfirmModal-BQIPBiA5.js} +1 -1
  11. package/dashboard/dist/assets/Controls-Cup1Mj2U.js +1 -0
  12. package/dashboard/dist/assets/DashboardPage-DF1bFyHx.js +1 -0
  13. package/dashboard/dist/assets/{DevicePage-DE-NSyZg.js → DevicePage-CC31pv5N.js} +1 -1
  14. package/dashboard/dist/assets/{DialogClose-DB6ZRDR3.js → DialogClose-CaYoc9xQ.js} +1 -1
  15. package/dashboard/dist/assets/DialogDescription-DZLOEWil.js +1 -0
  16. package/dashboard/dist/assets/{DialogTitle-CgwmTJf3.js → DialogTitle-BViOiFtR.js} +1 -1
  17. package/dashboard/dist/assets/{FadeIn-CMjPtMSB.js → FadeIn-BQYqxLXW.js} +1 -1
  18. package/dashboard/dist/assets/{HeaderGithubStar-D5cSnbcg.js → HeaderGithubStar-CpGwcrZV.js} +1 -1
  19. package/dashboard/dist/assets/{HoverTooltip-CFV8yLxb.js → HoverTooltip--hTrUe1c.js} +1 -1
  20. package/dashboard/dist/assets/{IpCheckPage-Ctu-GWJQ.js → IpCheckPage-gPQntnIE.js} +1 -1
  21. package/dashboard/dist/assets/{LandingPage-PAIF034o.js → LandingPage-Cw72oDTD.js} +2 -2
  22. package/dashboard/dist/assets/{LeaderboardAvatar-D3CsY-nP.js → LeaderboardAvatar-CnqlbA5v.js} +1 -1
  23. package/dashboard/dist/assets/{LeaderboardPage-qE8DD-Ia.js → LeaderboardPage-CvdG-SGy.js} +2 -2
  24. package/dashboard/dist/assets/{LeaderboardProfileModal-BY3uaIdF.js → LeaderboardProfileModal-CqzEW_FY.js} +4 -4
  25. package/dashboard/dist/assets/{LeaderboardProfilePage-CwX77U5Z.js → LeaderboardProfilePage-DCjJ7yF3.js} +3 -3
  26. package/dashboard/dist/assets/{LimitsPage-BlJgvFay.js → LimitsPage-DMGI3mL9.js} +2 -2
  27. package/dashboard/dist/assets/{LocalOnlyNotice-CLFKSu0V.js → LocalOnlyNotice-CpGpv1oF.js} +1 -1
  28. package/dashboard/dist/assets/{LoginCard-Bdp2gihl.js → LoginCard-B5W06Jrz.js} +1 -1
  29. package/dashboard/dist/assets/{LoginModal-o_t6uvYP.js → LoginModal-B4Y8XRkn.js} +1 -1
  30. package/dashboard/dist/assets/{LoginPage-Cc3ZHUU8.js → LoginPage-BLN-PdCh.js} +1 -1
  31. package/dashboard/dist/assets/PetPage-BvY7fR8_.js +1 -0
  32. package/dashboard/dist/assets/{PopoverPopup-CGJBBnIf.js → PopoverPopup-C4_m01Pc.js} +1 -1
  33. package/dashboard/dist/assets/{ProviderIcon-1OG4Kbkl.js → ProviderIcon-LgUYzw52.js} +1 -1
  34. package/dashboard/dist/assets/{ResetPasswordPage-DbfS1gfH.js → ResetPasswordPage-Dgzafk9_.js} +1 -1
  35. package/dashboard/dist/assets/{SegmentedControl-DPKl-28R.js → SegmentedControl-CMWf089j.js} +1 -1
  36. package/dashboard/dist/assets/{Select-BTA6mSut.js → Select-BNFp1Pww.js} +1 -1
  37. package/dashboard/dist/assets/{SelectItemText-CX5Br8hd.js → SelectItemText-Jz-4OEyW.js} +1 -1
  38. package/dashboard/dist/assets/{ServiceStatusPage-CaTOLkT7.js → ServiceStatusPage-AEW89tSP.js} +1 -1
  39. package/dashboard/dist/assets/{SessionsPage-Ixp6Fseo.js → SessionsPage-DTW1sFcW.js} +2 -2
  40. package/dashboard/dist/assets/SettingsPage-2z_a9T_C.js +2 -0
  41. package/dashboard/dist/assets/{SkillsPage-BUUfMFpU.js → SkillsPage-BkPEwhXO.js} +1 -1
  42. package/dashboard/dist/assets/{TokenGalaxy-DnfKnQhr.js → TokenGalaxy-Cs5wfOxe.js} +1 -1
  43. package/dashboard/dist/assets/{ToolbarRootContext-D0wMtL6Z.js → ToolbarRootContext-B3pyxVI-.js} +1 -1
  44. package/dashboard/dist/assets/TrendMonitor-BmSRt3h3.js +19 -0
  45. package/dashboard/dist/assets/{WidgetsPage-Bfod5aSH.js → WidgetsPage-BN5ePVqf.js} +1 -1
  46. package/dashboard/dist/assets/{WrappedPage-fYftPA8s.js → WrappedPage-DZTG3lCh.js} +1 -1
  47. package/dashboard/dist/assets/__vite-browser-external-4IDwIR3i.js +45 -0
  48. package/dashboard/dist/assets/__vite-browser-external-CAQ_i-1v.css +1 -0
  49. package/dashboard/dist/assets/{agent-logos-C0O8Lnmv.js → agent-logos-DQmv0J3b.js} +1 -1
  50. package/dashboard/dist/assets/{arrow-up-right-Bs0RpChC.js → arrow-up-right-BbkRcWv3.js} +1 -1
  51. package/dashboard/dist/assets/{check-Dj5Wi1I7.js → check-Dz72GL-k.js} +1 -1
  52. package/dashboard/dist/assets/{composite-BeHO1nUS.js → composite-COAFYYB2.js} +1 -1
  53. package/dashboard/dist/assets/{copy-Dgp1NqhD.js → copy-wEl0WhF0.js} +1 -1
  54. package/dashboard/dist/assets/{download-D1JugY5p.js → download-W7dmHuLF.js} +1 -1
  55. package/dashboard/dist/assets/{external-link-ow0tAluE.js → external-link-B-pvBbFP.js} +1 -1
  56. package/dashboard/dist/assets/{flame-Obus_5g4.js → flame-y6R5Plz2.js} +1 -1
  57. package/dashboard/dist/assets/{format-tokens-yToZ6yvw.js → format-tokens-CkWutEDc.js} +1 -1
  58. package/dashboard/dist/assets/{icons-Da1IxY6v.js → icons-C_ECRc6a.js} +1 -1
  59. package/dashboard/dist/assets/{index-0hD-aFe2.js → index-B1Am3t0P.js} +1 -1
  60. package/dashboard/dist/assets/{index-BtmxTdj8.js → index-BzmW4EmI.js} +1 -1
  61. package/dashboard/dist/assets/{index-DduY8rpi.js → index-Dtl4UhBX.js} +1 -1
  62. package/dashboard/dist/assets/{info-CQjQ1whr.js → info-CCfzQ-S6.js} +1 -1
  63. package/dashboard/dist/assets/{link-2-BGYfWbPb.js → link-2-DvFi_t5y.js} +1 -1
  64. package/dashboard/dist/assets/{loader-circle-WArW9cmn.js → loader-circle-BglSN88W.js} +1 -1
  65. package/dashboard/dist/assets/{maximize-2-Bhde1l0n.js → maximize-2-B7Nzrb8n.js} +1 -1
  66. package/dashboard/dist/assets/{modifiers.esm-BRxUZvoy.js → modifiers.esm-CsJhvsvC.js} +1 -1
  67. package/dashboard/dist/assets/{monitor-smartphone-2ZT8ACJS.js → monitor-smartphone-DpicOpX8.js} +1 -1
  68. package/dashboard/dist/assets/{plus-C_cg6gJE.js → plus-C40ubjK3.js} +1 -1
  69. package/dashboard/dist/assets/{provider-display-CM8dzzcI.js → provider-display-E2soZfeg.js} +2 -2
  70. package/dashboard/dist/assets/{react-C3vDx5Sr.js → react-BseCgHfW.js} +1 -1
  71. package/dashboard/dist/assets/{search-Vd9we7KU.js → search-Bu5sWW2G.js} +1 -1
  72. package/dashboard/dist/assets/{skills-api-CHxubvN1.js → skills-api-BBF6YhPQ.js} +1 -1
  73. package/dashboard/dist/assets/skins-Mc0sI20C.js +1 -0
  74. package/dashboard/dist/assets/{sortable.esm-rLR8Wj9r.js → sortable.esm-CLXvOgJi.js} +1 -1
  75. package/dashboard/dist/assets/{store-rdi5mnme.js → store-DdObzMvx.js} +1 -1
  76. package/dashboard/dist/assets/{terminal-xEPZhi6v.js → terminal-CRqmFLSX.js} +1 -1
  77. package/dashboard/dist/assets/{trash-Cad8nQc4.js → trash-Dknznc_u.js} +1 -1
  78. package/dashboard/dist/assets/{use-community-stats-SMq01IVW.js → use-community-stats-ZvEm6sF4.js} +1 -1
  79. package/dashboard/dist/assets/{use-latest-request-guard-CSQ3Ypu3.js → use-latest-request-guard-QHLPnnSW.js} +1 -1
  80. package/dashboard/dist/assets/{use-limits-display-prefs-DaaT6TL9.js → use-limits-display-prefs-CRg1sYpO.js} +1 -1
  81. package/dashboard/dist/assets/{use-ordered-list-g58JllvT.js → use-ordered-list-NpIo40Y4.js} +1 -1
  82. package/dashboard/dist/assets/use-pet-settings-BUroRCzh.js +2 -0
  83. package/dashboard/dist/assets/use-reduced-motion-CDg8lAbR.js +1 -0
  84. package/dashboard/dist/assets/{use-session-efficiency-pref-DyATLZ-L.js → use-session-efficiency-pref-aQVRBt9R.js} +1 -1
  85. package/dashboard/dist/assets/{use-transform-XYEzPgA-.js → use-transform-BP8NiCzk.js} +1 -1
  86. package/dashboard/dist/assets/use-usage-limits-CvIocz6v.js +1 -0
  87. package/dashboard/dist/assets/{useCurrency-4xq0xUFx.js → useCurrency-CyksuFrj.js} +1 -1
  88. package/dashboard/dist/assets/useTokenFormat-CUwCluPw.js +1 -0
  89. package/dashboard/dist/assets/{zap-InLQedtD.js → zap-BcWplbpF.js} +1 -1
  90. package/dashboard/dist/index.html +2 -2
  91. package/dashboard/dist/ip-check.html +2 -2
  92. package/dashboard/dist/leaderboard.html +2 -2
  93. package/dashboard/dist/share.html +2 -2
  94. package/package.json +3 -2
  95. package/src/commands/init.js +1 -1
  96. package/src/commands/sync.js +71 -34
  97. package/src/lib/claude-categorizer.js +4 -7
  98. package/src/lib/claude-user-identity.js +56 -0
  99. package/src/lib/cloud-sync-prefs.js +14 -0
  100. package/src/lib/cursor-store.js +636 -57
  101. package/src/lib/grok-limits.js +17 -4
  102. package/src/lib/kimi-profile.js +61 -0
  103. package/src/lib/local-api.js +17 -11
  104. package/src/lib/multi-install-parser.js +3 -1
  105. package/src/lib/pricing/seed-snapshot.json +1 -1
  106. package/src/lib/rollout.js +91 -39
  107. package/src/lib/usage-limits.js +128 -79
  108. package/dashboard/dist/assets/BotAnimated-srF64s63.js +0 -1
  109. package/dashboard/dist/assets/ClawdAnimated-C9a1u9lq.js +0 -2
  110. package/dashboard/dist/assets/Controls-D5XuFAlI.js +0 -1
  111. package/dashboard/dist/assets/DashboardPage-Ddb8k4DR.js +0 -1
  112. package/dashboard/dist/assets/DialogDescription-CaRzgdvP.js +0 -1
  113. package/dashboard/dist/assets/PetPage-DFaWnKsI.js +0 -1
  114. package/dashboard/dist/assets/SettingsPage-BmeQse01.js +0 -2
  115. package/dashboard/dist/assets/TrendMonitor-D3WafvUS.js +0 -19
  116. package/dashboard/dist/assets/__vite-browser-external-B98Yok9C.js +0 -45
  117. package/dashboard/dist/assets/__vite-browser-external-Bg0J-a5P.css +0 -1
  118. package/dashboard/dist/assets/skins-C8EyB85e.js +0 -1
  119. package/dashboard/dist/assets/use-reduced-motion-Dgdv5SSP.js +0 -1
  120. package/dashboard/dist/assets/use-usage-limits-DL260plD.js +0 -1
  121. package/dashboard/dist/assets/useTokenFormat-B6fHArH6.js +0 -1
@@ -1,3 +1,4 @@
1
+ const { claudeUserIdentity, collectClaudeForkAliases } = require("./claude-user-identity");
1
2
  const fs = require("node:fs/promises");
2
3
  const fssync = require("node:fs");
3
4
  const os = require("node:os");
@@ -982,6 +983,35 @@ async function parseClaudeIncremental({
982
983
  const prevHashes = Array.isArray(cursors.claudeHashes) ? cursors.claudeHashes : [];
983
984
  const seenMessageHashes = new Set(prevHashes);
984
985
  const defaultSource = normalizeSourceInput(source) || "claude";
986
+ // Resolve all changed-file lineage before counting: fork files may sort first.
987
+ const forkTimes = new Map();
988
+ const forkAliases = await collectClaudeForkAliases(files, cursors,
989
+ (obj) => { if (typeof obj.timestamp === "string") forkTimes.set(obj.uuid, obj.timestamp); });
990
+ {
991
+ // Correct only copies whose old UUID identities prove they were counted.
992
+ // Preserve all token totals and history whose source files were removed.
993
+ const families = new Map();
994
+ const familyTimes = new Map();
995
+ for (const [uuid, ts] of forkTimes) familyTimes.set(claudeUserIdentity({ uuid }, forkAliases), ts);
996
+ for (const hash of prevHashes) {
997
+ if (typeof hash !== "string" || !hash.startsWith("u:")) continue;
998
+ const canonical = claudeUserIdentity({ uuid: hash.slice(2) }, forkAliases);
999
+ families.set(canonical, (families.get(canonical) || 0) + 1);
1000
+ seenMessageHashes.delete(hash);
1001
+ seenMessageHashes.add(canonical);
1002
+ }
1003
+ for (const [canonical, count] of families) {
1004
+ if (count < 2) continue;
1005
+ const ts = familyTimes.get(canonical);
1006
+ const hour = ts ? toUtcHalfHourStart(ts) : null;
1007
+ if (!hour) continue;
1008
+ const key = bucketKey(defaultSource, DEFAULT_MODEL, hour);
1009
+ const bucket = hourlyState.buckets[key];
1010
+ if (!bucket || bucket.totals.conversation_count < count - 1) continue;
1011
+ bucket.totals.conversation_count -= count - 1;
1012
+ touchedBuckets.add(key);
1013
+ }
1014
+ }
985
1015
 
986
1016
  if (!cursors.files || typeof cursors.files !== "object") {
987
1017
  cursors.files = {};
@@ -1155,11 +1185,13 @@ async function parseClaudeIncremental({
1155
1185
  projectRef,
1156
1186
  projectKey,
1157
1187
  seenMessageHashes,
1188
+ forkAliases,
1158
1189
  });
1159
1190
 
1160
1191
  cursors.files[key] = {
1161
1192
  inode,
1162
1193
  offset: result.endOffset,
1194
+ claudeForkIndexed: true,
1163
1195
  updatedAt: new Date().toISOString(),
1164
1196
  ...(fileId ? { fileId, fileIdSize: st.size } : {}),
1165
1197
  ...(projectEnabled
@@ -1198,6 +1230,7 @@ async function parseClaudeIncremental({
1198
1230
  cursors.projectHourly = projectState;
1199
1231
  }
1200
1232
  // Persist message hashes for cross-sync dedup; cap at 100k entries to bound size.
1233
+ cursors.claudeForkAliases = forkAliases;
1201
1234
  const allHashes = Array.from(seenMessageHashes);
1202
1235
  cursors.claudeHashes =
1203
1236
  allHashes.length > 100_000 ? allHashes.slice(allHashes.length - 100_000) : allHashes;
@@ -2678,6 +2711,7 @@ async function parseClaudeFile({
2678
2711
  projectRef,
2679
2712
  projectKey,
2680
2713
  seenMessageHashes,
2714
+ forkAliases,
2681
2715
  }) {
2682
2716
  const st = fileStat || (await fs.stat(filePath).catch(() => null));
2683
2717
  if (!st || !st.isFile()) return { endOffset: startOffset, eventsAggregated: 0 };
@@ -2712,15 +2746,9 @@ async function parseClaudeFile({
2712
2746
  typeof content === "string" ||
2713
2747
  (Array.isArray(content) && content.some((b) => b?.type === "text"));
2714
2748
  if (hasText) {
2715
- // Dedup by the line's uuid so a synced copy of the session file
2716
- // (WSL mirror of a divergent native file, inode-reset re-read)
2717
- // cannot re-count the conversation. Usage rows get the same
2718
- // guarantee from claudeMessageDedupKey; user lines have no
2719
- // message.id, so the line uuid is the identity.
2720
- const userKey =
2721
- seenMessageHashes && typeof userObj?.uuid === "string" && userObj.uuid
2722
- ? `u:${userObj.uuid}`
2723
- : null;
2749
+ // Copied user rows share their original identity through the full
2750
+ // fork lineage. Token dedup remains independent.
2751
+ const userKey = seenMessageHashes ? claudeUserIdentity(userObj, forkAliases) : null;
2724
2752
  if (!userKey || !seenMessageHashes.has(userKey)) {
2725
2753
  if (userKey) seenMessageHashes.add(userKey);
2726
2754
  const userTs = typeof userObj?.timestamp === "string" ? userObj.timestamp : null;
@@ -3670,10 +3698,9 @@ function deriveOpencodeMessageFingerprint({ msg, totals, source }) {
3670
3698
  return crypto.createHash("sha256").update(raw).digest("base64url").slice(0, 22);
3671
3699
  }
3672
3700
 
3673
- // `fingerprint -> messageKey` for counted messages in this cursor namespace.
3674
- // Rebuilt per parse run. Pre-#426 entries are fingerprinted as they are read;
3675
- // the first claims ownership and later cross-session matches are retracted from
3676
- // persisted buckets once. Tombstoned copies never claim ownership themselves.
3701
+ // `fingerprint -> Set<messageKey>` for counted messages in this cursor namespace.
3702
+ // Same-session duplicates are all retained as owners; the first session remains
3703
+ // canonical for cross-session fork copies. Tombstoned copies never claim ownership.
3677
3704
  function buildOpencodeFingerprintIndex(messageIndex, wantedFingerprints = null) {
3678
3705
  const byFingerprint = new Map();
3679
3706
  if (!messageIndex || typeof messageIndex !== "object") return byFingerprint;
@@ -3681,12 +3708,9 @@ function buildOpencodeFingerprintIndex(messageIndex, wantedFingerprints = null)
3681
3708
  const entry = messageIndex[key];
3682
3709
  if (entry?.dedupedForkCopy === true) continue;
3683
3710
  const fingerprint = entry && typeof entry.fingerprint === "string" ? entry.fingerprint : null;
3684
- if (
3685
- fingerprint &&
3686
- (!wantedFingerprints || wantedFingerprints.has(fingerprint)) &&
3687
- !byFingerprint.has(fingerprint)
3688
- ) {
3689
- byFingerprint.set(fingerprint, key);
3711
+ if (fingerprint && (!wantedFingerprints || wantedFingerprints.has(fingerprint))) {
3712
+ if (!byFingerprint.has(fingerprint)) byFingerprint.set(fingerprint, new Set());
3713
+ byFingerprint.get(fingerprint).add(key);
3690
3714
  }
3691
3715
  }
3692
3716
  return byFingerprint;
@@ -3704,12 +3728,18 @@ function opencodeMessageKeySession(messageKey) {
3704
3728
  // must not delete usage.
3705
3729
  function isOpencodeForkCopy(fingerprintIndex, fingerprint, messageKey) {
3706
3730
  if (!fingerprint || !fingerprintIndex) return false;
3707
- const owner = fingerprintIndex.get(fingerprint);
3708
- if (!owner || owner === messageKey) return false;
3709
- const ownerSession = opencodeMessageKeySession(owner);
3731
+ const owners = fingerprintIndex.get(fingerprint);
3732
+ if (!owners) return false;
3710
3733
  const session = opencodeMessageKeySession(messageKey);
3711
- if (!ownerSession || !session) return false;
3712
- return ownerSession !== session;
3734
+ if (!session) return false;
3735
+ let hasCrossSessionOwner = false;
3736
+ for (const owner of owners instanceof Set ? owners : [owners]) {
3737
+ const ownerSession = opencodeMessageKeySession(owner);
3738
+ if (!ownerSession) continue;
3739
+ if (owner === messageKey) return false;
3740
+ if (ownerSession !== session) hasCrossSessionOwner = true;
3741
+ }
3742
+ return hasCrossSessionOwner;
3713
3743
  }
3714
3744
 
3715
3745
  function normalizeOpencodeAttribution(raw) {
@@ -3787,8 +3817,16 @@ function recordOpencodeMessage({
3787
3817
  prevDeduped === Boolean(dedupedForkCopy)
3788
3818
  ) return;
3789
3819
 
3790
- if (fingerprintIndex && prevFingerprint && prevFingerprint !== nextFingerprint) {
3791
- if (fingerprintIndex.get(prevFingerprint) === messageKey) {
3820
+ if (
3821
+ fingerprintIndex &&
3822
+ prevFingerprint &&
3823
+ (prevFingerprint !== nextFingerprint || dedupedForkCopy)
3824
+ ) {
3825
+ const owners = fingerprintIndex.get(prevFingerprint);
3826
+ if (owners instanceof Set) {
3827
+ owners.delete(messageKey);
3828
+ if (owners.size === 0) fingerprintIndex.delete(prevFingerprint);
3829
+ } else if (owners === messageKey) {
3792
3830
  fingerprintIndex.delete(prevFingerprint);
3793
3831
  }
3794
3832
  }
@@ -3797,13 +3835,12 @@ function recordOpencodeMessage({
3797
3835
  if (nextAttribution) entry.attribution = encodeOpencodeAttribution(nextAttribution);
3798
3836
  if (dedupedForkCopy) entry.dedupedForkCopy = true;
3799
3837
  messageIndex[messageKey] = entry;
3800
- if (
3801
- fingerprintIndex &&
3802
- nextFingerprint &&
3803
- !dedupedForkCopy &&
3804
- !fingerprintIndex.has(nextFingerprint)
3805
- ) {
3806
- fingerprintIndex.set(nextFingerprint, messageKey);
3838
+ if (fingerprintIndex && nextFingerprint && !dedupedForkCopy) {
3839
+ if (!fingerprintIndex.has(nextFingerprint)) {
3840
+ fingerprintIndex.set(nextFingerprint, new Set());
3841
+ }
3842
+ const owners = fingerprintIndex.get(nextFingerprint);
3843
+ if (owners instanceof Set) owners.add(messageKey);
3807
3844
  }
3808
3845
  }
3809
3846
 
@@ -5542,6 +5579,8 @@ async function parseOpencodeDbIncremental({
5542
5579
  source,
5543
5580
  cursorKey,
5544
5581
  publicRepoResolver,
5582
+ opencodeCursorStore,
5583
+ opencodeCursorNamespace = "flat",
5545
5584
  }) {
5546
5585
  await ensureDir(path.dirname(queuePath));
5547
5586
  let messagesProcessed = 0;
@@ -5557,22 +5596,35 @@ async function parseOpencodeDbIncremental({
5557
5596
  const projectMetaCache = projectEnabled ? new Map() : null;
5558
5597
  const publicRepoCache = projectEnabled ? new Map() : null;
5559
5598
  const cursorNamespace = typeof cursorKey === "string" && cursorKey.length > 0 ? cursorKey : "opencode";
5560
- const opencodeState = normalizeOpencodeState(cursors?.[cursorNamespace]);
5561
- const messageIndex = opencodeState.messages;
5562
5599
  const touchedBuckets = new Set();
5563
5600
  const defaultSource = normalizeSourceInput(source) || "opencode";
5601
+ const candidateMessageKeys = new Set();
5564
5602
  const candidateFingerprints = new Set();
5565
5603
  for (const entry of messages) {
5604
+ const msgForKey = { ...entry?.data };
5605
+ if (entry?.id && !msgForKey.id) msgForKey.id = entry.id;
5606
+ if (entry?.sessionID && !msgForKey.sessionID) msgForKey.sessionID = entry.sessionID;
5607
+ const messageKey = deriveOpencodeMessageKey(msgForKey, null);
5608
+ if (messageKey) candidateMessageKeys.add(messageKey);
5566
5609
  const totals = normalizeOpencodeTokens(entry?.data?.tokens);
5567
5610
  const fingerprint = totals
5568
5611
  ? deriveOpencodeMessageFingerprint({ msg: entry.data, totals, source: defaultSource })
5569
5612
  : null;
5570
5613
  if (fingerprint) candidateFingerprints.add(fingerprint);
5571
5614
  }
5572
- // A hook-triggered sync normally carries one changed row. Restrict the
5573
- // historical fingerprint map to fingerprints that can actually match this
5574
- // batch instead of duplicating the whole long-lived message index in memory.
5575
- const fingerprintIndex = buildOpencodeFingerprintIndex(messageIndex, candidateFingerprints);
5615
+ const loaded = cursorNamespace === "opencode" && opencodeCursorStore
5616
+ ? await opencodeCursorStore.loadOpencodeMessagesForBatch({
5617
+ namespace: opencodeCursorNamespace,
5618
+ messageKeys: candidateMessageKeys,
5619
+ fingerprints: candidateFingerprints,
5620
+ })
5621
+ : null;
5622
+ const opencodeState = normalizeOpencodeState(cursors?.[cursorNamespace]);
5623
+ const messageIndex = opencodeState.messages;
5624
+ // Direct parser callers and the legacy single-file store retain the in-memory
5625
+ // fallback; cursor-store generations provide the bounded reverse index.
5626
+ const fingerprintIndex = loaded?.fingerprintIndex ||
5627
+ buildOpencodeFingerprintIndex(messageIndex, candidateFingerprints);
5576
5628
 
5577
5629
  for (let idx = 0; idx < messages.length; idx++) {
5578
5630
  const entry = messages[idx];
@@ -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 { fetchProviderServiceStatus } = require("./provider-status");
39
+ const { resolveKimiProfile } = require("./kimi-profile");
39
40
  const { readSqliteJsonRows, readSqliteJsonRowsAsync } = require("./sqlite-reader");
40
41
  const {
41
42
  runCommand,
@@ -48,14 +49,17 @@ const execFileAsync = promisify(cp.execFile);
48
49
  // 2-minute in-memory cache. It also expires early at the earliest upcoming window
49
50
  // reset in the cached data (see cacheExpiresAtMs), floored so a provider reporting
50
51
  // a reset "right now" can't turn every poll into a full upstream round.
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";
52
+ // Partitioned by the Devin opt-in selection AND the Antigravity quota opt-out, so
53
+ // a request made while Devin is off is never served (or joined onto) a response
54
+ // fetched while it was on, and an aggregate fetched while Antigravity was enabled
55
+ // is never served after the opt-out is set. Key = `${devin}-${antigravity}`.
56
+ const USAGE_LIMITS_SELECTION_KEYS = ["off-on", "off-off", "on-on", "on-off"];
57
+ const cacheBySelection = {};
58
+ for (const key of USAGE_LIMITS_SELECTION_KEYS) cacheBySelection[key] = { data: null, expiresAtMs: 0 };
59
+ function usageLimitsSelectionKey(options) {
60
+ const devin = options?.devinEnabled === true ? "on" : "off";
61
+ const antigravity = isAntigravityQuotaDisabled() ? "off" : "on";
62
+ return `${devin}-${antigravity}`;
59
63
  }
60
64
  const CACHE_TTL_MS = 2 * 60 * 1000;
61
65
  // Must stay below the macOS app's post-reset re-fetch grace (10s in
@@ -297,16 +301,21 @@ async function fetchClaudeUsageLimits(accessToken, { fetchImpl = fetch, maxAttem
297
301
  throw new Error(`Claude API returned ${res.status}`);
298
302
  }
299
303
  const body = await res.json();
300
- return {
301
- five_hour: body.five_hour ?? null,
302
- seven_day: body.seven_day ?? null,
303
- seven_day_opus: body.seven_day_opus ?? null,
304
- weekly_scoped: extractClaudeScopedWeekly(body),
305
- extra_usage: body.extra_usage ?? null,
306
- };
304
+ return mapClaudeUsageBody(body);
307
305
  }
308
306
  }
309
307
 
308
+ /** Map an /api/oauth/usage body (live, or Claude Code's cached copy) onto the limits shape. */
309
+ function mapClaudeUsageBody(body) {
310
+ return {
311
+ five_hour: body?.five_hour ?? null,
312
+ seven_day: body?.seven_day ?? null,
313
+ seven_day_opus: body?.seven_day_opus ?? null,
314
+ weekly_scoped: extractClaudeScopedWeekly(body),
315
+ extra_usage: body?.extra_usage ?? null,
316
+ };
317
+ }
318
+
310
319
  // Classify a wham window by `limit_window_seconds` rather than its slot name.
311
320
  // 18000s = 5h session window. 604800s = 7d weekly window. Free-tier accounts only get a
312
321
  // weekly window, often delivered in the `primary_window` slot — naive position-based
@@ -777,28 +786,9 @@ async function fetchCursorLimits({ home, fetchImpl = fetch } = {}) {
777
786
  }
778
787
  }
779
788
 
780
- function resolveKimiHome({ home, env } = {}) {
781
- const explicit = typeof env?.KIMI_HOME === "string" ? env.KIMI_HOME.trim() : "";
782
- if (explicit) return path.resolve(explicit);
783
- const base = home || os.homedir();
784
- // Prefer the official Kimi Code (@moonshot-ai/kimi-code, ~/.kimi-code) when it
785
- // holds a login — its credential file shape (kimi-code.json) and the
786
- // auth/usages endpoints are identical to the legacy kimi-cli (~/.kimi), so the
787
- // existing fetch path works unchanged. Fall back to legacy when kimi-code has
788
- // no credentials, keeping old kimi-cli users untouched.
789
- const explicitCode = typeof env?.KIMI_CODE_HOME === "string" ? env.KIMI_CODE_HOME.trim() : "";
790
- const codeHome = explicitCode ? path.resolve(explicitCode) : path.join(base, ".kimi-code");
791
- const codeCredsPath = path.join(codeHome, "credentials", "kimi-code.json");
792
- try {
793
- const raw = fs.readFileSync(codeCredsPath, "utf8").trim();
794
- if (raw && JSON.parse(raw)?.access_token) return codeHome;
795
- } catch { /* missing / empty / corrupt — fall through to legacy */ }
796
- return path.join(base, ".kimi");
797
- }
798
-
799
- function loadKimiCredentials({ home, env } = {}) {
800
- const kimiHome = resolveKimiHome({ home, env });
801
- const credsPath = path.join(kimiHome, "credentials", "kimi-code.json");
789
+ function loadKimiCredentials({ home, env, profile = resolveKimiProfile({ home, env }) } = {}) {
790
+ if (profile.error) return null;
791
+ const { credsPath } = profile;
802
792
  if (!fs.existsSync(credsPath)) return null;
803
793
  try {
804
794
  return JSON.parse(fs.readFileSync(credsPath, "utf8"));
@@ -807,15 +797,10 @@ function loadKimiCredentials({ home, env } = {}) {
807
797
  }
808
798
  }
809
799
 
810
- function saveKimiCredentials(creds, { home, env } = {}) {
811
- const kimiHome = resolveKimiHome({ home, env });
812
- const credsPath = path.join(kimiHome, "credentials", "kimi-code.json");
800
+ function saveKimiCredentials(creds, { home, env, profile = resolveKimiProfile({ home, env }) } = {}) {
801
+ const { credsPath } = profile;
813
802
  fs.mkdirSync(path.dirname(credsPath), { recursive: true });
814
- fs.writeFileSync(credsPath, JSON.stringify(creds, null, 2));
815
- }
816
-
817
- function hasKimiConfig({ home, env } = {}) {
818
- return fs.existsSync(path.join(resolveKimiHome({ home, env }), "config.toml"));
803
+ fs.writeFileSync(credsPath, JSON.stringify(creds, null, 2), { mode: 0o600 });
819
804
  }
820
805
 
821
806
  function kimiNumber(value) {
@@ -867,7 +852,7 @@ function kimiCredentialsExpired(creds, nowMs = Date.now()) {
867
852
  return expiresAt * 1000 <= nowMs + 30_000;
868
853
  }
869
854
 
870
- async function refreshKimiAccessToken({ refreshToken, home, env, fetchImpl = fetch } = {}) {
855
+ async function refreshKimiAccessToken({ refreshToken, profile, fetchImpl = fetch } = {}) {
871
856
  if (typeof refreshToken !== "string" || !refreshToken.trim()) {
872
857
  throw new Error("Not logged in to Kimi. Run 'kimi' in Terminal to authenticate.");
873
858
  }
@@ -878,8 +863,9 @@ async function refreshKimiAccessToken({ refreshToken, home, env, fetchImpl = fet
878
863
  refresh_token: refreshToken,
879
864
  });
880
865
 
881
- const res = await fetchImpl("https://auth.kimi.com/api/oauth/token", {
866
+ const res = await fetchImpl(`${profile.oauthHost}/api/oauth/token`, {
882
867
  method: "POST",
868
+ redirect: "error",
883
869
  headers: {
884
870
  "Content-Type": "application/x-www-form-urlencoded",
885
871
  "X-Msh-Platform": "kimi_cli",
@@ -907,13 +893,14 @@ async function refreshKimiAccessToken({ refreshToken, home, env, fetchImpl = fet
907
893
  token_type: String(json.token_type || "Bearer"),
908
894
  expires_in: Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : 900,
909
895
  };
910
- saveKimiCredentials(next, { home, env });
896
+ saveKimiCredentials(next, { profile });
911
897
  return next.access_token;
912
898
  }
913
899
 
914
- async function fetchKimiUsage(accessToken, { fetchImpl = fetch } = {}) {
915
- const res = await fetchImpl("https://api.kimi.com/coding/v1/usages", {
900
+ async function fetchKimiUsage(accessToken, { profile, fetchImpl = fetch } = {}) {
901
+ const res = await fetchImpl(`${profile.baseUrl}/usages`, {
916
902
  method: "GET",
903
+ redirect: "error",
917
904
  headers: {
918
905
  Authorization: `Bearer ${accessToken}`,
919
906
  Accept: "application/json",
@@ -929,10 +916,12 @@ async function fetchKimiUsage(accessToken, { fetchImpl = fetch } = {}) {
929
916
  }
930
917
 
931
918
  async function fetchKimiLimits({ home, env, fetchImpl = fetch } = {}) {
932
- if (!hasKimiConfig({ home, env })) {
919
+ const profile = resolveKimiProfile({ home, env });
920
+ if (!profile.configured) {
933
921
  return { configured: false };
934
922
  }
935
- const creds = loadKimiCredentials({ home, env });
923
+ if (profile.error) return { configured: true, error: profile.error };
924
+ const creds = loadKimiCredentials({ profile });
936
925
  let accessToken = typeof creds?.access_token === "string" ? creds.access_token.trim() : "";
937
926
  if (!accessToken) {
938
927
  return { configured: false };
@@ -941,23 +930,21 @@ async function fetchKimiLimits({ home, env, fetchImpl = fetch } = {}) {
941
930
  if (kimiCredentialsExpired(creds) && creds?.refresh_token) {
942
931
  accessToken = await refreshKimiAccessToken({
943
932
  refreshToken: creds.refresh_token,
944
- home,
945
- env,
933
+ profile,
946
934
  fetchImpl,
947
935
  });
948
936
  }
949
937
  let body;
950
938
  try {
951
- body = await fetchKimiUsage(accessToken, { fetchImpl });
939
+ body = await fetchKimiUsage(accessToken, { profile, fetchImpl });
952
940
  } catch (error) {
953
941
  if (error?.message === "token_expired" && creds?.refresh_token) {
954
942
  accessToken = await refreshKimiAccessToken({
955
943
  refreshToken: creds.refresh_token,
956
- home,
957
- env,
944
+ profile,
958
945
  fetchImpl,
959
946
  });
960
- body = await fetchKimiUsage(accessToken, { fetchImpl });
947
+ body = await fetchKimiUsage(accessToken, { profile, fetchImpl });
961
948
  } else {
962
949
  throw error;
963
950
  }
@@ -2262,7 +2249,12 @@ function normalizeAntigravityCachedLimits(raw, { nowMs = Date.now() } = {}) {
2262
2249
  return hasAntigravityWindow(cached) ? cached : null;
2263
2250
  }
2264
2251
 
2252
+ function isAntigravityQuotaDisabled() {
2253
+ return process.env.TOKENTRACKER_DISABLE_ANTIGRAVITY_QUOTA === "1";
2254
+ }
2255
+
2265
2256
  function readAntigravityLimitsCache({ home, nowMs = Date.now() } = {}) {
2257
+ if (isAntigravityQuotaDisabled()) return null;
2266
2258
  const cachePath = resolveAntigravityLimitsCachePath({ home });
2267
2259
  try {
2268
2260
  const parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
@@ -2368,7 +2360,9 @@ function readClaudeLimitsCache({
2368
2360
  maxAgeMs = CLAUDE_LIMITS_CACHE_MAX_AGE_MS,
2369
2361
  stale = true,
2370
2362
  } = {}) {
2371
- return normalizeClaudeCachedLimits(readClaudeLimitsCacheRaw({ home }), { nowMs, maxAgeMs, stale });
2363
+ return pickNewestClaudeSnapshot(readClaudeLimitsCacheCandidates({ home }).map((raw) => (
2364
+ normalizeClaudeCachedLimits(raw, { nowMs, maxAgeMs, stale })
2365
+ )));
2372
2366
  }
2373
2367
 
2374
2368
  // A cached snapshot stops being "fresh" the moment any of its windows crosses the
@@ -2412,16 +2406,60 @@ function claudeCacheAwaitsNewWindow(raw, { home, nowMs } = {}) {
2412
2406
  }
2413
2407
  }
2414
2408
 
2415
- function readFreshClaudeLimitsCache({ home, nowMs = Date.now() } = {}) {
2416
- const raw = readClaudeLimitsCacheRaw({ home });
2417
- if (!raw || claudeCacheCrossedReset(raw, { nowMs }) || claudeCacheAwaitsNewWindow(raw, { home, nowMs })) {
2409
+ // Claude Code keeps its last /api/oauth/usage response in its global config
2410
+ // (`cachedUsageUtilization` in ~/.claude.json). That endpoint is throttled hard and its
2411
+ // budget is shared with Claude Code itself, so reading this copy costs no request. Only
2412
+ // usage figures are read — no credentials. The path deliberately ignores CLAUDE_CONFIG_DIR:
2413
+ // readClaudeCodeOauthToken reads the default profile's credentials (default Keychain item,
2414
+ // ~/.claude/.credentials.json), and the cache must come from that same profile so it can
2415
+ // never describe a different account than the token this process would query with.
2416
+ function resolveClaudeCodeGlobalConfigPath({ home } = {}) {
2417
+ return path.join(home || os.homedir(), ".claude.json");
2418
+ }
2419
+
2420
+ /**
2421
+ * Claude Code's cached usage as a raw cache entry (same shape as our own disk cache),
2422
+ * or null when absent, unparsable, or recorded for a different account than the one
2423
+ * Claude Code is currently signed in to.
2424
+ */
2425
+ function readClaudeCodeUsageCacheRaw({ home } = {}) {
2426
+ try {
2427
+ const config = JSON.parse(fs.readFileSync(resolveClaudeCodeGlobalConfigPath({ home }), "utf8"));
2428
+ const cached = config?.cachedUsageUtilization;
2429
+ const fetchedAtMs = Number(cached?.fetchedAtMs);
2430
+ if (!cached?.utilization || typeof cached.utilization !== "object" || !Number.isFinite(fetchedAtMs)) return null;
2431
+ const cachedAccount = typeof cached.accountUuid === "string" ? cached.accountUuid : null;
2432
+ const currentAccount = typeof config?.oauthAccount?.accountUuid === "string" ? config.oauthAccount.accountUuid : null;
2433
+ if (!cachedAccount || !currentAccount || cachedAccount !== currentAccount) return null;
2434
+ return { ...mapClaudeUsageBody(cached.utilization), cached_at: new Date(fetchedAtMs).toISOString() };
2435
+ } catch (_error) {
2418
2436
  return null;
2419
2437
  }
2420
- return normalizeClaudeCachedLimits(raw, {
2421
- nowMs,
2422
- maxAgeMs: CLAUDE_LIMITS_CACHE_FRESH_TTL_MS,
2423
- stale: false,
2424
- });
2438
+ }
2439
+
2440
+ /** Raw snapshots from both sources: our own disk cache and Claude Code's cached read. */
2441
+ function readClaudeLimitsCacheCandidates({ home } = {}) {
2442
+ return [readClaudeLimitsCacheRaw({ home }), readClaudeCodeUsageCacheRaw({ home })].filter(Boolean);
2443
+ }
2444
+
2445
+ /** Newest of the already-validated snapshots, by cached_at. */
2446
+ function pickNewestClaudeSnapshot(snapshots) {
2447
+ let newest = null;
2448
+ for (const snapshot of snapshots) {
2449
+ if (!snapshot) continue;
2450
+ if (!newest || parseTimeMs(snapshot.cached_at) > parseTimeMs(newest.cached_at)) newest = snapshot;
2451
+ }
2452
+ return newest;
2453
+ }
2454
+
2455
+ // Each source is validated on its own before picking the newest, so a newer snapshot
2456
+ // whose windows are all unusable can never displace an older usable one.
2457
+ function readFreshClaudeLimitsCache({ home, nowMs = Date.now() } = {}) {
2458
+ return pickNewestClaudeSnapshot(readClaudeLimitsCacheCandidates({ home }).map((raw) => (
2459
+ claudeCacheCrossedReset(raw, { nowMs }) || claudeCacheAwaitsNewWindow(raw, { home, nowMs })
2460
+ ? null
2461
+ : normalizeClaudeCachedLimits(raw, { nowMs, maxAgeMs: CLAUDE_LIMITS_CACHE_FRESH_TTL_MS, stale: false })
2462
+ )));
2425
2463
  }
2426
2464
 
2427
2465
  function writeClaudeLimitsCache(limits, { home, nowMs = Date.now() } = {}) {
@@ -3256,6 +3294,7 @@ function loadAntigravityCredentials({
3256
3294
  securityRunner,
3257
3295
  nowMs = Date.now(),
3258
3296
  } = {}) {
3297
+ if (isAntigravityQuotaDisabled()) return null;
3259
3298
  const candidates = collectAntigravityFileCredentials({ home });
3260
3299
  if (platform === "darwin" || typeof securityRunner === "function") {
3261
3300
  const parsed = parseAntigravityCredentialPayload(readAntigravityKeychainRaw({ securityRunner }));
@@ -3403,6 +3442,7 @@ async function fetchAntigravityRemoteLimits({
3403
3442
  signal,
3404
3443
  creds,
3405
3444
  } = {}) {
3445
+ if (isAntigravityQuotaDisabled()) return null;
3406
3446
  const resolvedCreds = creds !== undefined
3407
3447
  ? creds
3408
3448
  : loadAntigravityCredentials({ home, platform, securityRunner, nowMs });
@@ -3497,6 +3537,7 @@ async function fetchAntigravityLimits({
3497
3537
  securityRunner,
3498
3538
  signal,
3499
3539
  } = {}) {
3540
+ if (isAntigravityQuotaDisabled()) return { configured: false, error: null };
3500
3541
  const creds = loadAntigravityCredentials({ home, platform, securityRunner, nowMs });
3501
3542
  const startedAtMs = performance.now();
3502
3543
  // min(this step's ceiling, budget left after reserving the fallback guard).
@@ -3721,7 +3762,8 @@ function withPlanLabel(obj, raw, brand) {
3721
3762
  // hammered). Survives an external resetUsageLimitsCache() (refresh=1 path in
3722
3763
  // local-api.js): a refresh arriving while a fetch is already running reuses that
3723
3764
  // in-flight fetch and returns its result.
3724
- const inFlightByDevinSelection = { off: null, on: null };
3765
+ const inFlightBySelection = {};
3766
+ for (const key of USAGE_LIMITS_SELECTION_KEYS) inFlightBySelection[key] = null;
3725
3767
 
3726
3768
  // Codex stamps reset_at as unix seconds; every other provider (and Claude's
3727
3769
  // resets_at) uses ISO strings. Numbers that look like epoch milliseconds are
@@ -3759,19 +3801,19 @@ function cacheExpiresAtMs(data, fetchedAtMs) {
3759
3801
  }
3760
3802
 
3761
3803
  async function getUsageLimits(options = {}) {
3762
- const selection = devinSelectionKey(options);
3763
- const cache = cacheByDevinSelection[selection];
3804
+ const selection = usageLimitsSelectionKey(options);
3805
+ const cache = cacheBySelection[selection];
3764
3806
  const nowMs = Date.now();
3765
3807
  if (cache.data && nowMs < cache.expiresAtMs) {
3766
3808
  return cache.data;
3767
3809
  }
3768
- if (inFlightByDevinSelection[selection]) {
3769
- return inFlightByDevinSelection[selection];
3810
+ if (inFlightBySelection[selection]) {
3811
+ return inFlightBySelection[selection];
3770
3812
  }
3771
- const promise = fetchUsageLimitsUncached(options).finally(() => {
3772
- if (inFlightByDevinSelection[selection] === promise) inFlightByDevinSelection[selection] = null;
3813
+ const promise = fetchUsageLimitsUncached({ ...options, selectionKey: selection }).finally(() => {
3814
+ if (inFlightBySelection[selection] === promise) inFlightBySelection[selection] = null;
3773
3815
  });
3774
- inFlightByDevinSelection[selection] = promise;
3816
+ inFlightBySelection[selection] = promise;
3775
3817
  return promise;
3776
3818
  }
3777
3819
 
@@ -3787,6 +3829,10 @@ async function fetchUsageLimitsUncached({
3787
3829
  providerTimeoutMs = DEFAULT_PROVIDER_TIMEOUT_MS,
3788
3830
  forceRefresh = false,
3789
3831
  devinEnabled = false,
3832
+ // Selection key fixed by getUsageLimits before the fetch began. Re-deriving it
3833
+ // here would let an opt-out flipped mid-fetch store a pre-opt-out result in the
3834
+ // post-opt-out slot.
3835
+ selectionKey,
3790
3836
  } = {}) {
3791
3837
  const nowMs = Date.now();
3792
3838
 
@@ -3840,7 +3886,8 @@ async function fetchUsageLimitsUncached({
3840
3886
  : null;
3841
3887
  // Also avoid cross-process hammering after a recent successful read: embedded-server
3842
3888
  // restarts and background polls read the disk cache instead of spending another Claude
3843
- // OAuth usage request. An explicit user refresh (refresh=1 → forceRefresh) punches
3889
+ // OAuth usage request. Claude Code's own cached read counts too, so a session that just
3890
+ // refreshed its usage spares this process a call against the same throttled budget. An explicit user refresh (refresh=1 → forceRefresh) punches
3844
3891
  // through this cache — but never through the 429 cooldown above, which is exactly the
3845
3892
  // hammering the cooldown exists to prevent.
3846
3893
  const freshClaudeCache = claudeToken && !forceRefresh
@@ -4255,7 +4302,7 @@ async function fetchUsageLimitsUncached({
4255
4302
  };
4256
4303
  }
4257
4304
 
4258
- cacheByDevinSelection[devinSelectionKey({ devinEnabled })] = {
4305
+ cacheBySelection[selectionKey || usageLimitsSelectionKey({ devinEnabled })] = {
4259
4306
  data,
4260
4307
  expiresAtMs: cacheExpiresAtMs(data, nowMs),
4261
4308
  };
@@ -4263,8 +4310,9 @@ async function fetchUsageLimitsUncached({
4263
4310
  }
4264
4311
 
4265
4312
  function resetUsageLimitsCache() {
4266
- cacheByDevinSelection.off = { data: null, expiresAtMs: 0 };
4267
- cacheByDevinSelection.on = { data: null, expiresAtMs: 0 };
4313
+ for (const key of USAGE_LIMITS_SELECTION_KEYS) {
4314
+ cacheBySelection[key] = { data: null, expiresAtMs: 0 };
4315
+ }
4268
4316
  }
4269
4317
 
4270
4318
  module.exports = {
@@ -4275,6 +4323,7 @@ module.exports = {
4275
4323
  runCommand,
4276
4324
  extractGeminiOauthClientCredentials,
4277
4325
  loadKimiCredentials,
4326
+ fetchKimiLimits,
4278
4327
  normalizeCursorUsageSummary,
4279
4328
  normalizeCursorSandUsageStatus,
4280
4329
  normalizeGeminiQuotaResponse,