tokentracker-cli 0.70.0 → 0.72.0
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.de.md +2 -1
- package/README.ja.md +2 -1
- package/README.ko.md +2 -1
- package/README.md +2 -1
- package/README.zh-CN.md +2 -1
- package/dashboard/dist/assets/{ActivityHeatmap-HkjamKhQ.js → ActivityHeatmap-xXPEf7cL.js} +1 -1
- package/dashboard/dist/assets/{Card-BAu_q1P8.js → Card-SvNODc3B.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-Cvb5lxum.js → CommunityStatsModal-DQVjsjuv.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-Ckc0m2pF.js → ConfirmModal-BaWPDQpw.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-CdTpYDO0.js → DashboardPage-Byj7sLJ0.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-BIQ25ul5.js → DevicePage-mpqMx8oV.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-DiOWMlOM.js → DialogClose-CWzNkEbl.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-D15uXWxt.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-BrR7t4i0.js → DialogTitle-DAobFTUO.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-CkFXh9qu.js → FadeIn-D9ysa5Ls.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-BqKaM3ph.js → HeaderGithubStar-ByPo4mzv.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-k4RZKPiL.js → IpCheckPage-D5O_V9Ng.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-Duj0yl_C.js → LandingPage-NXu1Hrnf.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardAvatar-dILRiEW5.js → LeaderboardAvatar-DR9Gca2Q.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-yWly0uWz.js → LeaderboardPage-B5OEb0j0.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-DyBDRAKE.js → LeaderboardProfileModal-BP1VXIGY.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfilePage-CpXppYzc.js → LeaderboardProfilePage-CkU4G9rM.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-BG86Jtvl.js → LimitsPage-PvibsSfB.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-CYCe0N_S.js → LocalOnlyNotice-BPkaxkkB.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-D9XhznZH.js → LoginPage-B4EzkwXW.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-C8iZhys6.js → PopoverPopup-MwcAIRI1.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-B8mXGzFe.js → ResetPasswordPage-Cjobkt-M.js} +1 -1
- package/dashboard/dist/assets/{Select-Wgv6XKdi.js → Select-ZtaYokvF.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-rxW6xT66.js → SelectItemText-CdrhC29S.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-DRpIQZJx.js → SettingsPage-DI0HkBW_.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-Bl_FJwQu.js → SkillsPage-CWFTNZQ0.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-B0AlwiRQ.js → WidgetsPage-CEP1Ew6X.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-C-ojLydF.js → WrappedPage-fiNsURtg.js} +1 -1
- package/dashboard/dist/assets/{agent-logos-wKEmyIg8.js → agent-logos-B8iedQIH.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-BwT8ykRA.js → arrow-up-right-Cjb3EZGs.js} +1 -1
- package/dashboard/dist/assets/check-D1HYoTs4.js +1 -0
- package/dashboard/dist/assets/{copy-DvlBby5m.js → copy-DlDViZxV.js} +1 -1
- package/dashboard/dist/assets/{download-C7MsK41l.js → download-C5qoAiwD.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CtXNYfM2.js → format-tokens-e55_685N.js} +1 -1
- package/dashboard/dist/assets/{info-DXSmyEHB.js → info-BWJddEKJ.js} +1 -1
- package/dashboard/dist/assets/{main-DPyNBrFb.js → main-CjFvb-Kv.js} +2 -2
- package/dashboard/dist/assets/{use-community-stats-BC4Va1tk.js → use-community-stats-Ds7fV_8y.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-BL-NCgDn.js → use-limits-display-prefs-CWumBLAN.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-D0x_4uCT.js → use-native-settings-D5oSEEA_.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-DEi9FKn9.js → use-ordered-list-DFEm87T2.js} +1 -1
- package/dashboard/dist/assets/{use-quality-per-dollar-pref-BIf1mWJK.js → use-quality-per-dollar-pref-D5bAGtCm.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-CosJF8AC.js → use-usage-limits-D5q4cnNd.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-4cyeApuY.js → useCurrency-CHAjKv53.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-D0Td7eQX.js → useScrollLock-DDedroUH.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/serve.js +13 -0
- package/src/commands/sync.js +8 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/telemetry.js +192 -0
- package/src/lib/usage-limits.js +80 -14
- package/dashboard/dist/assets/DialogDescription-DNyMczI_.js +0 -1
- package/dashboard/dist/assets/check-gQkfPY06.js +0 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Anonymous daily heartbeat for counting active installs.
|
|
5
|
+
*
|
|
6
|
+
* Privacy contract (documented in README "Privacy" section — keep in sync):
|
|
7
|
+
* the payload carries exactly four fields — a one-way sha256 hash of the
|
|
8
|
+
* machine id, the app version, `process.platform`, and the app shell
|
|
9
|
+
* (cli / macos / windows). No token counts, no model names, no usernames,
|
|
10
|
+
* no paths. Opt out with TOKENTRACKER_NO_TELEMETRY=1, the DO_NOT_TRACK
|
|
11
|
+
* standard, or `"telemetry": false` in config.json.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const path = require("node:path");
|
|
15
|
+
const crypto = require("node:crypto");
|
|
16
|
+
|
|
17
|
+
const pkg = require("../../package.json");
|
|
18
|
+
const { readJson, writeJson } = require("./fs");
|
|
19
|
+
const { getOrCreateMachineId } = require("./machine-id");
|
|
20
|
+
const { resolveRuntimeConfig } = require("./runtime-config");
|
|
21
|
+
const {
|
|
22
|
+
decideAutoUpload,
|
|
23
|
+
recordUploadSuccess,
|
|
24
|
+
recordUploadFailure,
|
|
25
|
+
} = require("./upload-throttle");
|
|
26
|
+
|
|
27
|
+
const HEARTBEAT_FUNCTION_SLUG = "tokentracker-telemetry";
|
|
28
|
+
const HEARTBEAT_STATE_FILE = "telemetry.heartbeat.json";
|
|
29
|
+
const HEARTBEAT_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
30
|
+
// The hash namespace is versioned so the anonymous id can be rotated fleet-wide
|
|
31
|
+
// by bumping the string. sha256(namespace + machineId) cannot be joined back to
|
|
32
|
+
// the cloud device rows (those store the machineId itself).
|
|
33
|
+
const MACHINE_HASH_NAMESPACE = "tokentracker-telemetry-v1:";
|
|
34
|
+
// Generous because some networks need 3-4s just for the TLS handshake to the
|
|
35
|
+
// cloud host; sync awaits this at most once per day so the worst-case stall
|
|
36
|
+
// is bounded and rare.
|
|
37
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
38
|
+
const VALID_SHELLS = new Set(["cli", "macos", "windows"]);
|
|
39
|
+
|
|
40
|
+
const THROTTLE_CONFIG = {
|
|
41
|
+
intervalMs: HEARTBEAT_INTERVAL_MS,
|
|
42
|
+
jitterMsMax: 60_000,
|
|
43
|
+
backoffInitialMs: 60 * 60_000, // failures retry hourly at most
|
|
44
|
+
backoffMaxMs: HEARTBEAT_INTERVAL_MS,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function isTelemetryDisabled({ env = process.env, config = {} } = {}) {
|
|
48
|
+
if (isTruthyFlag(env?.TOKENTRACKER_NO_TELEMETRY)) return true;
|
|
49
|
+
if (isTruthyFlag(env?.DO_NOT_TRACK)) return true;
|
|
50
|
+
if (config && config.telemetry === false) return true;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isTruthyFlag(value) {
|
|
55
|
+
if (typeof value !== "string") return false;
|
|
56
|
+
const v = value.trim().toLowerCase();
|
|
57
|
+
return v === "1" || v === "true";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function hashMachineId(machineId) {
|
|
61
|
+
return crypto
|
|
62
|
+
.createHash("sha256")
|
|
63
|
+
.update(`${MACHINE_HASH_NAMESPACE}${machineId}`)
|
|
64
|
+
.digest("hex");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveShell(env = process.env) {
|
|
68
|
+
const raw = typeof env?.TOKENTRACKER_APP_SHELL === "string"
|
|
69
|
+
? env.TOKENTRACKER_APP_SHELL.trim().toLowerCase()
|
|
70
|
+
: "";
|
|
71
|
+
return VALID_SHELLS.has(raw) ? raw : "cli";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildHeartbeatPayload({ machineId, version = pkg.version, platform = process.platform, env = process.env }) {
|
|
75
|
+
return {
|
|
76
|
+
machine_hash: hashMachineId(machineId),
|
|
77
|
+
app_version: String(version || "").slice(0, 32),
|
|
78
|
+
platform: String(platform || "unknown").slice(0, 16),
|
|
79
|
+
shell: resolveShell(env),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Send the daily heartbeat if allowed. Never throws; every failure path is a
|
|
85
|
+
* silent no-op so serve/sync flow is never affected. Returns a small status
|
|
86
|
+
* object for tests and debug logging.
|
|
87
|
+
*/
|
|
88
|
+
async function maybeSendHeartbeat({
|
|
89
|
+
trackerDir,
|
|
90
|
+
fetchImpl = fetch,
|
|
91
|
+
nowMs = Date.now(),
|
|
92
|
+
env = process.env,
|
|
93
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
94
|
+
version,
|
|
95
|
+
} = {}) {
|
|
96
|
+
try {
|
|
97
|
+
if (!trackerDir) return { sent: false, reason: "no-tracker-dir" };
|
|
98
|
+
|
|
99
|
+
// Never phone home from the node test runner: many tests drive cmdSync /
|
|
100
|
+
// cmdServe directly from sandboxed temp homes, and their fresh throttle
|
|
101
|
+
// state would let a real POST through to production on every run. Tests
|
|
102
|
+
// that exercise this module inject their own env without this marker.
|
|
103
|
+
if (env?.NODE_TEST_CONTEXT !== undefined) {
|
|
104
|
+
return { sent: false, reason: "test-env" };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const configPath = path.join(trackerDir, "config.json");
|
|
108
|
+
const config = (await readJson(configPath)) || {};
|
|
109
|
+
if (isTelemetryDisabled({ env, config })) {
|
|
110
|
+
return { sent: false, reason: "opt-out" };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const statePath = path.join(trackerDir, HEARTBEAT_STATE_FILE);
|
|
114
|
+
const state = await readJson(statePath);
|
|
115
|
+
const decision = decideAutoUpload({
|
|
116
|
+
nowMs,
|
|
117
|
+
pendingBytes: 1, // heartbeats always have "one ping pending"
|
|
118
|
+
state,
|
|
119
|
+
config: THROTTLE_CONFIG,
|
|
120
|
+
});
|
|
121
|
+
if (!decision.allowed) {
|
|
122
|
+
return { sent: false, reason: "throttled", blockedUntilMs: decision.blockedUntilMs };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const machineId = getOrCreateMachineId(path.join(trackerDir, "queue.jsonl"));
|
|
126
|
+
if (!machineId) return { sent: false, reason: "no-machine-id" };
|
|
127
|
+
|
|
128
|
+
const runtime = resolveRuntimeConfig({ config, env });
|
|
129
|
+
const payload = buildHeartbeatPayload({ machineId, version, env });
|
|
130
|
+
|
|
131
|
+
const controller = new AbortController();
|
|
132
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
133
|
+
let response;
|
|
134
|
+
try {
|
|
135
|
+
response = await fetchImpl(`${runtime.baseUrl}/functions/${HEARTBEAT_FUNCTION_SLUG}`, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: {
|
|
138
|
+
"Content-Type": "application/json",
|
|
139
|
+
apikey: runtime.anonKey,
|
|
140
|
+
},
|
|
141
|
+
body: JSON.stringify(payload),
|
|
142
|
+
signal: controller.signal,
|
|
143
|
+
});
|
|
144
|
+
} finally {
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!response || !response.ok) {
|
|
149
|
+
const failedState = recordUploadFailure({
|
|
150
|
+
nowMs,
|
|
151
|
+
state,
|
|
152
|
+
error: { message: `heartbeat http ${response?.status || "error"}`, status: response?.status },
|
|
153
|
+
config: THROTTLE_CONFIG,
|
|
154
|
+
});
|
|
155
|
+
await writeJson(statePath, failedState);
|
|
156
|
+
return { sent: false, reason: "http-error", status: response?.status };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const nextState = recordUploadSuccess({ nowMs, state, config: THROTTLE_CONFIG });
|
|
160
|
+
await writeJson(statePath, nextState);
|
|
161
|
+
return { sent: true, reason: "sent" };
|
|
162
|
+
} catch (e) {
|
|
163
|
+
// Includes abort/network errors: record backoff if we can, stay silent.
|
|
164
|
+
try {
|
|
165
|
+
const statePath = path.join(trackerDir, HEARTBEAT_STATE_FILE);
|
|
166
|
+
const state = await readJson(statePath);
|
|
167
|
+
await writeJson(
|
|
168
|
+
statePath,
|
|
169
|
+
recordUploadFailure({
|
|
170
|
+
nowMs,
|
|
171
|
+
state,
|
|
172
|
+
error: { message: String(e?.message || e) },
|
|
173
|
+
config: THROTTLE_CONFIG,
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
} catch {
|
|
177
|
+
// Even state persistence failed — nothing else to do.
|
|
178
|
+
}
|
|
179
|
+
return { sent: false, reason: "error" };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = {
|
|
184
|
+
HEARTBEAT_FUNCTION_SLUG,
|
|
185
|
+
HEARTBEAT_STATE_FILE,
|
|
186
|
+
HEARTBEAT_INTERVAL_MS,
|
|
187
|
+
isTelemetryDisabled,
|
|
188
|
+
hashMachineId,
|
|
189
|
+
resolveShell,
|
|
190
|
+
buildHeartbeatPayload,
|
|
191
|
+
maybeSendHeartbeat,
|
|
192
|
+
};
|
package/src/lib/usage-limits.js
CHANGED
|
@@ -28,9 +28,15 @@ const { fetchZcodeLimits } = require("./zcode-limits");
|
|
|
28
28
|
const { fetchOpencodeGoLimits } = require("./opencode-go-limits");
|
|
29
29
|
const { readSqliteJsonRows } = require("./sqlite-reader");
|
|
30
30
|
|
|
31
|
-
// 2-minute in-memory cache
|
|
32
|
-
|
|
31
|
+
// 2-minute in-memory cache. It also expires early at the earliest upcoming window
|
|
32
|
+
// reset in the cached data (see cacheExpiresAtMs), floored so a provider reporting
|
|
33
|
+
// a reset "right now" can't turn every poll into a full upstream round.
|
|
34
|
+
let cache = { data: null, expiresAtMs: 0 };
|
|
33
35
|
const CACHE_TTL_MS = 2 * 60 * 1000;
|
|
36
|
+
// Must stay below the macOS app's post-reset re-fetch grace (10s in
|
|
37
|
+
// DashboardViewModel.resetBoundaryGrace), or that targeted refresh would be
|
|
38
|
+
// served this cache's pre-reset snapshot and miss the rollover.
|
|
39
|
+
const CACHE_MIN_TTL_MS = 5 * 1000;
|
|
34
40
|
const DEFAULT_PROVIDER_TIMEOUT_MS = 15_000;
|
|
35
41
|
const ANTIGRAVITY_LIMITS_CACHE_FILE = "usage-limits-cache.json";
|
|
36
42
|
const ANTIGRAVITY_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -1921,24 +1927,48 @@ function normalizeClaudeCachedLimits(
|
|
|
1921
1927
|
return hasClaudeWindow(cached) ? cached : null;
|
|
1922
1928
|
}
|
|
1923
1929
|
|
|
1930
|
+
function readClaudeLimitsCacheRaw({ home } = {}) {
|
|
1931
|
+
try {
|
|
1932
|
+
const parsed = JSON.parse(fs.readFileSync(resolveClaudeLimitsCachePath({ home }), "utf8"));
|
|
1933
|
+
return parsed?.claude ?? null;
|
|
1934
|
+
} catch (_error) {
|
|
1935
|
+
return null;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1924
1939
|
function readClaudeLimitsCache({
|
|
1925
1940
|
home,
|
|
1926
1941
|
nowMs = Date.now(),
|
|
1927
1942
|
maxAgeMs = CLAUDE_LIMITS_CACHE_MAX_AGE_MS,
|
|
1928
1943
|
stale = true,
|
|
1929
1944
|
} = {}) {
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1945
|
+
return normalizeClaudeCachedLimits(readClaudeLimitsCacheRaw({ home }), { nowMs, maxAgeMs, stale });
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// A cached snapshot stops being "fresh" the moment any of its windows crosses the
|
|
1949
|
+
// reset boundary it was written with: serving it past that point hides the rollover
|
|
1950
|
+
// from reset detection (the menu bar confetti) for up to the fresh TTL. Only resets
|
|
1951
|
+
// that were still ahead at write time count — a provider stamping already-past reset
|
|
1952
|
+
// times must not force a live call on every poll.
|
|
1953
|
+
function claudeCacheCrossedReset(raw, { nowMs } = {}) {
|
|
1954
|
+
const cachedAtMs = parseTimeMs(raw?.cached_at);
|
|
1955
|
+
if (!Number.isFinite(cachedAtMs)) return false;
|
|
1956
|
+
const windows = [
|
|
1957
|
+
raw?.five_hour,
|
|
1958
|
+
raw?.seven_day,
|
|
1959
|
+
raw?.seven_day_opus,
|
|
1960
|
+
...(Array.isArray(raw?.weekly_scoped) ? raw.weekly_scoped : []),
|
|
1961
|
+
];
|
|
1962
|
+
return windows.some((window) => {
|
|
1963
|
+
const resetMs = parseTimeMs(window?.resets_at);
|
|
1964
|
+
return resetMs !== null && resetMs > cachedAtMs && resetMs <= nowMs;
|
|
1965
|
+
});
|
|
1937
1966
|
}
|
|
1938
1967
|
|
|
1939
1968
|
function readFreshClaudeLimitsCache({ home, nowMs = Date.now() } = {}) {
|
|
1940
|
-
|
|
1941
|
-
|
|
1969
|
+
const raw = readClaudeLimitsCacheRaw({ home });
|
|
1970
|
+
if (!raw || claudeCacheCrossedReset(raw, { nowMs })) return null;
|
|
1971
|
+
return normalizeClaudeCachedLimits(raw, {
|
|
1942
1972
|
nowMs,
|
|
1943
1973
|
maxAgeMs: CLAUDE_LIMITS_CACHE_FRESH_TTL_MS,
|
|
1944
1974
|
stale: false,
|
|
@@ -2564,9 +2594,44 @@ function withPlanLabel(obj, raw, brand) {
|
|
|
2564
2594
|
// in-flight fetch and returns its result.
|
|
2565
2595
|
let inFlightFetch = null;
|
|
2566
2596
|
|
|
2597
|
+
// Codex stamps reset_at as unix seconds; every other provider (and Claude's
|
|
2598
|
+
// resets_at) uses ISO strings. Numbers that look like epoch milliseconds are
|
|
2599
|
+
// accepted as-is so a future provider can't silently regress this.
|
|
2600
|
+
function resetValueToMs(value) {
|
|
2601
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
2602
|
+
return value < 1e12 ? value * 1000 : value;
|
|
2603
|
+
}
|
|
2604
|
+
return parseTimeMs(value);
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
// Walk the aggregated response and collect every window reset timestamp,
|
|
2608
|
+
// whatever the provider-specific nesting looks like.
|
|
2609
|
+
function collectResetTimesMs(node, out) {
|
|
2610
|
+
if (!node || typeof node !== "object") return;
|
|
2611
|
+
for (const [key, value] of Object.entries(node)) {
|
|
2612
|
+
if (key === "reset_at" || key === "resets_at") {
|
|
2613
|
+
const ms = resetValueToMs(value);
|
|
2614
|
+
if (ms !== null) out.push(ms);
|
|
2615
|
+
} else if (value && typeof value === "object") {
|
|
2616
|
+
collectResetTimesMs(value, out);
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
// The moment any window rolls over, cached data stops being current — reset
|
|
2622
|
+
// detection (the menu bar confetti) would otherwise lag by up to the full TTL.
|
|
2623
|
+
function cacheExpiresAtMs(data, fetchedAtMs) {
|
|
2624
|
+
const times = [];
|
|
2625
|
+
collectResetTimesMs(data, times);
|
|
2626
|
+
const upcoming = times.filter((t) => t > fetchedAtMs);
|
|
2627
|
+
const ttlExpiry = fetchedAtMs + CACHE_TTL_MS;
|
|
2628
|
+
if (upcoming.length === 0) return ttlExpiry;
|
|
2629
|
+
return Math.min(ttlExpiry, Math.max(Math.min(...upcoming), fetchedAtMs + CACHE_MIN_TTL_MS));
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2567
2632
|
async function getUsageLimits(options = {}) {
|
|
2568
2633
|
const nowMs = Date.now();
|
|
2569
|
-
if (cache.data && nowMs
|
|
2634
|
+
if (cache.data && nowMs < cache.expiresAtMs) {
|
|
2570
2635
|
return cache.data;
|
|
2571
2636
|
}
|
|
2572
2637
|
if (inFlightFetch) {
|
|
@@ -2775,18 +2840,19 @@ async function fetchUsageLimitsUncached({
|
|
|
2775
2840
|
opencodeGo: withPlanLabel(opencodeGo, opencodeGo?.plan_label, "OpenCode Go"),
|
|
2776
2841
|
};
|
|
2777
2842
|
|
|
2778
|
-
cache = { data,
|
|
2843
|
+
cache = { data, expiresAtMs: cacheExpiresAtMs(data, nowMs) };
|
|
2779
2844
|
return data;
|
|
2780
2845
|
}
|
|
2781
2846
|
|
|
2782
2847
|
function resetUsageLimitsCache() {
|
|
2783
|
-
cache = { data: null,
|
|
2848
|
+
cache = { data: null, expiresAtMs: 0 };
|
|
2784
2849
|
}
|
|
2785
2850
|
|
|
2786
2851
|
module.exports = {
|
|
2787
2852
|
getUsageLimits,
|
|
2788
2853
|
normalizePlanLabel,
|
|
2789
2854
|
resetUsageLimitsCache,
|
|
2855
|
+
cacheExpiresAtMs,
|
|
2790
2856
|
runCommand,
|
|
2791
2857
|
extractGeminiOauthClientCredentials,
|
|
2792
2858
|
loadKimiCredentials,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{r as n,b9 as a}from"./main-DPyNBrFb.js";import{u as p}from"./DialogTitle-BrR7t4i0.js";import{u as c}from"./useScrollLock-D0Td7eQX.js";const x=n.forwardRef(function(e,r){const{render:l,className:u,style:m,id:s,...t}=e,{store:i}=p(),o=c(s);return i.useSyncedValueWithCleanup("descriptionElementId",o),a("p",e,{ref:r,props:[{id:o},t]})});export{x as D};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{c}from"./main-DPyNBrFb.js";const e=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],t=c("check",e);export{t as C};
|