tokentracker-cli 1.5.3 → 1.5.5
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-BYxdWhS1.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-BCTVT7Jo.js → AchievementsPage-Bv9o7Aj8.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-B6fJSJSr.js → AchievementsSection-DE5bZGxg.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-FjUN__9G.js → BadgeDetailModal-CgZaSVTp.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-B-KqY_Wr.js → BotAnimated-CwhPfUBI.js} +1 -1
- package/dashboard/dist/assets/{Card-DS8T8piP.js → Card-Y7qaxMuE.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-_0UjsFPu.js → CloudCheckoutPage-DVl7jk6b.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-BstgVBaV.js → CloudPage-BnIvpHGH.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-DtVsYZgc.js → CommandPalette-CzMv2Zxn.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-DTpEJqV3.js → CommunityStatsModal-D8sOZqUc.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-Lfdh2vr7.js → ConfirmModal-Bun5xKMm.js} +1 -1
- package/dashboard/dist/assets/{Controls-DpwQ7pkq.js → Controls-ltfnvkFW.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-C3Xu6_3t.js +1 -0
- package/dashboard/dist/assets/{DateRangePopover-CrWgrzT2.js → DateRangePopover-e3gJYqFJ.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-DbK5KWJS.js → DevicePage-JgT16dEp.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-Dd2B_o03.js → DialogClose-DeWert7h.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-Ca6W3nCT.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-3JQBVha3.js → DialogTitle-BfK_yOeR.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-LYAqG8pG.js → ElectricLogo-Bbev0Y8Q.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-DhDvUor7.js → FadeIn-fypYWLDK.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-9f1XmgYC.js → HeaderGithubStar-Cnil2Lbo.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-BXhHAkP7.js → HoverTooltip-Bw_8tru-.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-ChPdDeMl.js → IpCheckPage-CHnhUgA2.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-DnJ7Y9zw.js → LandingPage-Bq4iQS6S.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-BF9isLzu.js → LeaderboardPage-D42lUdw-.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-BM4JKgkC.js → LeaderboardProBadge-eTKceT90.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-DfZABOpN.js → LeaderboardProfileModal-D-3tRrOR.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-BaZ38iT5.js → LeaderboardProfilePage-DkRCJoiA.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-dUXjP3_1.js +6 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-DvhlYoqQ.js → LocalOnlyNotice-CtpjB6Z2.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-g_Gbnzhs.js → LoginCard-zrRd8o54.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-DkJrI6b7.js → LoginModal-DNl5Rt1e.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-CqCKSo0n.js → LoginPage-BuB707vV.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DbF7ROsj.js → PetPage-BpDriXkp.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-4WuH41ZT.js → PopoverPopup-rQNSs8R6.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-6j0A2KD9.js → ProviderIcon-CumyO371.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-DBd-iwuM.js → ResetPasswordPage-ecUHfJAk.js} +1 -1
- package/dashboard/dist/assets/{Select-BBEpLeiG.js → Select-YVEqEhk4.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-Cn_U_rrO.js → SelectItemText-D1NBc4uc.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-2WZc6Rml.js → SelfHostPage-8nAin68c.js} +1 -1
- package/dashboard/dist/assets/ServiceStatusPage-D1zxuzOg.js +1 -0
- package/dashboard/dist/assets/SessionsPage-B6ksEER_.js +3 -0
- package/dashboard/dist/assets/{SettingsPage-rXNr96i-.js → SettingsPage-COqP8ZbJ.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-CudTvbZL.js → SkillsPage-DCX7xTj0.js} +1 -1
- package/dashboard/dist/assets/TokenGalaxy-D4n7AR-R.js +4431 -0
- package/dashboard/dist/assets/{TooltipPopup-CsLKXhWC.js → TooltipPopup-DvSoB_eS.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-8onUTbkk.js → TrendMonitor-5GVzh0Ye.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-CcwE0lz8.js → WidgetsPage-BIc4MBiu.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-Cj9LLYqP.js → WrappedPage-BpQk4g0o.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-B_rKFTTN.js +45 -0
- package/dashboard/dist/assets/__vite-browser-external-CMBWGkOq.css +1 -0
- package/dashboard/dist/assets/{agent-logos-ChBA1nAH.js → agent-logos-DVg-sTm4.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-Bb5mfoPc.js → arrow-left-Ch6Q7eU5.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-D1Ti2XUI.js → arrow-right-B2B9GvSI.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-B_CjA_y8.js → arrow-up-right-DwNO_uSb.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-DdfrAqie.js → calendar-days---NjJlBi.js} +1 -1
- package/dashboard/dist/assets/{check-29zGw1MK.js → check-D3mhgRrW.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-DAzlFX7g.js → chevron-down-x-b39Zto.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-C2SeCjFM.js → cloud-checkout-CycWyTsg.js} +1 -1
- package/dashboard/dist/assets/{composite-DIf735Hq.js → composite-CnA1qtYn.js} +1 -1
- package/dashboard/dist/assets/{copy-BOuOM0HQ.js → copy-JRnRMgdX.js} +1 -1
- package/dashboard/dist/assets/{download-50tQRpGe.js → download-CRLqGD62.js} +1 -1
- package/dashboard/dist/assets/{external-link-BhlQClVe.js → external-link-BrmaWV_t.js} +1 -1
- package/dashboard/dist/assets/{flame-CeGfA-0L.js → flame-meQ5-vRD.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-Cq7YiAkJ.js → format-tokens-D8mIJGz1.js} +1 -1
- package/dashboard/dist/assets/{gift-Do-UFhiT.js → gift-Cd0FPXgO.js} +1 -1
- package/dashboard/dist/assets/{icons-C6EZ_HmP.js → icons-D-PTKw7b.js} +1 -1
- package/dashboard/dist/assets/{index-BWhbBAd9.js → index-CMV9UKNf.js} +1 -1
- package/dashboard/dist/assets/{index-c0crpe9Z.js → index-C_6gEM83.js} +1 -1
- package/dashboard/dist/assets/{index-BFrbVqsm.js → index-DCIZaLNC.js} +1 -1
- package/dashboard/dist/assets/index-HLYcwmPD.js +502 -0
- package/dashboard/dist/assets/{info-B4MXmsNE.js → info-e0tP9P3D.js} +1 -1
- package/dashboard/dist/assets/{link-2-DuWCslzl.js → link-2-DZ2TQhER.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-C2I3NZAO.js → loader-circle-DjflRntu.js} +1 -1
- package/dashboard/dist/assets/{local-usage-transfer-BQasvsUF.js → local-usage-transfer-XTZLA3XH.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-DbeNS6FT.js → modifiers.esm-BpEQ8-Gt.js} +1 -1
- package/dashboard/dist/assets/module-BbVsRaE2.js +3 -0
- package/dashboard/dist/assets/{monitor-smartphone-C6Y4ktsc.js → monitor-smartphone-VVBcI30G.js} +1 -1
- package/dashboard/dist/assets/{play-CwgcDJC2.js → play-DN4Sb3W_.js} +1 -1
- package/dashboard/dist/assets/{plus-B796wsUO.js → plus-Cz_k7arH.js} +1 -1
- package/dashboard/dist/assets/{provider-display-Cw26RFDm.js → provider-display-S1mSaydF.js} +1 -1
- package/dashboard/dist/assets/{react-d7Bk1-_K.js → react-MQBcjrn_.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-CDE0dNv_.js → safePolygon-InKxYcXd.js} +1 -1
- package/dashboard/dist/assets/{search-DtKvsdRs.js → search-C-N0RDi0.js} +1 -1
- package/dashboard/dist/assets/{server-gAURHUyz.js → server-CZkn8vIZ.js} +1 -1
- package/dashboard/dist/assets/{shield-check-DFVdDTwh.js → shield-check-DPrGFouZ.js} +1 -1
- package/dashboard/dist/assets/{skills-api-2rdPUrJI.js → skills-api-wMwM-GpF.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-DFlfwnJr.js → sortable.esm-CUPP1ihT.js} +1 -1
- package/dashboard/dist/assets/{store-BKPCk_de.js → store-BpVdRYnQ.js} +1 -1
- package/dashboard/dist/assets/{terminal-BtWFakvS.js → terminal-C-7TFeKg.js} +1 -1
- package/dashboard/dist/assets/{trash-bpr9zTAM.js → trash-Be8cd4HD.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DMnGgLne.js → use-community-stats-cG0WrB4Y.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-BSBlJJHe.js → use-latest-request-guard-C91enyPD.js} +1 -1
- package/dashboard/dist/assets/use-limits-display-prefs-DdwoLqPD.js +1 -0
- package/dashboard/dist/assets/{use-native-settings-OthqiF6g.js → use-native-settings-BR-NiMKh.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-DpAtr0R-.js → use-ordered-list-oqV724BP.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-DW5YHBBI.js +1 -0
- package/dashboard/dist/assets/{use-transform-BRz9dz_2.js → use-transform-BsNSgVD6.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-CwmhudAX.js → use-usage-limits-Dk8LcMU-.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-CAA-tQ27.js → useCurrency-FxerRfd2.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-Cj2DPmwM.js → usePositioner-D2J-rSfX.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-Bf5-f7Hx.js → useTokenFormat-CaI2nl2C.js} +1 -1
- package/dashboard/dist/assets/{zap-B8Fg6PaC.js → zap-D3C37XAj.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 +4 -4
- package/src/commands/serve.js +3 -2
- package/src/lib/accept-encoding.js +59 -0
- package/src/lib/cursor-config.js +18 -2
- 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/curated-overrides.json +14 -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/rollout.js +13 -4
- package/src/lib/session-analytics.js +194 -29
- package/src/lib/static-server.js +74 -2
- package/src/lib/usage-limits.js +801 -26
- 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/TokenGalaxy-BRKrbTsJ.js +0 -4426
- 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/index-D9kwYWLX.js +0 -502
- package/dashboard/dist/assets/module-ZyVsxBG_.js +0 -3
- 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
|
|
@@ -130,6 +131,7 @@ const ANTIGRAVITY_QUOTA_SUMMARY_URLS = Object.freeze([
|
|
|
130
131
|
"https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary",
|
|
131
132
|
"https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary",
|
|
132
133
|
]);
|
|
134
|
+
const ANTIGRAVITY_AVAILABLE_MODELS_URL = "https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels";
|
|
133
135
|
const ANTIGRAVITY_USER_AGENT = "antigravity/2.9.1";
|
|
134
136
|
const ANTIGRAVITY_TOKEN_REFRESH_SKEW_MS = 60 * 1000;
|
|
135
137
|
// Per-step ceilings for the Antigravity serial chain. `providerTimeoutMs` bounds the
|
|
@@ -172,6 +174,30 @@ const CODEX_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
172
174
|
const OPENCODE_GO_LIMITS_CACHE_FILE = "opencode-go-usage-limits-cache.json";
|
|
173
175
|
const OPENCODE_GO_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
174
176
|
const OPENCODE_GO_LOCAL_ESTIMATE_CACHE_MAX_AGE_MS = 60 * 60 * 1000;
|
|
177
|
+
// Cursor's web summary (cursor.com) and Grok Bot RPCs (api2.cursor.sh) both traverse
|
|
178
|
+
// proxied routes where a timeout or 5xx previously blanked the card until the next
|
|
179
|
+
// poll. Persist the last successful windows so a blip serves stale bars instead
|
|
180
|
+
// (mirrors Claude / Codex / OpenCode Go).
|
|
181
|
+
const CURSOR_LIMITS_CACHE_FILE = "cursor-usage-limits-cache.json";
|
|
182
|
+
const CURSOR_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
183
|
+
// Gemini's Code Assist quota RPCs (cloudcode-pa.googleapis.com) are routinely
|
|
184
|
+
// degraded on GFW-affected routes; a timeout or 5xx previously blanked the card
|
|
185
|
+
// until the next poll. Persist the last successful windows so a blip serves
|
|
186
|
+
// stale bars instead (mirrors Cursor / Codex / OpenCode Go).
|
|
187
|
+
const GEMINI_LIMITS_CACHE_FILE = "gemini-usage-limits-cache.json";
|
|
188
|
+
const GEMINI_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
189
|
+
// The Kimi coding API (api.kimi.com / api.kimi.ai) is another proxied route where
|
|
190
|
+
// a timeout or 5xx previously blanked the card until the next poll. Persist the
|
|
191
|
+
// last successful windows so a blip serves stale bars instead
|
|
192
|
+
// (mirrors Cursor / Codex / OpenCode Go).
|
|
193
|
+
const KIMI_LIMITS_CACHE_FILE = "kimi-usage-limits-cache.json";
|
|
194
|
+
const KIMI_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
195
|
+
// Grok Build's billing proxy (cli-chat-proxy.grok.com) sits behind the same kind of
|
|
196
|
+
// proxied routes where a timeout or 5xx previously blanked the card until the next
|
|
197
|
+
// poll. Persist the last successful windows so a blip serves stale bars instead
|
|
198
|
+
// (mirrors Claude / Codex / Cursor).
|
|
199
|
+
const GROK_LIMITS_CACHE_FILE = "grok-usage-limits-cache.json";
|
|
200
|
+
const GROK_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
175
201
|
// A 429 from the usage endpoint carries a long `retry-after` (often 20+ minutes). Persist
|
|
176
202
|
// the cooldown so every surface — this process, the menu bar app's embedded server, a later
|
|
177
203
|
// restart — stops calling until it expires. Hammering during the cooldown just renews the
|
|
@@ -860,11 +886,11 @@ function normalizeCursorSandUsageStatus(body, { eligible = true } = {}) {
|
|
|
860
886
|
return buildWindow({ usedPercent, resetAt, windowSeconds });
|
|
861
887
|
}
|
|
862
888
|
|
|
863
|
-
async function fetchCursorLimits({ home, fetchImpl = fetch } = {}) {
|
|
864
|
-
if (!isCursorInstalled({ home })) {
|
|
889
|
+
async function fetchCursorLimits({ home, env, fetchImpl = fetch } = {}) {
|
|
890
|
+
if (!isCursorInstalled({ home, env })) {
|
|
865
891
|
return { configured: false };
|
|
866
892
|
}
|
|
867
|
-
const auth = extractCursorAuth({ home });
|
|
893
|
+
const auth = extractCursorAuth({ home, env });
|
|
868
894
|
if (!auth?.cookie || !auth?.accessToken) {
|
|
869
895
|
return { configured: false };
|
|
870
896
|
}
|
|
@@ -887,11 +913,18 @@ async function fetchCursorLimits({ home, fetchImpl = fetch } = {}) {
|
|
|
887
913
|
error: null,
|
|
888
914
|
...normalizeCursorUsageSummary(body),
|
|
889
915
|
quaternary_window: grokBotWindow,
|
|
916
|
+
// Internal cache-partition key (stripped before the payload is served).
|
|
917
|
+
cache_identity: auth.userId,
|
|
890
918
|
};
|
|
891
919
|
} catch (error) {
|
|
892
920
|
return {
|
|
893
921
|
configured: true,
|
|
894
922
|
error: error?.message || "Unknown error",
|
|
923
|
+
// A session-expiry error is actionable (re-login) and must not be hidden
|
|
924
|
+
// behind the stale disk-cache fallback (opencode-go auth_error precedent).
|
|
925
|
+
// Internal marker — stripped before the payload is served.
|
|
926
|
+
...(error?.code === "AUTH_EXPIRED" ? { auth_error: true } : {}),
|
|
927
|
+
cache_identity: auth.userId,
|
|
895
928
|
};
|
|
896
929
|
}
|
|
897
930
|
}
|
|
@@ -964,7 +997,9 @@ function kimiCredentialsExpired(creds, nowMs = Date.now()) {
|
|
|
964
997
|
|
|
965
998
|
async function refreshKimiAccessToken({ refreshToken, profile, fetchImpl = fetch } = {}) {
|
|
966
999
|
if (typeof refreshToken !== "string" || !refreshToken.trim()) {
|
|
967
|
-
|
|
1000
|
+
const error = new Error("Not logged in to Kimi. Run 'kimi' in Terminal to authenticate.");
|
|
1001
|
+
error.code = "AUTH_EXPIRED";
|
|
1002
|
+
throw error;
|
|
968
1003
|
}
|
|
969
1004
|
|
|
970
1005
|
const body = new URLSearchParams({
|
|
@@ -983,7 +1018,9 @@ async function refreshKimiAccessToken({ refreshToken, profile, fetchImpl = fetch
|
|
|
983
1018
|
body,
|
|
984
1019
|
});
|
|
985
1020
|
if (res.status === 401 || res.status === 403) {
|
|
986
|
-
|
|
1021
|
+
const error = new Error("Not logged in to Kimi. Run 'kimi' in Terminal to authenticate.");
|
|
1022
|
+
error.code = "AUTH_EXPIRED";
|
|
1023
|
+
throw error;
|
|
987
1024
|
}
|
|
988
1025
|
if (!res.ok) {
|
|
989
1026
|
throw new Error(`Kimi token refresh failed (HTTP ${res.status})`);
|
|
@@ -1017,7 +1054,11 @@ async function fetchKimiUsage(accessToken, { profile, fetchImpl = fetch } = {})
|
|
|
1017
1054
|
},
|
|
1018
1055
|
});
|
|
1019
1056
|
if (res.status === 401) {
|
|
1020
|
-
|
|
1057
|
+
// The retry-with-refresh path keys off this message; the code marks the
|
|
1058
|
+
// error actionable (re-login) so the cache fallback never hides it.
|
|
1059
|
+
const error = new Error("token_expired");
|
|
1060
|
+
error.code = "AUTH_EXPIRED";
|
|
1061
|
+
throw error;
|
|
1021
1062
|
}
|
|
1022
1063
|
if (!res.ok) {
|
|
1023
1064
|
throw new Error(`Kimi API returned ${res.status}`);
|
|
@@ -1030,12 +1071,17 @@ async function fetchKimiLimits({ home, env, fetchImpl = fetch } = {}) {
|
|
|
1030
1071
|
if (!profile.configured) {
|
|
1031
1072
|
return { configured: false };
|
|
1032
1073
|
}
|
|
1033
|
-
|
|
1074
|
+
// A profile misconfiguration (unsupported endpoints, non-file credential
|
|
1075
|
+
// storage) is actionable — the user must fix their config — so it must not
|
|
1076
|
+
// be hidden behind the stale disk-cache fallback. Internal marker —
|
|
1077
|
+
// stripped before the payload is served.
|
|
1078
|
+
if (profile.error) return { configured: true, error: profile.error, config_error: true };
|
|
1034
1079
|
const creds = loadKimiCredentials({ profile });
|
|
1035
1080
|
let accessToken = typeof creds?.access_token === "string" ? creds.access_token.trim() : "";
|
|
1036
1081
|
if (!accessToken) {
|
|
1037
1082
|
return { configured: false };
|
|
1038
1083
|
}
|
|
1084
|
+
const cacheIdentity = kimiProfileIdentity(profile);
|
|
1039
1085
|
try {
|
|
1040
1086
|
if (kimiCredentialsExpired(creds) && creds?.refresh_token) {
|
|
1041
1087
|
accessToken = await refreshKimiAccessToken({
|
|
@@ -1063,11 +1109,18 @@ async function fetchKimiLimits({ home, env, fetchImpl = fetch } = {}) {
|
|
|
1063
1109
|
configured: true,
|
|
1064
1110
|
error: null,
|
|
1065
1111
|
...normalizeKimiUsageResponse(body),
|
|
1112
|
+
// Internal cache-partition key (stripped before the payload is served).
|
|
1113
|
+
cache_identity: cacheIdentity,
|
|
1066
1114
|
};
|
|
1067
1115
|
} catch (error) {
|
|
1068
1116
|
return {
|
|
1069
1117
|
configured: true,
|
|
1070
1118
|
error: error?.message || "Unknown error",
|
|
1119
|
+
// A session-expiry error is actionable (re-login) and must not be hidden
|
|
1120
|
+
// behind the stale disk-cache fallback (opencode-go auth_error precedent).
|
|
1121
|
+
// Internal marker — stripped before the payload is served.
|
|
1122
|
+
...(error?.code === "AUTH_EXPIRED" ? { auth_error: true } : {}),
|
|
1123
|
+
cache_identity: cacheIdentity,
|
|
1071
1124
|
};
|
|
1072
1125
|
}
|
|
1073
1126
|
}
|
|
@@ -1199,6 +1252,12 @@ async function extractGeminiOauthClientCredentials({ commandRunner, home } = {})
|
|
|
1199
1252
|
return { ...GEMINI_CLI_FALLBACK_OAUTH_CLIENT };
|
|
1200
1253
|
}
|
|
1201
1254
|
|
|
1255
|
+
// Shared by the refresh-401 path, the missing-credentials path, and the quota
|
|
1256
|
+
// endpoint's 401: all three mean the Google OAuth session is gone. Re-login is
|
|
1257
|
+
// actionable, so none of them may be hidden behind the stale disk-cache
|
|
1258
|
+
// fallback (opencode-go auth_error precedent).
|
|
1259
|
+
const GEMINI_NOT_LOGGED_IN_MESSAGE = "Not logged in to Gemini. Run 'gemini' in Terminal to authenticate.";
|
|
1260
|
+
|
|
1202
1261
|
async function refreshGeminiAccessToken({
|
|
1203
1262
|
refreshToken,
|
|
1204
1263
|
home,
|
|
@@ -1224,7 +1283,14 @@ async function refreshGeminiAccessToken({
|
|
|
1224
1283
|
body,
|
|
1225
1284
|
});
|
|
1226
1285
|
if (!res.ok) {
|
|
1227
|
-
|
|
1286
|
+
const error = new Error(GEMINI_NOT_LOGGED_IN_MESSAGE);
|
|
1287
|
+
// Confirmed credential failures only (invalid_grant = 400, invalid_client
|
|
1288
|
+
// = 401/403): a transient 429/5xx from the token endpoint must stay
|
|
1289
|
+
// cache-eligible so the stale fallback can still serve bars.
|
|
1290
|
+
if (res.status === 400 || res.status === 401 || res.status === 403) {
|
|
1291
|
+
error.code = "AUTH_EXPIRED";
|
|
1292
|
+
}
|
|
1293
|
+
throw error;
|
|
1228
1294
|
}
|
|
1229
1295
|
|
|
1230
1296
|
const json = await res.json();
|
|
@@ -1365,18 +1431,38 @@ async function fetchGeminiLimits({ home, env, fetchImpl = fetch, commandRunner }
|
|
|
1365
1431
|
if (!settings && !credentials) {
|
|
1366
1432
|
return { configured: false };
|
|
1367
1433
|
}
|
|
1434
|
+
// An unsupported auth mode is a stable, actionable configuration state: the
|
|
1435
|
+
// quota RPC can never succeed here and stale OAuth-era bars would be
|
|
1436
|
+
// misleading, so it punches through the cache like an auth error.
|
|
1368
1437
|
if (selectedType === "api-key") {
|
|
1369
|
-
return {
|
|
1438
|
+
return {
|
|
1439
|
+
configured: true,
|
|
1440
|
+
error: "Gemini API key auth not supported. Use Google account (OAuth) instead.",
|
|
1441
|
+
auth_error: true,
|
|
1442
|
+
};
|
|
1370
1443
|
}
|
|
1371
1444
|
if (selectedType === "vertex-ai") {
|
|
1372
|
-
return {
|
|
1445
|
+
return {
|
|
1446
|
+
configured: true,
|
|
1447
|
+
error: "Gemini Vertex AI auth not supported. Use Google account (OAuth) instead.",
|
|
1448
|
+
auth_error: true,
|
|
1449
|
+
};
|
|
1373
1450
|
}
|
|
1374
1451
|
|
|
1375
1452
|
const creds = credentials;
|
|
1376
1453
|
if (!creds?.access_token) {
|
|
1377
|
-
return {
|
|
1454
|
+
return {
|
|
1455
|
+
configured: true,
|
|
1456
|
+
error: GEMINI_NOT_LOGGED_IN_MESSAGE,
|
|
1457
|
+
auth_error: true,
|
|
1458
|
+
};
|
|
1378
1459
|
}
|
|
1379
1460
|
|
|
1461
|
+
// The Google account email from the local id_token JWT partitions the disk
|
|
1462
|
+
// cache (zcode login-identity precedent). Decoded before the network work —
|
|
1463
|
+
// it cannot throw — so the failure path below still knows who we serve for.
|
|
1464
|
+
const claims = decodeJwtPayload(creds.id_token);
|
|
1465
|
+
|
|
1380
1466
|
try {
|
|
1381
1467
|
let accessToken = creds.access_token;
|
|
1382
1468
|
const expiry = Number(creds.expiry_date);
|
|
@@ -1390,7 +1476,6 @@ async function fetchGeminiLimits({ home, env, fetchImpl = fetch, commandRunner }
|
|
|
1390
1476
|
});
|
|
1391
1477
|
}
|
|
1392
1478
|
|
|
1393
|
-
const claims = decodeJwtPayload(creds.id_token);
|
|
1394
1479
|
const codeAssist = await loadGeminiCodeAssistStatus(accessToken, { fetchImpl });
|
|
1395
1480
|
const res = await fetchImpl("https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota", {
|
|
1396
1481
|
method: "POST",
|
|
@@ -1401,7 +1486,18 @@ async function fetchGeminiLimits({ home, env, fetchImpl = fetch, commandRunner }
|
|
|
1401
1486
|
body: JSON.stringify(codeAssist.projectId ? { project: codeAssist.projectId } : {}),
|
|
1402
1487
|
});
|
|
1403
1488
|
if (res.status === 401) {
|
|
1404
|
-
|
|
1489
|
+
const error = new Error(GEMINI_NOT_LOGGED_IN_MESSAGE);
|
|
1490
|
+
error.code = "AUTH_EXPIRED";
|
|
1491
|
+
throw error;
|
|
1492
|
+
}
|
|
1493
|
+
if (res.status === 403) {
|
|
1494
|
+
// PERMISSION_DENIED (seen for Enterprise accounts, gemini-cli#25512):
|
|
1495
|
+
// re-login cannot fix it, but stale bars for a project the account can
|
|
1496
|
+
// no longer see would mislead — punch through like an auth error while
|
|
1497
|
+
// keeping the displayed message unchanged.
|
|
1498
|
+
const error = new Error(`Gemini API error: HTTP ${res.status}`);
|
|
1499
|
+
error.code = "AUTH_EXPIRED";
|
|
1500
|
+
throw error;
|
|
1405
1501
|
}
|
|
1406
1502
|
if (!res.ok) {
|
|
1407
1503
|
throw new Error(`Gemini API error: HTTP ${res.status}`);
|
|
@@ -1415,11 +1511,19 @@ async function fetchGeminiLimits({ home, env, fetchImpl = fetch, commandRunner }
|
|
|
1415
1511
|
email: claims?.email || null,
|
|
1416
1512
|
tier: codeAssist.tier,
|
|
1417
1513
|
}),
|
|
1514
|
+
// Internal cache-partition key (stripped before the payload is served):
|
|
1515
|
+
// the same non-secret id the payload already surfaces as account_email.
|
|
1516
|
+
cache_identity: claims?.email || null,
|
|
1418
1517
|
};
|
|
1419
1518
|
} catch (error) {
|
|
1420
1519
|
return {
|
|
1421
1520
|
configured: true,
|
|
1422
1521
|
error: error?.message || "Unknown error",
|
|
1522
|
+
// A session-expiry error is actionable (re-login) and must not be hidden
|
|
1523
|
+
// behind the stale disk-cache fallback. Internal marker — stripped
|
|
1524
|
+
// before the payload is served.
|
|
1525
|
+
...(error?.code === "AUTH_EXPIRED" ? { auth_error: true } : {}),
|
|
1526
|
+
cache_identity: claims?.email || null,
|
|
1423
1527
|
};
|
|
1424
1528
|
}
|
|
1425
1529
|
}
|
|
@@ -2767,6 +2871,423 @@ function writeOpencodeGoLimitsCache(limits, { trackerDir, home, nowMs = Date.now
|
|
|
2767
2871
|
} catch (_error) {}
|
|
2768
2872
|
}
|
|
2769
2873
|
|
|
2874
|
+
function resolveCursorLimitsCachePath({ home } = {}) {
|
|
2875
|
+
return path.join(home || os.homedir(), ".tokentracker", "tracker", CURSOR_LIMITS_CACHE_FILE);
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
function hasCursorWindow(limits) {
|
|
2879
|
+
return Boolean(
|
|
2880
|
+
limits?.primary_window
|
|
2881
|
+
|| limits?.secondary_window
|
|
2882
|
+
|| limits?.tertiary_window
|
|
2883
|
+
|| limits?.quaternary_window,
|
|
2884
|
+
);
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
// Cursor windows carry `reset_at` as an ISO string (billing-cycle end for the three
|
|
2888
|
+
// summary windows, the RPC's own expiry for the Grok Bot window). A window whose reset
|
|
2889
|
+
// has already passed is stale data, not a usable fallback, so drop it. Windows without
|
|
2890
|
+
// a reset stamp are kept — the overall cached_at max-age gate already bounds them.
|
|
2891
|
+
function isCursorCacheWindowUsable(window, { nowMs } = {}) {
|
|
2892
|
+
if (!window || typeof window !== "object") return false;
|
|
2893
|
+
const resetAtMs = parseTimeMs(window.reset_at);
|
|
2894
|
+
if (resetAtMs === null) return true;
|
|
2895
|
+
return resetAtMs > nowMs;
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
function normalizeCursorCachedLimits(
|
|
2899
|
+
raw,
|
|
2900
|
+
{ nowMs = Date.now(), maxAgeMs = CURSOR_LIMITS_CACHE_MAX_AGE_MS } = {},
|
|
2901
|
+
) {
|
|
2902
|
+
const cachedAtMs = parseTimeMs(raw?.cached_at);
|
|
2903
|
+
if (!Number.isFinite(cachedAtMs)) return null;
|
|
2904
|
+
if (cachedAtMs > nowMs + 60_000) return null;
|
|
2905
|
+
if (nowMs - cachedAtMs > maxAgeMs) return null;
|
|
2906
|
+
|
|
2907
|
+
const cached = {
|
|
2908
|
+
configured: true,
|
|
2909
|
+
error: null,
|
|
2910
|
+
membership_type: typeof raw?.membership_type === "string" ? raw.membership_type : null,
|
|
2911
|
+
primary_window: isCursorCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
|
|
2912
|
+
secondary_window: isCursorCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
|
|
2913
|
+
tertiary_window: isCursorCacheWindowUsable(raw?.tertiary_window, { nowMs }) ? raw.tertiary_window : null,
|
|
2914
|
+
quaternary_window: isCursorCacheWindowUsable(raw?.quaternary_window, { nowMs }) ? raw.quaternary_window : null,
|
|
2915
|
+
stale: true,
|
|
2916
|
+
cached_at: raw.cached_at,
|
|
2917
|
+
};
|
|
2918
|
+
return hasCursorWindow(cached) ? cached : null;
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
// The signed-in Cursor account partitions the cache (zcode login-identity precedent):
|
|
2922
|
+
// the id is the non-secret normalized subject Cursor itself puts in the session cookie.
|
|
2923
|
+
// A single home holds one active login, so one stamped entry suffices; a read requires
|
|
2924
|
+
// the stored identity to match the current one, and a write without an identity is
|
|
2925
|
+
// refused — an unreadable login must never pin another account's bars in place.
|
|
2926
|
+
function readCursorLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
|
|
2927
|
+
if (!identity) return null;
|
|
2928
|
+
try {
|
|
2929
|
+
const parsed = JSON.parse(fs.readFileSync(resolveCursorLimitsCachePath({ home }), "utf8"));
|
|
2930
|
+
const entry = parsed?.cursor;
|
|
2931
|
+
if (entry?.identity !== identity) return null;
|
|
2932
|
+
return normalizeCursorCachedLimits(entry, { nowMs });
|
|
2933
|
+
} catch (_error) {
|
|
2934
|
+
return null;
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
function writeCursorLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
|
|
2939
|
+
if (!identity) return;
|
|
2940
|
+
if (!limits?.configured || limits.error || !hasCursorWindow(limits)) return;
|
|
2941
|
+
const cachePath = resolveCursorLimitsCachePath({ home });
|
|
2942
|
+
const payload = {
|
|
2943
|
+
cursor: {
|
|
2944
|
+
identity,
|
|
2945
|
+
membership_type: limits.membership_type || null,
|
|
2946
|
+
primary_window: limits.primary_window || null,
|
|
2947
|
+
secondary_window: limits.secondary_window || null,
|
|
2948
|
+
tertiary_window: limits.tertiary_window || null,
|
|
2949
|
+
quaternary_window: limits.quaternary_window || null,
|
|
2950
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
2951
|
+
},
|
|
2952
|
+
};
|
|
2953
|
+
try {
|
|
2954
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
2955
|
+
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
2956
|
+
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
2957
|
+
fs.renameSync(tmpPath, cachePath);
|
|
2958
|
+
} catch (_error) {}
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
// Failure-only identity re-derivation for the cache fallback: when the provider slot's
|
|
2962
|
+
// result was produced by the outer timeout race, the fetcher's own identity field is
|
|
2963
|
+
// lost, so read the (still-local) state DB once to learn who we are serving for.
|
|
2964
|
+
function resolveCursorCacheIdentity({ home, env } = {}) {
|
|
2965
|
+
try {
|
|
2966
|
+
return extractCursorAuth({ home, env })?.userId || null;
|
|
2967
|
+
} catch (_error) {
|
|
2968
|
+
return null;
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
function resolveGeminiLimitsCachePath({ home } = {}) {
|
|
2973
|
+
return path.join(home || os.homedir(), ".tokentracker", "tracker", GEMINI_LIMITS_CACHE_FILE);
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
function hasGeminiWindow(limits) {
|
|
2977
|
+
return Boolean(
|
|
2978
|
+
limits?.primary_window
|
|
2979
|
+
|| limits?.secondary_window
|
|
2980
|
+
|| limits?.tertiary_window,
|
|
2981
|
+
);
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
// Gemini windows carry `reset_at` as an ISO string (the Code Assist bucket's own
|
|
2985
|
+
// resetTime — daily for free-tier quotas). A window whose reset has already passed
|
|
2986
|
+
// is stale data, not a usable fallback, so drop it. Windows without a reset stamp
|
|
2987
|
+
// are kept — the overall cached_at max-age gate already bounds them.
|
|
2988
|
+
function isGeminiCacheWindowUsable(window, { nowMs } = {}) {
|
|
2989
|
+
if (!window || typeof window !== "object") return false;
|
|
2990
|
+
const resetAtMs = parseTimeMs(window.reset_at);
|
|
2991
|
+
if (resetAtMs === null) return true;
|
|
2992
|
+
return resetAtMs > nowMs;
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
function normalizeGeminiCachedLimits(
|
|
2996
|
+
raw,
|
|
2997
|
+
{ nowMs = Date.now(), maxAgeMs = GEMINI_LIMITS_CACHE_MAX_AGE_MS } = {},
|
|
2998
|
+
) {
|
|
2999
|
+
const cachedAtMs = parseTimeMs(raw?.cached_at);
|
|
3000
|
+
if (!Number.isFinite(cachedAtMs)) return null;
|
|
3001
|
+
if (cachedAtMs > nowMs + 60_000) return null;
|
|
3002
|
+
if (nowMs - cachedAtMs > maxAgeMs) return null;
|
|
3003
|
+
|
|
3004
|
+
const cached = {
|
|
3005
|
+
configured: true,
|
|
3006
|
+
error: null,
|
|
3007
|
+
account_email: typeof raw?.account_email === "string" ? raw.account_email : null,
|
|
3008
|
+
account_plan: typeof raw?.account_plan === "string" ? raw.account_plan : null,
|
|
3009
|
+
primary_window: isGeminiCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
|
|
3010
|
+
secondary_window: isGeminiCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
|
|
3011
|
+
tertiary_window: isGeminiCacheWindowUsable(raw?.tertiary_window, { nowMs }) ? raw.tertiary_window : null,
|
|
3012
|
+
stale: true,
|
|
3013
|
+
cached_at: raw.cached_at,
|
|
3014
|
+
};
|
|
3015
|
+
return hasGeminiWindow(cached) ? cached : null;
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
// The signed-in Google account partitions the cache (zcode login-identity
|
|
3019
|
+
// precedent): the email decoded from the local id_token JWT is non-secret and is
|
|
3020
|
+
// the same id the payload already surfaces as account_email. GEMINI_HOME can aim
|
|
3021
|
+
// the credential lookup at a different installation, but the cache stays keyed by
|
|
3022
|
+
// account — a read requires the stored identity to match the account the current
|
|
3023
|
+
// credentials resolve to, and a write without an identity is refused: an
|
|
3024
|
+
// unreadable login must never pin another account's bars in place.
|
|
3025
|
+
function readGeminiLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
|
|
3026
|
+
if (!identity) return null;
|
|
3027
|
+
try {
|
|
3028
|
+
const parsed = JSON.parse(fs.readFileSync(resolveGeminiLimitsCachePath({ home }), "utf8"));
|
|
3029
|
+
const entry = parsed?.gemini;
|
|
3030
|
+
if (entry?.identity !== identity) return null;
|
|
3031
|
+
return normalizeGeminiCachedLimits(entry, { nowMs });
|
|
3032
|
+
} catch (_error) {
|
|
3033
|
+
return null;
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
function writeGeminiLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
|
|
3038
|
+
if (!identity) return;
|
|
3039
|
+
if (!limits?.configured || limits.error || !hasGeminiWindow(limits)) return;
|
|
3040
|
+
const cachePath = resolveGeminiLimitsCachePath({ home });
|
|
3041
|
+
const payload = {
|
|
3042
|
+
gemini: {
|
|
3043
|
+
identity,
|
|
3044
|
+
account_email: limits.account_email || null,
|
|
3045
|
+
account_plan: limits.account_plan || null,
|
|
3046
|
+
primary_window: limits.primary_window || null,
|
|
3047
|
+
secondary_window: limits.secondary_window || null,
|
|
3048
|
+
tertiary_window: limits.tertiary_window || null,
|
|
3049
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
3050
|
+
},
|
|
3051
|
+
};
|
|
3052
|
+
try {
|
|
3053
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
3054
|
+
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
3055
|
+
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
3056
|
+
fs.renameSync(tmpPath, cachePath);
|
|
3057
|
+
} catch (_error) {}
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
// Failure-only identity re-derivation for the cache fallback: when the provider
|
|
3061
|
+
// slot's result was produced by the outer timeout race, the fetcher's own
|
|
3062
|
+
// identity field is lost, so decode the (still-local) id_token once to learn
|
|
3063
|
+
// who we are serving for.
|
|
3064
|
+
function resolveGeminiCacheIdentity({ home, env } = {}) {
|
|
3065
|
+
try {
|
|
3066
|
+
return decodeJwtPayload(loadGeminiCredentials({ home, env })?.id_token)?.email || null;
|
|
3067
|
+
} catch (_error) {
|
|
3068
|
+
return null;
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
// The active Kimi profile partitions the cache (ark profile_identity precedent):
|
|
3073
|
+
// mainland and global logins — and any KIMI_HOME override — resolve to different
|
|
3074
|
+
// endpoint-scoped credential slots, and one slot must never serve another's
|
|
3075
|
+
// windows. The profile home plus OAuth host is non-secret and derivable from
|
|
3076
|
+
// local files alone (the credential file holds no account id).
|
|
3077
|
+
function kimiProfileIdentity(profile) {
|
|
3078
|
+
if (!profile?.home || !profile?.oauthHost) return null;
|
|
3079
|
+
return `${profile.home}::${profile.oauthHost}`;
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
// Failure-only identity re-derivation for the cache fallback: when the provider
|
|
3083
|
+
// slot's result was produced by the outer timeout race, the fetcher's own
|
|
3084
|
+
// identity field is lost, so re-resolve the (still-local) Kimi profile.
|
|
3085
|
+
function resolveKimiCacheIdentity({ home, env } = {}) {
|
|
3086
|
+
try {
|
|
3087
|
+
return kimiProfileIdentity(resolveKimiProfile({ home, env }));
|
|
3088
|
+
} catch (_error) {
|
|
3089
|
+
return null;
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
function resolveKimiLimitsCachePath({ home } = {}) {
|
|
3094
|
+
return path.join(home || os.homedir(), ".tokentracker", "tracker", KIMI_LIMITS_CACHE_FILE);
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
function hasKimiWindow(limits) {
|
|
3098
|
+
return Boolean(
|
|
3099
|
+
limits?.primary_window
|
|
3100
|
+
|| limits?.secondary_window
|
|
3101
|
+
|| limits?.tertiary_window,
|
|
3102
|
+
);
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
// Kimi windows carry `reset_at` (ISO) when the API reports a cycle end; a window
|
|
3106
|
+
// whose reset has already passed is stale data, not a usable fallback, so drop
|
|
3107
|
+
// it. Windows without a reset stamp are kept — the overall cached_at max-age
|
|
3108
|
+
// gate already bounds them.
|
|
3109
|
+
function isKimiCacheWindowUsable(window, { nowMs } = {}) {
|
|
3110
|
+
if (!window || typeof window !== "object") return false;
|
|
3111
|
+
const resetAtMs = parseTimeMs(window.reset_at);
|
|
3112
|
+
if (resetAtMs === null) return true;
|
|
3113
|
+
return resetAtMs > nowMs;
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
function normalizeKimiCachedLimits(
|
|
3117
|
+
raw,
|
|
3118
|
+
{ nowMs = Date.now(), maxAgeMs = KIMI_LIMITS_CACHE_MAX_AGE_MS } = {},
|
|
3119
|
+
) {
|
|
3120
|
+
const cachedAtMs = parseTimeMs(raw?.cached_at);
|
|
3121
|
+
if (!Number.isFinite(cachedAtMs)) return null;
|
|
3122
|
+
if (cachedAtMs > nowMs + 60_000) return null;
|
|
3123
|
+
if (nowMs - cachedAtMs > maxAgeMs) return null;
|
|
3124
|
+
|
|
3125
|
+
const cached = {
|
|
3126
|
+
configured: true,
|
|
3127
|
+
error: null,
|
|
3128
|
+
membership_level: typeof raw?.membership_level === "string" ? raw.membership_level : null,
|
|
3129
|
+
subscription_type: typeof raw?.subscription_type === "string" ? raw.subscription_type : null,
|
|
3130
|
+
parallel_limit: Number.isFinite(raw?.parallel_limit) ? raw.parallel_limit : null,
|
|
3131
|
+
primary_window: isKimiCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
|
|
3132
|
+
secondary_window: isKimiCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
|
|
3133
|
+
tertiary_window: isKimiCacheWindowUsable(raw?.tertiary_window, { nowMs }) ? raw.tertiary_window : null,
|
|
3134
|
+
stale: true,
|
|
3135
|
+
cached_at: raw.cached_at,
|
|
3136
|
+
};
|
|
3137
|
+
return hasKimiWindow(cached) ? cached : null;
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
function readKimiLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
|
|
3141
|
+
if (!identity) return null;
|
|
3142
|
+
try {
|
|
3143
|
+
const parsed = JSON.parse(fs.readFileSync(resolveKimiLimitsCachePath({ home }), "utf8"));
|
|
3144
|
+
const entry = parsed?.kimi;
|
|
3145
|
+
if (entry?.identity !== identity) return null;
|
|
3146
|
+
return normalizeKimiCachedLimits(entry, { nowMs });
|
|
3147
|
+
} catch (_error) {
|
|
3148
|
+
return null;
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
function writeKimiLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
|
|
3153
|
+
if (!identity) return;
|
|
3154
|
+
if (!limits?.configured || limits.error || !hasKimiWindow(limits)) return;
|
|
3155
|
+
const cachePath = resolveKimiLimitsCachePath({ home });
|
|
3156
|
+
const payload = {
|
|
3157
|
+
kimi: {
|
|
3158
|
+
identity,
|
|
3159
|
+
membership_level: limits.membership_level || null,
|
|
3160
|
+
subscription_type: limits.subscription_type || null,
|
|
3161
|
+
parallel_limit: Number.isFinite(limits.parallel_limit) ? limits.parallel_limit : null,
|
|
3162
|
+
primary_window: limits.primary_window || null,
|
|
3163
|
+
secondary_window: limits.secondary_window || null,
|
|
3164
|
+
tertiary_window: limits.tertiary_window || null,
|
|
3165
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
3166
|
+
},
|
|
3167
|
+
};
|
|
3168
|
+
try {
|
|
3169
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
3170
|
+
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
3171
|
+
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
3172
|
+
fs.renameSync(tmpPath, cachePath);
|
|
3173
|
+
} catch (_error) {}
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
function resolveGrokLimitsCachePath({ home } = {}) {
|
|
3177
|
+
return path.join(home || os.homedir(), ".tokentracker", "tracker", GROK_LIMITS_CACHE_FILE);
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
function hasGrokWindow(limits) {
|
|
3181
|
+
return Boolean(limits?.primary_window || limits?.secondary_window);
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
// Grok windows carry `reset_at` as an ISO string — the billing-period end shared
|
|
3185
|
+
// by the pool, on-demand, and per-product windows. A window whose reset has
|
|
3186
|
+
// already passed is stale data, not a usable fallback, so drop it. Windows
|
|
3187
|
+
// without a reset stamp are kept — the overall cached_at max-age gate already
|
|
3188
|
+
// bounds them (mirrors Cursor).
|
|
3189
|
+
function isGrokCacheWindowUsable(window, { nowMs } = {}) {
|
|
3190
|
+
if (!window || typeof window !== "object") return false;
|
|
3191
|
+
const resetAtMs = parseTimeMs(window.reset_at);
|
|
3192
|
+
if (resetAtMs === null) return true;
|
|
3193
|
+
return resetAtMs > nowMs;
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
function normalizeGrokCachedLimits(
|
|
3197
|
+
raw,
|
|
3198
|
+
{ nowMs = Date.now(), maxAgeMs = GROK_LIMITS_CACHE_MAX_AGE_MS } = {},
|
|
3199
|
+
) {
|
|
3200
|
+
const cachedAtMs = parseTimeMs(raw?.cached_at);
|
|
3201
|
+
if (!Number.isFinite(cachedAtMs)) return null;
|
|
3202
|
+
if (cachedAtMs > nowMs + 60_000) return null;
|
|
3203
|
+
if (nowMs - cachedAtMs > maxAgeMs) return null;
|
|
3204
|
+
|
|
3205
|
+
const cached = {
|
|
3206
|
+
configured: true,
|
|
3207
|
+
error: null,
|
|
3208
|
+
plan_label: typeof raw?.plan_label === "string" ? raw.plan_label : null,
|
|
3209
|
+
period_type: typeof raw?.period_type === "string" ? raw.period_type : null,
|
|
3210
|
+
monthly_credits_limit: raw?.monthly_credits_limit ?? null,
|
|
3211
|
+
monthly_credits_used: raw?.monthly_credits_used ?? null,
|
|
3212
|
+
credit_usage_percent: raw?.credit_usage_percent ?? null,
|
|
3213
|
+
on_demand_cap: raw?.on_demand_cap ?? null,
|
|
3214
|
+
on_demand_used: raw?.on_demand_used ?? null,
|
|
3215
|
+
billing_period_start: typeof raw?.billing_period_start === "string" ? raw.billing_period_start : null,
|
|
3216
|
+
primary_window: isGrokCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
|
|
3217
|
+
secondary_window: isGrokCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
|
|
3218
|
+
// Per-product shares of the pool, not independent windows — each is gated
|
|
3219
|
+
// by the same reset stamp as the pool window it attributes.
|
|
3220
|
+
products: Array.isArray(raw?.products)
|
|
3221
|
+
? raw.products.filter((entry) => isGrokCacheWindowUsable(entry?.window, { nowMs }))
|
|
3222
|
+
: [],
|
|
3223
|
+
stale: true,
|
|
3224
|
+
cached_at: raw.cached_at,
|
|
3225
|
+
};
|
|
3226
|
+
return hasGrokWindow(cached) ? cached : null;
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
// The signed-in Grok account partitions the cache (zcode login-identity precedent):
|
|
3230
|
+
// the id is the non-secret OIDC `sub` claim inside the access token — the
|
|
3231
|
+
// credential itself never enters the id (no token fingerprint fallback: CodeQL
|
|
3232
|
+
// js/insufficient-password-hash precedent). auth.x.ai issues JWT access tokens
|
|
3233
|
+
// (RFC 9068), so the subject is present in practice; an opaque token yields no
|
|
3234
|
+
// identity and the cache is simply refused on both the write and the read path
|
|
3235
|
+
// (fail-closed) — an unattributable login must never pin another account's bars,
|
|
3236
|
+
// which a scope-key fallback cannot guarantee because `issuer::client_id` stays
|
|
3237
|
+
// constant across account switches.
|
|
3238
|
+
function readGrokLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
|
|
3239
|
+
if (!identity) return null;
|
|
3240
|
+
try {
|
|
3241
|
+
const parsed = JSON.parse(fs.readFileSync(resolveGrokLimitsCachePath({ home }), "utf8"));
|
|
3242
|
+
const entry = parsed?.grok;
|
|
3243
|
+
if (entry?.identity !== identity) return null;
|
|
3244
|
+
return normalizeGrokCachedLimits(entry, { nowMs });
|
|
3245
|
+
} catch (_error) {
|
|
3246
|
+
return null;
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
function writeGrokLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
|
|
3251
|
+
if (!identity) return;
|
|
3252
|
+
if (!limits?.configured || limits.error || !hasGrokWindow(limits)) return;
|
|
3253
|
+
const cachePath = resolveGrokLimitsCachePath({ home });
|
|
3254
|
+
const payload = {
|
|
3255
|
+
grok: {
|
|
3256
|
+
identity,
|
|
3257
|
+
plan_label: limits.plan_label || null,
|
|
3258
|
+
period_type: limits.period_type || null,
|
|
3259
|
+
monthly_credits_limit: limits.monthly_credits_limit ?? null,
|
|
3260
|
+
monthly_credits_used: limits.monthly_credits_used ?? null,
|
|
3261
|
+
credit_usage_percent: limits.credit_usage_percent ?? null,
|
|
3262
|
+
on_demand_cap: limits.on_demand_cap ?? null,
|
|
3263
|
+
on_demand_used: limits.on_demand_used ?? null,
|
|
3264
|
+
billing_period_start: limits.billing_period_start || null,
|
|
3265
|
+
primary_window: limits.primary_window || null,
|
|
3266
|
+
secondary_window: limits.secondary_window || null,
|
|
3267
|
+
products: Array.isArray(limits.products) ? limits.products : [],
|
|
3268
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
3269
|
+
},
|
|
3270
|
+
};
|
|
3271
|
+
try {
|
|
3272
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
3273
|
+
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
3274
|
+
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
3275
|
+
fs.renameSync(tmpPath, cachePath);
|
|
3276
|
+
} catch (_error) {}
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
// Failure-only identity re-derivation for the cache fallback: when the provider
|
|
3280
|
+
// slot's result was produced by the outer timeout race, the fetcher's own
|
|
3281
|
+
// identity field is lost, so read auth.json once to learn who we are serving for.
|
|
3282
|
+
// Opaque tokens yield null → the read is refused (fail-closed, see above).
|
|
3283
|
+
function resolveGrokCacheIdentity({ home, env } = {}) {
|
|
3284
|
+
try {
|
|
3285
|
+
return grokTokenSubject(loadGrokAuthEntry({ home, env })?.entry?.key);
|
|
3286
|
+
} catch (_error) {
|
|
3287
|
+
return null;
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
|
|
2770
3291
|
function resolveClaudeRateLimitPath({ home } = {}) {
|
|
2771
3292
|
return path.join(home || os.homedir(), ".tokentracker", "tracker", CLAUDE_RATE_LIMIT_FILE);
|
|
2772
3293
|
}
|
|
@@ -3636,6 +4157,11 @@ async function fetchAntigravityQuotaSummaryJson(fetchImpl, accessToken, signal)
|
|
|
3636
4157
|
for (const url of ANTIGRAVITY_QUOTA_SUMMARY_URLS) {
|
|
3637
4158
|
try {
|
|
3638
4159
|
const res = await postAntigravityCloudCode(fetchImpl, url, accessToken, {}, signal);
|
|
4160
|
+
if (res.status === 403 && await isAntigravitySubscriptionRequired(res)) {
|
|
4161
|
+
const err = new Error("Antigravity quota summary requires a subscription.");
|
|
4162
|
+
err.code = "SUBSCRIPTION_REQUIRED";
|
|
4163
|
+
throw err;
|
|
4164
|
+
}
|
|
3639
4165
|
if (res.status === 401 || res.status === 403) {
|
|
3640
4166
|
const err = new Error(ANTIGRAVITY_AUTH_EXPIRED_MESSAGE);
|
|
3641
4167
|
err.code = "AUTH_EXPIRED";
|
|
@@ -3648,13 +4174,44 @@ async function fetchAntigravityQuotaSummaryJson(fetchImpl, accessToken, signal)
|
|
|
3648
4174
|
}
|
|
3649
4175
|
return await res.json();
|
|
3650
4176
|
} catch (error) {
|
|
3651
|
-
if (error?.code === "AUTH_EXPIRED") throw error;
|
|
4177
|
+
if (error?.code === "AUTH_EXPIRED" || error?.code === "SUBSCRIPTION_REQUIRED") throw error;
|
|
3652
4178
|
lastError = error;
|
|
3653
4179
|
}
|
|
3654
4180
|
}
|
|
3655
4181
|
throw lastError || new Error("Antigravity quota request failed.");
|
|
3656
4182
|
}
|
|
3657
4183
|
|
|
4184
|
+
// Free-tier ("Starter Quota") accounts get 403 SUBSCRIPTION_REQUIRED from
|
|
4185
|
+
// retrieveUserQuotaSummary even with a valid token — that is not an auth failure.
|
|
4186
|
+
async function isAntigravitySubscriptionRequired(res) {
|
|
4187
|
+
try {
|
|
4188
|
+
const body = await res.json();
|
|
4189
|
+
const details = Array.isArray(body?.error?.details) ? body.error.details : [];
|
|
4190
|
+
return details.some((d) => d?.reason === "SUBSCRIPTION_REQUIRED");
|
|
4191
|
+
} catch {
|
|
4192
|
+
return false;
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4196
|
+
// Per-model quotaInfo, available to every tier. Reshaped into the language
|
|
4197
|
+
// server's clientModelConfigs so normalizeAntigravityResponse can pick windows.
|
|
4198
|
+
async function fetchAntigravityAvailableModelsLimits(fetchImpl, accessToken, signal) {
|
|
4199
|
+
const res = await postAntigravityCloudCode(fetchImpl, ANTIGRAVITY_AVAILABLE_MODELS_URL, accessToken, {}, signal);
|
|
4200
|
+
if (res.status === 401 || res.status === 403) {
|
|
4201
|
+
const err = new Error(ANTIGRAVITY_AUTH_EXPIRED_MESSAGE);
|
|
4202
|
+
err.code = "AUTH_EXPIRED";
|
|
4203
|
+
err.status = res.status;
|
|
4204
|
+
throw err;
|
|
4205
|
+
}
|
|
4206
|
+
if (!res.ok) throw new Error(`Antigravity API error: HTTP ${res.status}`);
|
|
4207
|
+
const json = await res.json();
|
|
4208
|
+
const models = json?.models && typeof json.models === "object" ? json.models : {};
|
|
4209
|
+
const clientModelConfigs = Object.entries(models)
|
|
4210
|
+
.filter(([, m]) => typeof m?.displayName === "string" && m.displayName)
|
|
4211
|
+
.map(([id, m]) => ({ label: m.displayName, modelOrAlias: { model: id }, quotaInfo: m.quotaInfo }));
|
|
4212
|
+
return normalizeAntigravityResponse({ clientModelConfigs }, { fallbackToConfigs: true });
|
|
4213
|
+
}
|
|
4214
|
+
|
|
3658
4215
|
/** Read the paid plan label without failing a successful quota response if this lookup fails. */
|
|
3659
4216
|
async function fetchAntigravityPlanLabel(fetchImpl, accessToken, signal) {
|
|
3660
4217
|
try {
|
|
@@ -3694,8 +4251,14 @@ async function fetchAntigravityRemoteLimits({
|
|
|
3694
4251
|
if (!resolvedCreds) return null;
|
|
3695
4252
|
|
|
3696
4253
|
const loadWithToken = async (accessToken) => {
|
|
3697
|
-
|
|
3698
|
-
|
|
4254
|
+
let windows;
|
|
4255
|
+
try {
|
|
4256
|
+
const payload = await fetchAntigravityQuotaSummaryJson(fetchImpl, accessToken, signal);
|
|
4257
|
+
windows = normalizeAntigravityQuotaSummary(payload);
|
|
4258
|
+
} catch (error) {
|
|
4259
|
+
if (error?.code !== "SUBSCRIPTION_REQUIRED") throw error;
|
|
4260
|
+
windows = await fetchAntigravityAvailableModelsLimits(fetchImpl, accessToken, signal);
|
|
4261
|
+
}
|
|
3699
4262
|
const accountPlan = windows.account_plan || await fetchAntigravityPlanLabel(fetchImpl, accessToken, signal);
|
|
3700
4263
|
return {
|
|
3701
4264
|
configured: true,
|
|
@@ -4209,7 +4772,7 @@ async function fetchUsageLimitsUncached({
|
|
|
4209
4772
|
// (#956).
|
|
4210
4773
|
const hiddenSentinel = Promise.resolve({ configured: false });
|
|
4211
4774
|
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([
|
|
4775
|
+
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
4776
|
claudeToken && !freshClaudeCache && !claudeRetryAtMs
|
|
4214
4777
|
? withProviderTimeout(fetchClaudeUsageLimits(claudeToken, { fetchImpl: providerFetch, maxAttempts: 1 }), "Claude", providerTimeoutMs).then(
|
|
4215
4778
|
(value) => ({ status: "fulfilled", value }),
|
|
@@ -4226,7 +4789,7 @@ async function fetchUsageLimitsUncached({
|
|
|
4226
4789
|
(reason) => ({ status: "rejected", reason }),
|
|
4227
4790
|
)
|
|
4228
4791
|
: Promise.resolve(null),
|
|
4229
|
-
hiddenSet.has("cursor") ? hiddenSentinel : withProviderTimeout(fetchCursorLimits({ home, fetchImpl: providerFetch }), "Cursor", providerTimeoutMs)
|
|
4792
|
+
hiddenSet.has("cursor") ? hiddenSentinel : withProviderTimeout(fetchCursorLimits({ home, env, fetchImpl: providerFetch }), "Cursor", providerTimeoutMs)
|
|
4230
4793
|
.catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
|
|
4231
4794
|
hiddenSet.has("kimi") ? hiddenSentinel : withProviderTimeout(fetchKimiLimits({ home, env, fetchImpl: providerFetch }), "Kimi", providerTimeoutMs)
|
|
4232
4795
|
.catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
|
|
@@ -4343,6 +4906,25 @@ async function fetchUsageLimitsUncached({
|
|
|
4343
4906
|
.catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
|
|
4344
4907
|
hiddenSet.has("workbuddyAi") ? hiddenSentinel : withWorkbuddyProviderTimeout((signal) => fetchWorkbuddyAiLimits({ home, env, fetchImpl: providerFetch, signal, nowMs: injectedNowMs }), "WorkBuddy AI", providerTimeoutMs)
|
|
4345
4908
|
.catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
|
|
4909
|
+
// DeepSeek (platform.deepseek.com, pay-as-you-go): the balance endpoint is
|
|
4910
|
+
// the only documented programmatic source, so this slot yields a balance
|
|
4911
|
+
// card, not quota windows (#960). The last-good disk-cache fallback with a
|
|
4912
|
+
// pure TTL and a key fingerprint binding lives inside deepseek-limits.js;
|
|
4913
|
+
// auth errors bypass it so a rejected key stays actionable. persistCache
|
|
4914
|
+
// is sampled at slot creation — a mid-fetch key save can only leave a
|
|
4915
|
+
// cache entry bound to the OLD key's fingerprint, which a fetch under the
|
|
4916
|
+
// new key will never read.
|
|
4917
|
+
hiddenSet.has("deepseek") ? hiddenSentinel : withProviderTimeout(
|
|
4918
|
+
fetchDeepseekLimits({
|
|
4919
|
+
home,
|
|
4920
|
+
trackerDir,
|
|
4921
|
+
env,
|
|
4922
|
+
fetchImpl: providerFetch,
|
|
4923
|
+
persistCache: cacheGeneration === usageLimitsCacheGeneration,
|
|
4924
|
+
}),
|
|
4925
|
+
"DeepSeek",
|
|
4926
|
+
providerTimeoutMs,
|
|
4927
|
+
).catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
|
|
4346
4928
|
// Public status-page probe (fail-soft, own 5-min cache in provider-status.js).
|
|
4347
4929
|
// Only probed for configured accounts — without a token the Claude section
|
|
4348
4930
|
// never renders, so the reading would have nowhere to go.
|
|
@@ -4573,9 +5155,198 @@ async function fetchUsageLimitsUncached({
|
|
|
4573
5155
|
: { configured: true, error: reason?.message || "Unknown error" };
|
|
4574
5156
|
}
|
|
4575
5157
|
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
5158
|
+
// Cursor: persist last successful windows so a transient timeout/5xx on the
|
|
5159
|
+
// cursor.com / api2.cursor.sh routes serves stale bars instead of a flashing
|
|
5160
|
+
// "fetch failed" (mirrors Codex / Claude / OpenCode Go). A session-expiry error
|
|
5161
|
+
// (401/403 — "re-login in Cursor") is actionable and must not be hidden behind
|
|
5162
|
+
// stale cache, so it passes through untouched.
|
|
5163
|
+
let cursorWithCache;
|
|
5164
|
+
if (!cursor || cursor.configured === false) {
|
|
5165
|
+
cursorWithCache = cursor;
|
|
5166
|
+
} else if (cursor.auth_error === true) {
|
|
5167
|
+
cursorWithCache = cursor;
|
|
5168
|
+
} else if (!cursor.error && hasCursorWindow(cursor)) {
|
|
5169
|
+
cursorWithCache = {
|
|
5170
|
+
...cursor,
|
|
5171
|
+
stale: false,
|
|
5172
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
5173
|
+
};
|
|
5174
|
+
if (cacheGeneration === usageLimitsCacheGeneration) {
|
|
5175
|
+
writeCursorLimitsCache(cursorWithCache, {
|
|
5176
|
+
home,
|
|
5177
|
+
nowMs,
|
|
5178
|
+
identity: typeof cursor.cache_identity === "string"
|
|
5179
|
+
? cursor.cache_identity
|
|
5180
|
+
: resolveCursorCacheIdentity({ home, env }),
|
|
5181
|
+
});
|
|
5182
|
+
}
|
|
5183
|
+
} else {
|
|
5184
|
+
// The slot's error object loses the fetcher's identity field when the outer
|
|
5185
|
+
// timeout race wins (the common failure mode this cache exists for), so
|
|
5186
|
+
// re-derive it from the local state DB on this failure-only path.
|
|
5187
|
+
const identity = typeof cursor.cache_identity === "string"
|
|
5188
|
+
? cursor.cache_identity
|
|
5189
|
+
: resolveCursorCacheIdentity({ home, env });
|
|
5190
|
+
const cached = readCursorLimitsCache({ home, nowMs, identity });
|
|
5191
|
+
cursorWithCache = cached || cursor;
|
|
5192
|
+
}
|
|
5193
|
+
// cache_identity / auth_error are internal cache-plumbing markers, not payload.
|
|
5194
|
+
if (cursorWithCache && ("cache_identity" in cursorWithCache || "auth_error" in cursorWithCache)) {
|
|
5195
|
+
const { cache_identity: _identity, auth_error: _authError, ...cursorPayload } = cursorWithCache;
|
|
5196
|
+
cursorWithCache = cursorPayload;
|
|
5197
|
+
}
|
|
5198
|
+
|
|
5199
|
+
// Grok: persist last successful windows so a transient timeout/5xx on the
|
|
5200
|
+
// cli-chat-proxy.grok.com route serves stale bars instead of a flashing
|
|
5201
|
+
// "fetch failed" (mirrors Codex / Claude / Cursor). A re-login error
|
|
5202
|
+
// (GROK_AUTH_REQUIRED / GROK_REAUTH_REQUIRED — "run `grok login`") is
|
|
5203
|
+
// actionable and must not be hidden behind stale cache, so it passes
|
|
5204
|
+
// through untouched.
|
|
5205
|
+
let grokWithCache;
|
|
5206
|
+
if (!grok || grok.configured === false) {
|
|
5207
|
+
grokWithCache = grok;
|
|
5208
|
+
} else if (grok.auth_error === true) {
|
|
5209
|
+
grokWithCache = grok;
|
|
5210
|
+
} else if (!grok.error && hasGrokWindow(grok)) {
|
|
5211
|
+
grokWithCache = {
|
|
5212
|
+
...grok,
|
|
5213
|
+
stale: false,
|
|
5214
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
5215
|
+
};
|
|
5216
|
+
// Write only under the identity the fetch itself carried: re-reading
|
|
5217
|
+
// auth.json here could attribute a finished fetch's windows to an account
|
|
5218
|
+
// that logged in while the request was in flight (opaque → JWT switch).
|
|
5219
|
+
// No fetch-side identity (opaque token) → refuse the write (fail-closed).
|
|
5220
|
+
if (cacheGeneration === usageLimitsCacheGeneration && typeof grok.cache_identity === "string") {
|
|
5221
|
+
writeGrokLimitsCache(grokWithCache, {
|
|
5222
|
+
home,
|
|
5223
|
+
nowMs,
|
|
5224
|
+
identity: grok.cache_identity,
|
|
5225
|
+
});
|
|
5226
|
+
}
|
|
5227
|
+
} else {
|
|
5228
|
+
// The slot's error object loses the fetcher's identity field when the outer
|
|
5229
|
+
// timeout race wins (the common failure mode this cache exists for), so
|
|
5230
|
+
// re-derive it from auth.json on this failure-only path.
|
|
5231
|
+
const identity = typeof grok.cache_identity === "string"
|
|
5232
|
+
? grok.cache_identity
|
|
5233
|
+
: resolveGrokCacheIdentity({ home, env });
|
|
5234
|
+
const cached = readGrokLimitsCache({ home, nowMs, identity });
|
|
5235
|
+
grokWithCache = cached || grok;
|
|
5236
|
+
}
|
|
5237
|
+
// cache_identity / auth_error are internal cache-plumbing markers, not payload.
|
|
5238
|
+
if (grokWithCache && ("cache_identity" in grokWithCache || "auth_error" in grokWithCache)) {
|
|
5239
|
+
const { cache_identity: _identity, auth_error: _authError, ...grokPayload } = grokWithCache;
|
|
5240
|
+
grokWithCache = grokPayload;
|
|
5241
|
+
}
|
|
5242
|
+
|
|
5243
|
+
const cursorLimits = cursorSandDuplicatesGrokPool(cursorWithCache, grokWithCache)
|
|
5244
|
+
? { ...cursorWithCache, quaternary_window: null }
|
|
5245
|
+
: cursorWithCache;
|
|
5246
|
+
|
|
5247
|
+
// Kimi: persist last successful windows so a transient timeout/5xx on the
|
|
5248
|
+
// api.kimi.com / api.kimi.ai coding routes serves stale bars instead of a
|
|
5249
|
+
// flashing "fetch failed" (mirrors Cursor / Codex / OpenCode Go). Re-login
|
|
5250
|
+
// errors ("Not logged in to Kimi" / "token_expired") and actionable profile
|
|
5251
|
+
// misconfigurations must not be hidden behind stale cache, so they pass
|
|
5252
|
+
// through untouched.
|
|
5253
|
+
let kimiWithCache;
|
|
5254
|
+
if (!kimi || kimi.configured === false) {
|
|
5255
|
+
kimiWithCache = kimi;
|
|
5256
|
+
} else if (kimi.auth_error === true || kimi.config_error === true) {
|
|
5257
|
+
kimiWithCache = kimi;
|
|
5258
|
+
} else if (!kimi.error && hasKimiWindow(kimi)) {
|
|
5259
|
+
kimiWithCache = {
|
|
5260
|
+
...kimi,
|
|
5261
|
+
stale: false,
|
|
5262
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
5263
|
+
};
|
|
5264
|
+
if (cacheGeneration === usageLimitsCacheGeneration) {
|
|
5265
|
+
writeKimiLimitsCache(kimiWithCache, {
|
|
5266
|
+
home,
|
|
5267
|
+
nowMs,
|
|
5268
|
+
identity: typeof kimi.cache_identity === "string"
|
|
5269
|
+
? kimi.cache_identity
|
|
5270
|
+
: resolveKimiCacheIdentity({ home, env }),
|
|
5271
|
+
});
|
|
5272
|
+
}
|
|
5273
|
+
} else {
|
|
5274
|
+
// The slot's error object loses the fetcher's identity field when the outer
|
|
5275
|
+
// timeout race wins (the common failure mode this cache exists for), so
|
|
5276
|
+
// re-derive it from the local Kimi profile on this failure-only path.
|
|
5277
|
+
const identity = typeof kimi.cache_identity === "string"
|
|
5278
|
+
? kimi.cache_identity
|
|
5279
|
+
: resolveKimiCacheIdentity({ home, env });
|
|
5280
|
+
const cached = readKimiLimitsCache({ home, nowMs, identity });
|
|
5281
|
+
kimiWithCache = cached || kimi;
|
|
5282
|
+
}
|
|
5283
|
+
// cache_identity / auth_error / config_error are internal cache-plumbing
|
|
5284
|
+
// markers, not payload.
|
|
5285
|
+
if (kimiWithCache && ("cache_identity" in kimiWithCache || "auth_error" in kimiWithCache || "config_error" in kimiWithCache)) {
|
|
5286
|
+
const {
|
|
5287
|
+
cache_identity: _kimiIdentity,
|
|
5288
|
+
auth_error: _kimiAuthError,
|
|
5289
|
+
config_error: _kimiConfigError,
|
|
5290
|
+
...kimiPayload
|
|
5291
|
+
} = kimiWithCache;
|
|
5292
|
+
kimiWithCache = kimiPayload;
|
|
5293
|
+
}
|
|
5294
|
+
|
|
5295
|
+
// Gemini: persist last successful windows so a transient timeout/5xx on the
|
|
5296
|
+
// cloudcode-pa.googleapis.com routes serves stale bars instead of a flashing
|
|
5297
|
+
// "fetch failed" (mirrors Cursor / Codex / OpenCode Go). Auth errors (a dead
|
|
5298
|
+
// OAuth session, or the unsupported api-key / vertex-ai auth modes) are
|
|
5299
|
+
// actionable and must not be hidden behind stale cache, so they pass through
|
|
5300
|
+
// untouched.
|
|
5301
|
+
let geminiWithCache;
|
|
5302
|
+
if (!gemini || gemini.configured === false) {
|
|
5303
|
+
geminiWithCache = gemini;
|
|
5304
|
+
} else if (gemini.auth_error === true) {
|
|
5305
|
+
geminiWithCache = gemini;
|
|
5306
|
+
} else if (!gemini.error && hasGeminiWindow(gemini)) {
|
|
5307
|
+
geminiWithCache = {
|
|
5308
|
+
...gemini,
|
|
5309
|
+
stale: false,
|
|
5310
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
5311
|
+
};
|
|
5312
|
+
if (cacheGeneration === usageLimitsCacheGeneration) {
|
|
5313
|
+
// A slow in-flight fetch must not clobber the on-disk entry of an
|
|
5314
|
+
// account that has since been switched away: only write when the
|
|
5315
|
+
// snapshot's identity still matches the current credentials.
|
|
5316
|
+
const currentIdentity = resolveGeminiCacheIdentity({ home, env });
|
|
5317
|
+
const snapshotIdentity = typeof gemini.cache_identity === "string"
|
|
5318
|
+
? gemini.cache_identity
|
|
5319
|
+
: currentIdentity;
|
|
5320
|
+
if (snapshotIdentity && snapshotIdentity === currentIdentity) {
|
|
5321
|
+
writeGeminiLimitsCache(geminiWithCache, {
|
|
5322
|
+
home,
|
|
5323
|
+
nowMs,
|
|
5324
|
+
identity: snapshotIdentity,
|
|
5325
|
+
});
|
|
5326
|
+
}
|
|
5327
|
+
}
|
|
5328
|
+
} else {
|
|
5329
|
+
// The slot's error object loses the fetcher's identity field when the outer
|
|
5330
|
+
// timeout race wins (the common failure mode this cache exists for). Always
|
|
5331
|
+
// re-derive from the CURRENT local credentials — the request may have
|
|
5332
|
+
// started under an account that has since been switched away, in which
|
|
5333
|
+
// case the read's identity gate refuses the old entry. Revalidate the auth
|
|
5334
|
+
// mode too: a mid-flight switch to api-key/vertex-ai never reached the
|
|
5335
|
+
// punch-through branch above, but stale OAuth bars would still mislead.
|
|
5336
|
+
const selectedType = loadGeminiSettings({ home, env })?.security?.auth?.selectedType ?? null;
|
|
5337
|
+
if (selectedType === "api-key" || selectedType === "vertex-ai") {
|
|
5338
|
+
geminiWithCache = gemini;
|
|
5339
|
+
} else {
|
|
5340
|
+
const identity = resolveGeminiCacheIdentity({ home, env });
|
|
5341
|
+
const cached = readGeminiLimitsCache({ home, nowMs, identity });
|
|
5342
|
+
geminiWithCache = cached || gemini;
|
|
5343
|
+
}
|
|
5344
|
+
}
|
|
5345
|
+
// cache_identity / auth_error are internal cache-plumbing markers, not payload.
|
|
5346
|
+
if (geminiWithCache && ("cache_identity" in geminiWithCache || "auth_error" in geminiWithCache)) {
|
|
5347
|
+
const { cache_identity: _identity, auth_error: _authError, ...geminiPayload } = geminiWithCache;
|
|
5348
|
+
geminiWithCache = geminiPayload;
|
|
5349
|
+
}
|
|
4579
5350
|
|
|
4580
5351
|
const data = {
|
|
4581
5352
|
fetched_at: new Date(nowMs).toISOString(),
|
|
@@ -4586,8 +5357,8 @@ async function fetchUsageLimitsUncached({
|
|
|
4586
5357
|
// tier, and membership.level is an opaque enum (LEVEL_INTERMEDIATE) with no
|
|
4587
5358
|
// authoritative human-readable plan mapping. Both rendered as garbage like
|
|
4588
5359
|
// "Kimi Type_event" (issue #130), so show the bare brand instead.
|
|
4589
|
-
kimi: withPlanLabel(
|
|
4590
|
-
gemini: withPlanLabel(
|
|
5360
|
+
kimi: withPlanLabel(kimiWithCache, null, "Kimi"),
|
|
5361
|
+
gemini: withPlanLabel(geminiWithCache, geminiWithCache.account_plan, "Gemini"),
|
|
4591
5362
|
kiro: withPlanLabel(kiro, kiro.plan_name, "Kiro"),
|
|
4592
5363
|
antigravity: withPlanLabel(antigravity, antigravity.account_plan, "Antigravity"),
|
|
4593
5364
|
copilot: withPlanLabel(copilot, copilot.plan_name, "Copilot"),
|
|
@@ -4597,7 +5368,7 @@ async function fetchUsageLimitsUncached({
|
|
|
4597
5368
|
// outright, so surface the label as-is like CommandCode below. Error and
|
|
4598
5369
|
// unconfigured branches never carry plan_label, so the panel still falls
|
|
4599
5370
|
// back to the bare brand name.
|
|
4600
|
-
grok,
|
|
5371
|
+
grok: grokWithCache,
|
|
4601
5372
|
zcode: withPlanLabel(zcode, zcode.plan_label, "ZCode"),
|
|
4602
5373
|
opencodeGo: withPlanLabel(opencodeGo, opencodeGo?.plan_label, "OpenCode Go"),
|
|
4603
5374
|
// CommandCode tiers are acronym brands (GOAT/Pro/Max) — the fetcher already
|
|
@@ -4616,6 +5387,10 @@ async function fetchUsageLimitsUncached({
|
|
|
4616
5387
|
// as separate providers — one row per signed-in edition, never merged.
|
|
4617
5388
|
workbuddy,
|
|
4618
5389
|
workbuddyAi,
|
|
5390
|
+
// DeepSeek carries a balance card (no windows, no plan label): the fetcher
|
|
5391
|
+
// stamps cached_at/stale on both its live and last-good paths, so pass it
|
|
5392
|
+
// through verbatim like grok.
|
|
5393
|
+
deepseek,
|
|
4619
5394
|
};
|
|
4620
5395
|
|
|
4621
5396
|
for (const [providerName, provider] of Object.entries(data)) {
|