agent-dag 1.35.14 → 1.35.16
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
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
document.documentElement.setAttribute("data-theme", stored === "light" ? "light" : "dark");
|
|
41
41
|
})();
|
|
42
42
|
</script>
|
|
43
|
-
<script type="module" crossorigin src="/assets/index-
|
|
43
|
+
<script type="module" crossorigin src="/assets/index-DKJ6SyER.js"></script>
|
|
44
44
|
<link rel="stylesheet" crossorigin href="/assets/index-hLBidJXz.css">
|
|
45
45
|
</head>
|
|
46
46
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-dag",
|
|
3
|
-
"version": "1.35.
|
|
3
|
+
"version": "1.35.16",
|
|
4
4
|
"description": "Live deck of Claude Code and Codex agents — watch tool calls, token spend and every Claude Code subagent on one calm canvas. Also available as npx ccdeck and npx agent-dag.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/server/ccusage.mjs
CHANGED
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
// managed install is missing/broken we fall back to the old npx path so the
|
|
13
13
|
// feature still works on a fresh machine.
|
|
14
14
|
import { spawn, spawnSync } from "node:child_process";
|
|
15
|
-
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import os from "node:os";
|
|
18
18
|
import { killTree, spawnSpec } from "./exec.mjs";
|
|
19
|
+
import { oneLine, termColumns } from "./term.mjs";
|
|
19
20
|
import { PRODUCT } from "./brand.mjs";
|
|
20
21
|
|
|
21
22
|
const CACHE_MS = 120_000; // 2 min — modal is manual-open; cheap to keep warm
|
|
@@ -97,6 +98,43 @@ function tagged(reason, message) {
|
|
|
97
98
|
return Object.assign(new Error(message), { reason });
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
// Everything this module says out loud goes through here, and it says one line.
|
|
102
|
+
//
|
|
103
|
+
// Both failures below carry a subprocess's entire stderr as their `message`,
|
|
104
|
+
// and on a machine whose npm shim is broken that is a fifteen-line Node stack
|
|
105
|
+
// trace. Handed to console.error whole, it landed across the deck's own status
|
|
106
|
+
// rows and its `\r`-repainted pulse line — and because primeCcusage runs at
|
|
107
|
+
// boot, it was the first thing such a machine ever showed (#432). The evidence
|
|
108
|
+
// is not lost by shortening this: a failed RUN carries its full text back to
|
|
109
|
+
// the browser in `error`, which the usage-history modal keeps on the status
|
|
110
|
+
// line's title, one hover away — the same division of labour admin-failure.ts
|
|
111
|
+
// states for claude-swap's output. What this line is for is the operator
|
|
112
|
+
// watching the terminal, who needs to know which of the two things failed and
|
|
113
|
+
// why, not to read a stack.
|
|
114
|
+
//
|
|
115
|
+
// The width is read per call: a terminal can be resized while the deck runs,
|
|
116
|
+
// and this can fire hours in.
|
|
117
|
+
function note(what, err) {
|
|
118
|
+
const head = `${PRODUCT} ccusage: ${what}: `;
|
|
119
|
+
console.error(head + oneLine(err?.message ?? err, termColumns(process.stderr) - head.length));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Node saying it could not load a file it was pointed at.
|
|
124
|
+
*
|
|
125
|
+
* Both spellings are here because both happen: CommonJS throws
|
|
126
|
+
* `MODULE_NOT_FOUND`, ESM throws `ERR_MODULE_NOT_FOUND` with the wording
|
|
127
|
+
* "Cannot find package" for a bare specifier, and ccusage's entry point is ESM
|
|
128
|
+
* while the npm/npx shims it may be launched through are not.
|
|
129
|
+
*
|
|
130
|
+
* Exported for tests. The one caller is the managed-install branch of
|
|
131
|
+
* runCcusage, where the child is `node <our entry>` and nothing else — so a
|
|
132
|
+
* module Node cannot resolve is by construction a file of OURS that is missing,
|
|
133
|
+
* never the user's npm.
|
|
134
|
+
*/
|
|
135
|
+
export const cannotLoadModule = (text) =>
|
|
136
|
+
/\b(?:ERR_)?MODULE_NOT_FOUND\b|cannot find (?:module|package)/i.test(String(text ?? ""));
|
|
137
|
+
|
|
100
138
|
// ── managed install ─────────────────────────────────────────────────────────
|
|
101
139
|
|
|
102
140
|
// Absolute path to ccusage's CLI entry inside our managed install, or null if
|
|
@@ -220,7 +258,7 @@ async function getRunner() {
|
|
|
220
258
|
// Cold: install once (deduped across concurrent callers).
|
|
221
259
|
if (!_installing) {
|
|
222
260
|
_installing = (async () => { installSync("latest"); })()
|
|
223
|
-
.catch(e => {
|
|
261
|
+
.catch(e => { note("install failed", e); })
|
|
224
262
|
.finally(() => { _installing = null; });
|
|
225
263
|
}
|
|
226
264
|
await _installing;
|
|
@@ -231,13 +269,52 @@ async function getRunner() {
|
|
|
231
269
|
|
|
232
270
|
// ── invocation ──────────────────────────────────────────────────────────────
|
|
233
271
|
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
272
|
+
// At most one repair per process, so a package that is simply unrunnable —
|
|
273
|
+
// reinstalled and still broken — cannot put this into a loop of npm installs.
|
|
274
|
+
let _repairedThisRun = false;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Throw away a managed install that resolves but cannot run, so the next
|
|
278
|
+
* attempt builds a new one.
|
|
279
|
+
*
|
|
280
|
+
* This is the only genuinely permanent failure in this module, and it is the
|
|
281
|
+
* one a broken npm creates: `npm install` that dies partway leaves a
|
|
282
|
+
* package.json and an entry file on disk, resolveEntry answers "installed", and
|
|
283
|
+
* getRunner then hands back that entry FOREVER. Nothing re-checks it —
|
|
284
|
+
* maybeBackgroundUpdate only reinstalls when the registry has a newer version,
|
|
285
|
+
* so even a repaired npm never repaired the install. Every run failed
|
|
286
|
+
* identically, the modal said "try again", and the only thing that actually
|
|
287
|
+
* worked was deleting ~/.agents-deck/ccusage by hand, which nothing in the
|
|
288
|
+
* product tells anyone to do.
|
|
289
|
+
*
|
|
290
|
+
* Only the managed branch qualifies: the npx fallback's failures are npm's own
|
|
291
|
+
* and none of this deck's business to delete anything over.
|
|
292
|
+
*
|
|
293
|
+
* Not done under AGENTS_DECK_NO_INSTALL=1. That variable is a promise not to
|
|
294
|
+
* fetch, and removing the only copy on a machine that cannot replace it would
|
|
295
|
+
* turn a broken feature into an absent one.
|
|
296
|
+
*/
|
|
297
|
+
function discardDamagedInstall(runner, err) {
|
|
298
|
+
if (_repairedThisRun || runner.kind !== "node" || installsDisabled()) return false;
|
|
299
|
+
if (!cannotLoadModule(err?.message)) return false;
|
|
300
|
+
_repairedThisRun = true;
|
|
301
|
+
try {
|
|
302
|
+
rmSync(PKG_DIR, { recursive: true, force: true });
|
|
303
|
+
} catch {
|
|
304
|
+
// Windows holds a lock on a file inside a directory being removed more
|
|
305
|
+
// readily than POSIX does, and a half-removed install is still a resolvable
|
|
306
|
+
// one. Say the repair did not happen so the caller reports the real failure
|
|
307
|
+
// rather than retrying into the same broken entry point.
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
return !resolveEntry();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// One attempt with one runner. Neither branch gets a shell. The managed install
|
|
314
|
+
// is `node <entry> …`, which never needed one; the npx fallback is routed
|
|
315
|
+
// through spawnSpec instead — see npxSpec, and note that `args` here ends in
|
|
316
|
+
// whatever /api/ccusage was asked for.
|
|
317
|
+
function runOnce(runner, args) {
|
|
241
318
|
const { file, args: full, opts } = runner.kind === "node"
|
|
242
319
|
? { file: process.execPath, args: [runner.entry, ...args], opts: {} }
|
|
243
320
|
: fallbackSpec(args);
|
|
@@ -262,6 +339,21 @@ async function runCcusage(args) {
|
|
|
262
339
|
});
|
|
263
340
|
}
|
|
264
341
|
|
|
342
|
+
// Run ccusage with the given args, resolve raw stdout. One retry, and only for
|
|
343
|
+
// the one failure that is otherwise permanent — see discardDamagedInstall. The
|
|
344
|
+
// second getRunner() is what rebuilds the install, or falls through to npx when
|
|
345
|
+
// npm cannot.
|
|
346
|
+
async function runCcusage(args) {
|
|
347
|
+
const runner = await getRunner();
|
|
348
|
+
try {
|
|
349
|
+
return await runOnce(runner, args);
|
|
350
|
+
} catch (err) {
|
|
351
|
+
if (!discardDamagedInstall(runner, err)) throw err;
|
|
352
|
+
note("managed install was unusable, rebuilding it", err);
|
|
353
|
+
return runOnce(await getRunner(), args);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
265
357
|
// ccusage prints the JSON object somewhere in stdout; slice first { to last }.
|
|
266
358
|
function extractJson(out) {
|
|
267
359
|
const start = out.indexOf("{");
|
|
@@ -311,9 +403,12 @@ export async function fetchCcusageDaily({ since, until, force = false } = {}) {
|
|
|
311
403
|
fetchedAt: now,
|
|
312
404
|
};
|
|
313
405
|
} catch (err) {
|
|
314
|
-
|
|
406
|
+
note("fetch failed", err);
|
|
315
407
|
// Anything untagged got here from the child itself — a non-zero exit, or a
|
|
316
408
|
// spawn that never started one — which is exactly what run_failed means.
|
|
409
|
+
// `error` keeps the child's WHOLE output, stack trace and all: it is the
|
|
410
|
+
// modal's hover title, which is where the raw bytes are meant to live, and
|
|
411
|
+
// it is what somebody pastes into an issue. Only the terminal gets a line.
|
|
317
412
|
result = {
|
|
318
413
|
ok: false,
|
|
319
414
|
reason: err?.reason ?? "run_failed",
|
|
@@ -355,7 +450,7 @@ export function primeCcusage() {
|
|
|
355
450
|
}
|
|
356
451
|
if (!_installing) {
|
|
357
452
|
_installing = (async () => { installSync("latest"); })()
|
|
358
|
-
.catch(e => {
|
|
453
|
+
.catch(e => { note("install failed", e); })
|
|
359
454
|
.finally(() => { _installing = null; });
|
|
360
455
|
}
|
|
361
456
|
return { state: "installing" };
|
|
@@ -10,6 +10,17 @@
|
|
|
10
10
|
// Only ever touches its own entry, tagged `__agent-dag-sound`. Hooks the user
|
|
11
11
|
// wrote themselves are left exactly as found — including the platform-specific
|
|
12
12
|
// ones this replaces, which are reported rather than deleted.
|
|
13
|
+
//
|
|
14
|
+
// Claude Code only, and deliberately: everything here is one entry in Claude
|
|
15
|
+
// Code's settings.json, which Claude Code alone reads and executes. There is no
|
|
16
|
+
// Codex equivalent — the deck installs no Codex hooks and tails the rollout
|
|
17
|
+
// files instead — so a Codex turn ends in silence and no amount of writing to
|
|
18
|
+
// this file changes that. The browser is where that is said rather than
|
|
19
|
+
// guessed: the topbar button is drawn only where Claude Code is, and its
|
|
20
|
+
// tooltip names the limit and the mechanism behind it. If this module ever does
|
|
21
|
+
// learn a second provider, src/web/provider-copy.ts's finishSoundTitle is the
|
|
22
|
+
// sentence that has to move with it, and finish-sound-scope.test.ts fails until
|
|
23
|
+
// it does (#394).
|
|
13
24
|
import { readFile, mkdir } from "node:fs/promises";
|
|
14
25
|
import { existsSync } from "node:fs";
|
|
15
26
|
import { join, dirname } from "node:path";
|
package/src/server/term.mjs
CHANGED
|
@@ -282,6 +282,55 @@ export function fit(s, max, ellipsis = "…") {
|
|
|
282
282
|
return /\s/.test(text) ? text.slice(0, room) + ellipsis : ellipsis + text.slice(text.length - room);
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
/**
|
|
286
|
+
* A failure, reduced to the one line it is safe to print while the deck is
|
|
287
|
+
* painting.
|
|
288
|
+
*
|
|
289
|
+
* Everything bin/deck.js writes after the banner is a `\r`-rewritten row — the
|
|
290
|
+
* spinner in `step`, the pulse line, which repaints every 800ms — so a write
|
|
291
|
+
* that arrives from an async callback lands in the middle of one of them. A
|
|
292
|
+
* single short line is survivable: it ends in a newline, the next repaint
|
|
293
|
+
* starts on a fresh row, and the report above it is still on screen. A
|
|
294
|
+
* subprocess's stack trace is not: fifteen lines scroll the entire startup
|
|
295
|
+
* report away and leave the pulse mid-frame. Reported from Windows (#432),
|
|
296
|
+
* where a broken npm shim put two Node stack traces over the boot output.
|
|
297
|
+
*
|
|
298
|
+
* Three things happen here, and the order matters.
|
|
299
|
+
*
|
|
300
|
+
* The most informative line is chosen rather than the first one, because the
|
|
301
|
+
* first line of a Node stack trace is `node:internal/modules/cjs/loader:1573`
|
|
302
|
+
* and the line that names the actual failure — `Error: Cannot find module …` —
|
|
303
|
+
* is five lines below it.
|
|
304
|
+
*
|
|
305
|
+
* Every control character is replaced, not just the newlines. A lone `\r` is a
|
|
306
|
+
* cursor jump to column 0, so a CRLF stack trace that only had its `\n`
|
|
307
|
+
* removed would still overwrite whatever the deck had drawn on that row — and
|
|
308
|
+
* CRLF is what a Windows child writes, which is the platform this exists for.
|
|
309
|
+
* An ESC would open an escape sequence out of a string the deck did not write.
|
|
310
|
+
*
|
|
311
|
+
* And the result is fitted to the room the caller has left, so the one line
|
|
312
|
+
* stays one line: a message that wraps is two rows, and the `\r` that follows
|
|
313
|
+
* only ever reaches the second of them. The ellipsis defaults to the ASCII one
|
|
314
|
+
* rather than `…`, because callers of this are error paths in modules that
|
|
315
|
+
* know nothing about the terminal's glyph tier — and a `…` on a cmd.exe code
|
|
316
|
+
* page is a question mark in the middle of the only clue the user got.
|
|
317
|
+
*
|
|
318
|
+
* @param room how many columns are free after whatever prefix the caller prints.
|
|
319
|
+
*/
|
|
320
|
+
export function oneLine(text, room = 80, ellipsis = "...") {
|
|
321
|
+
const lines = stripAnsi(String(text ?? ""))
|
|
322
|
+
.split("\n")
|
|
323
|
+
// eslint-disable-next-line no-control-regex
|
|
324
|
+
.map((l) => l.replace(/[\x00-\x1f\x7f]/g, " ").trim())
|
|
325
|
+
.filter(Boolean);
|
|
326
|
+
if (!lines.length) return "";
|
|
327
|
+
// "Error:", "TypeError:", "Error [ERR_MODULE_NOT_FOUND]:" — the line a reader
|
|
328
|
+
// would have picked out of the dump themselves. The optional prefix is what
|
|
329
|
+
// makes a bare "Error:" match as well as a named subclass.
|
|
330
|
+
const named = lines.find((l) => /^[\w$]*Error\b/.test(l));
|
|
331
|
+
return fit(named ?? lines[0], Math.max(8, room), ellipsis);
|
|
332
|
+
}
|
|
333
|
+
|
|
285
334
|
const same = (s) => s;
|
|
286
335
|
|
|
287
336
|
/**
|