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