tokentracker-cli 1.5.2 → 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-B5kmt0VD.js → AchievementBadge-CKNb_cwg.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-C0FlkOo0.js → AchievementsPage-u9P8DXJI.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-CRcp80Pp.js → AchievementsSection-BSCUJ4DR.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-DAFjGbKW.js → BadgeDetailModal-Bg7cE3Zq.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-CO0bbimB.js → BotAnimated-C7D2WbQA.js} +1 -1
- package/dashboard/dist/assets/{Card-Xhr5exz8.js → Card-B4IqAhai.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-DsQ7FmsI.js → CloudCheckoutPage-P75pH9Ym.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-DkSexu5V.js → CloudPage-DDEZfmZg.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-Bm0huQdj.js → CommandPalette-CER_AklA.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-BsL8GN8Q.js → CommunityStatsModal-Cbb74eib.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-_WPp292g.js → ConfirmModal-Q1ZQdZHt.js} +1 -1
- package/dashboard/dist/assets/{Controls-ktzn68KM.js → Controls-DaGOfcoz.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-CXI7RmxO.js +1 -0
- package/dashboard/dist/assets/{DateRangePopover-DWr4zIzF.js → DateRangePopover-DSS1Ira_.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-CBa8Mk47.js → DevicePage-6zPLYydh.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-CZ5jzgtb.js → DialogClose-DdCQ36LK.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-DztHQvj-.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-Dr0N6q15.js → DialogTitle-BjP217Y1.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-sYqgrO6-.js → ElectricLogo-Dhq8PM4h.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-Qq-hcrZW.js → FadeIn-CGNbED4E.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-B77zSv7i.js → HeaderGithubStar-DwqEFzb6.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-lRDaUmoW.js → HoverTooltip-CQZTgyQy.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-B6AvHZjA.js → IpCheckPage-Bz09HXLn.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-DgzOjf3x.js → LandingPage-DC-lZsYR.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-BtjjtDMC.js → LeaderboardPage-D8MpLM_t.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-B2wNTUTU.js → LeaderboardProBadge-pdXgokhE.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-DJJckwHh.js → LeaderboardProfileModal-nHZLiE0m.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-DJdxByl2.js → LeaderboardProfilePage-BKv3oIAW.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-TmRTgSCb.js +6 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-B2lVqf_H.js → LocalOnlyNotice-aF01udyR.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-i0gg1yby.js → LoginCard-BaroD0Ct.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-B-loLNZM.js → LoginModal-0jgyMGVM.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-D_swI75j.js → LoginPage-BUx8U58i.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DDpKvpjG.js → PetPage-CpOjtJqg.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-Gja4MyEK.js → PopoverPopup-BYjfer-M.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-DOPj7tAZ.js → ProviderIcon-NtYa2h0u.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-BVADLk89.js → ResetPasswordPage-CYUg7TR1.js} +1 -1
- package/dashboard/dist/assets/{Select-D-38tN-E.js → Select-CYP95o5y.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-jN61ukxk.js → SelectItemText-BCDfusOM.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-k7AkGpl1.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-DfnMCAuA.js → SettingsPage-CCMbq5wc.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-YyIHIaHN.js → SkillsPage-C4O4fbaQ.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-BLcvZuQ9.js → TokenGalaxy-D2d738xr.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-3E8kyVzz.js → TooltipPopup-BJfFYesG.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-ByKAdXSF.js → TrendMonitor-CElakFSU.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-BgEVIIzL.js → WidgetsPage-Cvd1LbJm.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-HjbX8QaA.js → WrappedPage-wRC1tE_a.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-Br-KAXh6.css → __vite-browser-external-CMBWGkOq.css} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-V4TPLCJG.js +45 -0
- package/dashboard/dist/assets/{agent-logos-BVF2siPQ.js → agent-logos-Ba57HF3w.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-CYO24PGP.js → arrow-left-B1bKnxXL.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-Xg9PZYhw.js → arrow-right-DrOlKzX5.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-B7ah6Qv-.js → arrow-up-right-hlYpo3yw.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-BV_iG938.js → calendar-days-CSY6SEsI.js} +1 -1
- package/dashboard/dist/assets/{check--EZH8RTp.js → check-xrP1gLw5.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-BzGTlk0-.js → chevron-down-6-zT5n9d.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-BgyxPHEh.js → cloud-checkout-BZymZD4K.js} +1 -1
- package/dashboard/dist/assets/{composite-C0KrmLEK.js → composite-C9izWafa.js} +1 -1
- package/dashboard/dist/assets/{copy-BtucSK2U.js → copy-Dkas5wZX.js} +1 -1
- package/dashboard/dist/assets/{download-CWHq03Y2.js → download-CagGRs0_.js} +1 -1
- package/dashboard/dist/assets/{external-link-DCTXF5VZ.js → external-link-DRl10hxd.js} +1 -1
- package/dashboard/dist/assets/{flame-BE5TsqVF.js → flame-CkvmsTFB.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-ThNIMWnZ.js → format-tokens-BtwhO3mV.js} +1 -1
- package/dashboard/dist/assets/{gift-DgaN9s4-.js → gift-YzjXkMQx.js} +1 -1
- package/dashboard/dist/assets/{icons-B4r5EiDC.js → icons-CHhKYs0u.js} +1 -1
- package/dashboard/dist/assets/{index-DqCtgU0G.js → index-B0MQZTrM.js} +1 -1
- package/dashboard/dist/assets/{index-fNKH4U7j.js → index-C2DK8av-.js} +1 -1
- package/dashboard/dist/assets/{index-CKhcCLNC.js → index-CJknN9Jd.js} +1 -1
- package/dashboard/dist/assets/{index-DFF9xo6f.js → index-DojzoMqd.js} +1 -1
- package/dashboard/dist/assets/{info-B4YzRR4y.js → info-Torep-Ti.js} +1 -1
- package/dashboard/dist/assets/{link-2-DzbUbaBw.js → link-2-DE5Yu-z6.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-B9c7D-7v.js → loader-circle-BL8LxD9n.js} +1 -1
- package/dashboard/dist/assets/{local-usage-transfer-btuk_b0Y.js → local-usage-transfer-8X1InWix.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-BjrU-Xz7.js → modifiers.esm-D60-6UQa.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-Bwn4tb4q.js → monitor-smartphone-C-IFPjqk.js} +1 -1
- package/dashboard/dist/assets/{play-DIFOwGIQ.js → play-n6uWrdH9.js} +1 -1
- package/dashboard/dist/assets/{plus-BpBqnwVg.js → plus-B2f6TaNj.js} +1 -1
- package/dashboard/dist/assets/{provider-display-DSrs4u7T.js → provider-display-DWqCEcsP.js} +1 -1
- package/dashboard/dist/assets/{react-BgG7_MY0.js → react-B-zfi_j6.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-BUGWIDOM.js → safePolygon-C2PsvI-v.js} +1 -1
- package/dashboard/dist/assets/{search-aHrmBmtZ.js → search-DZOaTLF9.js} +1 -1
- package/dashboard/dist/assets/{server-DOEvwlEU.js → server-DIp4Qfsv.js} +1 -1
- package/dashboard/dist/assets/{shield-check-BPTnLhsT.js → shield-check-Cs2M8-iT.js} +1 -1
- package/dashboard/dist/assets/{skills-api-C052Mwez.js → skills-api-CoYy6Tov.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-DaZKhjhu.js → sortable.esm-B5_hjIfs.js} +1 -1
- package/dashboard/dist/assets/{store-Cq30Pofw.js → store-BLXqtJzb.js} +1 -1
- package/dashboard/dist/assets/{terminal-CoSDSkE7.js → terminal-CEMKX3ba.js} +1 -1
- package/dashboard/dist/assets/{trash-BAmt-kPj.js → trash-DOWYaXjK.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-Bxq0kBhm.js → use-community-stats-BjRI1Bzz.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-DmIQDlvk.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-s_CIC8Bb.js → use-native-settings-Cg5Zb4q_.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-BHhykAbt.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-CVeMFlS6.js → use-transform-CbJ1IcNc.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-Dgg3jpC4.js +1 -0
- package/dashboard/dist/assets/{useCurrency-DilqL2Iv.js → useCurrency-3oBHWfSe.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-BcnySw8a.js → usePositioner-D0YrIW0A.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-CvKW46WP.js → useTokenFormat-DZAHWE_R.js} +1 -1
- package/dashboard/dist/assets/{zap-D7VRNTwk.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 +25 -15
- 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/deepseek-status.js +157 -0
- package/src/lib/grok-limits.js +40 -1
- package/src/lib/local-api.js +248 -10
- 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 +27 -2
- package/src/lib/qoder-limits.js +17 -2
- package/src/lib/session-analytics.js +194 -29
- package/src/lib/static-server.js +74 -2
- package/src/lib/usage-limits.js +714 -53
- package/src/lib/workbuddy-limits.js +291 -26
- package/src/lib/zcode-limits.js +143 -35
- package/dashboard/dist/assets/DashboardPage-CdNXi3Im.js +0 -1
- package/dashboard/dist/assets/DialogDescription-kCZ4gN09.js +0 -1
- package/dashboard/dist/assets/LimitsPage-gSBHSBi_.js +0 -6
- package/dashboard/dist/assets/ServiceStatusPage-D_Fa_pYz.js +0 -1
- package/dashboard/dist/assets/SessionsPage-BymccEEz.js +0 -3
- package/dashboard/dist/assets/__vite-browser-external-D_cMeuqI.js +0 -45
- package/dashboard/dist/assets/use-limits-display-prefs-CyAn1fme.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-BmFyFE3S.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-DRZMI20u.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");
|
|
@@ -50,21 +51,54 @@ const execFileAsync = promisify(cp.execFile);
|
|
|
50
51
|
// 2-minute in-memory cache. It also expires early at the earliest upcoming window
|
|
51
52
|
// reset in the cached data (see cacheExpiresAtMs), floored so a provider reporting
|
|
52
53
|
// a reset "right now" can't turn every poll into a full upstream round.
|
|
53
|
-
// Partitioned by the Devin opt-in selection AND the Antigravity quota opt-out
|
|
54
|
-
// a request made while Devin is off
|
|
55
|
-
//
|
|
56
|
-
// is never served after the opt-out is set
|
|
57
|
-
|
|
54
|
+
// Partitioned by the Devin opt-in selection AND the Antigravity quota opt-out AND
|
|
55
|
+
// the per-request hidden-provider set (#956), so a request made while Devin is off
|
|
56
|
+
// is never served (or joined onto) a response fetched while it was on, an aggregate
|
|
57
|
+
// fetched while Antigravity was enabled is never served after the opt-out is set,
|
|
58
|
+
// and a response fetched for a visibility selection is never served to one made
|
|
59
|
+
// under a different one. Key = `${devin}-${antigravity}[-<hidden ids>]`.
|
|
60
|
+
const USAGE_LIMITS_BASE_SELECTION_KEYS = ["off-on", "off-off", "on-on", "on-off"];
|
|
58
61
|
const cacheBySelection = {};
|
|
59
|
-
for (const key of
|
|
62
|
+
for (const key of USAGE_LIMITS_BASE_SELECTION_KEYS) cacheBySelection[key] = { data: null, expiresAtMs: 0 };
|
|
60
63
|
// Credential replacements (provider-key save) bump this generation so a fetch
|
|
61
64
|
// that started under the old credential cannot repopulate the memory or disk
|
|
62
65
|
// caches after invalidation.
|
|
63
66
|
let usageLimitsCacheGeneration = 0;
|
|
67
|
+
// Provider row keys of the usage-limits aggregate — the id space `hidden`
|
|
68
|
+
// parameter values are validated against (dashboard rows, menu-bar
|
|
69
|
+
// LimitsSettingsStore.allProviders, and this payload all share it).
|
|
70
|
+
const USAGE_LIMITS_PROVIDER_IDS = Object.freeze([
|
|
71
|
+
"claude", "codex", "cursor", "gemini", "kimi", "kiro", "grok",
|
|
72
|
+
"copilot", "antigravity", "zcode", "opencodeGo", "commandCode",
|
|
73
|
+
"qoder", "qoderCn", "codingPlan", "agentPlan", "devin",
|
|
74
|
+
"workbuddy", "workbuddyAi", "deepseek",
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
// Normalize a caller-supplied hidden-provider list (array, Set, or
|
|
78
|
+
// comma-separated query value) into a sorted, de-duplicated id array, or null
|
|
79
|
+
// when nothing is hidden. Unknown ids are dropped: the parameter only ever
|
|
80
|
+
// narrows the fetch, so a silently ignored typo must not widen the cache-key
|
|
81
|
+
// space (and leniency matches the devin query param, which accepts any value
|
|
82
|
+
// that is not "1"/"true" as off).
|
|
83
|
+
function normalizeHiddenProviders(input) {
|
|
84
|
+
const raw = input instanceof Set
|
|
85
|
+
? [...input]
|
|
86
|
+
: Array.isArray(input)
|
|
87
|
+
? input
|
|
88
|
+
: typeof input === "string" && input.trim().length > 0
|
|
89
|
+
? input.split(",")
|
|
90
|
+
: [];
|
|
91
|
+
const ids = [...new Set(
|
|
92
|
+
raw.map((id) => String(id).trim()).filter((id) => USAGE_LIMITS_PROVIDER_IDS.includes(id)),
|
|
93
|
+
)].sort();
|
|
94
|
+
return ids.length > 0 ? ids : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
64
97
|
function usageLimitsSelectionKey(options) {
|
|
65
98
|
const devin = options?.devinEnabled === true ? "on" : "off";
|
|
66
99
|
const antigravity = isAntigravityQuotaDisabled() ? "off" : "on";
|
|
67
|
-
|
|
100
|
+
const hidden = normalizeHiddenProviders(options?.hiddenProviders);
|
|
101
|
+
return `${devin}-${antigravity}${hidden ? `-${hidden.join("+")}` : ""}`;
|
|
68
102
|
}
|
|
69
103
|
const CACHE_TTL_MS = 2 * 60 * 1000;
|
|
70
104
|
// Must stay below the macOS app's post-reset re-fetch grace (10s in
|
|
@@ -139,6 +173,24 @@ const CODEX_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
139
173
|
const OPENCODE_GO_LIMITS_CACHE_FILE = "opencode-go-usage-limits-cache.json";
|
|
140
174
|
const OPENCODE_GO_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
141
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;
|
|
142
194
|
// A 429 from the usage endpoint carries a long `retry-after` (often 20+ minutes). Persist
|
|
143
195
|
// the cooldown so every surface — this process, the menu bar app's embedded server, a later
|
|
144
196
|
// restart — stops calling until it expires. Hammering during the cooldown just renews the
|
|
@@ -237,6 +289,43 @@ function withAbortableProviderTimeout(start, label, timeoutMs) {
|
|
|
237
289
|
.finally(() => controller.abort());
|
|
238
290
|
}
|
|
239
291
|
|
|
292
|
+
// WorkBuddy bounds its own live chain (the IPC proxy timeout is internal), and
|
|
293
|
+
// its disk-cache fallback runs after the abort lands: the abort makes the
|
|
294
|
+
// socket fail within milliseconds and the cache read is synchronous. A plain
|
|
295
|
+
// provider-timeout race would settle on the timeout first and drop that
|
|
296
|
+
// fallback, so after the budget expires, abort and give the attempt a short
|
|
297
|
+
// grace to land its fallback — its outcome (cache hit or specific error)
|
|
298
|
+
// replaces the generic timeout. The hard bound is timeoutMs + graceMs.
|
|
299
|
+
function withWorkbuddyProviderTimeout(start, label, timeoutMs, graceMs = 2_000) {
|
|
300
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return start(undefined);
|
|
301
|
+
const controller = new AbortController();
|
|
302
|
+
const work = start(controller.signal);
|
|
303
|
+
let timeoutTimer;
|
|
304
|
+
let graceTimer;
|
|
305
|
+
let deadlineFired = false;
|
|
306
|
+
// Once the budget aborts the attempt, any rejection it produces is the
|
|
307
|
+
// deadline's doing — the IPC socket fails with its cancellation error, a
|
|
308
|
+
// fetch with an AbortError — so report the labeled timeout instead of the
|
|
309
|
+
// symptom. A post-abort resolution still wins the grace race (the
|
|
310
|
+
// disk-cache fallback).
|
|
311
|
+
const guardedWork = work.catch((error) => {
|
|
312
|
+
if (deadlineFired) throw new Error(`${label} usage request timed out.`);
|
|
313
|
+
throw error;
|
|
314
|
+
});
|
|
315
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
316
|
+
timeoutTimer = setTimeout(() => {
|
|
317
|
+
deadlineFired = true;
|
|
318
|
+
controller.abort();
|
|
319
|
+
graceTimer = setTimeout(() => reject(new Error(`${label} usage request timed out.`)), graceMs);
|
|
320
|
+
}, timeoutMs);
|
|
321
|
+
});
|
|
322
|
+
return Promise.race([guardedWork, timeoutPromise]).finally(() => {
|
|
323
|
+
clearTimeout(timeoutTimer);
|
|
324
|
+
clearTimeout(graceTimer);
|
|
325
|
+
controller.abort();
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
240
329
|
function parseRetryAfterSeconds(headers) {
|
|
241
330
|
const ra = headers?.get ? headers.get("retry-after") : null;
|
|
242
331
|
const sec = ra ? Number.parseInt(ra, 10) : NaN;
|
|
@@ -790,11 +879,11 @@ function normalizeCursorSandUsageStatus(body, { eligible = true } = {}) {
|
|
|
790
879
|
return buildWindow({ usedPercent, resetAt, windowSeconds });
|
|
791
880
|
}
|
|
792
881
|
|
|
793
|
-
async function fetchCursorLimits({ home, fetchImpl = fetch } = {}) {
|
|
794
|
-
if (!isCursorInstalled({ home })) {
|
|
882
|
+
async function fetchCursorLimits({ home, env, fetchImpl = fetch } = {}) {
|
|
883
|
+
if (!isCursorInstalled({ home, env })) {
|
|
795
884
|
return { configured: false };
|
|
796
885
|
}
|
|
797
|
-
const auth = extractCursorAuth({ home });
|
|
886
|
+
const auth = extractCursorAuth({ home, env });
|
|
798
887
|
if (!auth?.cookie || !auth?.accessToken) {
|
|
799
888
|
return { configured: false };
|
|
800
889
|
}
|
|
@@ -817,11 +906,18 @@ async function fetchCursorLimits({ home, fetchImpl = fetch } = {}) {
|
|
|
817
906
|
error: null,
|
|
818
907
|
...normalizeCursorUsageSummary(body),
|
|
819
908
|
quaternary_window: grokBotWindow,
|
|
909
|
+
// Internal cache-partition key (stripped before the payload is served).
|
|
910
|
+
cache_identity: auth.userId,
|
|
820
911
|
};
|
|
821
912
|
} catch (error) {
|
|
822
913
|
return {
|
|
823
914
|
configured: true,
|
|
824
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,
|
|
825
921
|
};
|
|
826
922
|
}
|
|
827
923
|
}
|
|
@@ -894,7 +990,9 @@ function kimiCredentialsExpired(creds, nowMs = Date.now()) {
|
|
|
894
990
|
|
|
895
991
|
async function refreshKimiAccessToken({ refreshToken, profile, fetchImpl = fetch } = {}) {
|
|
896
992
|
if (typeof refreshToken !== "string" || !refreshToken.trim()) {
|
|
897
|
-
|
|
993
|
+
const error = new Error("Not logged in to Kimi. Run 'kimi' in Terminal to authenticate.");
|
|
994
|
+
error.code = "AUTH_EXPIRED";
|
|
995
|
+
throw error;
|
|
898
996
|
}
|
|
899
997
|
|
|
900
998
|
const body = new URLSearchParams({
|
|
@@ -913,7 +1011,9 @@ async function refreshKimiAccessToken({ refreshToken, profile, fetchImpl = fetch
|
|
|
913
1011
|
body,
|
|
914
1012
|
});
|
|
915
1013
|
if (res.status === 401 || res.status === 403) {
|
|
916
|
-
|
|
1014
|
+
const error = new Error("Not logged in to Kimi. Run 'kimi' in Terminal to authenticate.");
|
|
1015
|
+
error.code = "AUTH_EXPIRED";
|
|
1016
|
+
throw error;
|
|
917
1017
|
}
|
|
918
1018
|
if (!res.ok) {
|
|
919
1019
|
throw new Error(`Kimi token refresh failed (HTTP ${res.status})`);
|
|
@@ -947,7 +1047,11 @@ async function fetchKimiUsage(accessToken, { profile, fetchImpl = fetch } = {})
|
|
|
947
1047
|
},
|
|
948
1048
|
});
|
|
949
1049
|
if (res.status === 401) {
|
|
950
|
-
|
|
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;
|
|
951
1055
|
}
|
|
952
1056
|
if (!res.ok) {
|
|
953
1057
|
throw new Error(`Kimi API returned ${res.status}`);
|
|
@@ -960,12 +1064,17 @@ async function fetchKimiLimits({ home, env, fetchImpl = fetch } = {}) {
|
|
|
960
1064
|
if (!profile.configured) {
|
|
961
1065
|
return { configured: false };
|
|
962
1066
|
}
|
|
963
|
-
|
|
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 };
|
|
964
1072
|
const creds = loadKimiCredentials({ profile });
|
|
965
1073
|
let accessToken = typeof creds?.access_token === "string" ? creds.access_token.trim() : "";
|
|
966
1074
|
if (!accessToken) {
|
|
967
1075
|
return { configured: false };
|
|
968
1076
|
}
|
|
1077
|
+
const cacheIdentity = kimiProfileIdentity(profile);
|
|
969
1078
|
try {
|
|
970
1079
|
if (kimiCredentialsExpired(creds) && creds?.refresh_token) {
|
|
971
1080
|
accessToken = await refreshKimiAccessToken({
|
|
@@ -993,11 +1102,18 @@ async function fetchKimiLimits({ home, env, fetchImpl = fetch } = {}) {
|
|
|
993
1102
|
configured: true,
|
|
994
1103
|
error: null,
|
|
995
1104
|
...normalizeKimiUsageResponse(body),
|
|
1105
|
+
// Internal cache-partition key (stripped before the payload is served).
|
|
1106
|
+
cache_identity: cacheIdentity,
|
|
996
1107
|
};
|
|
997
1108
|
} catch (error) {
|
|
998
1109
|
return {
|
|
999
1110
|
configured: true,
|
|
1000
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,
|
|
1001
1117
|
};
|
|
1002
1118
|
}
|
|
1003
1119
|
}
|
|
@@ -2697,6 +2813,323 @@ function writeOpencodeGoLimitsCache(limits, { trackerDir, home, nowMs = Date.now
|
|
|
2697
2813
|
} catch (_error) {}
|
|
2698
2814
|
}
|
|
2699
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
|
+
|
|
2700
3133
|
function resolveClaudeRateLimitPath({ home } = {}) {
|
|
2701
3134
|
return path.join(home || os.homedir(), ".tokentracker", "tracker", CLAUDE_RATE_LIMIT_FILE);
|
|
2702
3135
|
}
|
|
@@ -3959,7 +4392,6 @@ function withPlanLabel(obj, raw, brand) {
|
|
|
3959
4392
|
// local-api.js): a refresh arriving while a fetch is already running reuses that
|
|
3960
4393
|
// in-flight fetch and returns its result.
|
|
3961
4394
|
const inFlightBySelection = {};
|
|
3962
|
-
for (const key of USAGE_LIMITS_SELECTION_KEYS) inFlightBySelection[key] = null;
|
|
3963
4395
|
|
|
3964
4396
|
// Codex stamps reset_at as unix seconds; every other provider (and Claude's
|
|
3965
4397
|
// resets_at) uses ISO strings. Numbers that look like epoch milliseconds are
|
|
@@ -4001,7 +4433,9 @@ async function getUsageLimits(options = {}) {
|
|
|
4001
4433
|
const selection = usageLimitsSelectionKey(options);
|
|
4002
4434
|
const cache = cacheBySelection[selection];
|
|
4003
4435
|
const nowMs = Date.now();
|
|
4004
|
-
|
|
4436
|
+
// Unseen selections (a hidden set never requested before this process's
|
|
4437
|
+
// lifetime) simply miss — partitions are created lazily on first fetch.
|
|
4438
|
+
if (cache?.data && nowMs < cache.expiresAtMs) {
|
|
4005
4439
|
return cache.data;
|
|
4006
4440
|
}
|
|
4007
4441
|
if (inFlightBySelection[selection]) {
|
|
@@ -4037,6 +4471,11 @@ async function fetchUsageLimitsUncached({
|
|
|
4037
4471
|
providerTimeoutMs = DEFAULT_PROVIDER_TIMEOUT_MS,
|
|
4038
4472
|
forceRefresh = false,
|
|
4039
4473
|
devinEnabled = false,
|
|
4474
|
+
// Provider visibility gate (#956): ids to skip entirely — no upstream call,
|
|
4475
|
+
// no credential read, no subprocess, no cache read/write. Null/empty means
|
|
4476
|
+
// "fetch every configured provider" (the legacy behavior clients that don't
|
|
4477
|
+
// send the parameter keep getting).
|
|
4478
|
+
hiddenProviders = null,
|
|
4040
4479
|
// Generation captured by getUsageLimits before the fetch began. A provider
|
|
4041
4480
|
// key save bumps it, and this fetch must then stop at returning its own
|
|
4042
4481
|
// result: publishing it would restore pre-save data for every later caller.
|
|
@@ -4045,13 +4484,30 @@ async function fetchUsageLimitsUncached({
|
|
|
4045
4484
|
// here would let an opt-out flipped mid-fetch store a pre-opt-out result in the
|
|
4046
4485
|
// post-opt-out slot.
|
|
4047
4486
|
selectionKey,
|
|
4487
|
+
// Injectable clock for tests. Production leaves it unset so per-provider
|
|
4488
|
+
// cache gates sample the time when they actually run (the live attempt can
|
|
4489
|
+
// spend the whole budget before its fallback reads the cache) instead of
|
|
4490
|
+
// inheriting this fetch's start instant.
|
|
4491
|
+
nowMs,
|
|
4048
4492
|
} = {}) {
|
|
4049
|
-
const
|
|
4050
|
-
|
|
4493
|
+
const injectedNowMs = Number.isFinite(nowMs) ? nowMs : undefined;
|
|
4494
|
+
nowMs = injectedNowMs ?? Date.now();
|
|
4495
|
+
const hiddenSet = new Set(normalizeHiddenProviders(hiddenProviders) || []);
|
|
4496
|
+
|
|
4497
|
+
// A hidden provider must not have its local credentials read at all (#956):
|
|
4498
|
+
// these upfront token/subscription probes are the first file and keychain
|
|
4499
|
+
// touches of the Claude/Codex chains, so they are gated here and every
|
|
4500
|
+
// downstream step keys off the resulting null token.
|
|
4051
4501
|
const [claudeOauth, claudeSubscription, codexAuth] = await Promise.all([
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4502
|
+
hiddenSet.has("claude")
|
|
4503
|
+
? Promise.resolve(null)
|
|
4504
|
+
: Promise.resolve().then(() => readClaudeCodeOauthToken({ platform, securityRunner, home, nowMs })),
|
|
4505
|
+
hiddenSet.has("claude")
|
|
4506
|
+
? Promise.resolve(null)
|
|
4507
|
+
: Promise.resolve().then(() => detectClaudeCodeSubscriptionDetails({ platform, securityRunner, home })),
|
|
4508
|
+
hiddenSet.has("codex")
|
|
4509
|
+
? Promise.resolve(null)
|
|
4510
|
+
: readCodexAuthBundle({ home, env }),
|
|
4055
4511
|
]);
|
|
4056
4512
|
const claudeToken = claudeOauth?.accessToken || null;
|
|
4057
4513
|
const claudeTokenExpiresAtMs = claudeOauth?.expiresAtMs ?? null;
|
|
@@ -4107,7 +4563,16 @@ async function fetchUsageLimitsUncached({
|
|
|
4107
4563
|
: null;
|
|
4108
4564
|
|
|
4109
4565
|
const providerFetch = withFetchTimeout(fetchImpl, providerTimeoutMs);
|
|
4110
|
-
|
|
4566
|
+
// What a hidden provider's Promise.all slot resolves to. Both shapes flow
|
|
4567
|
+
// through the same assembly branches as a genuinely unconfigured provider —
|
|
4568
|
+
// the CommandCode/Devin fulfilled branches keep the value verbatim, the
|
|
4569
|
+
// OpenCode Go branch passes `configured === false` through, Claude/Codex
|
|
4570
|
+
// naturally produce `{ configured: false }` from their gated null tokens —
|
|
4571
|
+
// so no hidden error, cache read, or credential touch can reach the payload
|
|
4572
|
+
// (#956).
|
|
4573
|
+
const hiddenSentinel = Promise.resolve({ configured: false });
|
|
4574
|
+
const hiddenSettled = Promise.resolve({ status: "fulfilled", value: { configured: false } });
|
|
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([
|
|
4111
4576
|
claudeToken && !freshClaudeCache && !claudeRetryAtMs
|
|
4112
4577
|
? withProviderTimeout(fetchClaudeUsageLimits(claudeToken, { fetchImpl: providerFetch, maxAttempts: 1 }), "Claude", providerTimeoutMs).then(
|
|
4113
4578
|
(value) => ({ status: "fulfilled", value }),
|
|
@@ -4124,17 +4589,17 @@ async function fetchUsageLimitsUncached({
|
|
|
4124
4589
|
(reason) => ({ status: "rejected", reason }),
|
|
4125
4590
|
)
|
|
4126
4591
|
: Promise.resolve(null),
|
|
4127
|
-
withProviderTimeout(fetchCursorLimits({ home, fetchImpl: providerFetch }), "Cursor", providerTimeoutMs)
|
|
4592
|
+
hiddenSet.has("cursor") ? hiddenSentinel : withProviderTimeout(fetchCursorLimits({ home, env, fetchImpl: providerFetch }), "Cursor", providerTimeoutMs)
|
|
4128
4593
|
.catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
|
|
4129
|
-
withProviderTimeout(fetchKimiLimits({ home, env, fetchImpl: providerFetch }), "Kimi", providerTimeoutMs)
|
|
4594
|
+
hiddenSet.has("kimi") ? hiddenSentinel : withProviderTimeout(fetchKimiLimits({ home, env, fetchImpl: providerFetch }), "Kimi", providerTimeoutMs)
|
|
4130
4595
|
.catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
|
|
4131
|
-
withProviderTimeout(fetchGeminiLimits({ home, env, fetchImpl: providerFetch, commandRunner }), "Gemini", providerTimeoutMs)
|
|
4596
|
+
hiddenSet.has("gemini") ? hiddenSentinel : withProviderTimeout(fetchGeminiLimits({ home, env, fetchImpl: providerFetch, commandRunner }), "Gemini", providerTimeoutMs)
|
|
4132
4597
|
.catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
|
|
4133
|
-
fetchKiroLimits({ commandRunner, now, platform, home }),
|
|
4598
|
+
hiddenSet.has("kiro") ? hiddenSentinel : fetchKiroLimits({ commandRunner, now, platform, home }),
|
|
4134
4599
|
// Antigravity's own budget keeps the serial chain inside providerTimeoutMs; this
|
|
4135
4600
|
// outer race is the enforcing backstop every other provider already has, and the
|
|
4136
4601
|
// signal makes a fired race actually kill the spawned scans and open sockets.
|
|
4137
|
-
withAbortableProviderTimeout(
|
|
4602
|
+
hiddenSet.has("antigravity") ? hiddenSentinel : withAbortableProviderTimeout(
|
|
4138
4603
|
(signal) => fetchAntigravityLimits({
|
|
4139
4604
|
home,
|
|
4140
4605
|
commandRunner,
|
|
@@ -4150,18 +4615,18 @@ async function fetchUsageLimitsUncached({
|
|
|
4150
4615
|
"Antigravity",
|
|
4151
4616
|
providerTimeoutMs,
|
|
4152
4617
|
).catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
|
|
4153
|
-
withProviderTimeout(fetchCopilotLimits({ home, env, fetchImpl: providerFetch, platform, securityRunner }), "GitHub Copilot", providerTimeoutMs)
|
|
4618
|
+
hiddenSet.has("copilot") ? hiddenSentinel : withProviderTimeout(fetchCopilotLimits({ home, env, fetchImpl: providerFetch, platform, securityRunner }), "GitHub Copilot", providerTimeoutMs)
|
|
4154
4619
|
.catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
|
|
4155
|
-
withProviderTimeout(fetchGrokLimits({ home, env, fetchImpl: providerFetch, timeoutMs: providerTimeoutMs }), "Grok Build", providerTimeoutMs)
|
|
4620
|
+
hiddenSet.has("grok") ? hiddenSentinel : withProviderTimeout(fetchGrokLimits({ home, env, fetchImpl: providerFetch, timeoutMs: providerTimeoutMs }), "Grok Build", providerTimeoutMs)
|
|
4156
4621
|
.catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
|
|
4157
|
-
withProviderTimeout(fetchZcodeLimits({ home, env, fetchImpl: providerFetch, providerTimeoutMs }), "ZCode", providerTimeoutMs)
|
|
4622
|
+
hiddenSet.has("zcode") ? hiddenSentinel : withProviderTimeout(fetchZcodeLimits({ home, env, fetchImpl: providerFetch, providerTimeoutMs }), "ZCode", providerTimeoutMs)
|
|
4158
4623
|
.catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
|
|
4159
4624
|
// OpenCode Go: authoritative subscription windows come from the dashboard
|
|
4160
4625
|
// scrape; local opencode.db cost is available only as an explicit estimate.
|
|
4161
4626
|
// See src/lib/opencode-go-limits.js.
|
|
4162
|
-
withProviderTimeout(fetchOpencodeGoLimits({ home, trackerDir, env, fetchImpl: providerFetch }), "OpenCode Go", providerTimeoutMs)
|
|
4627
|
+
hiddenSet.has("opencodeGo") ? hiddenSentinel : withProviderTimeout(fetchOpencodeGoLimits({ home, trackerDir, env, fetchImpl: providerFetch }), "OpenCode Go", providerTimeoutMs)
|
|
4163
4628
|
.catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
|
|
4164
|
-
withProviderTimeout(
|
|
4629
|
+
hiddenSet.has("qoder") ? hiddenSentinel : withProviderTimeout(
|
|
4165
4630
|
fetchQoderLimits({
|
|
4166
4631
|
home,
|
|
4167
4632
|
env,
|
|
@@ -4171,7 +4636,7 @@ async function fetchUsageLimitsUncached({
|
|
|
4171
4636
|
"Qoder",
|
|
4172
4637
|
providerTimeoutMs,
|
|
4173
4638
|
).catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
|
|
4174
|
-
withProviderTimeout(
|
|
4639
|
+
hiddenSet.has("qoderCn") ? hiddenSentinel : withProviderTimeout(
|
|
4175
4640
|
fetchQoderCnLimits({
|
|
4176
4641
|
home,
|
|
4177
4642
|
env,
|
|
@@ -4185,7 +4650,7 @@ async function fetchUsageLimitsUncached({
|
|
|
4185
4650
|
// products sharing the same arkcli binary. No token-consumption source —
|
|
4186
4651
|
// consumption for the compatible CLIs is already counted from their
|
|
4187
4652
|
// local files; these only surface the 5h/week/month quota percentages.
|
|
4188
|
-
withAbortableProviderTimeout(
|
|
4653
|
+
hiddenSet.has("codingPlan") ? hiddenSentinel : withAbortableProviderTimeout(
|
|
4189
4654
|
(signal) => fetchArkCodingPlanLimits({
|
|
4190
4655
|
commandRunner,
|
|
4191
4656
|
home,
|
|
@@ -4197,7 +4662,7 @@ async function fetchUsageLimitsUncached({
|
|
|
4197
4662
|
"Ark Coding Plan",
|
|
4198
4663
|
providerTimeoutMs,
|
|
4199
4664
|
).catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
|
|
4200
|
-
withAbortableProviderTimeout(
|
|
4665
|
+
hiddenSet.has("agentPlan") ? hiddenSentinel : withAbortableProviderTimeout(
|
|
4201
4666
|
(signal) => fetchArkAgentPlanLimits({
|
|
4202
4667
|
commandRunner,
|
|
4203
4668
|
home,
|
|
@@ -4217,7 +4682,7 @@ async function fetchUsageLimitsUncached({
|
|
|
4217
4682
|
// outer race bounds the whole slot: without it three sequential per-request
|
|
4218
4683
|
// timeouts (whoami, then credits+subscriptions) could hold the aggregate
|
|
4219
4684
|
// ~2x longer than any sibling provider.
|
|
4220
|
-
withProviderTimeout(fetchCommandcodeLimits({ home, trackerDir, env, fetchImpl: providerFetch }), "CommandCode", providerTimeoutMs)
|
|
4685
|
+
hiddenSet.has("commandCode") ? hiddenSettled : withProviderTimeout(fetchCommandcodeLimits({ home, trackerDir, env, fetchImpl: providerFetch }), "CommandCode", providerTimeoutMs)
|
|
4221
4686
|
.then(
|
|
4222
4687
|
(value) => ({ status: "fulfilled", value }),
|
|
4223
4688
|
(reason) => ({ status: "rejected", reason }),
|
|
@@ -4227,18 +4692,39 @@ async function fetchUsageLimitsUncached({
|
|
|
4227
4692
|
// ~/.local/share/devin/credentials.toml. No local fallback — window state
|
|
4228
4693
|
// lives server-side. fetchDevinLimits throws on auth expiry so the
|
|
4229
4694
|
// assemble step below can flag auth_action_required.
|
|
4230
|
-
withProviderTimeout(fetchDevinLimits({ home, env, enabled: devinEnabled === true, fetchImpl: providerFetch }), "Devin", providerTimeoutMs)
|
|
4695
|
+
hiddenSet.has("devin") ? hiddenSettled : withProviderTimeout(fetchDevinLimits({ home, env, enabled: devinEnabled === true, fetchImpl: providerFetch }), "Devin", providerTimeoutMs)
|
|
4231
4696
|
.then(
|
|
4232
4697
|
(value) => ({ status: "fulfilled", value }),
|
|
4233
4698
|
(reason) => ({ status: "rejected", reason }),
|
|
4234
4699
|
),
|
|
4235
4700
|
// WorkBuddy CN and WorkBuddy AI are separate editions. Each one calls only
|
|
4236
4701
|
// its own local authenticated billing proxy. Abort closes the IPC socket
|
|
4237
|
-
// when the provider budget expires
|
|
4238
|
-
|
|
4702
|
+
// when the provider budget expires; the post-abort grace lets the edition's
|
|
4703
|
+
// disk-cache fallback land instead of the bare timeout. Reward credits are
|
|
4704
|
+
// not a plan window.
|
|
4705
|
+
hiddenSet.has("workbuddy") ? hiddenSentinel : withWorkbuddyProviderTimeout((signal) => fetchWorkbuddyLimits({ home, env, fetchImpl: providerFetch, signal, nowMs: injectedNowMs }), "WorkBuddy", providerTimeoutMs)
|
|
4239
4706
|
.catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
|
|
4240
|
-
|
|
4707
|
+
hiddenSet.has("workbuddyAi") ? hiddenSentinel : withWorkbuddyProviderTimeout((signal) => fetchWorkbuddyAiLimits({ home, env, fetchImpl: providerFetch, signal, nowMs: injectedNowMs }), "WorkBuddy AI", providerTimeoutMs)
|
|
4241
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" })),
|
|
4242
4728
|
// Public status-page probe (fail-soft, own 5-min cache in provider-status.js).
|
|
4243
4729
|
// Only probed for configured accounts — without a token the Claude section
|
|
4244
4730
|
// never renders, so the reading would have nowhere to go.
|
|
@@ -4255,8 +4741,10 @@ async function fetchUsageLimitsUncached({
|
|
|
4255
4741
|
// that still exists means the latter — reporting `configured: false` there hides the
|
|
4256
4742
|
// whole Claude section while the usage bars (parsed from local logs, no auth needed)
|
|
4257
4743
|
// keep updating, which reads as "TokenTracker doesn't support my plan" rather than
|
|
4258
|
-
// "your CLI login expired". Existence-only probe: no secret is read here
|
|
4259
|
-
|
|
4744
|
+
// "your CLI login expired". Existence-only probe: no secret is read here — and a
|
|
4745
|
+
// hidden Claude skips even that (#956): its row is the plain sentinel either way.
|
|
4746
|
+
const credentialsPresent = !hiddenSet.has("claude")
|
|
4747
|
+
&& detectClaudeCodeCredentialsPresence({ platform, securityRunner, home });
|
|
4260
4748
|
claude = credentialsPresent
|
|
4261
4749
|
? { configured: true, error: CLAUDE_AUTH_EXPIRED_MESSAGE, auth_action_required: "reauth" }
|
|
4262
4750
|
: { configured: false };
|
|
@@ -4467,9 +4955,142 @@ async function fetchUsageLimitsUncached({
|
|
|
4467
4955
|
: { configured: true, error: reason?.message || "Unknown error" };
|
|
4468
4956
|
}
|
|
4469
4957
|
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
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
|
+
}
|
|
4473
5094
|
|
|
4474
5095
|
const data = {
|
|
4475
5096
|
fetched_at: new Date(nowMs).toISOString(),
|
|
@@ -4480,7 +5101,7 @@ async function fetchUsageLimitsUncached({
|
|
|
4480
5101
|
// tier, and membership.level is an opaque enum (LEVEL_INTERMEDIATE) with no
|
|
4481
5102
|
// authoritative human-readable plan mapping. Both rendered as garbage like
|
|
4482
5103
|
// "Kimi Type_event" (issue #130), so show the bare brand instead.
|
|
4483
|
-
kimi: withPlanLabel(
|
|
5104
|
+
kimi: withPlanLabel(kimiWithCache, null, "Kimi"),
|
|
4484
5105
|
gemini: withPlanLabel(gemini, gemini.account_plan, "Gemini"),
|
|
4485
5106
|
kiro: withPlanLabel(kiro, kiro.plan_name, "Kiro"),
|
|
4486
5107
|
antigravity: withPlanLabel(antigravity, antigravity.account_plan, "Antigravity"),
|
|
@@ -4491,7 +5112,7 @@ async function fetchUsageLimitsUncached({
|
|
|
4491
5112
|
// outright, so surface the label as-is like CommandCode below. Error and
|
|
4492
5113
|
// unconfigured branches never carry plan_label, so the panel still falls
|
|
4493
5114
|
// back to the bare brand name.
|
|
4494
|
-
grok,
|
|
5115
|
+
grok: grokWithCache,
|
|
4495
5116
|
zcode: withPlanLabel(zcode, zcode.plan_label, "ZCode"),
|
|
4496
5117
|
opencodeGo: withPlanLabel(opencodeGo, opencodeGo?.plan_label, "OpenCode Go"),
|
|
4497
5118
|
// CommandCode tiers are acronym brands (GOAT/Pro/Max) — the fetcher already
|
|
@@ -4510,10 +5131,18 @@ async function fetchUsageLimitsUncached({
|
|
|
4510
5131
|
// as separate providers — one row per signed-in edition, never merged.
|
|
4511
5132
|
workbuddy,
|
|
4512
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,
|
|
4513
5138
|
};
|
|
4514
5139
|
|
|
4515
5140
|
for (const [providerName, provider] of Object.entries(data)) {
|
|
4516
5141
|
if (providerName === "fetched_at" || !provider || typeof provider !== "object") continue;
|
|
5142
|
+
// An exact not-configured sentinel (hidden provider #956, or an opted-out
|
|
5143
|
+
// Devin) is metadata-free by contract — it was never fetched, so it must
|
|
5144
|
+
// not be aged into a "disk-cache"/stale provenance.
|
|
5145
|
+
if (provider.configured === false && Object.keys(provider).length === 1) continue;
|
|
4517
5146
|
const capturedAt = provider.cached_at || data.fetched_at;
|
|
4518
5147
|
const ageMs = Math.max(0, nowMs - Date.parse(capturedAt || ""));
|
|
4519
5148
|
const stale = provider.stale === true || (Number.isFinite(ageMs) && ageMs > 10 * 60 * 1000);
|
|
@@ -4534,7 +5163,8 @@ async function fetchUsageLimitsUncached({
|
|
|
4534
5163
|
}
|
|
4535
5164
|
|
|
4536
5165
|
if (cacheGeneration === usageLimitsCacheGeneration) {
|
|
4537
|
-
|
|
5166
|
+
pruneSelectionPartitions(nowMs);
|
|
5167
|
+
cacheBySelection[selectionKey || usageLimitsSelectionKey({ devinEnabled, hiddenProviders })] = {
|
|
4538
5168
|
data,
|
|
4539
5169
|
expiresAtMs: cacheExpiresAtMs(data, nowMs),
|
|
4540
5170
|
};
|
|
@@ -4542,20 +5172,50 @@ async function fetchUsageLimitsUncached({
|
|
|
4542
5172
|
return data;
|
|
4543
5173
|
}
|
|
4544
5174
|
|
|
5175
|
+
// Hidden-set keys make the partition space user-shaped and unbounded in
|
|
5176
|
+
// principle, while realistic sessions only ever hold a handful of selections.
|
|
5177
|
+
// Keep it bounded: once past the soft cap, drop expired partitions first and
|
|
5178
|
+
// then the oldest still-unexpired ones — they are simply refetched on demand.
|
|
5179
|
+
const SELECTION_PARTITION_CAP = 48;
|
|
5180
|
+
function pruneSelectionPartitions(nowMs) {
|
|
5181
|
+
const keys = Object.keys(cacheBySelection);
|
|
5182
|
+
if (keys.length <= SELECTION_PARTITION_CAP) return;
|
|
5183
|
+
const expired = [];
|
|
5184
|
+
const live = [];
|
|
5185
|
+
for (const key of keys) {
|
|
5186
|
+
(cacheBySelection[key].expiresAtMs <= nowMs ? expired : live).push(key);
|
|
5187
|
+
}
|
|
5188
|
+
const dropCount = keys.length - SELECTION_PARTITION_CAP;
|
|
5189
|
+
const drop = expired.length >= dropCount
|
|
5190
|
+
? expired.slice(0, dropCount)
|
|
5191
|
+
: [...expired, ...live.slice(0, dropCount - expired.length)];
|
|
5192
|
+
for (const key of drop) delete cacheBySelection[key];
|
|
5193
|
+
}
|
|
5194
|
+
|
|
4545
5195
|
function resetUsageLimitsCache({ invalidateInFlight = false } = {}) {
|
|
4546
5196
|
if (invalidateInFlight) usageLimitsCacheGeneration += 1;
|
|
4547
|
-
|
|
5197
|
+
// Selection partitions are created lazily (hidden sets extend the key
|
|
5198
|
+
// space), so reset whatever exists instead of a fixed key list.
|
|
5199
|
+
for (const key of Object.keys(cacheBySelection)) {
|
|
4548
5200
|
cacheBySelection[key] = { data: null, expiresAtMs: 0 };
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
5201
|
+
}
|
|
5202
|
+
// Ordinary resets still let a running fetch serve refresh=1 callers; a
|
|
5203
|
+
// credential replacement must not, so drop the old-key promises too. The
|
|
5204
|
+
// in-flight map is walked on its own: a selection can hold a promise before
|
|
5205
|
+
// its partition exists (stored only when the fetch completes).
|
|
5206
|
+
if (invalidateInFlight) {
|
|
5207
|
+
for (const key of Object.keys(inFlightBySelection)) {
|
|
5208
|
+
delete inFlightBySelection[key];
|
|
5209
|
+
}
|
|
4552
5210
|
}
|
|
4553
5211
|
}
|
|
4554
5212
|
|
|
4555
5213
|
module.exports = {
|
|
4556
5214
|
getUsageLimits,
|
|
4557
5215
|
normalizePlanLabel,
|
|
5216
|
+
normalizeHiddenProviders,
|
|
4558
5217
|
resetUsageLimitsCache,
|
|
5218
|
+
USAGE_LIMITS_PROVIDER_IDS,
|
|
4559
5219
|
OPENCODE_GO_LIMITS_CACHE_FILE,
|
|
4560
5220
|
resolveOpencodeGoLimitsCachePath,
|
|
4561
5221
|
cacheExpiresAtMs,
|
|
@@ -4601,4 +5261,5 @@ module.exports = {
|
|
|
4601
5261
|
fetchQoderCnLimits,
|
|
4602
5262
|
fetchWorkbuddyLimits,
|
|
4603
5263
|
fetchWorkbuddyAiLimits,
|
|
5264
|
+
withWorkbuddyProviderTimeout,
|
|
4604
5265
|
};
|