agent-dag 1.35.0 → 1.35.2

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/README.md CHANGED
@@ -117,10 +117,21 @@ ccdeck [options]
117
117
  --no-persist RAM-only mode — don't write or replay the log
118
118
  --codex Force Codex capture even if ~/.codex/ is missing
119
119
  --no-codex Skip Codex capture (Claude only)
120
+ --claude Force Claude capture even if Claude Code wasn't found
121
+ --no-claude Skip Claude entirely — no hooks, no claude-swap,
122
+ no Accounts panel (Codex only)
120
123
  --uninstall Remove ccdeck's hooks from settings files
121
124
  -h, --help Show this help
122
125
  ```
123
126
 
127
+ ccdeck looks for each CLI before it does anything on that CLI's behalf. Claude
128
+ Code counts as present when its binary is on `PATH` (or in one of the places its
129
+ installers put it), or when its config dir carries traces of having been used;
130
+ Codex counts as present when `~/.codex/` exists. On a machine with only one of
131
+ them, the other one's hooks, installs and panels are skipped rather than shown
132
+ empty — the boot banner says which way it went, and `--claude` / `--codex`
133
+ override it if the guess is wrong.
134
+
124
135
  `--workspace` is a filter this deck applies to itself, not a claim on the sessions it matches: **every** running deck whose workspace contains a session's directory draws that session, so a machine-wide deck and one scoped to `~/proj` both show the agents working inside `~/proj`. It reads the same way on both capture paths — Claude Code's hook and Codex's rollout files — and the events log still gets exactly one copy of each event, whichever decks are up. A relative path is resolved against the directory you start the deck in, and once, so both paths scope to the same tree.
125
136
 
126
137
  Environment:
@@ -147,6 +158,14 @@ npx ccdeck --uninstall
147
158
 
148
159
  Removes every hook entry ccdeck injected from `~/.claude/settings.json`, and `~/.codex/hooks.json` if present.
149
160
 
161
+ It removes the hook entries and nothing else. The forwarder script
162
+ (`~/.claude/agent-dag/hook.js`), the discovery directory around it, the events
163
+ log, and the tools ccdeck installed for you — claude-swap, ccusage, and a `uv`
164
+ binary if it had to fetch one — are all left in place, and each has its own
165
+ uninstaller. Deleting `~/.claude/agent-dag/` and `~/.agents-deck/` clears
166
+ ccdeck's own files; `uv tool uninstall claude-swap` (or `pipx uninstall
167
+ claude-swap`) removes the account switcher.
168
+
150
169
  ## Design
151
170
 
152
171
  One canvas. No tabs. No kanban.
package/bin/deck.js CHANGED
@@ -83,7 +83,7 @@ const rawWorkspace = flags.workspace != null
83
83
  const openBrowser = flags.noOpen !== true;
84
84
  // The events log lives beside the discovery files, so it follows the Claude
85
85
  // config dir rather than assuming ~/.claude — see src/server/claude-dir.mjs.
86
- const { claudeConfigDir } =
86
+ const { claudeConfigDir, hasClaudeInstalled } =
87
87
  await import(pathToFileURL(join(PKG_ROOT, "src/server/claude-dir.mjs")).href);
88
88
  // Resolved here rather than left as typed: the discovery file publishes this
89
89
  // path so the hook can tell which decks share one log and elect a single
@@ -120,6 +120,25 @@ const wantCodex = flags.noCodex
120
120
  ? false
121
121
  : (flags.codex === true || hasCodexInstalled());
122
122
 
123
+ // The same question for the other CLI, and the one nobody was asking. README
124
+ // offers "Claude Code CLI or OpenAI Codex CLI (or both)"; a Codex-only machine
125
+ // nonetheless got a Python account-switcher installed for a CLI it does not
126
+ // have, an accounts panel open on first run, and a banner line telling it to
127
+ // sign into that CLI (#402). Everything the deck installs or opens on the
128
+ // Claude side now hangs off this one answer, and it is stated in the banner so
129
+ // a wrong answer is visible rather than mysterious.
130
+ //
131
+ // `--claude` is the escape hatch for a false negative, which is the failure
132
+ // that matters: hasClaudeInstalled looks for the binary and for traces of use,
133
+ // and a machine that hides Claude Code from both would otherwise lose its hooks
134
+ // with no way to ask for them back. `--no-claude` is the opt-out the Claude side
135
+ // never had — the mirror of --no-codex — and it is also what a Codex-only user
136
+ // with a settings.json the installer refuses to rewrite needs, since that
137
+ // refusal is fatal at boot on a component they do not use.
138
+ const wantClaude = flags.noClaude
139
+ ? false
140
+ : (flags.claude === true || hasClaudeInstalled());
141
+
123
142
  const WEB_DIST = join(PKG_ROOT, "dist", "web", "index.html");
124
143
  if (!existsSync(WEB_DIST)) {
125
144
  console.error(`${PRODUCT}: ui not built. run \`npm run build\` (or \`pnpm build\`) first.`);
@@ -239,16 +258,28 @@ async function step(label, work) {
239
258
  * rejection.
240
259
  */
241
260
  function startupWork() {
261
+ // Every job below this line serves Claude Code and only Claude Code: the
262
+ // hooks go in Claude Code's settings.json, claude-swap switches Claude
263
+ // accounts, and ccusage reads Claude Code's own session logs. On a machine
264
+ // without Claude Code all three are work done for a CLI that is not there —
265
+ // two of them installs the user did not ask for — so they are not started at
266
+ // all rather than started and then reported as failures. `null` is how each
267
+ // one says "not attempted", which reportStartup tells apart from "tried and
268
+ // could not".
269
+
242
270
  // Settings the installer cannot parse are settings it cannot rewrite without
243
271
  // losing them, so it refuses — and that refusal is reported rather than
244
272
  // thrown, because it is the only thing the user can act on.
245
- const hooks = installHooks({ provider: "claude" }).then(v => ({ ok: true, v }), err => ({ ok: false, err }));
273
+ const hooks = wantClaude
274
+ ? installHooks({ provider: "claude" }).then(v => ({ ok: true, v }), err => ({ ok: false, err }))
275
+ : Promise.resolve(null);
246
276
 
247
277
  // claude-swap backs the multi-account panel, and an empty store leaves that
248
278
  // panel useless even when the tool is there — so the account already signed
249
279
  // in is registered once. Bounded inside seedFirstAccount: empty store only,
250
280
  // once ever, never with NO_INSTALL set.
251
281
  const cswap = (async () => {
282
+ if (!wantClaude) return null;
252
283
  const { ensureCswap } = await import(pathToFileURL(join(PKG_ROOT, "src/server/cswap-install.mjs")).href);
253
284
  const cs = await ensureCswap();
254
285
  const usable = cs.state === "present" || cs.state === "installed" || cs.state === "upgrading";
@@ -259,7 +290,10 @@ function startupWork() {
259
290
 
260
291
  // ccusage backs the usage-history modal. Primed at boot rather than on first
261
292
  // open so a cold machine pays the install while the deck is still starting.
293
+ // Nothing is lost by skipping the prime: runCcusage falls back to npx, so the
294
+ // modal still answers if it is ever opened — it just pays the wait itself.
262
295
  const ccusage = (async () => {
296
+ if (!wantClaude) return null;
263
297
  if (process.env.AGENTS_DECK_NO_INSTALL === "1") return null;
264
298
  const { primeCcusage } = await import(pathToFileURL(join(PKG_ROOT, "src/server/ccusage.mjs")).href);
265
299
  return primeCcusage();
@@ -290,14 +324,22 @@ async function reportStartup(jobs) {
290
324
  }));
291
325
 
292
326
  const hooks = await step(`installing Claude hooks${G.ellipsis}`, jobs.hooks);
293
- if (!hooks.ok) {
327
+ if (hooks === null) {
328
+ // Said in the same shape as the Codex row below, because it is the same
329
+ // sentence: this deck is not watching that CLI, and here is why. It also
330
+ // retires the one boot failure a Codex-only machine could hit — an
331
+ // unparseable or unwritable settings.json used to exit(1) below, killing a
332
+ // deck over a file belonging to a CLI the user does not run.
333
+ write(row({ label: "Claude hooks", detail: `skipped ${G.dash} no Claude Code found, or --no-claude` }));
334
+ } else if (!hooks.ok) {
294
335
  // The file it names is one only the user can repair, and every Claude Code
295
336
  // session on this machine is reading it too.
296
337
  write(row({ mark: G.fail, tone: P.err, label: "Claude hooks", detail: "not installed" }));
297
338
  console.error(`\n ${PRODUCT}: ${hooks.err.message}\n`);
298
339
  process.exit(1);
340
+ } else {
341
+ write(row({ mark: G.ok, label: "Claude hooks", detail: fileLink(hooks.v.hookPath) }));
299
342
  }
300
- write(row({ mark: G.ok, label: "Claude hooks", detail: fileLink(hooks.v.hookPath) }));
301
343
 
302
344
  // Codex CLI hooks never fire on Windows (sandbox refuses to spawn the hook
303
345
  // command). Instead the server tails Codex's rollout JSONL files directly, so
@@ -316,7 +358,13 @@ async function reportStartup(jobs) {
316
358
 
317
359
  const swap = await step(`checking claude-swap${G.ellipsis}`, jobs.cswap);
318
360
  const cs = swap?.cs;
319
- if (cs?.state === "present") {
361
+ if (!wantClaude) {
362
+ // claude-swap is a Python tool that switches Claude Code accounts, and the
363
+ // deck used to fetch a uv binary to install it on machines with no Claude
364
+ // Code at all. Saying so is the point of the row: it is the one place a
365
+ // user can learn that the accounts panel is missing on purpose.
366
+ write(row({ label: "claude-swap", detail: `skipped ${G.dash} accounts are Claude-only` }));
367
+ } else if (cs?.state === "present") {
320
368
  write(row({ mark: G.ok, label: "claude-swap", detail: `v${cs.version} (accounts panel enabled)` }));
321
369
  } else if (cs?.state === "installed") {
322
370
  write(row({ mark: G.ok, label: "claude-swap", detail: `installed v${cs.version} via ${cs.via}` }));
@@ -460,7 +508,7 @@ function restartTarget() {
460
508
  }
461
509
 
462
510
  const starting = startServer({
463
- port, persist, workspace, codex: wantCodex,
511
+ port, persist, workspace, codex: wantCodex, claude: wantClaude,
464
512
  // Withheld when nothing is supervising us: without a parent, exiting is just
465
513
  // exiting, and /api/restart answers 501 so the UI hides the control.
466
514
  onRestart: SUPERVISED ? requestRestart : null,
@@ -619,6 +667,8 @@ function parseArgs(args) {
619
667
  else if (a === "--history") out.history = args[++i];
620
668
  else if (a === "--codex") out.codex = true;
621
669
  else if (a === "--no-codex") out.noCodex = true;
670
+ else if (a === "--claude") out.claude = true;
671
+ else if (a === "--no-claude") out.noClaude = true;
622
672
  }
623
673
  return out;
624
674
  }
@@ -639,8 +689,12 @@ Options:
639
689
  --no-persist Don't write or replay events log (RAM-only)
640
690
  --codex Force-enable Codex capture even if ~/.codex/ missing
641
691
  --no-codex Skip Codex capture (Claude only)
692
+ --claude Force-enable Claude capture even if Claude Code wasn't found
693
+ --no-claude Skip Claude entirely: no hooks, no claude-swap, no accounts panel
642
694
  --uninstall Remove ${PRODUCT}'s hooks from ~/.claude/settings.json and
643
- ~/.codex/hooks.json, and restore any sound hooks of yours it parked
695
+ ~/.codex/hooks.json, and restore any sound hooks of yours it parked.
696
+ Hook entries only: the forwarder script, ~/.claude/agent-dag/,
697
+ the events log, ~/.agents-deck/ and claude-swap all stay
644
698
  -h, --help Show this help
645
699
  `);
646
700
  }