@ipv9/tokentracker-cli 0.39.43 → 0.39.45
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 +18 -10
- package/dashboard/dist/assets/{Card-LPizs_gs.js → Card-2-TQg7P7.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-yb9ss9uE.js +60 -0
- package/dashboard/dist/assets/{FadeIn-B8aDegoD.js → FadeIn-BOc6XtOK.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-Vo2ZZXov.js → IpCheckPage-C2tIb68P.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-C5-Q9Q30.js → LimitsPage-BsuLQ9co.js} +1 -1
- package/dashboard/dist/assets/LocalOnlyNotice-C8R9KLef.js +1 -0
- package/dashboard/dist/assets/{PopoverPopup-CJf61ahu.js → PopoverPopup-BpfseoI7.js} +1 -1
- package/dashboard/dist/assets/{Select-BLGoaqgw.js → Select-Ctk8zze5.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-Bt02Fgwf.js → SelectItemText-BKZlFyFs.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-BnUJew-8.js → SettingsPage-CQXM8qGU.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-ImHg3Puy.js → SkillsPage-4EMm0eBX.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-qscVE2nO.js → WidgetsPage-C2sdX5g6.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-qM_7aClE.js → WrappedPage-CLiuEcQZ.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-CByq3BPT.js → arrow-up-right-BDkp93DX.js} +1 -1
- package/dashboard/dist/assets/{download-CTwO-YeA.js → download-DZ6SoCSn.js} +1 -1
- package/dashboard/dist/assets/{format-4chvNBjF.js → format-CaW9kvsA.js} +1 -1
- package/dashboard/dist/assets/limitDisplay-DNU_w4O7.js +1 -0
- package/dashboard/dist/assets/{main-CCPcJ7ti.js → main-DgyymGht.js} +16 -3
- package/dashboard/dist/assets/main-t7dbBL4x.css +1 -0
- package/dashboard/dist/assets/{mock-data-DSiJ-9lr.js → mock-data-D6C7Fba3.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-Dgd-bQBC.js → use-limits-display-prefs-B7cHBa7Y.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-CjZRLdFT.js → use-native-settings-BKAzGuxw.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-BJRU0syn.js → useCurrency-BVr6Ajuu.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/package.json +5 -3
- package/src/commands/doctor.js +8 -0
- package/src/commands/init.js +1 -1
- package/src/commands/sync.js +67 -0
- package/src/lib/doctor.js +227 -1
- package/src/lib/local-api.js +385 -113
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/process-list.js +91 -0
- package/src/lib/queue-compact.js +220 -0
- package/src/lib/rollout.js +81 -14
- package/src/lib/single-flight.js +59 -0
- package/src/lib/skills-manager.js +2 -2
- package/src/lib/transcript-suppression.js +133 -0
- package/src/lib/usage-limits.js +99 -22
- package/dashboard/dist/assets/DashboardPage-CkhqD3x3.js +0 -60
- package/dashboard/dist/assets/LocalOnlyNotice-DXVmRcyV.js +0 -1
- package/dashboard/dist/assets/limitDisplay-CXlkWhjp.js +0 -1
- package/dashboard/dist/assets/main-ZrWkoMlr.css +0 -1
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const { listProcessLines, parseProcessLine } = require("./process-list");
|
|
2
|
+
|
|
3
|
+
// Detects Claude Code CLI processes started with `--no-session-persistence`.
|
|
4
|
+
//
|
|
5
|
+
// Why this exists: every Claude figure TokenTracker reports is parsed out of the
|
|
6
|
+
// session transcripts under `~/.claude/projects`. That flag tells the CLI to
|
|
7
|
+
// write no transcript at all, so those calls are unobservable here — they cost
|
|
8
|
+
// real tokens and contribute zero. Before this check, the only visible symptom
|
|
9
|
+
// was a source quietly reporting less than it should, which is indistinguishable
|
|
10
|
+
// from "the user worked less today".
|
|
11
|
+
//
|
|
12
|
+
// This is a *live* signal, not a historical one: it answers "is something
|
|
13
|
+
// running right now that I cannot see", which is why it needs no baseline, no
|
|
14
|
+
// threshold, and no per-source history.
|
|
15
|
+
//
|
|
16
|
+
// PRIVACY (CONTRIBUTING.md): the command line is read and discarded inside this
|
|
17
|
+
// call. What leaves this module is a count, a coarse reason, and the model ids —
|
|
18
|
+
// never a pid, an argv string, an environment value, or a file path. Model ids
|
|
19
|
+
// are already first-class tracked data (every queue row carries one), and they
|
|
20
|
+
// are the one field that tells a user *which* stream is unobservable, so they
|
|
21
|
+
// are included deliberately; they are also length- and charset-clamped below so
|
|
22
|
+
// that no arbitrary command-line text can ride out through this field.
|
|
23
|
+
|
|
24
|
+
const SUPPRESSION_FLAG = "--no-session-persistence";
|
|
25
|
+
const CLAUDE_BINARY_NAME = "claude";
|
|
26
|
+
const MODEL_FLAG = /(?:^|\s)--model[=\s]+([A-Za-z0-9._:-]{1,64})(?=\s|$)/;
|
|
27
|
+
const SUPPRESSION_FLAG_PATTERN = /(?:^|\s)--no-session-persistence(?:=|\s|$)/;
|
|
28
|
+
const DEFAULT_TTL_MS = 30_000;
|
|
29
|
+
|
|
30
|
+
let cache = null;
|
|
31
|
+
|
|
32
|
+
function resetTranscriptSuppressionCache() {
|
|
33
|
+
cache = null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// argv[0] must be the `claude` binary. Matching on the flag alone would also
|
|
37
|
+
// match any shell, editor, or grep whose own command line happens to contain
|
|
38
|
+
// the string — including the process asking this question.
|
|
39
|
+
//
|
|
40
|
+
// The executable region is everything before the first `--flag`, not the first
|
|
41
|
+
// whitespace-delimited token: `ps` gives one flat string, so a binary installed
|
|
42
|
+
// under a path containing a space ("/Users/me/My Tools/claude") would otherwise
|
|
43
|
+
// be read as argv[0] = "/Users/me/My" and missed. A miss is the failure mode
|
|
44
|
+
// this whole check exists to prevent, so it is worth the wider window.
|
|
45
|
+
function isClaudeInvocation(command) {
|
|
46
|
+
const executableRegion = String(command || "").split(/\s+--/)[0] || "";
|
|
47
|
+
const base = executableRegion.trim().split(/[/\\]/).pop() || "";
|
|
48
|
+
return base.replace(/\.exe$/i, "").toLowerCase() === CLAUDE_BINARY_NAME;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hasSuppressionFlag(command) {
|
|
52
|
+
return SUPPRESSION_FLAG_PATTERN.test(String(command || ""));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function extractModel(command) {
|
|
56
|
+
const match = String(command || "").match(MODEL_FLAG);
|
|
57
|
+
return match?.[1] || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Pure: takes `ps` output lines, returns the deduplicated model ids of every
|
|
61
|
+
// suppressed Claude process. Deliberately returns models rather than processes —
|
|
62
|
+
// there is no pid in the return value for anything downstream to leak.
|
|
63
|
+
function findSuppressedModels(lines = []) {
|
|
64
|
+
const models = new Set();
|
|
65
|
+
let count = 0;
|
|
66
|
+
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
const parsed = parseProcessLine(line);
|
|
69
|
+
if (!parsed) continue;
|
|
70
|
+
if (!isClaudeInvocation(parsed.command)) continue;
|
|
71
|
+
if (!hasSuppressionFlag(parsed.command)) continue;
|
|
72
|
+
count += 1;
|
|
73
|
+
const model = extractModel(parsed.command);
|
|
74
|
+
if (model) models.add(model);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { count, models: [...models].sort() };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Returns { supported, checked, count, models, reason, checked_at }.
|
|
81
|
+
//
|
|
82
|
+
// `checked: false` is never reported as a clean result by callers. "I could not
|
|
83
|
+
// look" and "I looked and found nothing" are different answers, and collapsing
|
|
84
|
+
// them is how a monitor starts lying.
|
|
85
|
+
function detectTranscriptSuppression({
|
|
86
|
+
commandRunner,
|
|
87
|
+
platform = process.platform,
|
|
88
|
+
now = () => new Date(),
|
|
89
|
+
ttlMs = DEFAULT_TTL_MS,
|
|
90
|
+
useCache = true,
|
|
91
|
+
} = {}) {
|
|
92
|
+
const at = now();
|
|
93
|
+
const atMs = at.getTime();
|
|
94
|
+
|
|
95
|
+
if (useCache && cache && atMs - cache.atMs < ttlMs) {
|
|
96
|
+
return cache.value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const listed = listProcessLines({ commandRunner, platform });
|
|
100
|
+
|
|
101
|
+
let value;
|
|
102
|
+
if (!listed.ok) {
|
|
103
|
+
value = {
|
|
104
|
+
supported: listed.supported,
|
|
105
|
+
checked: false,
|
|
106
|
+
count: 0,
|
|
107
|
+
models: [],
|
|
108
|
+
reason: listed.reason,
|
|
109
|
+
checked_at: at.toISOString(),
|
|
110
|
+
};
|
|
111
|
+
} else {
|
|
112
|
+
const { count, models } = findSuppressedModels(listed.lines);
|
|
113
|
+
value = {
|
|
114
|
+
supported: true,
|
|
115
|
+
checked: true,
|
|
116
|
+
count,
|
|
117
|
+
models,
|
|
118
|
+
reason: null,
|
|
119
|
+
checked_at: at.toISOString(),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (useCache) cache = { atMs, value };
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
DEFAULT_TTL_MS,
|
|
129
|
+
SUPPRESSION_FLAG,
|
|
130
|
+
detectTranscriptSuppression,
|
|
131
|
+
findSuppressedModels,
|
|
132
|
+
resetTranscriptSuppressionCache,
|
|
133
|
+
};
|
package/src/lib/usage-limits.js
CHANGED
|
@@ -21,12 +21,21 @@ const {
|
|
|
21
21
|
extractCursorSessionToken,
|
|
22
22
|
fetchCursorUsageSummary,
|
|
23
23
|
} = require("./cursor-config");
|
|
24
|
+
const { PS_ARGS, PS_BINARY, parseProcessLine } = require("./process-list");
|
|
25
|
+
const { createSingleFlight } = require("./single-flight");
|
|
24
26
|
|
|
25
27
|
// 2-minute in-memory cache
|
|
26
28
|
let cache = { data: null, fetchedAt: 0 };
|
|
29
|
+
// One slot per module instance, not a shared singleton: `delete require.cache[…]`
|
|
30
|
+
// is how the tests get a clean cache, and a shared instance would survive it.
|
|
31
|
+
const runUsageLimitsFetch = createSingleFlight();
|
|
27
32
|
const CACHE_TTL_MS = 2 * 60 * 1000;
|
|
28
33
|
const DEFAULT_PROVIDER_TIMEOUT_MS = 15_000;
|
|
29
34
|
const ANTIGRAVITY_LIMITS_CACHE_FILE = "usage-limits-cache.json";
|
|
35
|
+
// Trust marker, not a user identity: only cache blocks written after the
|
|
36
|
+
// current-user process-scan fix may be served. Missing/unknown markers fail
|
|
37
|
+
// closed so a legacy multi-user cache cannot disclose another account.
|
|
38
|
+
const ANTIGRAVITY_CACHE_PROCESS_SCOPE = "current-user-v1";
|
|
30
39
|
const ANTIGRAVITY_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
31
40
|
const ANTIGRAVITY_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
|
|
32
41
|
const CLAUDE_LIMITS_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
@@ -218,10 +227,23 @@ async function fetchCodexUsageLimits(
|
|
|
218
227
|
method: "GET",
|
|
219
228
|
headers,
|
|
220
229
|
});
|
|
221
|
-
// 401/403/404 from wham means "no usage data available for this auth state" —
|
|
222
|
-
// a
|
|
230
|
+
// 401/403/404 from wham means "no usage data available for this auth state" —
|
|
231
|
+
// a free or multi-account user, or a token that went stale. #52 asked for a
|
|
232
|
+
// neutral state here rather than a red "Fetch failed".
|
|
233
|
+
//
|
|
234
|
+
// It got NO state. With no error and no windows, LimitChips falls through to
|
|
235
|
+
// `windows.length === 0 -> return null` and the chip disappears entirely,
|
|
236
|
+
// which is the outcome #105 calls worse than never having had a chip: the user
|
|
237
|
+
// has been trained to look there and now reads absence as "plenty left".
|
|
238
|
+
//
|
|
239
|
+
// `notice` is the third state the UI was missing. Visible and subtle, so #52's
|
|
240
|
+
// intent survives without #105's failure mode.
|
|
223
241
|
if (res.status === 401 || res.status === 403 || res.status === 404) {
|
|
224
|
-
return {
|
|
242
|
+
return {
|
|
243
|
+
primary_window: null,
|
|
244
|
+
secondary_window: null,
|
|
245
|
+
notice: "No usage data for this sign-in. Run `codex` to sign in again.",
|
|
246
|
+
};
|
|
225
247
|
}
|
|
226
248
|
if (!res.ok) {
|
|
227
249
|
throw new Error(`Codex API returned ${res.status}`);
|
|
@@ -973,11 +995,22 @@ async function fetchGeminiLimits({ home, env, fetchImpl = fetch, commandRunner }
|
|
|
973
995
|
|
|
974
996
|
function runCommand(commandRunner, command, args, options = {}) {
|
|
975
997
|
const runner = typeof commandRunner === "function" ? commandRunner : cp.spawnSync;
|
|
976
|
-
|
|
998
|
+
const result = runner(command, args, {
|
|
977
999
|
encoding: "utf8",
|
|
978
1000
|
maxBuffer: 10 * 1024 * 1024,
|
|
979
1001
|
...options,
|
|
980
1002
|
});
|
|
1003
|
+
// Every consumer reads spawnSync-style fields immediately. An async runner
|
|
1004
|
+
// is therefore a contract error, not supported concurrency. Attach a handler
|
|
1005
|
+
// immediately, then return a synchronous failure shape so the Promise cannot
|
|
1006
|
+
// escape as unhandled process state.
|
|
1007
|
+
if (result && typeof result.then === "function") {
|
|
1008
|
+
Promise.resolve(result).catch(() => {});
|
|
1009
|
+
const error = new TypeError("commandRunner must return synchronously");
|
|
1010
|
+
error.code = "COMMAND_RUNNER_ASYNC_UNSUPPORTED";
|
|
1011
|
+
return { status: 1, stdout: "", stderr: error.message, error };
|
|
1012
|
+
}
|
|
1013
|
+
return result;
|
|
981
1014
|
}
|
|
982
1015
|
|
|
983
1016
|
function whichBinary(binary, { commandRunner } = {}) {
|
|
@@ -1319,17 +1352,6 @@ function fetchKiroLimits({ commandRunner, now = new Date() } = {}) {
|
|
|
1319
1352
|
}
|
|
1320
1353
|
}
|
|
1321
1354
|
|
|
1322
|
-
function parseProcessLine(line) {
|
|
1323
|
-
const match = String(line || "")
|
|
1324
|
-
.trim()
|
|
1325
|
-
.match(/^(\d+)\s+(.*)$/);
|
|
1326
|
-
if (!match) return null;
|
|
1327
|
-
return {
|
|
1328
|
-
pid: Number(match[1]),
|
|
1329
|
-
command: match[2],
|
|
1330
|
-
};
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
1355
|
function isAntigravityCommandLine(command) {
|
|
1334
1356
|
const lower = String(command || "").toLowerCase();
|
|
1335
1357
|
return lower.includes("language_server")
|
|
@@ -1347,10 +1369,34 @@ function extractCommandFlag(command, flag) {
|
|
|
1347
1369
|
return match?.[1] || null;
|
|
1348
1370
|
}
|
|
1349
1371
|
|
|
1372
|
+
// Scans for the local Antigravity language server and reads its `--csrf_token`
|
|
1373
|
+
// out of the command line, so that the quota request below can authenticate to
|
|
1374
|
+
// it.
|
|
1375
|
+
//
|
|
1376
|
+
// The scan is scoped to the current user, and must stay that way. Under the
|
|
1377
|
+
// previous `-ax` this walked every account on the box and attached to whichever
|
|
1378
|
+
// Antigravity matched first. The token itself never reached an HTTP response —
|
|
1379
|
+
// `processInfo` is read field by field and never spread into a return value —
|
|
1380
|
+
// but what the token *fetches* does: `normalizeAntigravityResponse` returns
|
|
1381
|
+
// `account_email` and `account_plan`, `finalize` spreads them into the result,
|
|
1382
|
+
// and `getUsageLimits` serves that at `/functions/tokentracker-usage-limits`.
|
|
1383
|
+
// On a shared host this meant showing another person's email, plan and quota as
|
|
1384
|
+
// the local user's, and `writeAntigravityLimitsCache` persisted it to disk,
|
|
1385
|
+
// where the `!configured` branch would keep serving it after their process
|
|
1386
|
+
// exited.
|
|
1387
|
+
//
|
|
1388
|
+
// Sharing PS_ARGS with process-list.js is deliberate: two scans that must both
|
|
1389
|
+
// stay own-user should not be able to drift apart.
|
|
1350
1390
|
function detectAntigravityProcess({ commandRunner } = {}) {
|
|
1351
|
-
const result = runCommand(commandRunner,
|
|
1391
|
+
const result = runCommand(commandRunner, PS_BINARY, PS_ARGS, {
|
|
1352
1392
|
timeout: 4000,
|
|
1353
1393
|
});
|
|
1394
|
+
if (result?.error?.code === "COMMAND_RUNNER_ASYNC_UNSUPPORTED") {
|
|
1395
|
+
return {
|
|
1396
|
+
configured: true,
|
|
1397
|
+
error: result.error?.message || "Antigravity process detection failed.",
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1354
1400
|
const lines = String(result?.stdout || "").split("\n");
|
|
1355
1401
|
|
|
1356
1402
|
let sawProcess = false;
|
|
@@ -1422,6 +1468,7 @@ function hasAntigravityWindow(limits) {
|
|
|
1422
1468
|
}
|
|
1423
1469
|
|
|
1424
1470
|
function normalizeAntigravityCachedLimits(raw, { nowMs = Date.now() } = {}) {
|
|
1471
|
+
if (raw?.process_scope !== ANTIGRAVITY_CACHE_PROCESS_SCOPE) return null;
|
|
1425
1472
|
const cachedAtMs = parseTimeMs(raw?.cached_at);
|
|
1426
1473
|
if (!Number.isFinite(cachedAtMs)) return null;
|
|
1427
1474
|
if (cachedAtMs > nowMs + 60_000) return null;
|
|
@@ -1450,6 +1497,7 @@ function writeAntigravityLimitsCache(limits, { home, nowMs = Date.now() } = {})
|
|
|
1450
1497
|
if (!limits?.configured || limits.error || !hasAntigravityWindow(limits)) return;
|
|
1451
1498
|
const payload = {
|
|
1452
1499
|
antigravity: {
|
|
1500
|
+
process_scope: ANTIGRAVITY_CACHE_PROCESS_SCOPE,
|
|
1453
1501
|
account_email: limits.account_email || null,
|
|
1454
1502
|
account_plan: limits.account_plan || null,
|
|
1455
1503
|
primary_window: limits.primary_window || null,
|
|
@@ -1886,7 +1934,20 @@ function withPlanLabel(obj, raw, brand) {
|
|
|
1886
1934
|
return { ...obj, plan_label: normalizePlanLabel(raw, brand) };
|
|
1887
1935
|
}
|
|
1888
1936
|
|
|
1889
|
-
async function getUsageLimits({
|
|
1937
|
+
async function getUsageLimits(options = {}) {
|
|
1938
|
+
const nowMs = Date.now();
|
|
1939
|
+
if (cache.data && nowMs - cache.fetchedAt < CACHE_TTL_MS) {
|
|
1940
|
+
return cache.data;
|
|
1941
|
+
}
|
|
1942
|
+
// Past the cache there is exactly one thing to do, and it is expensive: sweep
|
|
1943
|
+
// every configured provider. Concurrent tabs, route mounts and revalidations
|
|
1944
|
+
// land here together on a cold cache, and a forced refresh clears the cache
|
|
1945
|
+
// first (local-api.js, the `refresh` param) — so "no cached data" is a common
|
|
1946
|
+
// state for several callers at once, not a rare one. They share one sweep.
|
|
1947
|
+
return runUsageLimitsFetch(() => fetchUsageLimits(options));
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
async function fetchUsageLimits({
|
|
1890
1951
|
home,
|
|
1891
1952
|
env,
|
|
1892
1953
|
platform,
|
|
@@ -1898,9 +1959,10 @@ async function getUsageLimits({
|
|
|
1898
1959
|
providerTimeoutMs = DEFAULT_PROVIDER_TIMEOUT_MS,
|
|
1899
1960
|
} = {}) {
|
|
1900
1961
|
const nowMs = Date.now();
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1962
|
+
// The stale-Codex refresh is part of the same provider sweep and must share
|
|
1963
|
+
// its abort bound. Creating this before the serial prelude prevents one raw
|
|
1964
|
+
// OAuth fetch from holding the process-wide single-flight slot forever.
|
|
1965
|
+
const providerFetch = withFetchTimeout(fetchImpl, providerTimeoutMs);
|
|
1904
1966
|
|
|
1905
1967
|
const [claudeToken, claudeSubscription, codexAuth] = await Promise.all([
|
|
1906
1968
|
Promise.resolve().then(() => readClaudeCodeAccessToken({ platform, securityRunner, home })),
|
|
@@ -1920,7 +1982,7 @@ async function getUsageLimits({
|
|
|
1920
1982
|
try {
|
|
1921
1983
|
const newTokens = await refreshCodexTokens({
|
|
1922
1984
|
refreshToken: codexAuth.refreshToken,
|
|
1923
|
-
fetchImpl,
|
|
1985
|
+
fetchImpl: providerFetch,
|
|
1924
1986
|
});
|
|
1925
1987
|
const updatedAuth = await persistRefreshedAuth(
|
|
1926
1988
|
codexAuth.authPath,
|
|
@@ -1943,7 +2005,6 @@ async function getUsageLimits({
|
|
|
1943
2005
|
const codexAccountId = codexAuthRefreshed?.accountId || null;
|
|
1944
2006
|
const codexPlanType = codexAuthRefreshed?.planType || null;
|
|
1945
2007
|
|
|
1946
|
-
const providerFetch = withFetchTimeout(fetchImpl, providerTimeoutMs);
|
|
1947
2008
|
const [claudeResult, codexResult, cursor, kimi, zai, gemini, kiro, antigravity, copilot] = await Promise.all([
|
|
1948
2009
|
claudeToken
|
|
1949
2010
|
? withProviderTimeout(fetchClaudeUsageLimits(claudeToken, { fetchImpl: providerFetch, maxAttempts: 1 }), "Claude", providerTimeoutMs).then(
|
|
@@ -2013,6 +2074,10 @@ async function getUsageLimits({
|
|
|
2013
2074
|
codex = {
|
|
2014
2075
|
configured: true,
|
|
2015
2076
|
error: null,
|
|
2077
|
+
// A 4xx from wham produces no windows and no error. Carrying its `notice`
|
|
2078
|
+
// through is what keeps the chip on screen instead of falling through to
|
|
2079
|
+
// LimitChips' `windows.length === 0 -> return null`.
|
|
2080
|
+
notice: codexResult.value.notice || null,
|
|
2016
2081
|
plan_type: codexPlanType || null,
|
|
2017
2082
|
primary_window: codexResult.value.primary_window,
|
|
2018
2083
|
secondary_window: codexResult.value.secondary_window,
|
|
@@ -2040,8 +2105,20 @@ function resetUsageLimitsCache() {
|
|
|
2040
2105
|
cache = { data: null, fetchedAt: 0 };
|
|
2041
2106
|
}
|
|
2042
2107
|
|
|
2108
|
+
// The force protocol for the quota cache, in one place because two handlers
|
|
2109
|
+
// speak it: the CLI local API and the Vite dev middleware. They used to carry
|
|
2110
|
+
// their own copy of the same comparison, which is how one of them quietly stops
|
|
2111
|
+
// honouring a spelling — and a force that is silently downgraded to a cached
|
|
2112
|
+
// read looks exactly like a working one. `1` is what the dashboard client sends
|
|
2113
|
+
// (dashboard/src/lib/api.ts); `true` is what a human types into the URL bar.
|
|
2114
|
+
// Deliberately exact: no trimming, no case folding, no truthiness.
|
|
2115
|
+
function isForcedRefresh(value) {
|
|
2116
|
+
return value === "1" || value === "true";
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2043
2119
|
module.exports = {
|
|
2044
2120
|
getUsageLimits,
|
|
2121
|
+
isForcedRefresh,
|
|
2045
2122
|
normalizePlanLabel,
|
|
2046
2123
|
resetUsageLimitsCache,
|
|
2047
2124
|
extractGeminiOauthClientCredentials,
|