agents-deck 1.20.1 → 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,21 +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
|
|
|
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
|
+
|
|
14
135
|
let _cache = null;
|
|
15
136
|
let _cacheAt = 0;
|
|
16
137
|
let _inflight = null; // deduplicates concurrent exec() calls
|
|
138
|
+
let _lastGood = null; // last result that had real quota percentages
|
|
17
139
|
const CACHE_MS = 60_000;
|
|
18
140
|
|
|
141
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
142
|
+
|
|
19
143
|
function stripAnsi(s) {
|
|
20
144
|
return s
|
|
21
145
|
.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "")
|
|
@@ -132,11 +256,11 @@ export async function fetchClaudeQuota({ force = false } = {}) {
|
|
|
132
256
|
return _inflight;
|
|
133
257
|
}
|
|
134
258
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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) {
|
|
140
264
|
try {
|
|
141
265
|
const { stdout, stderr } = await execAsync(shellCmd, {
|
|
142
266
|
timeout: 15_000,
|
|
@@ -144,38 +268,71 @@ async function _doFetch(now) {
|
|
|
144
268
|
maxBuffer: 1024 * 1024,
|
|
145
269
|
});
|
|
146
270
|
const combined = stdout + "\n" + stderr;
|
|
147
|
-
cliOk = /subscription/i.test(combined) || /claude code usage/i.test(combined);
|
|
148
|
-
parsed
|
|
271
|
+
const cliOk = /subscription/i.test(combined) || /claude code usage/i.test(combined);
|
|
272
|
+
return { cliOk, parsed: parseUsageText(combined) };
|
|
149
273
|
} catch (err) {
|
|
150
274
|
const msg = err?.stderr ? stripAnsi(err.stderr).trim() : (err?.message ?? String(err));
|
|
151
275
|
console.error("agents-deck quota: claude CLI failed:", msg);
|
|
152
276
|
if (err?.stdout || err?.stderr) {
|
|
153
277
|
const combined = (err.stdout ?? "") + "\n" + (err.stderr ?? "");
|
|
154
|
-
cliOk
|
|
155
|
-
parsed = parseUsageText(combined);
|
|
278
|
+
return { cliOk: /subscription/i.test(combined), parsed: parseUsageText(combined) };
|
|
156
279
|
}
|
|
280
|
+
return { cliOk: false, parsed: null };
|
|
157
281
|
}
|
|
282
|
+
}
|
|
158
283
|
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
session5hPct: 0, session5hWindowSec: 18000,
|
|
168
|
-
week7dPct: 0, week7dWindowSec: 604800,
|
|
169
|
-
};
|
|
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;
|
|
170
292
|
}
|
|
171
293
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
+
}
|
|
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
|
+
}
|
|
175
317
|
|
|
176
|
-
|
|
177
|
-
//
|
|
178
|
-
|
|
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 };
|
|
334
|
+
_cache = result;
|
|
335
|
+
_cacheAt = now - (CACHE_MS - 5_000);
|
|
179
336
|
return result;
|
|
180
337
|
}
|
|
181
338
|
|