agents-deck 1.20.0 → 1.20.2
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/dist/web/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>agents-deck</title>
|
|
7
7
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='84' font-size='84'%3E%E2%97%89%3C/text%3E%3C/svg%3E" />
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-Cb1bIpZv.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-_qWQOt8_.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
package/src/server/quota.mjs
CHANGED
|
@@ -1,20 +1,145 @@
|
|
|
1
|
-
// Fetches Claude rate-limit quota
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Fetches Claude rate-limit quota.
|
|
2
|
+
//
|
|
3
|
+
// Primary source: Anthropic's OAuth usage API
|
|
4
|
+
// GET https://api.anthropic.com/api/oauth/usage
|
|
5
|
+
// Auth: Bearer token from ~/.claude/.credentials.json (claudeAiOauth.accessToken)
|
|
6
|
+
// This is instant and exact — same data the `/usage` command shows, but with no
|
|
7
|
+
// cold-start gap (the CLI omits the quota lines on its first invocation after
|
|
8
|
+
// idle). Mechanism reverse-engineered from steipete/CodexBar.
|
|
9
|
+
//
|
|
10
|
+
// Fallback: parse `claude --print /usage` CLI output (used only if the API call
|
|
11
|
+
// fails — no token, expired token, network error). On Windows the binary is a
|
|
12
|
+
// .cmd wrapper, so we use exec() (shell-based) for correct quoting + stdin.
|
|
13
|
+
//
|
|
14
|
+
// Result is cached for 60s.
|
|
5
15
|
import { exec } from "node:child_process";
|
|
6
16
|
import { promisify } from "node:util";
|
|
7
17
|
import { existsSync } from "node:fs";
|
|
18
|
+
import { readFile } from "node:fs/promises";
|
|
8
19
|
import { join } from "node:path";
|
|
9
20
|
import { homedir, platform } from "node:os";
|
|
10
21
|
|
|
11
22
|
const execAsync = promisify(exec);
|
|
12
23
|
const IS_WIN = platform() === "win32";
|
|
13
24
|
|
|
14
|
-
|
|
15
|
-
|
|
25
|
+
const CREDS_PATH = join(homedir(), ".claude", ".credentials.json");
|
|
26
|
+
const USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
27
|
+
const BETA_HEADER = "oauth-2025-04-20";
|
|
28
|
+
const WIN_5H_SEC = 18000;
|
|
29
|
+
const WIN_7D_SEC = 604800;
|
|
30
|
+
|
|
31
|
+
// 429 cooldown gate — after a rate-limit, skip the API until this passes.
|
|
32
|
+
let _rateLimitedUntil = 0;
|
|
33
|
+
|
|
34
|
+
async function readOAuthToken() {
|
|
35
|
+
try {
|
|
36
|
+
const raw = await readFile(CREDS_PATH, "utf8");
|
|
37
|
+
const auth = JSON.parse(raw)?.claudeAiOauth;
|
|
38
|
+
if (!auth?.accessToken) return null;
|
|
39
|
+
// expiresAt is epoch milliseconds. If expired, the CLI fallback handles it.
|
|
40
|
+
if (auth.expiresAt && Date.now() >= auth.expiresAt) return null;
|
|
41
|
+
return auth.accessToken;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ISO-8601 → "Jun 19, 1:19pm" (local time, matching the CLI display format).
|
|
48
|
+
function fmtResetIso(iso) {
|
|
49
|
+
if (!iso) return null;
|
|
50
|
+
const d = new Date(iso);
|
|
51
|
+
if (isNaN(d.getTime())) return null;
|
|
52
|
+
// "Jun 19, 1:19 PM" → "Jun 19, 1:19pm" (matches the CLI display format)
|
|
53
|
+
return d.toLocaleString("en-US", {
|
|
54
|
+
month: "short", day: "numeric", hour: "numeric", minute: "2-digit", hour12: true,
|
|
55
|
+
}).replace(/\s+(AM|PM)/, (_, p) => p.toLowerCase());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isoToSec(iso) {
|
|
59
|
+
if (!iso) return null;
|
|
60
|
+
const t = new Date(iso).getTime();
|
|
61
|
+
return isNaN(t) ? null : Math.floor(t / 1000);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Map the OAuth usage JSON to our quota result shape.
|
|
65
|
+
// utilization is already a 0–100 percentage. 5h falls back to 7d if absent.
|
|
66
|
+
function mapOAuthUsage(data) {
|
|
67
|
+
const fh = data?.five_hour;
|
|
68
|
+
const sd = data?.seven_day;
|
|
69
|
+
const son = data?.seven_day_sonnet;
|
|
70
|
+
const opus = data?.seven_day_opus;
|
|
71
|
+
|
|
72
|
+
const primary = (fh?.utilization != null) ? fh : sd;
|
|
73
|
+
if (!primary || primary.utilization == null) return null;
|
|
74
|
+
|
|
75
|
+
const round = (v) => Math.min(100, Math.max(0, Math.round(v)));
|
|
76
|
+
const result = {
|
|
77
|
+
session5hPct: round(primary.utilization),
|
|
78
|
+
session5hWindowSec: WIN_5H_SEC,
|
|
79
|
+
session5hReset: fmtResetIso(primary.resets_at),
|
|
80
|
+
session5hResetAt: isoToSec(primary.resets_at),
|
|
81
|
+
week7dWindowSec: WIN_7D_SEC,
|
|
82
|
+
};
|
|
83
|
+
if (sd?.utilization != null) {
|
|
84
|
+
result.week7dPct = round(sd.utilization);
|
|
85
|
+
result.week7dReset = fmtResetIso(sd.resets_at);
|
|
86
|
+
result.week7dResetAt = isoToSec(sd.resets_at);
|
|
87
|
+
} else {
|
|
88
|
+
result.week7dPct = 0;
|
|
89
|
+
}
|
|
90
|
+
if (son?.utilization != null) result.weekSonnetPct = round(son.utilization);
|
|
91
|
+
if (opus?.utilization != null) result.weekOpusPct = round(opus.utilization);
|
|
92
|
+
|
|
93
|
+
// extra usage credits (pay-as-you-go top-up), if enabled
|
|
94
|
+
const extra = data?.extra_usage;
|
|
95
|
+
if (extra?.is_enabled) {
|
|
96
|
+
result.extraEnabled = true;
|
|
97
|
+
if (extra.used_credits != null) result.extraUsedCredits = extra.used_credits;
|
|
98
|
+
if (extra.monthly_limit != null) result.extraMonthlyLimit = extra.monthly_limit;
|
|
99
|
+
if (extra.currency) result.extraCurrency = extra.currency;
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function fetchOAuthUsage() {
|
|
105
|
+
if (Date.now() < _rateLimitedUntil) return null;
|
|
106
|
+
const token = await readOAuthToken();
|
|
107
|
+
if (!token) return null;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const res = await fetch(USAGE_URL, {
|
|
111
|
+
headers: {
|
|
112
|
+
"Authorization": `Bearer ${token}`,
|
|
113
|
+
"anthropic-beta": BETA_HEADER,
|
|
114
|
+
"Accept": "application/json",
|
|
115
|
+
"Content-Type": "application/json",
|
|
116
|
+
"User-Agent": "claude-code/2.1.0",
|
|
117
|
+
},
|
|
118
|
+
signal: AbortSignal.timeout(15_000),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (res.status === 429) {
|
|
122
|
+
const retryAfter = parseInt(res.headers.get("retry-after") ?? "", 10);
|
|
123
|
+
const cooldownMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 5 * 60_000;
|
|
124
|
+
_rateLimitedUntil = Date.now() + cooldownMs;
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
if (!res.ok) return null;
|
|
128
|
+
|
|
129
|
+
return mapOAuthUsage(await res.json());
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let _cache = null;
|
|
136
|
+
let _cacheAt = 0;
|
|
137
|
+
let _inflight = null; // deduplicates concurrent exec() calls
|
|
138
|
+
let _lastGood = null; // last result that had real quota percentages
|
|
16
139
|
const CACHE_MS = 60_000;
|
|
17
140
|
|
|
141
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
142
|
+
|
|
18
143
|
function stripAnsi(s) {
|
|
19
144
|
return s
|
|
20
145
|
.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "")
|
|
@@ -122,10 +247,20 @@ export async function fetchClaudeQuota({ force = false } = {}) {
|
|
|
122
247
|
const now = Date.now();
|
|
123
248
|
if (!force && _cache && now - _cacheAt < CACHE_MS) return _cache;
|
|
124
249
|
|
|
125
|
-
|
|
126
|
-
|
|
250
|
+
// If another exec() is already in flight, wait for it instead of spawning a
|
|
251
|
+
// second concurrent process (which can return empty output and overwrite the
|
|
252
|
+
// good result with 0%).
|
|
253
|
+
if (_inflight) return _inflight;
|
|
127
254
|
|
|
128
|
-
|
|
255
|
+
_inflight = _doFetch(now).finally(() => { _inflight = null; });
|
|
256
|
+
return _inflight;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Run `claude --print /usage` once. Returns { cliOk, parsed }.
|
|
260
|
+
// cliOk — the CLI ran and we recognized its output (preamble present)
|
|
261
|
+
// parsed — quota percentages object, or null if the "Current session/week"
|
|
262
|
+
// lines were absent (CLI cold-start, or genuinely <1% usage)
|
|
263
|
+
async function _execOnce(shellCmd) {
|
|
129
264
|
try {
|
|
130
265
|
const { stdout, stderr } = await execAsync(shellCmd, {
|
|
131
266
|
timeout: 15_000,
|
|
@@ -133,34 +268,71 @@ export async function fetchClaudeQuota({ force = false } = {}) {
|
|
|
133
268
|
maxBuffer: 1024 * 1024,
|
|
134
269
|
});
|
|
135
270
|
const combined = stdout + "\n" + stderr;
|
|
136
|
-
|
|
137
|
-
cliOk
|
|
138
|
-
parsed = parseUsageText(combined);
|
|
271
|
+
const cliOk = /subscription/i.test(combined) || /claude code usage/i.test(combined);
|
|
272
|
+
return { cliOk, parsed: parseUsageText(combined) };
|
|
139
273
|
} catch (err) {
|
|
140
274
|
const msg = err?.stderr ? stripAnsi(err.stderr).trim() : (err?.message ?? String(err));
|
|
141
275
|
console.error("agents-deck quota: claude CLI failed:", msg);
|
|
142
276
|
if (err?.stdout || err?.stderr) {
|
|
143
277
|
const combined = (err.stdout ?? "") + "\n" + (err.stderr ?? "");
|
|
144
|
-
cliOk
|
|
145
|
-
parsed = parseUsageText(combined);
|
|
278
|
+
return { cliOk: /subscription/i.test(combined), parsed: parseUsageText(combined) };
|
|
146
279
|
}
|
|
280
|
+
return { cliOk: false, parsed: null };
|
|
147
281
|
}
|
|
282
|
+
}
|
|
148
283
|
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
284
|
+
async function _doFetch(now) {
|
|
285
|
+
// Primary: OAuth usage API — instant, exact, no cold-start gap.
|
|
286
|
+
const api = await fetchOAuthUsage();
|
|
287
|
+
if (api) {
|
|
288
|
+
const result = { ok: true, ...api, source: "api", fetchedAt: now };
|
|
289
|
+
_cache = result; _cacheAt = now;
|
|
290
|
+
_lastGood = result;
|
|
291
|
+
return result;
|
|
156
292
|
}
|
|
157
293
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
294
|
+
// Fallback: parse `claude --print /usage` CLI output.
|
|
295
|
+
const shellCmd = buildQuotaShellCmd();
|
|
296
|
+
|
|
297
|
+
// The CLI sometimes omits the "Current session/week" quota lines on a cold
|
|
298
|
+
// invocation (right after the server starts, or after the page is hard-
|
|
299
|
+
// refreshed). The real lines appear on a subsequent call. Retry a couple
|
|
300
|
+
// times before giving up so the first paint already shows real values.
|
|
301
|
+
let cliOk = false;
|
|
302
|
+
let parsed = null;
|
|
303
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
304
|
+
if (attempt > 0) await sleep(1200);
|
|
305
|
+
const r = await _execOnce(shellCmd);
|
|
306
|
+
cliOk = r.cliOk || cliOk;
|
|
307
|
+
if (r.parsed) { parsed = r.parsed; break; }
|
|
308
|
+
}
|
|
161
309
|
|
|
310
|
+
// Got real quota lines — cache normally and remember as last-known-good.
|
|
311
|
+
if (parsed) {
|
|
312
|
+
const result = { ok: true, ...parsed, fetchedAt: now };
|
|
313
|
+
_cache = result; _cacheAt = now;
|
|
314
|
+
_lastGood = result;
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// No quota lines after retries. If we've ever seen real values, keep showing
|
|
319
|
+
// them rather than regressing to 0% on a transient empty read. Short-cache so
|
|
320
|
+
// we retry the CLI again soon.
|
|
321
|
+
if (_lastGood) {
|
|
322
|
+
const result = { ..._lastGood, fetchedAt: now, stale: true };
|
|
323
|
+
_cache = result;
|
|
324
|
+
_cacheAt = now - (CACHE_MS - 5_000);
|
|
325
|
+
return result;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Never had good data. CLI ran but lines absent → treat as genuine <1%.
|
|
329
|
+
// CLI failed entirely → ok:false. Either way short-cache for a quick retry.
|
|
330
|
+
const result = cliOk
|
|
331
|
+
? { ok: true, session5hPct: 0, session5hWindowSec: 18000,
|
|
332
|
+
week7dPct: 0, week7dWindowSec: 604800, fetchedAt: now }
|
|
333
|
+
: { ok: false, fetchedAt: now };
|
|
162
334
|
_cache = result;
|
|
163
|
-
_cacheAt = now;
|
|
335
|
+
_cacheAt = now - (CACHE_MS - 5_000);
|
|
164
336
|
return result;
|
|
165
337
|
}
|
|
166
338
|
|