agent-dag 1.30.2 → 1.30.4
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-BLOOP_Fy.css → index-D3L9GICQ.css} +1 -1
- package/dist/web/assets/index-U1M_-2Ot.js +66 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/server/claude-accounts.mjs +58 -0
- package/src/server/cswap-auto.mjs +3 -18
- package/src/server/index.mjs +0 -3
- package/src/server/quota.mjs +162 -16
- package/dist/web/assets/index-DtE3W-dV.js +0 -66
package/dist/web/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
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-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-U1M_-2Ot.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-D3L9GICQ.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-dag",
|
|
3
|
-
"version": "1.30.
|
|
3
|
+
"version": "1.30.4",
|
|
4
4
|
"description": "Live deck of Claude Code and Codex agents — watch parallel subagents fork, call tools, and return on one calm canvas. Also available as npx ccdeck and npx agent-dag.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -219,6 +219,64 @@ export async function fetchClaudeAccounts({ force = false } = {}) {
|
|
|
219
219
|
return finish({ ok: true, accounts, activeNum: seq.activeAccountNumber ?? null, fetchedAt: now });
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
/**
|
|
223
|
+
* claude-swap's last good usage for whichever account is active right now.
|
|
224
|
+
*
|
|
225
|
+
* The Usage panel wants the same numbers for one account that this panel shows
|
|
226
|
+
* for all of them, and claude-swap has already paid for them. Reading its row
|
|
227
|
+
* instead of asking Anthropic again is the difference between one collector on
|
|
228
|
+
* this machine and two competing for the same per-token budget — the second
|
|
229
|
+
* one is what was 429ing the first.
|
|
230
|
+
*
|
|
231
|
+
* Returns null rather than a partial when anything about the row is unsure:
|
|
232
|
+
* the caller's fallback is to fetch for itself, and a wrong number is worse
|
|
233
|
+
* than a slow one.
|
|
234
|
+
*/
|
|
235
|
+
export async function activeAccountUsage() {
|
|
236
|
+
const root = backupRoot();
|
|
237
|
+
const seq = await readJson(join(root, "sequence.json"));
|
|
238
|
+
const num = seq?.activeAccountNumber != null ? String(seq.activeAccountNumber) : null;
|
|
239
|
+
const acct = num ? seq?.accounts?.[num] : null;
|
|
240
|
+
if (!acct) return null;
|
|
241
|
+
|
|
242
|
+
const usage = await readJson(join(root, "cache", "usage.json"));
|
|
243
|
+
if (usage?.schemaVersion !== 2) return null;
|
|
244
|
+
|
|
245
|
+
const row = usage.accounts?.[num];
|
|
246
|
+
// Same identity guard the panel uses: rows are keyed by slot, and slots are
|
|
247
|
+
// reused, so a row can outlive the account it was written for.
|
|
248
|
+
if (!row?.lastGood
|
|
249
|
+
|| row.email !== acct.email
|
|
250
|
+
|| (row.organizationUuid ?? "") !== (acct.organizationUuid ?? "")
|
|
251
|
+
|| typeof row.fetchedAt !== "number") return null;
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
num: Number(num),
|
|
255
|
+
email: acct.email ?? null,
|
|
256
|
+
lastGood: row.lastGood,
|
|
257
|
+
fetchedAt: Math.round(row.fetchedAt * 1000),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Ask claude-swap to collect now, if its own schedule agrees.
|
|
263
|
+
*
|
|
264
|
+
* Exported for the Usage panel's refresh button, which has no other way to ask
|
|
265
|
+
* for fresher numbers once it stopped fetching them itself. Goes through the
|
|
266
|
+
* same throttle and the same `cswap list` as the accounts panel, so pressing
|
|
267
|
+
* refresh cannot outrun the request budget either.
|
|
268
|
+
*/
|
|
269
|
+
export async function requestCollection() {
|
|
270
|
+
const root = backupRoot();
|
|
271
|
+
const seq = await readJson(join(root, "sequence.json"));
|
|
272
|
+
if (!seq?.accounts) return false;
|
|
273
|
+
const usage = await readJson(join(root, "cache", "usage.json"));
|
|
274
|
+
const rows = usage?.schemaVersion === 2 ? (usage.accounts ?? {}) : {};
|
|
275
|
+
const before = _lastNudge;
|
|
276
|
+
nudgeCollector(rows, Object.keys(seq.accounts), Date.now());
|
|
277
|
+
return _lastNudge !== before;
|
|
278
|
+
}
|
|
279
|
+
|
|
222
280
|
export function invalidateClaudeAccountsCache() {
|
|
223
281
|
_cache = null;
|
|
224
282
|
_cacheAt = 0;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Auto-switch controls: read and write claude-swap's autoswitch settings
|
|
2
|
-
// a tick on a schedule
|
|
1
|
+
// Auto-switch controls: read and write claude-swap's autoswitch settings and
|
|
2
|
+
// run a tick on a schedule.
|
|
3
3
|
//
|
|
4
4
|
// The engine is claude-swap's own — `cswap auto --once` evaluates one tick and
|
|
5
5
|
// exits, honouring the cooldown, quarantine and poll-budget state it keeps in
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
// that owns them: nothing here decides when to switch, only when to ask.
|
|
9
9
|
//
|
|
10
10
|
// A tick can move the user's live Claude account, so it is off unless turned
|
|
11
|
-
// on
|
|
12
|
-
// WOULD do (--dry-run) before committing to letting it happen.
|
|
11
|
+
// on and the setting survives restarts.
|
|
13
12
|
import { run } from "./exec.mjs";
|
|
14
13
|
import { cswapBin } from "./cswap-install.mjs";
|
|
15
14
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
@@ -30,7 +29,6 @@ const SETTINGS = {
|
|
|
30
29
|
"autoswitch.intervalSeconds": { type: "number", min: 15, max: 3600 },
|
|
31
30
|
"autoswitch.cooldownSeconds": { type: "number", min: 0, max: 86400 },
|
|
32
31
|
"autoswitch.hysteresisPct": { type: "number", min: 0, max: 50 },
|
|
33
|
-
"autoswitch.strategy": { type: "enum", values: ["best", "consume-first"] },
|
|
34
32
|
"autoswitch.model": { type: "model" },
|
|
35
33
|
};
|
|
36
34
|
|
|
@@ -100,19 +98,6 @@ function summarise(stdout) {
|
|
|
100
98
|
};
|
|
101
99
|
}
|
|
102
100
|
|
|
103
|
-
/**
|
|
104
|
-
* Evaluate a tick without acting. Safe to call from a UI button: --dry-run
|
|
105
|
-
* never switches and never writes claude-swap's state.
|
|
106
|
-
*/
|
|
107
|
-
export async function previewAutoSwitch() {
|
|
108
|
-
const r = await run(await cswapBin(), ["auto", "--once", "--dry-run", "--json"], { timeout: TICK_TIMEOUT_MS });
|
|
109
|
-
if (!r.ok && !r.stdout) {
|
|
110
|
-
return { ok: false, reason: r.code === "ENOENT" ? "no_cswap" : "tick_failed",
|
|
111
|
-
detail: (r.stderr || "").trim().slice(0, 300) };
|
|
112
|
-
}
|
|
113
|
-
return { ok: true, dryRun: true, ...summarise(r.stdout) };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
101
|
/** Evaluate a tick for real. May switch the active account. */
|
|
117
102
|
async function runAutoTick() {
|
|
118
103
|
const r = await run(await cswapBin(), ["auto", "--once", "--json"], { timeout: TICK_TIMEOUT_MS });
|
package/src/server/index.mjs
CHANGED
|
@@ -1100,9 +1100,6 @@ async function handleCswapAutoAction(req, res) {
|
|
|
1100
1100
|
case "enable":
|
|
1101
1101
|
result = await mod.setAutoEnabled(parsed.enabled === true);
|
|
1102
1102
|
break;
|
|
1103
|
-
case "preview":
|
|
1104
|
-
result = await mod.previewAutoSwitch();
|
|
1105
|
-
break;
|
|
1106
1103
|
case "setting":
|
|
1107
1104
|
result = await mod.setCswapConfig(String(parsed.key ?? ""), parsed.value);
|
|
1108
1105
|
break;
|
package/src/server/quota.mjs
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Claude rate-limit quota, from whichever source costs least.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// All three sources below end at the same place: GET /api/oauth/usage, which
|
|
4
|
+
// Anthropic budgets at roughly 28-30 calls per rolling hour PER TOKEN, shared
|
|
5
|
+
// by every tool on the machine. That budget is the constraint this module is
|
|
6
|
+
// built around, because it was being blown by this module: a 60s poll is 60
|
|
7
|
+
// calls an hour on its own, and the account the deck was polling started
|
|
8
|
+
// answering http-429 to claude-swap, whose collections the accounts panel is
|
|
9
|
+
// entirely made of. One panel went stale so the other could be a minute
|
|
10
|
+
// fresher.
|
|
9
11
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
12
|
+
// 1. claude-swap's store — free. It polls the active account on its own
|
|
13
|
+
// schedule and writes what it got; reading that file costs nothing and
|
|
14
|
+
// spends none of the budget. Used whenever it holds a recent enough row.
|
|
15
|
+
// 2. The OAuth usage API directly, with the token from
|
|
16
|
+
// ~/.claude/.credentials.json. Exact and instant. Mechanism
|
|
17
|
+
// reverse-engineered from steipete/CodexBar.
|
|
18
|
+
// 3. `claude --print /usage`, parsed. Used when there is no readable token —
|
|
19
|
+
// notably on macOS, where Claude Code keeps credentials in the Keychain
|
|
20
|
+
// and that file does not exist, so this is the ONLY self-service path
|
|
21
|
+
// there. It is also the most expensive: a whole Claude Code process per
|
|
22
|
+
// poll. On Windows the binary is a .cmd wrapper, so exec() (shell-based)
|
|
23
|
+
// is used for correct quoting + stdin.
|
|
13
24
|
//
|
|
14
|
-
//
|
|
25
|
+
// 2 and 3 are rate-floored (SELF_POLL_MS) and gated behind the same 429
|
|
26
|
+
// cooldown; 1 is not, because it is a local file read.
|
|
27
|
+
import { activeAccountUsage, requestCollection } from "./claude-accounts.mjs";
|
|
15
28
|
import { exec } from "node:child_process";
|
|
16
29
|
import { promisify } from "node:util";
|
|
17
30
|
import { existsSync } from "node:fs";
|
|
@@ -136,8 +149,75 @@ let _cache = null;
|
|
|
136
149
|
let _cacheAt = 0;
|
|
137
150
|
let _inflight = null; // deduplicates concurrent exec() calls
|
|
138
151
|
let _lastGood = null; // last result that had real quota percentages
|
|
152
|
+
let _lastSelfPollAt = 0;
|
|
153
|
+
|
|
139
154
|
const CACHE_MS = 60_000;
|
|
140
155
|
|
|
156
|
+
// Floor between two polls WE pay for. Twelve an hour against a budget of
|
|
157
|
+
// ~28-30 leaves claude-swap room to collect for every account, which is what
|
|
158
|
+
// the accounts panel is made of. Only reached when the store cannot answer.
|
|
159
|
+
const SELF_POLL_MS = 5 * 60_000;
|
|
160
|
+
|
|
161
|
+
// The refresh button may beat that floor, but not turn into a poll loop when
|
|
162
|
+
// held down. It never beats the 429 cooldown.
|
|
163
|
+
const FORCE_POLL_MS = 60_000;
|
|
164
|
+
|
|
165
|
+
// How old a claude-swap row may be before we stop treating it as the answer.
|
|
166
|
+
// Its own default poll interval is 1800s, so a row older than this means its
|
|
167
|
+
// collector is backing off or not running — the case self-polling exists for.
|
|
168
|
+
const STORE_TRUSTED_MS = 45 * 60_000;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* claude-swap's row for the active account, in the shape the panel speaks.
|
|
172
|
+
*
|
|
173
|
+
* Exported for tests: the mapping is where a wrong number would come from, and
|
|
174
|
+
* it is pure.
|
|
175
|
+
*/
|
|
176
|
+
export function quotaFromStore(entry) {
|
|
177
|
+
const good = entry?.lastGood;
|
|
178
|
+
const fh = good?.five_hour;
|
|
179
|
+
const sd = good?.seven_day;
|
|
180
|
+
const primary = (typeof fh?.pct === "number") ? fh : sd;
|
|
181
|
+
if (typeof primary?.pct !== "number") return null;
|
|
182
|
+
|
|
183
|
+
const round = (v) => Math.min(100, Math.max(0, Math.round(v)));
|
|
184
|
+
const out = {
|
|
185
|
+
ok: true,
|
|
186
|
+
source: "claude-swap",
|
|
187
|
+
session5hPct: round(primary.pct),
|
|
188
|
+
session5hWindowSec: WIN_5H_SEC,
|
|
189
|
+
session5hReset: fmtResetIso(primary.resets_at),
|
|
190
|
+
session5hResetAt: isoToSec(primary.resets_at),
|
|
191
|
+
week7dWindowSec: WIN_7D_SEC,
|
|
192
|
+
week7dPct: typeof sd?.pct === "number" ? round(sd.pct) : 0,
|
|
193
|
+
week7dReset: fmtResetIso(sd?.resets_at),
|
|
194
|
+
week7dResetAt: isoToSec(sd?.resets_at),
|
|
195
|
+
// The age of the DATA, not of our read of it. The panel prints this, and
|
|
196
|
+
// "30s ago" over numbers claude-swap collected twenty minutes back is the
|
|
197
|
+
// kind of true-looking lie this whole change exists to remove.
|
|
198
|
+
fetchedAt: entry.fetchedAt,
|
|
199
|
+
};
|
|
200
|
+
// claude-swap keeps per-model windows in a named list rather than fixed
|
|
201
|
+
// fields, because which ones an account has depends on its plan.
|
|
202
|
+
for (const s of Array.isArray(good.scoped) ? good.scoped : []) {
|
|
203
|
+
if (typeof s?.pct !== "number") continue;
|
|
204
|
+
if (/sonnet/i.test(s.name ?? "")) out.weekSonnetPct = round(s.pct);
|
|
205
|
+
else if (/opus/i.test(s.name ?? "")) out.weekOpusPct = round(s.pct);
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Whether we may spend a request of the user's budget right now.
|
|
212
|
+
*
|
|
213
|
+
* Exported for tests — this is the rule that stopped the deck from starving
|
|
214
|
+
* claude-swap, and it is worth pinning down.
|
|
215
|
+
*/
|
|
216
|
+
export function maySelfPoll({ now, force, lastSelfPollAt, rateLimitedUntil }) {
|
|
217
|
+
if (now < rateLimitedUntil) return false;
|
|
218
|
+
return now - lastSelfPollAt >= (force ? FORCE_POLL_MS : SELF_POLL_MS);
|
|
219
|
+
}
|
|
220
|
+
|
|
141
221
|
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
142
222
|
|
|
143
223
|
function stripAnsi(s) {
|
|
@@ -252,10 +332,45 @@ export async function fetchClaudeQuota({ force = false } = {}) {
|
|
|
252
332
|
// good result with 0%).
|
|
253
333
|
if (_inflight) return _inflight;
|
|
254
334
|
|
|
255
|
-
_inflight = _doFetch(now).finally(() => { _inflight = null; });
|
|
335
|
+
_inflight = _doFetch(now, force).finally(() => { _inflight = null; });
|
|
256
336
|
return _inflight;
|
|
257
337
|
}
|
|
258
338
|
|
|
339
|
+
/**
|
|
340
|
+
* claude-swap's numbers for the active account, if it has any.
|
|
341
|
+
*
|
|
342
|
+
* Never throws and never blocks on the network: worst case the store is
|
|
343
|
+
* missing, unparseable, or about a different account than the one that is
|
|
344
|
+
* active, and the caller falls through to fetching for itself.
|
|
345
|
+
*/
|
|
346
|
+
async function storeQuota() {
|
|
347
|
+
try {
|
|
348
|
+
return quotaFromStore(await activeAccountUsage());
|
|
349
|
+
} catch {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// After asking claude-swap to collect, how long to keep looking for the row it
|
|
355
|
+
// writes. Its fetch is a single HTTPS call; three tries covers a slow one
|
|
356
|
+
// without making the refresh button feel stuck.
|
|
357
|
+
const REREAD_TRIES = 3;
|
|
358
|
+
const REREAD_GAP_MS = 800;
|
|
359
|
+
|
|
360
|
+
/** Ask for a collection, then watch the store for the result. */
|
|
361
|
+
async function nudgeAndReread(previous) {
|
|
362
|
+
let asked = false;
|
|
363
|
+
try { asked = await requestCollection(); } catch { /* cswap missing */ }
|
|
364
|
+
if (!asked) return previous;
|
|
365
|
+
|
|
366
|
+
for (let i = 0; i < REREAD_TRIES; i++) {
|
|
367
|
+
await sleep(REREAD_GAP_MS);
|
|
368
|
+
const fresh = await storeQuota();
|
|
369
|
+
if (fresh && (!previous || fresh.fetchedAt > previous.fetchedAt)) return fresh;
|
|
370
|
+
}
|
|
371
|
+
return previous;
|
|
372
|
+
}
|
|
373
|
+
|
|
259
374
|
// Run `claude --print /usage` once. Returns { cliOk, parsed }.
|
|
260
375
|
// cliOk — the CLI ran and we recognized its output (preamble present)
|
|
261
376
|
// parsed — quota percentages object, or null if the "Current session/week"
|
|
@@ -286,8 +401,39 @@ async function _execOnce(shellCmd) {
|
|
|
286
401
|
}
|
|
287
402
|
}
|
|
288
403
|
|
|
289
|
-
async function _doFetch(now) {
|
|
290
|
-
//
|
|
404
|
+
async function _doFetch(now, force = false) {
|
|
405
|
+
// Source 1: claude-swap's store. Free, and already paid for.
|
|
406
|
+
let store = await storeQuota();
|
|
407
|
+
|
|
408
|
+
// Refresh asks for newer numbers, and the honest way to get them from this
|
|
409
|
+
// source is to ask the collector that owns it — which applies its own
|
|
410
|
+
// schedule and backoff, so this cannot become a poll loop.
|
|
411
|
+
if (force && (!store || now - store.fetchedAt > FORCE_POLL_MS)) {
|
|
412
|
+
store = await nudgeAndReread(store);
|
|
413
|
+
}
|
|
414
|
+
if (store && now - store.fetchedAt <= STORE_TRUSTED_MS) {
|
|
415
|
+
_cache = store; _cacheAt = now;
|
|
416
|
+
_lastGood = store;
|
|
417
|
+
return store;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Nothing usable in the store. Everything below spends the user's budget, so
|
|
421
|
+
// it happens on a floor, and not at all while a 429 cooldown is running.
|
|
422
|
+
if (!maySelfPoll({ now, force, lastSelfPollAt: _lastSelfPollAt, rateLimitedUntil: _rateLimitedUntil })) {
|
|
423
|
+
// A stale row still beats an empty panel, and says how stale it is.
|
|
424
|
+
const held = store ?? _lastGood;
|
|
425
|
+
if (held) {
|
|
426
|
+
const result = { ...held, stale: true };
|
|
427
|
+
_cache = result; _cacheAt = now;
|
|
428
|
+
return result;
|
|
429
|
+
}
|
|
430
|
+
const result = { ok: false, reason: now < _rateLimitedUntil ? "rate_limited" : "waiting", fetchedAt: now };
|
|
431
|
+
_cache = result; _cacheAt = now - (CACHE_MS - 5_000);
|
|
432
|
+
return result;
|
|
433
|
+
}
|
|
434
|
+
_lastSelfPollAt = now;
|
|
435
|
+
|
|
436
|
+
// Source 2: OAuth usage API — instant, exact, no cold-start gap.
|
|
291
437
|
const api = await fetchOAuthUsage();
|
|
292
438
|
if (api) {
|
|
293
439
|
const result = { ok: true, ...api, source: "api", fetchedAt: now };
|
|
@@ -296,7 +442,7 @@ async function _doFetch(now) {
|
|
|
296
442
|
return result;
|
|
297
443
|
}
|
|
298
444
|
|
|
299
|
-
//
|
|
445
|
+
// Source 3: parse `claude --print /usage` CLI output.
|
|
300
446
|
const shellCmd = buildQuotaShellCmd();
|
|
301
447
|
|
|
302
448
|
// The CLI sometimes omits the "Current session/week" quota lines on a cold
|
|
@@ -314,7 +460,7 @@ async function _doFetch(now) {
|
|
|
314
460
|
|
|
315
461
|
// Got real quota lines — cache normally and remember as last-known-good.
|
|
316
462
|
if (parsed) {
|
|
317
|
-
const result = { ok: true, ...parsed, fetchedAt: now };
|
|
463
|
+
const result = { ok: true, ...parsed, source: "cli", fetchedAt: now };
|
|
318
464
|
_cache = result; _cacheAt = now;
|
|
319
465
|
_lastGood = result;
|
|
320
466
|
return result;
|