agents-deck 1.19.0 → 1.20.1
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/assets/index-Bf_B--gp.js +62 -0
- package/dist/web/assets/{index-CVsr6RfQ.css → index-_qWQOt8_.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/server/codex-quota.mjs +114 -0
- package/src/server/codex-usage.mjs +162 -0
- package/src/server/index.mjs +27 -3
- package/src/server/quota.mjs +94 -42
- package/dist/web/assets/index-Bs89BdrR.js +0 -62
package/src/server/quota.mjs
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
// Fetches Claude rate-limit quota by running `claude --print /usage`.
|
|
2
|
-
// On Windows the binary is a .cmd wrapper — we
|
|
2
|
+
// On Windows the binary is a .cmd wrapper — we use exec() (shell-based)
|
|
3
|
+
// so that cmd.exe handles quoting and stdin redirect correctly.
|
|
3
4
|
// Caches the result for 2 minutes.
|
|
4
|
-
import {
|
|
5
|
+
import { exec } from "node:child_process";
|
|
5
6
|
import { promisify } from "node:util";
|
|
6
7
|
import { existsSync } from "node:fs";
|
|
7
8
|
import { join } from "node:path";
|
|
8
9
|
import { homedir, platform } from "node:os";
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
+
const execAsync = promisify(exec);
|
|
11
12
|
const IS_WIN = platform() === "win32";
|
|
12
13
|
|
|
13
|
-
let _cache
|
|
14
|
-
let _cacheAt
|
|
15
|
-
|
|
14
|
+
let _cache = null;
|
|
15
|
+
let _cacheAt = 0;
|
|
16
|
+
let _inflight = null; // deduplicates concurrent exec() calls
|
|
17
|
+
const CACHE_MS = 60_000;
|
|
16
18
|
|
|
17
19
|
function stripAnsi(s) {
|
|
18
20
|
return s
|
|
@@ -30,6 +32,21 @@ function stripAnsi(s) {
|
|
|
30
32
|
* "Current week (Sonnet only): 48% used · resets Jun 21, 9am (Europe/Chisinau)"
|
|
31
33
|
* "Current week (Opus only): ..." (if present)
|
|
32
34
|
*/
|
|
35
|
+
// Parse "Jun 18, 4:09pm" (local time, no tz) into unix seconds.
|
|
36
|
+
// Claude shows times in the user's local timezone, so parsing as local is correct.
|
|
37
|
+
function parseResetToSec(resetStr) {
|
|
38
|
+
if (!resetStr) return null;
|
|
39
|
+
try {
|
|
40
|
+
const year = new Date().getFullYear();
|
|
41
|
+
// "4:09pm" → "4:09 PM" so Date.parse handles it
|
|
42
|
+
const norm = resetStr
|
|
43
|
+
.replace(/(\d{1,2}:\d{2})(am|pm)/i, "$1 $2")
|
|
44
|
+
.trim();
|
|
45
|
+
const d = new Date(`${norm} ${year}`);
|
|
46
|
+
return isNaN(d.getTime()) ? null : Math.floor(d.getTime() / 1000);
|
|
47
|
+
} catch { return null; }
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
function parseUsageText(raw) {
|
|
34
51
|
const text = stripAnsi(raw);
|
|
35
52
|
const result = {};
|
|
@@ -40,27 +57,30 @@ function parseUsageText(raw) {
|
|
|
40
57
|
if (!line) return null;
|
|
41
58
|
const pctM = line.match(/(\d{1,3})\s*%/);
|
|
42
59
|
const resetM = line.match(/resets\s+(.+)/i);
|
|
60
|
+
const resetFull = resetM
|
|
61
|
+
? resetM[1].replace(/\(.*?\)/g, "").replace(/·/g, "").trim()
|
|
62
|
+
: null;
|
|
43
63
|
return {
|
|
44
|
-
pct:
|
|
45
|
-
reset:
|
|
46
|
-
|
|
47
|
-
.replace(/\(.*?\)/g, "") // strip timezone in parens
|
|
48
|
-
.replace(/·/g, "")
|
|
49
|
-
.trim()
|
|
50
|
-
: null,
|
|
64
|
+
pct: pctM ? Math.min(100, parseInt(pctM[1], 10)) : null,
|
|
65
|
+
reset: resetFull,
|
|
66
|
+
resetAt: parseResetToSec(resetFull),
|
|
51
67
|
};
|
|
52
68
|
};
|
|
53
69
|
|
|
54
70
|
const session = extract(/current session/i);
|
|
55
71
|
if (session?.pct != null) {
|
|
56
|
-
result.session5hPct
|
|
57
|
-
|
|
72
|
+
result.session5hPct = session.pct;
|
|
73
|
+
result.session5hWindowSec = 18000;
|
|
74
|
+
if (session.reset) result.session5hReset = session.reset;
|
|
75
|
+
if (session.resetAt) result.session5hResetAt = session.resetAt;
|
|
58
76
|
}
|
|
59
77
|
|
|
60
78
|
const weekAll = extract(/current week\s*\(all models\)/i) || extract(/current week\s*[:·]/i);
|
|
61
79
|
if (weekAll?.pct != null) {
|
|
62
|
-
result.week7dPct
|
|
63
|
-
|
|
80
|
+
result.week7dPct = weekAll.pct;
|
|
81
|
+
result.week7dWindowSec = 604800;
|
|
82
|
+
if (weekAll.reset) result.week7dReset = weekAll.reset;
|
|
83
|
+
if (weekAll.resetAt) result.week7dResetAt = weekAll.resetAt;
|
|
64
84
|
}
|
|
65
85
|
|
|
66
86
|
const weekSon = extract(/current week\s*\(sonnet/i);
|
|
@@ -72,17 +92,21 @@ function parseUsageText(raw) {
|
|
|
72
92
|
return Object.keys(result).length > 0 ? result : null;
|
|
73
93
|
}
|
|
74
94
|
|
|
75
|
-
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
|
|
95
|
+
/** Build the shell command string for `claude --print /usage`.
|
|
96
|
+
*
|
|
97
|
+
* We use exec() (shell-based) so cmd.exe / sh processes redirects.
|
|
98
|
+
* On Windows: `< nul` closes stdin immediately, preventing the 3-second
|
|
99
|
+
* "no stdin data" wait the claude CLI does when it detects a pipe.
|
|
100
|
+
* On Unix: `< /dev/null` has the same effect.
|
|
101
|
+
*/
|
|
102
|
+
function buildQuotaShellCmd() {
|
|
79
103
|
if (IS_WIN) {
|
|
80
104
|
const npmBin = join(homedir(), "AppData", "Roaming", "npm", "claude.cmd");
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
|
|
105
|
+
const bin = existsSync(npmBin) ? npmBin : "claude.cmd";
|
|
106
|
+
// exec() on Windows uses cmd /c, so < nul redirect works fine.
|
|
107
|
+
// Wrap path in quotes in case of spaces in username.
|
|
108
|
+
return `"${bin}" --print /usage < nul`;
|
|
84
109
|
}
|
|
85
|
-
// Unix: check PATH, then known install locations.
|
|
86
110
|
const candidates = [
|
|
87
111
|
"claude",
|
|
88
112
|
join(homedir(), ".local", "bin", "claude"),
|
|
@@ -90,40 +114,68 @@ function findClaudeBin() {
|
|
|
90
114
|
"/opt/homebrew/bin/claude",
|
|
91
115
|
];
|
|
92
116
|
for (const c of candidates) {
|
|
93
|
-
if (!c.includes("/")) return {
|
|
94
|
-
if (existsSync(c)) return { cmd: c, args: [] };
|
|
117
|
+
if (!c.includes("/") || existsSync(c)) return `${c} --print /usage < /dev/null`;
|
|
95
118
|
}
|
|
96
|
-
return
|
|
119
|
+
return "claude --print /usage < /dev/null";
|
|
97
120
|
}
|
|
98
121
|
|
|
99
122
|
export async function fetchClaudeQuota({ force = false } = {}) {
|
|
100
123
|
const now = Date.now();
|
|
101
124
|
if (!force && _cache && now - _cacheAt < CACHE_MS) return _cache;
|
|
102
125
|
|
|
103
|
-
|
|
126
|
+
// If another exec() is already in flight, wait for it instead of spawning a
|
|
127
|
+
// second concurrent process (which can return empty output and overwrite the
|
|
128
|
+
// good result with 0%).
|
|
129
|
+
if (_inflight) return _inflight;
|
|
130
|
+
|
|
131
|
+
_inflight = _doFetch(now).finally(() => { _inflight = null; });
|
|
132
|
+
return _inflight;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function _doFetch(now) {
|
|
136
|
+
const shellCmd = buildQuotaShellCmd();
|
|
104
137
|
let parsed = null;
|
|
138
|
+
let cliOk = false;
|
|
105
139
|
|
|
106
140
|
try {
|
|
107
|
-
const { stdout, stderr } = await
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
);
|
|
115
|
-
parsed = parseUsageText(stdout + "\n" + stderr);
|
|
141
|
+
const { stdout, stderr } = await execAsync(shellCmd, {
|
|
142
|
+
timeout: 15_000,
|
|
143
|
+
env: { ...process.env, NO_COLOR: "1", TERM: "dumb" },
|
|
144
|
+
maxBuffer: 1024 * 1024,
|
|
145
|
+
});
|
|
146
|
+
const combined = stdout + "\n" + stderr;
|
|
147
|
+
cliOk = /subscription/i.test(combined) || /claude code usage/i.test(combined);
|
|
148
|
+
parsed = parseUsageText(combined);
|
|
116
149
|
} catch (err) {
|
|
117
|
-
|
|
118
|
-
console.error("agents-deck quota: claude CLI failed:",
|
|
150
|
+
const msg = err?.stderr ? stripAnsi(err.stderr).trim() : (err?.message ?? String(err));
|
|
151
|
+
console.error("agents-deck quota: claude CLI failed:", msg);
|
|
152
|
+
if (err?.stdout || err?.stderr) {
|
|
153
|
+
const combined = (err.stdout ?? "") + "\n" + (err.stderr ?? "");
|
|
154
|
+
cliOk = /subscription/i.test(combined);
|
|
155
|
+
parsed = parseUsageText(combined);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// CLI ran OK but quota lines absent — this happens when:
|
|
160
|
+
// (a) near-zero usage in the rolling window (Claude omits bars below ~1%), or
|
|
161
|
+
// (b) CLI cold-start: the server-side rolling window hasn't been computed yet.
|
|
162
|
+
// In case (b) the real value appears within ~60s. Use a 5s short-cache so
|
|
163
|
+
// the UI polls again quickly and shows real values as soon as they're ready.
|
|
164
|
+
const shortCache = cliOk && !parsed;
|
|
165
|
+
if (shortCache) {
|
|
166
|
+
parsed = {
|
|
167
|
+
session5hPct: 0, session5hWindowSec: 18000,
|
|
168
|
+
week7dPct: 0, week7dWindowSec: 604800,
|
|
169
|
+
};
|
|
119
170
|
}
|
|
120
171
|
|
|
121
172
|
const result = parsed
|
|
122
173
|
? { ok: true, ...parsed, fetchedAt: now }
|
|
123
174
|
: { ok: false, fetchedAt: now };
|
|
124
175
|
|
|
125
|
-
_cache
|
|
126
|
-
|
|
176
|
+
_cache = result;
|
|
177
|
+
// Short-cache the 0% fallback so the UI retries in ~5s rather than 60s.
|
|
178
|
+
_cacheAt = shortCache ? now - (CACHE_MS - 5_000) : now;
|
|
127
179
|
return result;
|
|
128
180
|
}
|
|
129
181
|
|