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.
@@ -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-C5yaHo5q.js"></script>
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.14",
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": {
@@ -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 => { console.error(`${PRODUCT} ccusage: install failed:`, e?.message ?? 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
- // Run ccusage with the given args, resolve raw stdout.
235
- async function runCcusage(args) {
236
- const runner = await getRunner();
237
- // Neither branch gets a shell. The managed install is `node <entry> …`, which
238
- // never needed one; the npx fallback is routed through spawnSpec instead —
239
- // see npxSpec, and note that `args` here ends in whatever /api/ccusage was
240
- // asked for.
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
- console.error(`${PRODUCT} ccusage: fetch failed:`, err?.message ?? err);
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 => { console.error(`${PRODUCT} ccusage: install failed:`, e?.message ?? 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";
@@ -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
  /**