tokentracker-cli 1.5.6 → 1.5.7
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/README.md +2 -0
- package/dashboard/dist/assets/{AchievementBadge-NKpY___e.js → AchievementBadge-DIHKXKgm.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-9nXmSX72.js → AchievementsPage-U66jjKFf.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DttQciSY.js → AchievementsSection-BAW79xKg.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-bvWEdJbe.js → BadgeDetailModal-IfomOw9B.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-ChuUfph8.js → BotAnimated-B33gSUM3.js} +1 -1
- package/dashboard/dist/assets/{Card-SWtLDXt1.js → Card-dtrkef-y.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-c2fNl1Rl.js → CloudCheckoutPage-jYPgHJYE.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-CfwDjZMS.js → CloudPage-BumdgM5w.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-tWgPa295.js → CommandPalette-CFEeoIo3.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-D7UaaUdM.js → CommunityStatsModal-DL3MrPmb.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-BrBcKhfW.js → ConfirmModal-BRdtCv2g.js} +1 -1
- package/dashboard/dist/assets/{Controls-CcvBkk7W.js → Controls-Bb-_ZAsT.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-GXwt6DoC.js +1 -0
- package/dashboard/dist/assets/{DateRangePopover-BkN_pird.js → DateRangePopover-CHkFCFkE.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-BbUL7WwT.js → DevicePage-Da753sLS.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-BXO8SBFr.js → DialogClose-4A_jISLc.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-CiEhDvt9.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-Du332auj.js → DialogTitle-C0oQdI9n.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-CXLYfOt7.js → ElectricLogo-Bv-sB-O-.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-HSQBOc8B.js → FadeIn-Ch2AuGxY.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-D5tyQDGE.js → HeaderGithubStar-C_dy8a5K.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-BhidhbnD.js → HoverTooltip-iuzo5YeN.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-WMEF1oS3.js → IpCheckPage-BKEID9fw.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-BmO-59ZP.js → LandingPage-DZt5JWZ7.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-C2WyniEt.js → LeaderboardPage-CYrCQF5P.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-DAZnMRNn.js → LeaderboardProBadge-B8vG2SAW.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-D1BxcloV.js → LeaderboardProfileModal-DpAvBJPT.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-RPJDTwpG.js → LeaderboardProfilePage-CofOk8GO.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-9ZOhKvn5.js → LimitsPage-DXknvxtI.js} +6 -6
- package/dashboard/dist/assets/{LocalOnlyNotice-CH6w4hRw.js → LocalOnlyNotice-BBVCEm5Q.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-D24HsRjs.js → LoginCard-DcPazHo1.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-sLvHyi9U.js → LoginModal-CGXPaeWD.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-PIvLEw1Y.js → LoginPage-Ct6Al1pz.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DYcw_4P1.js → PetPage-8NPEAa-c.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-D7ZCWq5f.js → PopoverPopup-R69ks89h.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-B0kB52K-.js → ProviderIcon-svF44YE0.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-B_NT4Xfl.js → ResetPasswordPage-DJIYKu8C.js} +1 -1
- package/dashboard/dist/assets/{Select-BtxjjP2Q.js → Select-BVK4XZD5.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-Co1M6XRC.js → SelectItemText-Bh74D8lr.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-XadfhFnd.js → SelfHostPage-8gj2lAFp.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-ZEJyAxsQ.js → ServiceStatusPage-CWPV_Mou.js} +1 -1
- package/dashboard/dist/assets/SessionsPage-Bo5plK6O.js +3 -0
- package/dashboard/dist/assets/{SettingsPage-D4qHgIxn.js → SettingsPage-BptJrWn-.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-CkQCk2uF.js → SkillsPage-CRgGXnJS.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-Bx1vr4b0.js → TokenGalaxy-dgOy0Eo7.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-DQW8FWJO.js → TooltipPopup-DpGnyoFz.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-BMNcY3N8.js → TrendMonitor-BEy_GbMg.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-luRTge7O.js → WidgetsPage-ChUelFrP.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-B1j3WgXx.js → WrappedPage-n63RsX8z.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-CvvpsW-U.js → __vite-browser-external-D_JnEwiu.js} +3 -3
- package/dashboard/dist/assets/{agent-logos-DL4fdks6.js → agent-logos-DQ1h5uQF.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-DiOsjgdu.js → arrow-left-Bo3Sg_cg.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-DBFXV7GU.js → arrow-right-DdxkL8Ul.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-BIvpDQHo.js → arrow-up-right-CpvelPD2.js} +1 -1
- package/dashboard/dist/assets/{calendar-days--psnXUPW.js → calendar-days-BvNVya0e.js} +1 -1
- package/dashboard/dist/assets/{check-DLqbsN00.js → check-tO40Glrg.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-C0P0oLzP.js → chevron-down-Dnrra3mC.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-NSmb7QNy.js → cloud-checkout-COC4zB8s.js} +1 -1
- package/dashboard/dist/assets/{composite-D7ahPHYQ.js → composite-C0NgnRTl.js} +1 -1
- package/dashboard/dist/assets/{copy-COD7vwd6.js → copy-DTKN2SO4.js} +1 -1
- package/dashboard/dist/assets/{download-CpSUX4U-.js → download-CSq2Yd3D.js} +1 -1
- package/dashboard/dist/assets/{external-link-DXUjrooT.js → external-link-BtGedmuQ.js} +1 -1
- package/dashboard/dist/assets/{flame-Dl3ZT9oo.js → flame-ChjXIFSx.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-Ci49THbK.js → format-tokens-CXqZY114.js} +1 -1
- package/dashboard/dist/assets/{gift-DTHCN1Of.js → gift-YFwBvsUA.js} +1 -1
- package/dashboard/dist/assets/{icons-CB9s1e1k.js → icons-DCiBpbK0.js} +1 -1
- package/dashboard/dist/assets/{index-DFdfNKJM.js → index-B7sRX2EC.js} +1 -1
- package/dashboard/dist/assets/{index-BGT8ZQyn.js → index-DMqIbTL4.js} +1 -1
- package/dashboard/dist/assets/{index-DeX4t_5p.js → index-Dpmdg98T.js} +1 -1
- package/dashboard/dist/assets/{index-Dozwi-Se.js → index-XSya-vft.js} +1 -1
- package/dashboard/dist/assets/{info-CqM2wmSI.js → info-PYzcoobJ.js} +1 -1
- package/dashboard/dist/assets/{link-2-Byo7Rj3t.js → link-2-BxSrdKyR.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-CMxd78Na.js → loader-circle-CgvCCDTH.js} +1 -1
- package/dashboard/dist/assets/{local-usage-transfer-CBjnoeck.js → local-usage-transfer-eTzMdhbs.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-Oowlk_FC.js → modifiers.esm-B796CRz-.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-B2EgB_9q.js → monitor-smartphone-M5nbcGRy.js} +1 -1
- package/dashboard/dist/assets/{play-BSuP-UBL.js → play-12ZzPH9r.js} +1 -1
- package/dashboard/dist/assets/{plus-BWemSIuC.js → plus-OqDA7xZy.js} +1 -1
- package/dashboard/dist/assets/{provider-display-BpzWuFDH.js → provider-display-JlEBv1VB.js} +1 -1
- package/dashboard/dist/assets/{react-cIJ4zx62.js → react-CeDcnQho.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-C_ZMtbgL.js → safePolygon-B_SGhQbo.js} +1 -1
- package/dashboard/dist/assets/{search-BK7eyNds.js → search-D9wz4rWp.js} +1 -1
- package/dashboard/dist/assets/{server-BqQHX-rd.js → server-UCMw4AQF.js} +1 -1
- package/dashboard/dist/assets/{shield-check-CO5U6gRw.js → shield-check-lJ0-Z5Ot.js} +1 -1
- package/dashboard/dist/assets/{skills-api-CMye76ag.js → skills-api-Da-WZbvW.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-Bvb61Lvj.js → sortable.esm-Bj1ZkbqW.js} +1 -1
- package/dashboard/dist/assets/{store-CICV1KXb.js → store-Bpwwxxsy.js} +1 -1
- package/dashboard/dist/assets/{terminal-BKMWT1Ux.js → terminal-BHQMC4hm.js} +1 -1
- package/dashboard/dist/assets/{trash-D2Oqim4r.js → trash-yTV6vhGX.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-CEqwmjct.js → use-community-stats-rrE4Ohic.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-CTuzzu3F.js → use-latest-request-guard-D4Y1aCyd.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-CA2BLWkY.js → use-limits-display-prefs-ChHr4XEF.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-B54wHSj9.js → use-native-settings-BmoCls3D.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list--PU9ekxY.js → use-ordered-list-BZSSYcLx.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-CKTIXp4-.js +1 -0
- package/dashboard/dist/assets/{use-transform-BRyR8szg.js → use-transform-CJO1b0Iu.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-sufKdfDY.js → use-usage-limits-BbAC_LWP.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-6oy7kSX8.js → useCurrency-B2enOMwJ.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-ZtDdVdJg.js → usePositioner-BaoW4QRL.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-tiOJ-PeE.js → useTokenFormat-B2bbeEu-.js} +1 -1
- package/dashboard/dist/assets/{zap-Dr1dXUjN.js → zap-CT5a8-_j.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/init.js +9 -0
- package/src/commands/status.js +12 -6
- package/src/commands/sync.js +22 -4
- package/src/lib/claude-backend.js +291 -0
- package/src/lib/claude-config.js +1 -1
- package/src/lib/cursor-config.js +34 -9
- package/src/lib/cursor-safe-storage.js +119 -0
- package/src/lib/devin-limits.js +65 -25
- package/src/lib/diagnostics.js +3 -0
- package/src/lib/doctor.js +13 -0
- package/src/lib/local-api.js +2 -0
- package/src/lib/local-usage-transfer.js +22 -1
- package/src/lib/pricing/index.js +12 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +70 -15
- package/src/lib/session-analytics.js +20 -8
- package/src/lib/trae-cn-config.js +32 -2
- package/src/lib/usage-limits.js +601 -35
- package/dashboard/dist/assets/DashboardPage-DJSgRHGq.js +0 -1
- package/dashboard/dist/assets/DialogDescription-B1HyKP0x.js +0 -1
- package/dashboard/dist/assets/SessionsPage-Cte1taUu.js +0 -3
- package/dashboard/dist/assets/use-reduced-motion-CZ8_YWc2.js +0 -1
package/src/lib/usage-limits.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { readCustomBackend } = require("./claude-backend");
|
|
1
2
|
const cp = require("node:child_process");
|
|
2
3
|
const crypto = require("node:crypto");
|
|
3
4
|
const fs = require("node:fs");
|
|
@@ -31,7 +32,7 @@ const { fetchGrokLimits, grokTokenSubject, loadGrokAuthEntry } = require("./grok
|
|
|
31
32
|
const { fetchZcodeLimits } = require("./zcode-limits");
|
|
32
33
|
const { fetchOpencodeGoLimits } = require("./opencode-go-limits");
|
|
33
34
|
const { fetchCommandcodeLimits } = require("./commandcode-limits");
|
|
34
|
-
const { fetchDevinLimits } = require("./devin-limits");
|
|
35
|
+
const { fetchDevinLimits, resolveDevinCacheIdentity } = require("./devin-limits");
|
|
35
36
|
const { fetchQoderLimits, fetchQoderCnLimits } = require("./qoder-limits");
|
|
36
37
|
const { fetchArkCodingPlanLimits } = require("./ark-coding-plan-limits");
|
|
37
38
|
const { fetchArkAgentPlanLimits } = require("./ark-agent-plan-limits");
|
|
@@ -56,7 +57,8 @@ const execFileAsync = promisify(cp.execFile);
|
|
|
56
57
|
// is never served (or joined onto) a response fetched while it was on, an aggregate
|
|
57
58
|
// fetched while Antigravity was enabled is never served after the opt-out is set,
|
|
58
59
|
// and a response fetched for a visibility selection is never served to one made
|
|
59
|
-
// under a different one.
|
|
60
|
+
// under a different one. Custom Claude origins also partition cached/in-flight
|
|
61
|
+
// responses so switching endpoints cannot resurrect Anthropic quota bars.
|
|
60
62
|
const USAGE_LIMITS_BASE_SELECTION_KEYS = ["off-on", "off-off", "on-on", "on-off"];
|
|
61
63
|
const cacheBySelection = {};
|
|
62
64
|
for (const key of USAGE_LIMITS_BASE_SELECTION_KEYS) cacheBySelection[key] = { data: null, expiresAtMs: 0 };
|
|
@@ -198,6 +200,29 @@ const KIMI_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
198
200
|
// (mirrors Claude / Codex / Cursor).
|
|
199
201
|
const GROK_LIMITS_CACHE_FILE = "grok-usage-limits-cache.json";
|
|
200
202
|
const GROK_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
203
|
+
// Kiro's quota windows come from a local kiro-cli round-trip (2s version probe +
|
|
204
|
+
// 20s per attempt) whose upstream call can time out or return an unparseable
|
|
205
|
+
// panel. Persist the last successful windows so a blip serves stale bars instead
|
|
206
|
+
// (mirrors Claude / Codex / OpenCode Go / Cursor).
|
|
207
|
+
const KIRO_LIMITS_CACHE_FILE = "kiro-usage-limits-cache.json";
|
|
208
|
+
const KIRO_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
209
|
+
// kiro-cli persists the signed-in account's CodeWhisperer profile ARN — a
|
|
210
|
+
// non-secret resource id — alongside its tokens in this file. It is the cache
|
|
211
|
+
// partition key, never the token itself.
|
|
212
|
+
const KIRO_AUTH_TOKEN_FILE = "kiro-auth-token.json";
|
|
213
|
+
// api.github.com is the most stable of the network providers, but a timeout or 5xx
|
|
214
|
+
// still blanks the Copilot card until the next poll. Persist the last successful
|
|
215
|
+
// windows so a blip serves stale bars instead (mirrors Claude / Codex / OpenCode Go /
|
|
216
|
+
// Cursor).
|
|
217
|
+
const COPILOT_LIMITS_CACHE_FILE = "copilot-usage-limits-cache.json";
|
|
218
|
+
const COPILOT_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
219
|
+
// Devin's GetPlanStatus RPC (server.codeium.com) is the only source of its
|
|
220
|
+
// daily/weekly quota windows — window state lives server-side. A transient
|
|
221
|
+
// timeout / 5xx previously blanked the card until the next poll — persist the
|
|
222
|
+
// last successful windows so a blip serves stale bars instead
|
|
223
|
+
// (mirrors OpenCode Go / Cursor).
|
|
224
|
+
const DEVIN_LIMITS_CACHE_FILE = "devin-usage-limits-cache.json";
|
|
225
|
+
const DEVIN_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
201
226
|
// A 429 from the usage endpoint carries a long `retry-after` (often 20+ minutes). Persist
|
|
202
227
|
// the cooldown so every surface — this process, the menu bar app's embedded server, a later
|
|
203
228
|
// restart — stops calling until it expires. Hammering during the cooldown just renews the
|
|
@@ -1775,6 +1800,112 @@ function readKiroCreditsSummary({ home = os.homedir() } = {}) {
|
|
|
1775
1800
|
}
|
|
1776
1801
|
}
|
|
1777
1802
|
|
|
1803
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1804
|
+
// Kiro limits disk cache — last good windows parsed from the kiro-cli panel.
|
|
1805
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1806
|
+
|
|
1807
|
+
function resolveKiroLimitsCachePath({ home } = {}) {
|
|
1808
|
+
return path.join(home || os.homedir(), ".tokentracker", "tracker", KIRO_LIMITS_CACHE_FILE);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
function hasKiroWindow(limits) {
|
|
1812
|
+
return Boolean(limits?.primary_window || limits?.secondary_window);
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// Kiro windows carry `reset_at` as an ISO string (the plan's monthly reset date,
|
|
1816
|
+
// the bonus credits' own expiry). A window whose reset has already passed is
|
|
1817
|
+
// stale data, not a usable fallback, so drop it. Windows without a reset stamp
|
|
1818
|
+
// are kept — the overall cached_at max-age gate already bounds them.
|
|
1819
|
+
function isKiroCacheWindowUsable(window, { nowMs } = {}) {
|
|
1820
|
+
if (!window || typeof window !== "object") return false;
|
|
1821
|
+
const resetAtMs = parseTimeMs(window.reset_at);
|
|
1822
|
+
if (resetAtMs === null) return true;
|
|
1823
|
+
return resetAtMs > nowMs;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
function normalizeKiroCachedLimits(
|
|
1827
|
+
raw,
|
|
1828
|
+
{ nowMs = Date.now(), maxAgeMs = KIRO_LIMITS_CACHE_MAX_AGE_MS } = {},
|
|
1829
|
+
) {
|
|
1830
|
+
const cachedAtMs = parseTimeMs(raw?.cached_at);
|
|
1831
|
+
if (!Number.isFinite(cachedAtMs)) return null;
|
|
1832
|
+
if (cachedAtMs > nowMs + 60_000) return null;
|
|
1833
|
+
if (nowMs - cachedAtMs > maxAgeMs) return null;
|
|
1834
|
+
|
|
1835
|
+
const cached = {
|
|
1836
|
+
configured: true,
|
|
1837
|
+
error: null,
|
|
1838
|
+
plan_name: typeof raw?.plan_name === "string" ? raw.plan_name : null,
|
|
1839
|
+
primary_window: isKiroCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
|
|
1840
|
+
secondary_window: isKiroCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
|
|
1841
|
+
stale: true,
|
|
1842
|
+
cached_at: raw.cached_at,
|
|
1843
|
+
};
|
|
1844
|
+
return hasKiroWindow(cached) ? cached : null;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// The signed-in Kiro account partitions the cache (zcode / Cursor login-identity
|
|
1848
|
+
// precedent): the id is the CodeWhisperer profile ARN kiro-cli itself persists
|
|
1849
|
+
// next to its tokens. Reads require the stored identity to match the current
|
|
1850
|
+
// one, and a write without an identity is refused — after an account switch the
|
|
1851
|
+
// previous account's windows must never resurface as another login's fallback.
|
|
1852
|
+
function readKiroLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
|
|
1853
|
+
if (!identity) return null;
|
|
1854
|
+
try {
|
|
1855
|
+
const parsed = JSON.parse(fs.readFileSync(resolveKiroLimitsCachePath({ home }), "utf8"));
|
|
1856
|
+
const entry = parsed?.kiro;
|
|
1857
|
+
if (entry?.identity !== identity) return null;
|
|
1858
|
+
return normalizeKiroCachedLimits(entry, { nowMs });
|
|
1859
|
+
} catch (_error) {
|
|
1860
|
+
return null;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
function writeKiroLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
|
|
1865
|
+
if (!identity) return;
|
|
1866
|
+
if (!limits?.configured || limits.error || !hasKiroWindow(limits)) return;
|
|
1867
|
+
const cachePath = resolveKiroLimitsCachePath({ home });
|
|
1868
|
+
const payload = {
|
|
1869
|
+
kiro: {
|
|
1870
|
+
identity,
|
|
1871
|
+
plan_name: limits.plan_name || null,
|
|
1872
|
+
primary_window: limits.primary_window || null,
|
|
1873
|
+
secondary_window: limits.secondary_window || null,
|
|
1874
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
1875
|
+
},
|
|
1876
|
+
};
|
|
1877
|
+
try {
|
|
1878
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
1879
|
+
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
1880
|
+
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
1881
|
+
fs.renameSync(tmpPath, cachePath);
|
|
1882
|
+
} catch (_error) {}
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
function resolveKiroAuthIdentity({ home } = {}) {
|
|
1886
|
+
try {
|
|
1887
|
+
const parsed = JSON.parse(
|
|
1888
|
+
fs.readFileSync(
|
|
1889
|
+
path.join(home || os.homedir(), ".aws", "sso", "cache", KIRO_AUTH_TOKEN_FILE),
|
|
1890
|
+
"utf8",
|
|
1891
|
+
),
|
|
1892
|
+
);
|
|
1893
|
+
const profileArn = parsed?.profileArn;
|
|
1894
|
+
return typeof profileArn === "string" && profileArn ? profileArn : null;
|
|
1895
|
+
} catch (_error) {
|
|
1896
|
+
return null;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// Any login-shaped CLI output (not-logged-in banner, oauth error, auth portal
|
|
1901
|
+
// failure) collapses into one "Not logged in to Kiro..." error inside
|
|
1902
|
+
// parseKiroUsageOutput, so the slot's error string is the whole auth story.
|
|
1903
|
+
function isKiroAuthError(message) {
|
|
1904
|
+
return /not logged in to kiro|kiro-cli login|login required|oauth error|failed to initialize auth portal/i.test(
|
|
1905
|
+
String(message || ""),
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1778
1909
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1779
1910
|
// GitHub Copilot — `GET https://api.github.com/copilot_internal/user`
|
|
1780
1911
|
// Reuses the OAuth token from the user's existing Copilot install. Older clients
|
|
@@ -1796,7 +1927,10 @@ const COPILOT_AUTH_DB_SQL =
|
|
|
1796
1927
|
"SELECT user_login, auth_authority, scopes, token_schema_version, hex(token_ciphertext) AS token_hex, "
|
|
1797
1928
|
+ "last_used_at, updated_at FROM oauth_tokens ORDER BY last_used_at DESC, updated_at DESC";
|
|
1798
1929
|
|
|
1799
|
-
|
|
1930
|
+
// Returns { token, user } for the first usable entry — `user` is the non-secret
|
|
1931
|
+
// GitHub login stored alongside the OAuth token by the editors that write these
|
|
1932
|
+
// files (absent on hand-rolled setups), or null when a token carries no login.
|
|
1933
|
+
function readPlaintextCopilotAuth({ home }) {
|
|
1800
1934
|
const candidates = [
|
|
1801
1935
|
path.join(home, ".config", "github-copilot", "apps.json"),
|
|
1802
1936
|
path.join(home, ".config", "github-copilot", "hosts.json"),
|
|
@@ -1819,14 +1953,21 @@ function readPlaintextCopilotOauthToken({ home }) {
|
|
|
1819
1953
|
if (!value || typeof value !== "object") continue;
|
|
1820
1954
|
const token = typeof value.oauth_token === "string" ? value.oauth_token : "";
|
|
1821
1955
|
if (!token) continue;
|
|
1956
|
+
const user = typeof value.user === "string" && value.user.trim()
|
|
1957
|
+
? value.user.trim()
|
|
1958
|
+
: null;
|
|
1822
1959
|
const host = String(key).split(":")[0];
|
|
1823
|
-
if (host === "github.com") return token;
|
|
1824
|
-
if (!fallback) fallback = token;
|
|
1960
|
+
if (host === "github.com") return { token, user };
|
|
1961
|
+
if (!fallback) fallback = { token, user };
|
|
1825
1962
|
}
|
|
1826
1963
|
}
|
|
1827
1964
|
return fallback;
|
|
1828
1965
|
}
|
|
1829
1966
|
|
|
1967
|
+
function readPlaintextCopilotOauthToken({ home }) {
|
|
1968
|
+
return readPlaintextCopilotAuth({ home })?.token || null;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1830
1971
|
function readMacosKeychainGenericPassword({ service, account, securityRunner } = {}) {
|
|
1831
1972
|
const runner = typeof securityRunner === "function" ? securityRunner : cp.spawnSync;
|
|
1832
1973
|
if (runner === cp.spawnSync && !fs.existsSync(MACOS_SECURITY_BIN)) return null;
|
|
@@ -1967,6 +2108,14 @@ function readCopilotAuthDbRows({ home, sqliteReader, asyncReader = false } = {})
|
|
|
1967
2108
|
return reader(dbPath, COPILOT_AUTH_DB_SQL, { label: "GitHub Copilot" });
|
|
1968
2109
|
}
|
|
1969
2110
|
|
|
2111
|
+
// The non-secret GitHub login of the row the token came from — used as the
|
|
2112
|
+
// cache-partition identity for the Copilot limits fallback.
|
|
2113
|
+
function copilotRowLogin(row) {
|
|
2114
|
+
return typeof row?.user_login === "string" && row.user_login.trim()
|
|
2115
|
+
? row.user_login.trim()
|
|
2116
|
+
: null;
|
|
2117
|
+
}
|
|
2118
|
+
|
|
1970
2119
|
function resolveCopilotAuthDbTokenFromRows(rows, {
|
|
1971
2120
|
platform = process.platform,
|
|
1972
2121
|
securityRunner,
|
|
@@ -1982,7 +2131,7 @@ function resolveCopilotAuthDbTokenFromRows(rows, {
|
|
|
1982
2131
|
const schemaVersion = parseCopilotAuthDbSchemaVersion(row?.token_schema_version);
|
|
1983
2132
|
if (schemaVersion === 0) {
|
|
1984
2133
|
const plaintextToken = decodeCopilotSchemaV0Token(row);
|
|
1985
|
-
if (plaintextToken) return plaintextToken;
|
|
2134
|
+
if (plaintextToken) return { token: plaintextToken, login: copilotRowLogin(row) };
|
|
1986
2135
|
continue;
|
|
1987
2136
|
}
|
|
1988
2137
|
|
|
@@ -2001,7 +2150,7 @@ function resolveCopilotAuthDbTokenFromRows(rows, {
|
|
|
2001
2150
|
}
|
|
2002
2151
|
if (!keyBase64) continue;
|
|
2003
2152
|
const token = decryptCopilotAuthDbToken(keyBase64, ciphertextHex);
|
|
2004
|
-
if (token) return token;
|
|
2153
|
+
if (token) return { token, login: copilotRowLogin(row) };
|
|
2005
2154
|
}
|
|
2006
2155
|
return null;
|
|
2007
2156
|
}
|
|
@@ -2019,7 +2168,7 @@ async function resolveCopilotAuthDbTokenFromRowsAsync(rows, {
|
|
|
2019
2168
|
const schemaVersion = parseCopilotAuthDbSchemaVersion(row?.token_schema_version);
|
|
2020
2169
|
if (schemaVersion === 0) {
|
|
2021
2170
|
const plaintextToken = decodeCopilotSchemaV0Token(row);
|
|
2022
|
-
if (plaintextToken) return plaintextToken;
|
|
2171
|
+
if (plaintextToken) return { token: plaintextToken, login: copilotRowLogin(row) };
|
|
2023
2172
|
continue;
|
|
2024
2173
|
}
|
|
2025
2174
|
|
|
@@ -2035,7 +2184,7 @@ async function resolveCopilotAuthDbTokenFromRowsAsync(rows, {
|
|
|
2035
2184
|
}
|
|
2036
2185
|
if (!keyBase64) continue;
|
|
2037
2186
|
const token = decryptCopilotAuthDbToken(keyBase64, ciphertextHex);
|
|
2038
|
-
if (token) return token;
|
|
2187
|
+
if (token) return { token, login: copilotRowLogin(row) };
|
|
2039
2188
|
}
|
|
2040
2189
|
return null;
|
|
2041
2190
|
}
|
|
@@ -2047,7 +2196,7 @@ function readCopilotAuthDbToken({ home, platform = process.platform, securityRun
|
|
|
2047
2196
|
} catch (_e) {
|
|
2048
2197
|
return null;
|
|
2049
2198
|
}
|
|
2050
|
-
return resolveCopilotAuthDbTokenFromRows(rows, { platform, securityRunner });
|
|
2199
|
+
return resolveCopilotAuthDbTokenFromRows(rows, { platform, securityRunner })?.token || null;
|
|
2051
2200
|
}
|
|
2052
2201
|
|
|
2053
2202
|
async function readCopilotAuthDbTokenAsync({ home, platform = process.platform, securityRunner, sqliteReader } = {}) {
|
|
@@ -2057,19 +2206,59 @@ async function readCopilotAuthDbTokenAsync({ home, platform = process.platform,
|
|
|
2057
2206
|
} catch (_e) {
|
|
2058
2207
|
return null;
|
|
2059
2208
|
}
|
|
2060
|
-
|
|
2209
|
+
const resolved = await resolveCopilotAuthDbTokenFromRowsAsync(rows, { platform, securityRunner });
|
|
2210
|
+
return resolved?.token || null;
|
|
2061
2211
|
}
|
|
2062
2212
|
|
|
2063
|
-
|
|
2213
|
+
// Token + cache-partition identity in one resolution: the identity is the
|
|
2214
|
+
// non-secret GitHub login of whichever auth source produced the token
|
|
2215
|
+
// (the plaintext files' `user` field, or the auth.db row's `user_login`).
|
|
2216
|
+
// A token without a readable login yields identity null — the cache write
|
|
2217
|
+
// side refuses null identities.
|
|
2218
|
+
function readCopilotAuth({
|
|
2064
2219
|
home = os.homedir(),
|
|
2065
2220
|
platform = process.platform,
|
|
2066
2221
|
securityRunner,
|
|
2067
2222
|
sqliteReader,
|
|
2068
2223
|
} = {}) {
|
|
2069
|
-
const plaintext =
|
|
2070
|
-
if (plaintext) return plaintext;
|
|
2224
|
+
const plaintext = readPlaintextCopilotAuth({ home });
|
|
2225
|
+
if (plaintext?.token) return { token: plaintext.token, identity: plaintext.user };
|
|
2071
2226
|
// No plaintext token found — recover the live one from the encrypted auth.db.
|
|
2072
|
-
|
|
2227
|
+
let rows;
|
|
2228
|
+
try {
|
|
2229
|
+
rows = readCopilotAuthDbRows({ home, sqliteReader });
|
|
2230
|
+
} catch (_e) {
|
|
2231
|
+
return null;
|
|
2232
|
+
}
|
|
2233
|
+
const resolved = resolveCopilotAuthDbTokenFromRows(rows, { platform, securityRunner });
|
|
2234
|
+
return resolved ? { token: resolved.token, identity: resolved.login } : null;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
async function readCopilotAuthAsync({
|
|
2238
|
+
home = os.homedir(),
|
|
2239
|
+
platform = process.platform,
|
|
2240
|
+
securityRunner,
|
|
2241
|
+
sqliteReader,
|
|
2242
|
+
} = {}) {
|
|
2243
|
+
const plaintext = readPlaintextCopilotAuth({ home });
|
|
2244
|
+
if (plaintext?.token) return { token: plaintext.token, identity: plaintext.user };
|
|
2245
|
+
let rows;
|
|
2246
|
+
try {
|
|
2247
|
+
rows = await readCopilotAuthDbRows({ home, sqliteReader, asyncReader: true });
|
|
2248
|
+
} catch (_e) {
|
|
2249
|
+
return null;
|
|
2250
|
+
}
|
|
2251
|
+
const resolved = await resolveCopilotAuthDbTokenFromRowsAsync(rows, { platform, securityRunner });
|
|
2252
|
+
return resolved ? { token: resolved.token, identity: resolved.login } : null;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
function readCopilotOauthToken({
|
|
2256
|
+
home = os.homedir(),
|
|
2257
|
+
platform = process.platform,
|
|
2258
|
+
securityRunner,
|
|
2259
|
+
sqliteReader,
|
|
2260
|
+
} = {}) {
|
|
2261
|
+
return readCopilotAuth({ home, platform, securityRunner, sqliteReader })?.token || null;
|
|
2073
2262
|
}
|
|
2074
2263
|
|
|
2075
2264
|
async function readCopilotOauthTokenAsync({
|
|
@@ -2078,9 +2267,7 @@ async function readCopilotOauthTokenAsync({
|
|
|
2078
2267
|
securityRunner,
|
|
2079
2268
|
sqliteReader,
|
|
2080
2269
|
} = {}) {
|
|
2081
|
-
|
|
2082
|
-
if (plaintext) return plaintext;
|
|
2083
|
-
return readCopilotAuthDbTokenAsync({ home, platform, securityRunner, sqliteReader });
|
|
2270
|
+
return (await readCopilotAuthAsync({ home, platform, securityRunner, sqliteReader }))?.token || null;
|
|
2084
2271
|
}
|
|
2085
2272
|
|
|
2086
2273
|
function copilotRequestHeaders(token) {
|
|
@@ -2165,12 +2352,13 @@ async function fetchCopilotLimits({
|
|
|
2165
2352
|
sqliteReader,
|
|
2166
2353
|
} = {}) {
|
|
2167
2354
|
const otel = describeCopilotOtelStatus({ home, env });
|
|
2168
|
-
const
|
|
2355
|
+
const auth = await readCopilotAuthAsync({
|
|
2169
2356
|
home: home || (env.HOME || os.homedir()),
|
|
2170
2357
|
platform,
|
|
2171
2358
|
securityRunner,
|
|
2172
2359
|
sqliteReader,
|
|
2173
2360
|
});
|
|
2361
|
+
const token = auth?.token || null;
|
|
2174
2362
|
if (!token) return { configured: false, ...otel };
|
|
2175
2363
|
|
|
2176
2364
|
try {
|
|
@@ -2179,7 +2367,9 @@ async function fetchCopilotLimits({
|
|
|
2179
2367
|
headers: copilotRequestHeaders(token),
|
|
2180
2368
|
});
|
|
2181
2369
|
if (res.status === 401 || res.status === 403) {
|
|
2182
|
-
|
|
2370
|
+
const authError = new Error("GitHub Copilot token rejected. Re-authenticate via GitHub Copilot CLI/extension.");
|
|
2371
|
+
authError.code = "AUTH_EXPIRED";
|
|
2372
|
+
throw authError;
|
|
2183
2373
|
}
|
|
2184
2374
|
if (!res.ok) {
|
|
2185
2375
|
throw new Error(`GitHub Copilot API error: HTTP ${res.status}`);
|
|
@@ -2194,7 +2384,16 @@ async function fetchCopilotLimits({
|
|
|
2194
2384
|
const chatWindow = buildCopilotWindow(snapshots.chat, resetIso);
|
|
2195
2385
|
|
|
2196
2386
|
if (!premiumWindow && !chatWindow) {
|
|
2197
|
-
return {
|
|
2387
|
+
return {
|
|
2388
|
+
configured: true,
|
|
2389
|
+
error: null,
|
|
2390
|
+
plan_name: planName,
|
|
2391
|
+
primary_window: null,
|
|
2392
|
+
secondary_window: null,
|
|
2393
|
+
// Internal cache-partition key (stripped before the payload is served).
|
|
2394
|
+
cache_identity: auth.identity,
|
|
2395
|
+
...otel,
|
|
2396
|
+
};
|
|
2198
2397
|
}
|
|
2199
2398
|
|
|
2200
2399
|
return {
|
|
@@ -2203,10 +2402,21 @@ async function fetchCopilotLimits({
|
|
|
2203
2402
|
plan_name: planName,
|
|
2204
2403
|
primary_window: premiumWindow,
|
|
2205
2404
|
secondary_window: chatWindow,
|
|
2405
|
+
// Internal cache-partition key (stripped before the payload is served).
|
|
2406
|
+
cache_identity: auth.identity,
|
|
2206
2407
|
...otel,
|
|
2207
2408
|
};
|
|
2208
2409
|
} catch (error) {
|
|
2209
|
-
return {
|
|
2410
|
+
return {
|
|
2411
|
+
configured: true,
|
|
2412
|
+
error: error?.message || "Unknown error",
|
|
2413
|
+
// A token-rejection error is actionable (re-authenticate) and must not be hidden
|
|
2414
|
+
// behind the stale disk-cache fallback (opencode-go auth_error precedent).
|
|
2415
|
+
// Internal marker — stripped before the payload is served.
|
|
2416
|
+
...(error?.code === "AUTH_EXPIRED" ? { auth_error: true } : {}),
|
|
2417
|
+
cache_identity: auth.identity,
|
|
2418
|
+
...otel,
|
|
2419
|
+
};
|
|
2210
2420
|
}
|
|
2211
2421
|
}
|
|
2212
2422
|
|
|
@@ -2216,11 +2426,16 @@ async function fetchKiroLimits({
|
|
|
2216
2426
|
platform = process.platform,
|
|
2217
2427
|
home = os.homedir(),
|
|
2218
2428
|
} = {}) {
|
|
2429
|
+
// Identity captured BEFORE the CLI runs: if the account switches while
|
|
2430
|
+
// /usage is in flight, the write side can compare against a fresh read and
|
|
2431
|
+
// refuse instead of stamping the previous account's panel under the new
|
|
2432
|
+
// login. Internal marker — stripped before the payload is served.
|
|
2433
|
+
const cacheIdentity = resolveKiroAuthIdentity({ home });
|
|
2219
2434
|
const trackedCredits = readKiroCreditsSummary({ home });
|
|
2220
2435
|
const binaryPath = await whichBinary("kiro-cli", { commandRunner });
|
|
2221
2436
|
if (!binaryPath) {
|
|
2222
2437
|
return trackedCredits
|
|
2223
|
-
? { configured: true, error: null, ...trackedCredits }
|
|
2438
|
+
? { configured: true, error: null, cache_identity: cacheIdentity, ...trackedCredits }
|
|
2224
2439
|
: { configured: false };
|
|
2225
2440
|
}
|
|
2226
2441
|
|
|
@@ -2278,6 +2493,7 @@ async function fetchKiroLimits({
|
|
|
2278
2493
|
configured: true,
|
|
2279
2494
|
error: null,
|
|
2280
2495
|
...parseKiroCommandResult(result, { now }),
|
|
2496
|
+
cache_identity: cacheIdentity,
|
|
2281
2497
|
...(trackedCredits || {}),
|
|
2282
2498
|
};
|
|
2283
2499
|
} catch (error) {
|
|
@@ -2289,6 +2505,7 @@ async function fetchKiroLimits({
|
|
|
2289
2505
|
return {
|
|
2290
2506
|
configured: true,
|
|
2291
2507
|
error: error?.message || "Unknown error",
|
|
2508
|
+
cache_identity: cacheIdentity,
|
|
2292
2509
|
...(trackedCredits || {}),
|
|
2293
2510
|
};
|
|
2294
2511
|
}
|
|
@@ -3193,6 +3410,25 @@ function isGrokCacheWindowUsable(window, { nowMs } = {}) {
|
|
|
3193
3410
|
return resetAtMs > nowMs;
|
|
3194
3411
|
}
|
|
3195
3412
|
|
|
3413
|
+
function resolveCopilotLimitsCachePath({ home } = {}) {
|
|
3414
|
+
return path.join(home || os.homedir(), ".tokentracker", "tracker", COPILOT_LIMITS_CACHE_FILE);
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
function hasCopilotWindow(limits) {
|
|
3418
|
+
return Boolean(limits?.primary_window || limits?.secondary_window);
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3421
|
+
// Copilot windows carry `reset_at` as an ISO string built from the monthly
|
|
3422
|
+
// quota_reset_date. A window whose reset has already passed is stale data, not a
|
|
3423
|
+
// usable fallback, so drop it. Windows without a reset stamp are kept — the overall
|
|
3424
|
+
// cached_at max-age gate already bounds them.
|
|
3425
|
+
function isCopilotCacheWindowUsable(window, { nowMs } = {}) {
|
|
3426
|
+
if (!window || typeof window !== "object") return false;
|
|
3427
|
+
const resetAtMs = parseTimeMs(window.reset_at);
|
|
3428
|
+
if (resetAtMs === null) return true;
|
|
3429
|
+
return resetAtMs > nowMs;
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3196
3432
|
function normalizeGrokCachedLimits(
|
|
3197
3433
|
raw,
|
|
3198
3434
|
{ nowMs = Date.now(), maxAgeMs = GROK_LIMITS_CACHE_MAX_AGE_MS } = {},
|
|
@@ -3247,6 +3483,45 @@ function readGrokLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
|
|
|
3247
3483
|
}
|
|
3248
3484
|
}
|
|
3249
3485
|
|
|
3486
|
+
function normalizeCopilotCachedLimits(
|
|
3487
|
+
raw,
|
|
3488
|
+
{ nowMs = Date.now(), maxAgeMs = COPILOT_LIMITS_CACHE_MAX_AGE_MS } = {},
|
|
3489
|
+
) {
|
|
3490
|
+
const cachedAtMs = parseTimeMs(raw?.cached_at);
|
|
3491
|
+
if (!Number.isFinite(cachedAtMs)) return null;
|
|
3492
|
+
if (cachedAtMs > nowMs + 60_000) return null;
|
|
3493
|
+
if (nowMs - cachedAtMs > maxAgeMs) return null;
|
|
3494
|
+
|
|
3495
|
+
const cached = {
|
|
3496
|
+
configured: true,
|
|
3497
|
+
error: null,
|
|
3498
|
+
plan_name: typeof raw?.plan_name === "string" ? raw.plan_name : null,
|
|
3499
|
+
primary_window: isCopilotCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
|
|
3500
|
+
secondary_window: isCopilotCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
|
|
3501
|
+
stale: true,
|
|
3502
|
+
cached_at: raw.cached_at,
|
|
3503
|
+
};
|
|
3504
|
+
return hasCopilotWindow(cached) ? cached : null;
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
// The signed-in GitHub account partitions the cache (zcode login-identity precedent):
|
|
3508
|
+
// the id is the non-secret GitHub login stored alongside the OAuth token (the
|
|
3509
|
+
// plaintext files' `user` field, or the auth.db row's `user_login`). A single home
|
|
3510
|
+
// resolves one active token, so one stamped entry suffices; a read requires the stored
|
|
3511
|
+
// identity to match the current one, and a write without an identity is refused — an
|
|
3512
|
+
// unreadable login must never pin another account's bars in place.
|
|
3513
|
+
function readCopilotLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
|
|
3514
|
+
if (!identity) return null;
|
|
3515
|
+
try {
|
|
3516
|
+
const parsed = JSON.parse(fs.readFileSync(resolveCopilotLimitsCachePath({ home }), "utf8"));
|
|
3517
|
+
const entry = parsed?.copilot;
|
|
3518
|
+
if (entry?.identity !== identity) return null;
|
|
3519
|
+
return normalizeCopilotCachedLimits(entry, { nowMs });
|
|
3520
|
+
} catch (_error) {
|
|
3521
|
+
return null;
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3250
3525
|
function writeGrokLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
|
|
3251
3526
|
if (!identity) return;
|
|
3252
3527
|
if (!limits?.configured || limits.error || !hasGrokWindow(limits)) return;
|
|
@@ -3276,6 +3551,27 @@ function writeGrokLimitsCache(limits, { home, nowMs = Date.now(), identity } = {
|
|
|
3276
3551
|
} catch (_error) {}
|
|
3277
3552
|
}
|
|
3278
3553
|
|
|
3554
|
+
function writeCopilotLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
|
|
3555
|
+
if (!identity) return;
|
|
3556
|
+
if (!limits?.configured || limits.error || !hasCopilotWindow(limits)) return;
|
|
3557
|
+
const cachePath = resolveCopilotLimitsCachePath({ home });
|
|
3558
|
+
const payload = {
|
|
3559
|
+
copilot: {
|
|
3560
|
+
identity,
|
|
3561
|
+
plan_name: limits.plan_name || null,
|
|
3562
|
+
primary_window: limits.primary_window || null,
|
|
3563
|
+
secondary_window: limits.secondary_window || null,
|
|
3564
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
3565
|
+
},
|
|
3566
|
+
};
|
|
3567
|
+
try {
|
|
3568
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
3569
|
+
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
3570
|
+
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
3571
|
+
fs.renameSync(tmpPath, cachePath);
|
|
3572
|
+
} catch (_error) {}
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3279
3575
|
// Failure-only identity re-derivation for the cache fallback: when the provider
|
|
3280
3576
|
// slot's result was produced by the outer timeout race, the fetcher's own
|
|
3281
3577
|
// identity field is lost, so read auth.json once to learn who we are serving for.
|
|
@@ -3288,6 +3584,118 @@ function resolveGrokCacheIdentity({ home, env } = {}) {
|
|
|
3288
3584
|
}
|
|
3289
3585
|
}
|
|
3290
3586
|
|
|
3587
|
+
// Failure-only identity re-derivation for the cache fallback: when the provider slot's
|
|
3588
|
+
// result was produced by the outer timeout race, the fetcher's identity field is lost,
|
|
3589
|
+
// so re-resolve the (still-local) auth store — mirroring the token-source priority so
|
|
3590
|
+
// the identity always belongs to the account the token resolution would pick. Async
|
|
3591
|
+
// like the fetcher's own resolution so a failed poll never blocks the embedded
|
|
3592
|
+
// server's event loop on SQLite / Keychain.
|
|
3593
|
+
async function resolveCopilotCacheIdentity({
|
|
3594
|
+
home = os.homedir(),
|
|
3595
|
+
platform = process.platform,
|
|
3596
|
+
securityRunner,
|
|
3597
|
+
sqliteReader,
|
|
3598
|
+
} = {}) {
|
|
3599
|
+
try {
|
|
3600
|
+
const resolved = await readCopilotAuthAsync({ home, platform, securityRunner, sqliteReader });
|
|
3601
|
+
return resolved?.identity || null;
|
|
3602
|
+
} catch (_error) {
|
|
3603
|
+
return null;
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
function resolveDevinLimitsCachePath({ home } = {}) {
|
|
3608
|
+
return path.join(home || os.homedir(), ".tokentracker", "tracker", DEVIN_LIMITS_CACHE_FILE);
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
function hasDevinWindow(limits) {
|
|
3612
|
+
return Boolean(limits?.primary_window || limits?.secondary_window);
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3615
|
+
// Devin windows carry `reset_at` as an ISO string. A window whose reset has
|
|
3616
|
+
// already passed is stale data, not a usable fallback, so drop it. Windows
|
|
3617
|
+
// without a reset stamp are kept — the overall cached_at max-age gate already
|
|
3618
|
+
// bounds them.
|
|
3619
|
+
function isDevinCacheWindowUsable(window, { nowMs } = {}) {
|
|
3620
|
+
if (!window || typeof window !== "object") return false;
|
|
3621
|
+
const resetAtMs = parseTimeMs(window.reset_at);
|
|
3622
|
+
if (resetAtMs === null) return true;
|
|
3623
|
+
return resetAtMs > nowMs;
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
function normalizeDevinCachedLimits(
|
|
3627
|
+
raw,
|
|
3628
|
+
{ nowMs = Date.now(), maxAgeMs = DEVIN_LIMITS_CACHE_MAX_AGE_MS } = {},
|
|
3629
|
+
) {
|
|
3630
|
+
const cachedAtMs = parseTimeMs(raw?.cached_at);
|
|
3631
|
+
if (!Number.isFinite(cachedAtMs)) return null;
|
|
3632
|
+
if (cachedAtMs > nowMs + 60_000) return null;
|
|
3633
|
+
if (nowMs - cachedAtMs > maxAgeMs) return null;
|
|
3634
|
+
|
|
3635
|
+
const cached = {
|
|
3636
|
+
configured: true,
|
|
3637
|
+
error: null,
|
|
3638
|
+
plan_label: typeof raw?.plan_label === "string" ? raw.plan_label : null,
|
|
3639
|
+
primary_window: isDevinCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
|
|
3640
|
+
secondary_window: isDevinCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
|
|
3641
|
+
stale: true,
|
|
3642
|
+
cached_at: raw.cached_at,
|
|
3643
|
+
};
|
|
3644
|
+
return hasDevinWindow(cached) ? cached : null;
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
// Match the current credential-file revision, including on timeout fallback.
|
|
3648
|
+
// Legacy unpartitioned entries are deliberately ignored.
|
|
3649
|
+
function readDevinLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
|
|
3650
|
+
if (!identity) return null;
|
|
3651
|
+
try {
|
|
3652
|
+
const parsed = JSON.parse(fs.readFileSync(resolveDevinLimitsCachePath({ home }), "utf8"));
|
|
3653
|
+
if (parsed?.devin?.identity !== identity) return null;
|
|
3654
|
+
return normalizeDevinCachedLimits(parsed.devin, { nowMs });
|
|
3655
|
+
} catch (_error) {
|
|
3656
|
+
return null;
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
function writeDevinLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
|
|
3661
|
+
if (!identity) return;
|
|
3662
|
+
if (!limits?.configured || limits.error || !hasDevinWindow(limits)) return;
|
|
3663
|
+
const cachePath = resolveDevinLimitsCachePath({ home });
|
|
3664
|
+
const payload = {
|
|
3665
|
+
devin: {
|
|
3666
|
+
identity,
|
|
3667
|
+
plan_label: limits.plan_label || null,
|
|
3668
|
+
primary_window: limits.primary_window || null,
|
|
3669
|
+
secondary_window: limits.secondary_window || null,
|
|
3670
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
3671
|
+
},
|
|
3672
|
+
};
|
|
3673
|
+
try {
|
|
3674
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
3675
|
+
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
3676
|
+
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
3677
|
+
fs.renameSync(tmpPath, cachePath);
|
|
3678
|
+
} catch (_error) {}
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3681
|
+
function clearDevinLimitsCache({ home } = {}) {
|
|
3682
|
+
try {
|
|
3683
|
+
fs.rmSync(resolveDevinLimitsCachePath({ home }), { force: true });
|
|
3684
|
+
} catch (_error) {}
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
// Only transient failures fall back to the disk cache. Requests without a
|
|
3688
|
+
// status (timeouts, transport failures, non-JSON or schema-malformed bodies)
|
|
3689
|
+
// are blips by construction; 429 and 5xx are the classic retryable classes.
|
|
3690
|
+
// Everything else — the ambiguous HTTP 400 the docs pin as a plain provider
|
|
3691
|
+
// error, and any other non-auth 4xx — is potentially persistent, and stale
|
|
3692
|
+
// bars must not mask it for up to the whole TTL.
|
|
3693
|
+
function isDevinTransientFetchError(reason) {
|
|
3694
|
+
const status = reason?.statusCode;
|
|
3695
|
+
if (typeof status !== "number") return true;
|
|
3696
|
+
return status === 429 || status >= 500;
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3291
3699
|
function resolveClaudeRateLimitPath({ home } = {}) {
|
|
3292
3700
|
return path.join(home || os.homedir(), ".tokentracker", "tracker", CLAUDE_RATE_LIMIT_FILE);
|
|
3293
3701
|
}
|
|
@@ -4630,7 +5038,14 @@ function cacheExpiresAtMs(data, fetchedAtMs) {
|
|
|
4630
5038
|
|
|
4631
5039
|
/** Return cached quota or share one in-flight fetch for the requested provider selection. */
|
|
4632
5040
|
async function getUsageLimits(options = {}) {
|
|
4633
|
-
|
|
5041
|
+
// Resolve current routing before any aggregate cache or in-flight selection.
|
|
5042
|
+
// Keep this snapshot for the fetch so a settings change during an await cannot
|
|
5043
|
+
// publish one backend's response into another backend's cache partition.
|
|
5044
|
+
const claudeBackend = normalizeHiddenProviders(options.hiddenProviders)?.includes("claude")
|
|
5045
|
+
? null
|
|
5046
|
+
: readCustomBackend({ home: options.home || os.homedir(), env: options.env });
|
|
5047
|
+
const customOrigin = claudeBackend?.backend === "custom" ? claudeBackend.backend_origin : null;
|
|
5048
|
+
const selection = usageLimitsSelectionKey(options) + (customOrigin ? `-claude:${customOrigin}` : "");
|
|
4634
5049
|
const cache = cacheBySelection[selection];
|
|
4635
5050
|
const nowMs = Date.now();
|
|
4636
5051
|
// Unseen selections (a hidden set never requested before this process's
|
|
@@ -4644,6 +5059,7 @@ async function getUsageLimits(options = {}) {
|
|
|
4644
5059
|
const cacheGeneration = usageLimitsCacheGeneration;
|
|
4645
5060
|
const promise = fetchUsageLimitsUncached({
|
|
4646
5061
|
...options,
|
|
5062
|
+
claudeBackend,
|
|
4647
5063
|
selectionKey: selection,
|
|
4648
5064
|
cacheGeneration,
|
|
4649
5065
|
}).finally(() => {
|
|
@@ -4676,6 +5092,7 @@ async function fetchUsageLimitsUncached({
|
|
|
4676
5092
|
// "fetch every configured provider" (the legacy behavior clients that don't
|
|
4677
5093
|
// send the parameter keep getting).
|
|
4678
5094
|
hiddenProviders = null,
|
|
5095
|
+
claudeBackend = null,
|
|
4679
5096
|
// Generation captured by getUsageLimits before the fetch began. A provider
|
|
4680
5097
|
// key save bumps it, and this fetch must then stop at returning its own
|
|
4681
5098
|
// result: publishing it would restore pre-save data for every later caller.
|
|
@@ -4693,16 +5110,17 @@ async function fetchUsageLimitsUncached({
|
|
|
4693
5110
|
const injectedNowMs = Number.isFinite(nowMs) ? nowMs : undefined;
|
|
4694
5111
|
nowMs = injectedNowMs ?? Date.now();
|
|
4695
5112
|
const hiddenSet = new Set(normalizeHiddenProviders(hiddenProviders) || []);
|
|
5113
|
+
const customClaudeBackend = !hiddenSet.has("claude") && claudeBackend?.backend === "custom";
|
|
4696
5114
|
|
|
4697
5115
|
// A hidden provider must not have its local credentials read at all (#956):
|
|
4698
5116
|
// these upfront token/subscription probes are the first file and keychain
|
|
4699
5117
|
// touches of the Claude/Codex chains, so they are gated here and every
|
|
4700
5118
|
// downstream step keys off the resulting null token.
|
|
4701
5119
|
const [claudeOauth, claudeSubscription, codexAuth] = await Promise.all([
|
|
4702
|
-
hiddenSet.has("claude")
|
|
5120
|
+
hiddenSet.has("claude") || customClaudeBackend
|
|
4703
5121
|
? Promise.resolve(null)
|
|
4704
5122
|
: Promise.resolve().then(() => readClaudeCodeOauthToken({ platform, securityRunner, home, nowMs })),
|
|
4705
|
-
hiddenSet.has("claude")
|
|
5123
|
+
hiddenSet.has("claude") || customClaudeBackend
|
|
4706
5124
|
? Promise.resolve(null)
|
|
4707
5125
|
: Promise.resolve().then(() => detectClaudeCodeSubscriptionDetails({ platform, securityRunner, home })),
|
|
4708
5126
|
hiddenSet.has("codex")
|
|
@@ -4889,9 +5307,10 @@ async function fetchUsageLimitsUncached({
|
|
|
4889
5307
|
),
|
|
4890
5308
|
// Devin (devin.ai): daily/weekly subscription quota from the official
|
|
4891
5309
|
// GetPlanStatus RPC, keyed by the session token the Devin CLI stores in
|
|
4892
|
-
// ~/.local/share/devin/credentials.toml
|
|
4893
|
-
//
|
|
4894
|
-
//
|
|
5310
|
+
// ~/.local/share/devin/credentials.toml — window state lives server-side,
|
|
5311
|
+
// so the assemble step below serves the last-good disk cache when this
|
|
5312
|
+
// poll fails transiently. fetchDevinLimits throws on auth expiry so the
|
|
5313
|
+
// assemble step can flag auth_action_required.
|
|
4895
5314
|
hiddenSet.has("devin") ? hiddenSettled : withProviderTimeout(fetchDevinLimits({ home, env, enabled: devinEnabled === true, fetchImpl: providerFetch }), "Devin", providerTimeoutMs)
|
|
4896
5315
|
.then(
|
|
4897
5316
|
(value) => ({ status: "fulfilled", value }),
|
|
@@ -4934,7 +5353,9 @@ async function fetchUsageLimitsUncached({
|
|
|
4934
5353
|
]);
|
|
4935
5354
|
|
|
4936
5355
|
let claude;
|
|
4937
|
-
if (
|
|
5356
|
+
if (customClaudeBackend) {
|
|
5357
|
+
claude = { configured: true, ...claudeBackend, limits_status: "not_applicable", cost_status: "unknown", error: null };
|
|
5358
|
+
} else if (!claudeToken) {
|
|
4938
5359
|
// Claude Code blanks `accessToken`/`refreshToken` in place when its login expires
|
|
4939
5360
|
// (macOS Keychain item and .credentials.json alike) instead of removing the entry,
|
|
4940
5361
|
// so "no token" is ambiguous: never signed in, or signed in and expired. An entry
|
|
@@ -5010,7 +5431,7 @@ async function fetchUsageLimitsUncached({
|
|
|
5010
5431
|
// waiting out the poll interval. Re-read here (not the pre-fetch value) so a
|
|
5011
5432
|
// cool-down just armed by this cycle's 429 is included; a successful read above
|
|
5012
5433
|
// cleared the file, so this is null in the happy path.
|
|
5013
|
-
if (claude.configured) {
|
|
5434
|
+
if (claude.configured && claude.limits_status !== "not_applicable") {
|
|
5014
5435
|
const claudeCooldownMs = readClaudeRateLimitRetryAtMs({
|
|
5015
5436
|
home,
|
|
5016
5437
|
nowMs,
|
|
@@ -5141,8 +5562,13 @@ async function fetchUsageLimitsUncached({
|
|
|
5141
5562
|
|
|
5142
5563
|
// Devin: server-owned quota windows like CommandCode — a fulfilled
|
|
5143
5564
|
// `configured: false` means no CLI credentials; a rejected fetch with
|
|
5144
|
-
// AUTH_EXPIRED flags auth_action_required for the re-sign-in hint
|
|
5565
|
+
// AUTH_EXPIRED flags auth_action_required for the re-sign-in hint, and a
|
|
5566
|
+
// CREDENTIALS_INVALID error is a broken local sign-in (unreadable file,
|
|
5567
|
+
// missing token, unsupported api_server_url) — both actionable, never
|
|
5568
|
+
// masked by the disk cache below.
|
|
5145
5569
|
let devinObj;
|
|
5570
|
+
const devinCredentialsInvalid =
|
|
5571
|
+
devinRaw?.status === "rejected" && devinRaw.reason?.code === "CREDENTIALS_INVALID";
|
|
5146
5572
|
if (devinRaw?.status === "fulfilled") {
|
|
5147
5573
|
const value = devinRaw.value;
|
|
5148
5574
|
devinObj = value && value.configured === false
|
|
@@ -5155,6 +5581,50 @@ async function fetchUsageLimitsUncached({
|
|
|
5155
5581
|
: { configured: true, error: reason?.message || "Unknown error" };
|
|
5156
5582
|
}
|
|
5157
5583
|
|
|
5584
|
+
// Devin: persist last successful windows so a transient timeout/5xx on the
|
|
5585
|
+
// server.codeium.com route serves stale bars instead of a flashing "fetch
|
|
5586
|
+
// failed" (mirrors OpenCode Go / Cursor). Auth and broken-credential errors
|
|
5587
|
+
// punch through above, ambiguous/persistent HTTP rejections punch through
|
|
5588
|
+
// below, an opted-out / unconfigured provider has no window state to fall
|
|
5589
|
+
// back on, and a successful windowless read (legacy billing strategy)
|
|
5590
|
+
// invalidates any prior entry instead of leaving it for a later blip to
|
|
5591
|
+
// resurrect the former plan's bars.
|
|
5592
|
+
if (
|
|
5593
|
+
devinObj
|
|
5594
|
+
&& devinObj.configured !== false
|
|
5595
|
+
&& devinObj.auth_action_required !== "reauth"
|
|
5596
|
+
&& !devinCredentialsInvalid
|
|
5597
|
+
) {
|
|
5598
|
+
const currentIdentity = resolveDevinCacheIdentity({ home, env });
|
|
5599
|
+
if (!devinObj.error) {
|
|
5600
|
+
// A response belongs to the credentials read before its request. Never
|
|
5601
|
+
// write or clear a new login's cache after an in-flight replacement.
|
|
5602
|
+
if (cacheGeneration === usageLimitsCacheGeneration
|
|
5603
|
+
&& currentIdentity && devinObj.cache_identity === currentIdentity) {
|
|
5604
|
+
if (hasDevinWindow(devinObj)) {
|
|
5605
|
+
writeDevinLimitsCache(devinObj, { home, nowMs, identity: currentIdentity });
|
|
5606
|
+
} else {
|
|
5607
|
+
clearDevinLimitsCache({ home });
|
|
5608
|
+
}
|
|
5609
|
+
}
|
|
5610
|
+
} else if (isDevinTransientFetchError(devinRaw?.reason)) {
|
|
5611
|
+
// The fallback lands after the whole provider round: sample the clock
|
|
5612
|
+
// here (tests keep their injected clock) so a window that rolled over
|
|
5613
|
+
// while the poll was waiting is dropped instead of served.
|
|
5614
|
+
const cached = readDevinLimitsCache({
|
|
5615
|
+
home,
|
|
5616
|
+
nowMs: injectedNowMs ?? Date.now(),
|
|
5617
|
+
identity: currentIdentity,
|
|
5618
|
+
});
|
|
5619
|
+
if (cached) devinObj = cached;
|
|
5620
|
+
}
|
|
5621
|
+
}
|
|
5622
|
+
|
|
5623
|
+
if (devinObj && "cache_identity" in devinObj) {
|
|
5624
|
+
const { cache_identity: _identity, ...devinPayload } = devinObj;
|
|
5625
|
+
devinObj = devinPayload;
|
|
5626
|
+
}
|
|
5627
|
+
|
|
5158
5628
|
// Cursor: persist last successful windows so a transient timeout/5xx on the
|
|
5159
5629
|
// cursor.com / api2.cursor.sh routes serves stale bars instead of a flashing
|
|
5160
5630
|
// "fetch failed" (mirrors Codex / Claude / OpenCode Go). A session-expiry error
|
|
@@ -5244,6 +5714,45 @@ async function fetchUsageLimitsUncached({
|
|
|
5244
5714
|
? { ...cursorWithCache, quaternary_window: null }
|
|
5245
5715
|
: cursorWithCache;
|
|
5246
5716
|
|
|
5717
|
+
// Copilot: same fallback contract as Cursor for the api.github.com route — a
|
|
5718
|
+
// transient timeout/5xx serves stale bars instead of a flashing "fetch failed",
|
|
5719
|
+
// while a token-rejection error (401/403 — "re-authenticate") is actionable and
|
|
5720
|
+
// passes through untouched. The otel probe fields ride along on every slot
|
|
5721
|
+
// result, so a cached overlay spreads the live object first and lets the cached
|
|
5722
|
+
// entry win only the limit fields.
|
|
5723
|
+
let copilotWithCache;
|
|
5724
|
+
if (!copilot || copilot.configured === false) {
|
|
5725
|
+
copilotWithCache = copilot;
|
|
5726
|
+
} else if (copilot.auth_error === true) {
|
|
5727
|
+
copilotWithCache = copilot;
|
|
5728
|
+
} else if (!copilot.error && hasCopilotWindow(copilot)) {
|
|
5729
|
+
copilotWithCache = {
|
|
5730
|
+
...copilot,
|
|
5731
|
+
stale: false,
|
|
5732
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
5733
|
+
};
|
|
5734
|
+
// Only the login captured with the request can own its windows. An
|
|
5735
|
+
// unknown login must never be promoted to a newly signed-in account.
|
|
5736
|
+
const identity = copilot.cache_identity;
|
|
5737
|
+
if (typeof identity === "string" && identity
|
|
5738
|
+
&& identity === await resolveCopilotCacheIdentity({ home, platform, securityRunner })
|
|
5739
|
+
&& cacheGeneration === usageLimitsCacheGeneration) {
|
|
5740
|
+
writeCopilotLimitsCache(copilotWithCache, { home, nowMs, identity });
|
|
5741
|
+
}
|
|
5742
|
+
} else {
|
|
5743
|
+
// Re-read the current login even when the failed request carries an identity:
|
|
5744
|
+
// the account may have changed while the request was in flight.
|
|
5745
|
+
const identity = await resolveCopilotCacheIdentity({ home, platform, securityRunner });
|
|
5746
|
+
// Sample after the async auth lookup too: a slow poll can straddle a reset.
|
|
5747
|
+
const cached = readCopilotLimitsCache({ home, nowMs: Date.now(), identity });
|
|
5748
|
+
copilotWithCache = cached ? { ...copilot, ...cached } : copilot;
|
|
5749
|
+
}
|
|
5750
|
+
// cache_identity / auth_error are internal cache-plumbing markers, not payload.
|
|
5751
|
+
if (copilotWithCache && ("cache_identity" in copilotWithCache || "auth_error" in copilotWithCache)) {
|
|
5752
|
+
const { cache_identity: _identity, auth_error: _authError, ...copilotPayload } = copilotWithCache;
|
|
5753
|
+
copilotWithCache = copilotPayload;
|
|
5754
|
+
}
|
|
5755
|
+
|
|
5247
5756
|
// Kimi: persist last successful windows so a transient timeout/5xx on the
|
|
5248
5757
|
// api.kimi.com / api.kimi.ai coding routes serves stale bars instead of a
|
|
5249
5758
|
// flashing "fetch failed" (mirrors Cursor / Codex / OpenCode Go). Re-login
|
|
@@ -5348,9 +5857,61 @@ async function fetchUsageLimitsUncached({
|
|
|
5348
5857
|
geminiWithCache = geminiPayload;
|
|
5349
5858
|
}
|
|
5350
5859
|
|
|
5860
|
+
// Kiro: the slot runs the local kiro-cli (its own 2s/20s command budgets,
|
|
5861
|
+
// never rejects), so the failure modes are CLI timeouts, unparseable panels
|
|
5862
|
+
// and backend hiccups — persist the last successful windows and serve them
|
|
5863
|
+
// stale on any of those (mirrors Codex / OpenCode Go / Cursor). A
|
|
5864
|
+
// not-logged-in panel is actionable and must not be hidden behind stale
|
|
5865
|
+
// cache, so it passes through untouched. The credits-only success (kiro-cli
|
|
5866
|
+
// absent, sidecar present) is not a fetch failure and keeps its shape.
|
|
5867
|
+
// Identity comes from the fetcher's pre-CLI capture (cache_identity), and a
|
|
5868
|
+
// write only lands while a fresh read of the auth file still agrees — an
|
|
5869
|
+
// account switch mid-flight refuses the write instead of stamping the
|
|
5870
|
+
// previous account's panel under the new login.
|
|
5871
|
+
const kiroAuthBlocked = isKiroAuthError(kiro?.error);
|
|
5872
|
+
let kiroWithCache;
|
|
5873
|
+
if (!kiro || kiro.configured === false || kiroAuthBlocked) {
|
|
5874
|
+
kiroWithCache = kiro;
|
|
5875
|
+
} else if (!kiro.error && hasKiroWindow(kiro)) {
|
|
5876
|
+
kiroWithCache = {
|
|
5877
|
+
...kiro,
|
|
5878
|
+
stale: false,
|
|
5879
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
5880
|
+
};
|
|
5881
|
+
if (cacheGeneration === usageLimitsCacheGeneration) {
|
|
5882
|
+
const identity = typeof kiro.cache_identity === "string"
|
|
5883
|
+
? kiro.cache_identity
|
|
5884
|
+
: resolveKiroAuthIdentity({ home });
|
|
5885
|
+
if (identity && identity === resolveKiroAuthIdentity({ home })) {
|
|
5886
|
+
writeKiroLimitsCache(kiroWithCache, { home, nowMs, identity });
|
|
5887
|
+
}
|
|
5888
|
+
}
|
|
5889
|
+
} else if (!kiro.error) {
|
|
5890
|
+
kiroWithCache = kiro;
|
|
5891
|
+
} else {
|
|
5892
|
+
const cached = readKiroLimitsCache({
|
|
5893
|
+
home,
|
|
5894
|
+
// Gate against the wall clock, not the aggregate's entry stamp: a slow
|
|
5895
|
+
// failed CLI read can straddle a reset, and an expired window must not
|
|
5896
|
+
// pass the reset gate on a timestamp from before the read started.
|
|
5897
|
+
nowMs: Date.now(),
|
|
5898
|
+
// The current login, never the fetch-time capture — a switch that
|
|
5899
|
+
// happened mid-flight must not match the previous account's entry.
|
|
5900
|
+
identity: resolveKiroAuthIdentity({ home }),
|
|
5901
|
+
});
|
|
5902
|
+
// The failed slot already carries the credits-sidecar fields the fetcher
|
|
5903
|
+
// merged in; the cached windows overlay on top of them.
|
|
5904
|
+
kiroWithCache = cached ? { ...kiro, ...cached } : kiro;
|
|
5905
|
+
}
|
|
5906
|
+
// cache_identity is internal cache plumbing, not payload.
|
|
5907
|
+
if (kiroWithCache && "cache_identity" in kiroWithCache) {
|
|
5908
|
+
const { cache_identity: _identity, ...kiroPayload } = kiroWithCache;
|
|
5909
|
+
kiroWithCache = kiroPayload;
|
|
5910
|
+
}
|
|
5911
|
+
|
|
5351
5912
|
const data = {
|
|
5352
5913
|
fetched_at: new Date(nowMs).toISOString(),
|
|
5353
|
-
claude: withPlanLabel(claude, claudePlanType, "Claude"),
|
|
5914
|
+
claude: claude.limits_status === "not_applicable" ? claude : withPlanLabel(claude, claudePlanType, "Claude"),
|
|
5354
5915
|
codex: withPlanLabel(codex, codex.plan_type, "Codex"),
|
|
5355
5916
|
cursor: withPlanLabel(cursorLimits, cursorLimits.membership_type, "Cursor"),
|
|
5356
5917
|
// Kimi's subType (TYPE_PURCHASE/TYPE_EVENT) is the credit *source*, not a plan
|
|
@@ -5359,9 +5920,9 @@ async function fetchUsageLimitsUncached({
|
|
|
5359
5920
|
// "Kimi Type_event" (issue #130), so show the bare brand instead.
|
|
5360
5921
|
kimi: withPlanLabel(kimiWithCache, null, "Kimi"),
|
|
5361
5922
|
gemini: withPlanLabel(geminiWithCache, geminiWithCache.account_plan, "Gemini"),
|
|
5362
|
-
kiro: withPlanLabel(
|
|
5923
|
+
kiro: withPlanLabel(kiroWithCache, kiroWithCache.plan_name, "Kiro"),
|
|
5363
5924
|
antigravity: withPlanLabel(antigravity, antigravity.account_plan, "Antigravity"),
|
|
5364
|
-
copilot: withPlanLabel(
|
|
5925
|
+
copilot: withPlanLabel(copilotWithCache, copilotWithCache.plan_name, "Copilot"),
|
|
5365
5926
|
// grok-limits already maps subscriptionTier to xAI's own product names
|
|
5366
5927
|
// ("SuperGrok Heavy", "X Premium+", "API Key"). The shared normalization
|
|
5367
5928
|
// would Title-Case them into "Supergrok Heavy" / "Api Key" and drop "Free"
|
|
@@ -5399,6 +5960,9 @@ async function fetchUsageLimitsUncached({
|
|
|
5399
5960
|
// Devin) is metadata-free by contract — it was never fetched, so it must
|
|
5400
5961
|
// not be aged into a "disk-cache"/stale provenance.
|
|
5401
5962
|
if (provider.configured === false && Object.keys(provider).length === 1) continue;
|
|
5963
|
+
// A custom-backend state is local routing information, not an official
|
|
5964
|
+
// provider API reading with a capture time or quota freshness.
|
|
5965
|
+
if (provider.limits_status === "not_applicable") continue;
|
|
5402
5966
|
const capturedAt = provider.cached_at || data.fetched_at;
|
|
5403
5967
|
const ageMs = Math.max(0, nowMs - Date.parse(capturedAt || ""));
|
|
5404
5968
|
const stale = provider.stale === true || (Number.isFinite(ageMs) && ageMs > 10 * 60 * 1000);
|
|
@@ -5503,6 +6067,8 @@ module.exports = {
|
|
|
5503
6067
|
detectAntigravityProcess,
|
|
5504
6068
|
fetchAntigravityLimits,
|
|
5505
6069
|
fetchCopilotLimits,
|
|
6070
|
+
readCopilotAuth,
|
|
6071
|
+
readCopilotAuthAsync,
|
|
5506
6072
|
readCopilotOauthToken,
|
|
5507
6073
|
readCopilotOauthTokenAsync,
|
|
5508
6074
|
readCopilotAuthDbToken,
|