agent-dag 1.35.32 → 1.35.34

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-Ca-CsYuj.js"></script>
43
+ <script type="module" crossorigin src="/assets/index-BiZ-D_yB.js"></script>
44
44
  <link rel="stylesheet" crossorigin href="/assets/index-jWyLguyW.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.32",
3
+ "version": "1.35.34",
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": {
@@ -113,8 +113,15 @@ const CLAUDE_USE_MARKERS = [
113
113
  * PATH walk is done by hand, with PATHEXT applied on Windows exactly as cmd.exe
114
114
  * would — a `claude.cmd` shim from npm and a bare `claude.exe` from the native
115
115
  * installer both have to count, and neither is spelled `claude`.
116
+ *
117
+ * Not exported (#383). It is the first half of `hasClaudeInstalled` below, and
118
+ * that function takes the same four injected dependencies — platform, env, home
119
+ * and `exists` — so every branch of this walk is already driven from a test with
120
+ * no host state reaching it, including the Windows PATHEXT branch exercised from
121
+ * a Mac. Exporting it would offer a second, narrower answer to "is Claude Code
122
+ * here", and the point of #402 was that there is one.
116
123
  */
117
- export function claudeCliOnDisk({
124
+ function claudeCliOnDisk({
118
125
  platform = process.platform,
119
126
  env = process.env,
120
127
  home = homedir(),
@@ -100,7 +100,20 @@ const PERMANENT_CODES = new Set([
100
100
  "invalid_grant",
101
101
  ]);
102
102
 
103
- /** Decode a JWT payload. Returns null for anything that isn't a 3-part JWT. */
103
+ /**
104
+ * Decode a JWT payload. Returns null for anything that isn't a 3-part JWT.
105
+ *
106
+ * Exported for its test and not for a caller (#383). Its two readers are
107
+ * `expiryMs` below — which decides whether the deck spends the single-use
108
+ * refresh token, the one mistake in this file that costs the user a `codex
109
+ * login` — and `identityFrom`, which reads the plan, the account id and the
110
+ * email out of the id_token. Both take the answer from a file the deck did not
111
+ * write and cannot validate, so every way this can be handed something that is
112
+ * not a JWT is a real input, and neither reader can be driven far enough to
113
+ * exercise them: `expiryMs` collapses the whole result to one number and
114
+ * `identityFrom` needs a full auth file plus a refresh round-trip to reach.
115
+ * See codex-jwt-decode.test.ts.
116
+ */
104
117
  export function decodeJwt(token) {
105
118
  if (typeof token !== "string") return null;
106
119
  const parts = token.split(".");
@@ -110,7 +110,19 @@ export function newSlot(before, after) {
110
110
  return fresh.length === 1 ? fresh[0] : null;
111
111
  }
112
112
 
113
- /** Anthropic's own view of who is signed in. Null when it cannot be read. */
113
+ /**
114
+ * Anthropic's own view of who is signed in. Null when it cannot be read.
115
+ *
116
+ * Exported for its test rather than for a caller (#383). Both callers are inside
117
+ * the login flow and neither can show what was parsed: `spawnLogin` keeps only
118
+ * `identity?.email` as the address to restore to, and `submitLoginCode` turns
119
+ * the whole thing into a pass/fail — a null there is the difference between a
120
+ * sign-in the deck accepts and one it reports as "signed in, but the claude CLI
121
+ * still reports nobody logged in". The success path, where the email read here
122
+ * is what matches the new credential to a cswap slot, is reachable only with a
123
+ * real signed-in CLI on the machine running the suite. See
124
+ * cswap-identity.test.ts.
125
+ */
114
126
  export async function currentIdentity() {
115
127
  const r = await run(await claudeBin(), ["auth", "status", "--json"], { timeout: 20_000 });
116
128
  if (!r.ok) return null;
@@ -34,7 +34,18 @@ const SETTINGS = {
34
34
 
35
35
  // ── settings ───────────────────────────────────────────────────────────────
36
36
 
37
- /** Parse `cswap config` — "key value (default)" per line. */
37
+ /**
38
+ * Parse `cswap config` — "key value (default)" per line.
39
+ *
40
+ * Exported for its test rather than for a caller (#383). The two callers here —
41
+ * `autoStatus`, which hands the map straight to the settings panel, and
42
+ * `tickInterval`, which takes the poll interval out of it — both reduce the
43
+ * parse to something a test cannot see through: the panel takes whatever shape
44
+ * it is given, and the interval collapses four fields to one number that is
45
+ * clamped anyway. The parse itself is a regex over human-formatted output from a
46
+ * separate Python tool, on both line-ending conventions. See
47
+ * cswap-auto-readers.test.ts.
48
+ */
38
49
  export async function readCswapConfig() {
39
50
  const r = await run(await cswapBin(), ["config"]);
40
51
  if (!r.ok) return null;
@@ -117,6 +128,13 @@ async function runAutoTick() {
117
128
  * budget that is already the scarce resource here, and the user would have two
118
129
  * things switching their account with no single place showing why. So the deck
119
130
  * reports it and stays out of the way.
131
+ *
132
+ * Exported for its test rather than for a caller (#383). Its two callers reduce
133
+ * it to a boolean on a status object and to a skipped tick, so neither can show
134
+ * WHICH command line was matched — and the matching is the whole function. The
135
+ * two halves also run completely different commands, `ps` against
136
+ * `Get-CimInstance`, so on any one machine only half of it is ever exercised at
137
+ * all. See cswap-auto-readers.test.ts, which drives both from either host.
120
138
  */
121
139
  export async function externalAutoRunning() {
122
140
  // A line is the user's loop if it runs `cswap auto` without --once. Our own
@@ -87,7 +87,26 @@ export async function cswapBin() {
87
87
  return "cswap"; // not found; leave the bare name so errors read sensibly
88
88
  }
89
89
 
90
- /** Forget the cached resolution — call after installing. */
90
+ /**
91
+ * Forget the cached resolution — call after installing.
92
+ *
93
+ * WHAT IS ACTUALLY STALE HERE (#383). Note that `cswapBin` above memoizes only
94
+ * SUCCESS: the "not found" answer is the bare name returned without ever being
95
+ * written to `_bin`, so a lookup that found nothing is already re-run on the
96
+ * next call and there is no negative result for this to clear. What it clears is
97
+ * a positive one — a path that resolved, was cached, and has since been
98
+ * superseded. That is not hypothetical on the install path: `cswapVersion` runs
99
+ * `--version` a SECOND time under the default timeout after `cswapBin` has
100
+ * already cached the copy its own 8s probe accepted, so a copy that is present
101
+ * but too slow, half-written or broken caches a path and still reports no
102
+ * version — and the install that follows lands a working cswap somewhere the
103
+ * cached path may not point at. Clearing is cheap; a deck driving the wrong
104
+ * binary for the life of the process is not.
105
+ *
106
+ * Exported for its test rather than for a caller (#383): the one caller is
107
+ * ensureCswap below, whose return value says nothing about which binary the
108
+ * following twenty account operations will be sent to. See cswap-bin-memo.test.ts.
109
+ */
91
110
  export function resetCswapBin() { _bin = null; }
92
111
 
93
112
  /** Installed version string, or null when cswap cannot be found. */
@@ -327,7 +346,11 @@ export async function ensureCswap() {
327
346
 
328
347
  const result = await installCswap();
329
348
  if (!result.ok) return { state: "unavailable", ...result };
330
- resetCswapBin(); // it exists now; the earlier "not on PATH" answer is stale
349
+ // Something was just installed, so any path resolved before it is a guess made
350
+ // against a different filesystem. Nothing is cached when the earlier lookup
351
+ // found nothing — see resetCswapBin — but a lookup that DID resolve, to a copy
352
+ // whose `--version` then failed, is exactly the case that got us here.
353
+ resetCswapBin();
331
354
 
332
355
  // Freshly installed tools land in ~/.local/bin, which may not be on the PATH
333
356
  // of the shell that launched us — cswapBin looks there directly, so this
@@ -530,7 +530,13 @@ export function keepDiscovery({ port, workspace, token, persist = null, codex =
530
530
  return { file: discoveryPath(), check, stop: () => clearInterval(timer) };
531
531
  }
532
532
 
533
- export { AGENT_DAG_DIR, CLAUDE_DIR, CODEX_DIR, CLAUDE_EVENTS };
533
+ // CLAUDE_EVENTS used to ride along here (#383). It is the events list of the
534
+ // `claude` entry in PROVIDERS and has never had a reader outside this file; it
535
+ // was easy to miss because the three directories beside it ARE imported and
536
+ // because the long justification below belongs to the SECOND export, not this
537
+ // one. Which events the deck asks Claude Code for is answered by installHooks
538
+ // writing settings.json, which is what the tests read.
539
+ export { AGENT_DAG_DIR, CLAUDE_DIR, CODEX_DIR };
534
540
  // Exported for the other modules that rewrite settings.json — the sound toggle
535
541
  // today. Every one of them needs the same two guarantees: a file we cannot
536
542
  // parse is never treated as an empty one, and the replacement is a single
@@ -103,7 +103,12 @@ export function npxLaunch(args, {
103
103
  // Long enough for a cold tarball on a slow line; short enough that a connection
104
104
  // hanging on a dead proxy does not hold the deck's Update button for the three
105
105
  // minutes the tab waits before giving up on its own.
106
- export const PREFETCH_TIMEOUT_MS = 120_000;
106
+ //
107
+ // Not exported (#383): it is the default of `prefetch`'s `timeoutMs` parameter
108
+ // and nothing else. A caller that wants a different budget passes one, and a
109
+ // caller that wants this one passes nothing — there is no third caller for the
110
+ // number to be worth naming across the module boundary.
111
+ const PREFETCH_TIMEOUT_MS = 120_000;
107
112
 
108
113
  /**
109
114
  * What to hand npx to DOWNLOAD a spec without running it.
@@ -132,8 +132,15 @@ export function isNpxInstall(pkgRoot) {
132
132
 
133
133
  /** A git checkout is the maintainer's own tree. Its version routinely sits
134
134
  * ahead of npm, and telling someone to `npm i -g` over their working copy is
135
- * actively wrong, so the registry side of the check is skipped there. */
136
- export function isGitCheckout(pkgRoot) {
135
+ * actively wrong, so the registry side of the check is skipped there.
136
+ *
137
+ * Not exported (#383). It gates three answers and each of the three is already
138
+ * asserted against a directory with a real `.git` in it — `upgradeCommand`
139
+ * returning "git pull && npm run build", `upgradeName` refusing to move a
140
+ * checkout onto a published alias, and `startUpgrade` refusing with
141
+ * `git_checkout`. A test importing the predicate would restate what those three
142
+ * already prove, one level further from the behaviour a user can see. */
143
+ function isGitCheckout(pkgRoot) {
137
144
  try { return existsSync(join(pkgRoot, ".git")); } catch { return false; }
138
145
  }
139
146
 
@@ -411,22 +411,54 @@ function renderWord(word) {
411
411
  return rows.map((r) => r.replace(/\s+$/, ""));
412
412
  }
413
413
 
414
+ /** The drawn mark itself, three text rows of half-blocks.
415
+ *
416
+ * Exported for one reader, and it is a test rather than a caller (#383). This
417
+ * is the ONLY place the art's true width can be measured: `renderWord` trims
418
+ * each row's trailing blanks, so the rows are not all the same length, and the
419
+ * arithmetic in WORDMARK_WIDTH below is a hand-computed prediction of the
420
+ * widest of them. Nothing in the banner path compares the two — `wordmark()`
421
+ * gates on WORDMARK_WIDTH and then prints THESE lines — so an under-counting
422
+ * width lets the art render into a terminal too narrow to hold it and the
423
+ * three rows wrap into six. See wordmark-art-width.test.ts, which is the
424
+ * comparison this export exists to make possible. */
414
425
  export const WORDMARK_LINES = renderWord("ccdeck");
415
426
  /** 29 columns of art, plus the two-space indent every other line uses. */
416
427
  export const WORDMARK_WIDTH = 2 + LETTERS.c[0].length * 6 + 5;
417
428
 
418
- /** The tagline, longest version that fits — the name and version are the part
419
- * that must survive, the rest is context. */
420
- function tagline(version, columns, bullet) {
429
+ /**
430
+ * The tagline, longest version that fits — the name and version are the part
431
+ * that must survive, the rest is context.
432
+ *
433
+ * `prefix` is how many columns are already spent on the line the tagline lands
434
+ * on, and it is a parameter because the three layouts below do not agree on it
435
+ * (#383). In the full banner the tagline gets a line of its own behind a
436
+ * two-space indent; in the compact and plain ones it shares the line with the
437
+ * product name, which costs eight or nine columns more. This budget was
438
+ * hard-coded at 2 for all three, so between 21 and 31 columns — the widths where
439
+ * the compact form is chosen and the medium tagline still looks affordable —
440
+ * the banner printed a 36-column line into a terminal that could not hold it and
441
+ * the first thing a user saw wrapped. `columns - 1` keeps the last cell empty,
442
+ * because a line ending exactly at the right margin makes some terminals wrap
443
+ * anyway.
444
+ */
445
+ function tagline(version, columns, bullet, prefix) {
421
446
  const v = `v${version}`;
422
447
  const options = [
423
448
  `${v} ${bullet} live agent DAG ${bullet} Claude Code + Codex`,
424
449
  `${v} ${bullet} live agent DAG`,
425
450
  v,
426
451
  ];
427
- return options.find((o) => o.length + 2 <= columns - 1) ?? v;
452
+ return options.find((o) => o.length + prefix <= columns - 1) ?? v;
428
453
  }
429
454
 
455
+ // What each layout spends before the tagline starts, counted off the strings
456
+ // built in `wordmark` below: " ccdeck " for plain, " ccdeck " for compact,
457
+ // and the bare two-space indent for the full banner's own tagline line.
458
+ const PLAIN_PREFIX_W = " ccdeck ".length;
459
+ const COMPACT_PREFIX_W = " ccdeck ".length;
460
+ const TAG_INDENT_W = " ".length;
461
+
430
462
  /**
431
463
  * The wordmark, in the largest form this terminal can hold.
432
464
  *
@@ -442,11 +474,13 @@ export function wordmark({
442
474
  columns = 80, version = "0.0.0", profile = "none", unicode = true, pal = palette(profile),
443
475
  } = {}) {
444
476
  const g = glyphs(unicode);
445
- const tag = tagline(version, columns, g.bullet);
477
+ // Each layout asks for its own tagline, because each leaves it a different
478
+ // amount of room — see the prefix constants above.
479
+ const tag = (prefix) => tagline(version, columns, g.bullet, prefix);
446
480
 
447
- if (profile === "none") return { kind: "plain", lines: ["", ` ccdeck ${tag}`, ""] };
481
+ if (profile === "none") return { kind: "plain", lines: ["", ` ccdeck ${tag(PLAIN_PREFIX_W)}`, ""] };
448
482
  if (!unicode || columns < WORDMARK_WIDTH + 1) {
449
- return { kind: "compact", lines: ["", ` ${pal.bold}${pal.accent}ccdeck${pal.reset} ${pal.muted}${tag}${pal.reset}`, ""] };
483
+ return { kind: "compact", lines: ["", ` ${pal.bold}${pal.accent}ccdeck${pal.reset} ${pal.muted}${tag(COMPACT_PREFIX_W)}${pal.reset}`, ""] };
450
484
  }
451
485
 
452
486
  const ramp = [pal.accentSoft, pal.accent, pal.accentDeep];
@@ -456,7 +490,7 @@ export function wordmark({
456
490
  "",
457
491
  ...WORDMARK_LINES.map((l, i) => ` ${ramp[i]}${l}${pal.reset}`),
458
492
  "",
459
- ` ${pal.muted}${tag}${pal.reset}`,
493
+ ` ${pal.muted}${tag(TAG_INDENT_W)}${pal.reset}`,
460
494
  "",
461
495
  ],
462
496
  };