claude-usage-limits 1.9.2 → 1.11.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +196 -25
- package/bin/cli.js +21 -1
- package/commands/panel.md +14 -0
- package/commands/statusline.md +15 -0
- package/package.json +1 -1
- package/skills/usage-limits/SKILL.md +36 -0
- package/skills/usage-limits/references/how-it-works.md +115 -2
- package/skills/usage-limits/scripts/activity.js +188 -0
- package/skills/usage-limits/scripts/bars.js +340 -0
- package/skills/usage-limits/scripts/brief.js +59 -6
- package/skills/usage-limits/scripts/feed.js +377 -0
- package/skills/usage-limits/scripts/live.js +422 -0
- package/skills/usage-limits/scripts/panel.js +706 -0
- package/skills/usage-limits/scripts/pulse.js +24 -0
- package/skills/usage-limits/scripts/recommend.js +56 -5
- package/skills/usage-limits/scripts/sessionend.js +5 -1
- package/skills/usage-limits/scripts/statusline.js +305 -0
- package/skills/usage-limits/scripts/stop.js +5 -1
- package/skills/usage-limits/scripts/tally.js +4 -10
- package/skills/usage-limits/scripts/usage.js +477 -17
- package/skills/usage-limits/scripts/view.js +217 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The reading itself, taken the way Claude Code takes it.
|
|
4
|
+
//
|
|
5
|
+
// /usage in Claude Code is one GET to Anthropic's usage endpoint with the
|
|
6
|
+
// login token Claude Code already holds. This file makes that same call, with
|
|
7
|
+
// the same headers and the same timeout, and keeps the answer in a small file
|
|
8
|
+
// of its own so the rest of the plugin can read it without going anywhere
|
|
9
|
+
// near the network.
|
|
10
|
+
//
|
|
11
|
+
// Two rules that matter more than anything else here:
|
|
12
|
+
//
|
|
13
|
+
// - The token is read, used for this one request, and sent nowhere else. It is
|
|
14
|
+
// never written to the live file, never printed, never refreshed or rotated.
|
|
15
|
+
// Refreshing it would race Claude Code's own refresh and could sign the user
|
|
16
|
+
// out; if it has expired the endpoint says 401 and Claude Code fixes it on
|
|
17
|
+
// its next call.
|
|
18
|
+
// - Every failure is a kind, not an exception. Offline, signed out, busy and
|
|
19
|
+
// broken all need different waits and different words, and the panel has to
|
|
20
|
+
// keep drawing through all of them.
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const os = require('os');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const http = require('http');
|
|
26
|
+
const https = require('https');
|
|
27
|
+
const { execFileSync } = require('child_process');
|
|
28
|
+
|
|
29
|
+
const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
30
|
+
const BETA = 'oauth-2025-04-20';
|
|
31
|
+
// Claude Code gives the call five seconds; so does this.
|
|
32
|
+
const DEFAULT_TIMEOUT_MS = 5000;
|
|
33
|
+
const KEYCHAIN_SERVICE = 'Claude Code-credentials';
|
|
34
|
+
const MAX_BODY = 1024 * 1024;
|
|
35
|
+
const MINUTE = 60 * 1000;
|
|
36
|
+
|
|
37
|
+
function configDir() {
|
|
38
|
+
return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function credentialsFile() {
|
|
42
|
+
return path.join(configDir(), '.credentials.json');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function liveFile() {
|
|
46
|
+
return path.join(configDir(), 'usage-limits-live.json');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function version() {
|
|
50
|
+
try {
|
|
51
|
+
return require('../../../package.json').version;
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return '0';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function userAgent() {
|
|
58
|
+
return 'claude-usage-limits/' + version();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseCredentials(text) {
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(text);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
69
|
+
const oauth = parsed.claudeAiOauth;
|
|
70
|
+
if (!oauth || typeof oauth !== 'object') return { token: null, expiresAt: null };
|
|
71
|
+
return {
|
|
72
|
+
token: typeof oauth.accessToken === 'string' && oauth.accessToken ? oauth.accessToken : null,
|
|
73
|
+
expiresAt: Number.isFinite(oauth.expiresAt) ? oauth.expiresAt : null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function defaultExec(args) {
|
|
78
|
+
return execFileSync('security', args, {
|
|
79
|
+
encoding: 'utf8',
|
|
80
|
+
timeout: 3000,
|
|
81
|
+
windowsHide: true,
|
|
82
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Where Claude Code keeps the login: a file beside the config directory on
|
|
87
|
+
// Windows and Linux, the keychain on macOS. The file is checked first on every
|
|
88
|
+
// platform because CLAUDE_CONFIG_DIR installs write it there too.
|
|
89
|
+
function readToken(options) {
|
|
90
|
+
const opts = options || {};
|
|
91
|
+
const platform = opts.platform || process.platform;
|
|
92
|
+
const file = opts.file || credentialsFile();
|
|
93
|
+
|
|
94
|
+
let raw = null;
|
|
95
|
+
try {
|
|
96
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
97
|
+
} catch (err) {
|
|
98
|
+
if (err && err.code !== 'ENOENT') return { token: null, reason: 'unreadable', detail: err.code };
|
|
99
|
+
}
|
|
100
|
+
if (raw !== null) {
|
|
101
|
+
const parsed = parseCredentials(raw);
|
|
102
|
+
if (!parsed) return { token: null, reason: 'unreadable' };
|
|
103
|
+
if (!parsed.token) return { token: null, reason: 'no_credentials' };
|
|
104
|
+
return { token: parsed.token, expiresAt: parsed.expiresAt, source: 'file' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (platform === 'darwin') {
|
|
108
|
+
const exec = opts.exec || defaultExec;
|
|
109
|
+
try {
|
|
110
|
+
const out = exec(['find-generic-password', '-s', KEYCHAIN_SERVICE, '-w']);
|
|
111
|
+
const parsed = parseCredentials(String(out || '').trim());
|
|
112
|
+
if (parsed && parsed.token) {
|
|
113
|
+
return { token: parsed.token, expiresAt: parsed.expiresAt, source: 'keychain' };
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
// No keychain entry, or no permission to read it: same answer as no file.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { token: null, reason: 'no_credentials' };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function bad(kind, status, retryAfterMs, message) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
kind,
|
|
127
|
+
status: Number.isFinite(status) ? status : null,
|
|
128
|
+
retryAfterMs: Number.isFinite(retryAfterMs) ? retryAfterMs : null,
|
|
129
|
+
message: message || kind,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function retryAfterMs(headers) {
|
|
134
|
+
const value = headers && headers['retry-after'];
|
|
135
|
+
if (!value) return null;
|
|
136
|
+
const seconds = Number(value);
|
|
137
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
|
|
138
|
+
const at = Date.parse(value);
|
|
139
|
+
return Number.isFinite(at) ? Math.max(0, at - Date.now()) : null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Every transport failure is "offline" as far as the panel is concerned: there
|
|
143
|
+
// is nothing different to do about a DNS failure and a reset connection.
|
|
144
|
+
function classify(err) {
|
|
145
|
+
return 'offline';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function fetchUsage(options) {
|
|
149
|
+
const opts = options || {};
|
|
150
|
+
return new Promise((resolve) => {
|
|
151
|
+
if (!opts.token) return resolve(bad('no_credentials', null, null, 'no Claude login found'));
|
|
152
|
+
|
|
153
|
+
let url;
|
|
154
|
+
try {
|
|
155
|
+
url = new URL(opts.url || USAGE_URL);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
return resolve(bad('bad_response', null, null, 'the usage url is not a url'));
|
|
158
|
+
}
|
|
159
|
+
const client = url.protocol === 'http:' ? http : https;
|
|
160
|
+
const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
161
|
+
|
|
162
|
+
let settled = false;
|
|
163
|
+
const done = (outcome) => {
|
|
164
|
+
if (settled) return;
|
|
165
|
+
settled = true;
|
|
166
|
+
resolve(outcome);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const req = client.request(
|
|
170
|
+
{
|
|
171
|
+
protocol: url.protocol,
|
|
172
|
+
hostname: url.hostname,
|
|
173
|
+
port: url.port || undefined,
|
|
174
|
+
path: url.pathname + url.search,
|
|
175
|
+
method: 'GET',
|
|
176
|
+
headers: {
|
|
177
|
+
Authorization: 'Bearer ' + opts.token,
|
|
178
|
+
'anthropic-beta': BETA,
|
|
179
|
+
'Content-Type': 'application/json',
|
|
180
|
+
Accept: 'application/json',
|
|
181
|
+
'User-Agent': opts.userAgent || userAgent(),
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
(res) => {
|
|
185
|
+
let body = '';
|
|
186
|
+
res.setEncoding('utf8');
|
|
187
|
+
res.on('data', (chunk) => {
|
|
188
|
+
if (body.length < MAX_BODY) body += chunk;
|
|
189
|
+
});
|
|
190
|
+
res.on('error', (err) => done(bad('offline', null, null, (err && err.code) || 'read error')));
|
|
191
|
+
res.on('end', () => {
|
|
192
|
+
const status = res.statusCode;
|
|
193
|
+
if (status === 200) {
|
|
194
|
+
let parsed;
|
|
195
|
+
try {
|
|
196
|
+
parsed = JSON.parse(body);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
return done(bad('bad_response', status, null, 'the usage endpoint did not answer with JSON'));
|
|
199
|
+
}
|
|
200
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
201
|
+
return done(bad('bad_response', status, null, 'the usage endpoint answered with something unexpected'));
|
|
202
|
+
}
|
|
203
|
+
return done({ ok: true, status, utilization: parsed, fetchedAtMs: Date.now() });
|
|
204
|
+
}
|
|
205
|
+
if (status === 401) return done(bad('unauthorized', status, null, 'the login has expired'));
|
|
206
|
+
if (status === 403) return done(bad('forbidden', status, null, 'usage is not available for this login'));
|
|
207
|
+
if (status === 429) {
|
|
208
|
+
return done(bad('rate_limited', status, retryAfterMs(res.headers), 'the usage endpoint is busy'));
|
|
209
|
+
}
|
|
210
|
+
if (status >= 500) return done(bad('server', status, null, 'the usage endpoint returned ' + status));
|
|
211
|
+
return done(bad('http', status, null, 'the usage endpoint returned ' + status));
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
req.setTimeout(timeoutMs, () => {
|
|
216
|
+
req.destroy(new Error('timeout'));
|
|
217
|
+
});
|
|
218
|
+
req.on('error', (err) => done(bad(classify(err), null, null, (err && err.code) || (err && err.message) || 'offline')));
|
|
219
|
+
req.end();
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function finite(value, fallback) {
|
|
224
|
+
return Number.isFinite(value) ? value : fallback;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// How long to wait before trying again. The shape matters: offline should be
|
|
228
|
+
// retried soon and then less often, a busy server told us exactly how long, a
|
|
229
|
+
// login problem is something Claude Code fixes on its own next call, and
|
|
230
|
+
// server trouble is not helped by hammering it.
|
|
231
|
+
function nextDelayMs(outcome, previousMs, options) {
|
|
232
|
+
const opts = options || {};
|
|
233
|
+
const base = finite(opts.baseMs, 60 * 1000);
|
|
234
|
+
const max = finite(opts.maxMs, 120 * 1000);
|
|
235
|
+
const prev = finite(previousMs, 0);
|
|
236
|
+
if (!outcome || outcome.ok) return base;
|
|
237
|
+
switch (outcome.kind) {
|
|
238
|
+
case 'disabled':
|
|
239
|
+
return max;
|
|
240
|
+
case 'rate_limited':
|
|
241
|
+
return Number.isFinite(outcome.retryAfterMs) && outcome.retryAfterMs > 0
|
|
242
|
+
? Math.min(Math.max(outcome.retryAfterMs, 1000), 10 * MINUTE)
|
|
243
|
+
: 60 * 1000;
|
|
244
|
+
case 'unauthorized':
|
|
245
|
+
case 'forbidden':
|
|
246
|
+
case 'no_credentials':
|
|
247
|
+
return 30 * 1000;
|
|
248
|
+
case 'offline':
|
|
249
|
+
return Math.min(Math.max(5 * 1000, prev * 2), 60 * 1000);
|
|
250
|
+
default:
|
|
251
|
+
return Math.min(Math.max(15 * 1000, prev * 2), 120 * 1000);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// A few words for a footer.
|
|
256
|
+
function describe(outcome) {
|
|
257
|
+
if (!outcome) return '';
|
|
258
|
+
if (outcome.ok) return 'live';
|
|
259
|
+
switch (outcome.kind) {
|
|
260
|
+
case 'disabled':
|
|
261
|
+
return 'network off';
|
|
262
|
+
case 'offline':
|
|
263
|
+
return 'offline';
|
|
264
|
+
case 'unauthorized':
|
|
265
|
+
return 'sign in to Claude Code again';
|
|
266
|
+
case 'forbidden':
|
|
267
|
+
return 'usage not available for this login';
|
|
268
|
+
case 'no_credentials':
|
|
269
|
+
return 'no Claude login found';
|
|
270
|
+
case 'rate_limited':
|
|
271
|
+
return 'usage endpoint busy';
|
|
272
|
+
default:
|
|
273
|
+
return 'usage endpoint error';
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function readLive() {
|
|
278
|
+
let parsed;
|
|
279
|
+
try {
|
|
280
|
+
parsed = JSON.parse(fs.readFileSync(liveFile(), 'utf8'));
|
|
281
|
+
} catch (err) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
285
|
+
if (!Number.isFinite(parsed.fetchedAtMs)) return null;
|
|
286
|
+
if (!parsed.utilization || typeof parsed.utilization !== 'object') return null;
|
|
287
|
+
return parsed;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Through a temporary file, so a reader never sees half a reading.
|
|
291
|
+
function writeLive(snapshot) {
|
|
292
|
+
try {
|
|
293
|
+
const file = liveFile();
|
|
294
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
295
|
+
const temp = file + '.usage-limits-tmp';
|
|
296
|
+
fs.writeFileSync(temp, JSON.stringify(snapshot), 'utf8');
|
|
297
|
+
fs.renameSync(temp, file);
|
|
298
|
+
return true;
|
|
299
|
+
} catch (err) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function fetchDisabled(env) {
|
|
305
|
+
return String((env || process.env).USAGE_LIMITS_FETCH || '').toLowerCase() === 'off';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Take a reading and keep it. Whatever goes wrong, the previous reading on
|
|
309
|
+
// disk comes back so the caller always has something to draw.
|
|
310
|
+
async function refresh(options) {
|
|
311
|
+
const opts = options || {};
|
|
312
|
+
const env = opts.env || process.env;
|
|
313
|
+
if (opts.fetch === false || fetchDisabled(env)) {
|
|
314
|
+
return { outcome: bad('disabled', null, null, 'network use is off'), snapshot: readLive() };
|
|
315
|
+
}
|
|
316
|
+
const creds = readToken({ file: opts.credentialsFile, platform: opts.platform, exec: opts.exec });
|
|
317
|
+
if (!creds.token) {
|
|
318
|
+
return {
|
|
319
|
+
outcome: bad('no_credentials', null, null, creds.reason === 'unreadable' ? 'the login file could not be read' : 'no Claude login found'),
|
|
320
|
+
snapshot: readLive(),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const outcome = await fetchUsage({
|
|
324
|
+
token: creds.token,
|
|
325
|
+
url: opts.url || env.USAGE_LIMITS_USAGE_URL || USAGE_URL,
|
|
326
|
+
timeoutMs: opts.timeoutMs,
|
|
327
|
+
});
|
|
328
|
+
if (!outcome.ok) return { outcome, snapshot: readLive() };
|
|
329
|
+
const snapshot = {
|
|
330
|
+
fetchedAtMs: Number.isFinite(opts.now) ? opts.now : outcome.fetchedAtMs,
|
|
331
|
+
utilization: outcome.utilization,
|
|
332
|
+
accountUuid: opts.accountUuid || null,
|
|
333
|
+
source: 'api',
|
|
334
|
+
};
|
|
335
|
+
writeLive(snapshot);
|
|
336
|
+
return { outcome, snapshot };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// When the last attempt was, and how long to leave it. Kept apart from the
|
|
340
|
+
// reading so a failed attempt never disturbs a good reading.
|
|
341
|
+
function attemptFile() {
|
|
342
|
+
return path.join(configDir(), 'usage-limits-fetch.json');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function readAttempt() {
|
|
346
|
+
try {
|
|
347
|
+
const parsed = JSON.parse(fs.readFileSync(attemptFile(), 'utf8'));
|
|
348
|
+
return parsed && Number.isFinite(parsed.attemptedAtMs) ? parsed : null;
|
|
349
|
+
} catch (err) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function writeAttempt(record) {
|
|
355
|
+
try {
|
|
356
|
+
fs.writeFileSync(attemptFile(), JSON.stringify(record), 'utf8');
|
|
357
|
+
} catch (err) {
|
|
358
|
+
// Without it the next caller merely tries again a little sooner.
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// A reading only when the one on disk has aged. This is what the hooks call:
|
|
363
|
+
// the budget line was once seventeen minutes old while eight agents spent half
|
|
364
|
+
// a window in parallel, and the plugin said 42 percent as the wall arrived. A
|
|
365
|
+
// reading that is younger than maxAgeMs (from this file or from Claude Code's
|
|
366
|
+
// own cache, whichever is newer) is left alone, a failed attempt is not
|
|
367
|
+
// repeated until its backoff has passed, and the whole thing is skipped when
|
|
368
|
+
// the network is off.
|
|
369
|
+
async function refreshIfStale(options) {
|
|
370
|
+
const opts = options || {};
|
|
371
|
+
const env = opts.env || process.env;
|
|
372
|
+
const now = Number.isFinite(opts.now) ? opts.now : Date.now();
|
|
373
|
+
const maxAgeMs = Number.isFinite(opts.maxAgeMs) ? opts.maxAgeMs : 3 * MINUTE;
|
|
374
|
+
const current = readLive();
|
|
375
|
+
if (opts.fetch === false || fetchDisabled(env)) return { outcome: null, snapshot: current, skipped: 'disabled' };
|
|
376
|
+
|
|
377
|
+
const newestAt = Math.max(
|
|
378
|
+
current ? current.fetchedAtMs : 0,
|
|
379
|
+
Number.isFinite(opts.cacheFetchedAtMs) ? opts.cacheFetchedAtMs : 0
|
|
380
|
+
);
|
|
381
|
+
if (newestAt > 0 && now - newestAt < maxAgeMs) return { outcome: null, snapshot: current, skipped: 'fresh' };
|
|
382
|
+
|
|
383
|
+
const attempt = readAttempt();
|
|
384
|
+
if (attempt && Number.isFinite(attempt.delayMs) && now - attempt.attemptedAtMs < attempt.delayMs) {
|
|
385
|
+
return { outcome: null, snapshot: current, skipped: 'backoff' };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const result = await refresh({
|
|
389
|
+
now,
|
|
390
|
+
accountUuid: opts.accountUuid,
|
|
391
|
+
env,
|
|
392
|
+
timeoutMs: opts.timeoutMs,
|
|
393
|
+
url: opts.url,
|
|
394
|
+
credentialsFile: opts.credentialsFile,
|
|
395
|
+
});
|
|
396
|
+
writeAttempt({
|
|
397
|
+
attemptedAtMs: now,
|
|
398
|
+
delayMs: nextDelayMs(result.outcome, attempt ? attempt.delayMs : 0, { baseMs: maxAgeMs, maxMs: 10 * MINUTE }),
|
|
399
|
+
kind: result.outcome.ok ? 'ok' : result.outcome.kind,
|
|
400
|
+
});
|
|
401
|
+
return { outcome: result.outcome, snapshot: result.snapshot, skipped: null };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
module.exports = {
|
|
405
|
+
USAGE_URL,
|
|
406
|
+
BETA,
|
|
407
|
+
DEFAULT_TIMEOUT_MS,
|
|
408
|
+
KEYCHAIN_SERVICE,
|
|
409
|
+
credentialsFile,
|
|
410
|
+
liveFile,
|
|
411
|
+
attemptFile,
|
|
412
|
+
userAgent,
|
|
413
|
+
readToken,
|
|
414
|
+
fetchUsage,
|
|
415
|
+
nextDelayMs,
|
|
416
|
+
describe,
|
|
417
|
+
readLive,
|
|
418
|
+
writeLive,
|
|
419
|
+
fetchDisabled,
|
|
420
|
+
refresh,
|
|
421
|
+
refreshIfStale,
|
|
422
|
+
};
|