agent-dag 1.34.6 → 1.35.1

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
@@ -48,7 +48,7 @@ No config file. No account. No telemetry — nothing about your sessions is repo
48
48
  | **Survives restarts** | Events are appended to `~/.claude/agent-dag/events.jsonl` and replayed on open. |
49
49
  | **Accounts without a terminal** | Sign a new Claude account in, share one to another machine, rename, reorder, remove — from the panel. |
50
50
  | **Knows when it is stale** | Node caches modules at startup, so an upgraded-while-running deck keeps executing old code. This one says so, and can restart itself when nothing is running. |
51
- | **Workspace scoping** | `--scope` for the current directory, `--workspace <path>` for any subtree. |
51
+ | **Workspace scoping** | `--scope` for the current directory, `--workspace <path>` for any subtree — for Claude Code and Codex alike. |
52
52
 
53
53
  ## How it works
54
54
 
@@ -117,10 +117,23 @@ 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
+
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.
136
+
124
137
  Environment:
125
138
 
126
139
  | Variable | Effect |
@@ -145,6 +158,14 @@ npx ccdeck --uninstall
145
158
 
146
159
  Removes every hook entry ccdeck injected from `~/.claude/settings.json`, and `~/.codex/hooks.json` if present.
147
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
+
148
169
  ## Design
149
170
 
150
171
  One canvas. No tabs. No kanban.
package/bin/deck.js CHANGED
@@ -75,14 +75,15 @@ if (flags.uninstall) {
75
75
 
76
76
  const port = Number(flags.port ?? process.env.AGENT_DAG_PORT ?? 4317);
77
77
  // Default = machine-wide (capture every CC session on this box). Pass
78
- // `--workspace <path>` (or `--scope`) to restrict to a single tree.
79
- const workspace = flags.workspace != null
78
+ // `--workspace <path>` (or `--scope`) to restrict to a single tree. Canonicalized
79
+ // just below, once the module that owns that rule is loaded.
80
+ const rawWorkspace = flags.workspace != null
80
81
  ? flags.workspace
81
82
  : (flags.scope ? process.cwd() : "");
82
83
  const openBrowser = flags.noOpen !== true;
83
84
  // The events log lives beside the discovery files, so it follows the Claude
84
85
  // config dir rather than assuming ~/.claude — see src/server/claude-dir.mjs.
85
- const { claudeConfigDir } =
86
+ const { claudeConfigDir, hasClaudeInstalled } =
86
87
  await import(pathToFileURL(join(PKG_ROOT, "src/server/claude-dir.mjs")).href);
87
88
  // Resolved here rather than left as typed: the discovery file publishes this
88
89
  // path so the hook can tell which decks share one log and elect a single
@@ -99,9 +100,18 @@ const { installHooks, keepDiscovery, removeDiscovery, hasCodexInstalled } =
99
100
  // the watcher tails, and the watcher lives in that module. Recomputing the path
100
101
  // here is how the banner came to print ~/.codex/sessions on machines whose
101
102
  // sessions are somewhere else entirely — see the row further down.
102
- const { startServer, hookToken, releaseRestart, CODEX_SESSIONS_DIR } =
103
+ const { startServer, hookToken, releaseRestart, CODEX_SESSIONS_DIR, canonicalWorkspace } =
103
104
  await import(pathToFileURL(join(PKG_ROOT, "src/server/index.mjs")).href);
104
105
 
106
+ // Resolved here rather than left as typed, for the reason the events log above
107
+ // is: the discovery file publishes this path, and the hook that reads it runs in
108
+ // a process whose cwd is the agent's — so a relative `--workspace ./sub` meant
109
+ // one directory to the Codex watcher inside this process and a different one per
110
+ // agent to the hook. One canonical spelling, computed in the one process that
111
+ // knows what the user meant, is what both capture paths compare against. See
112
+ // canonicalWorkspace.
113
+ const workspace = canonicalWorkspace(rawWorkspace);
114
+
105
115
  // Whether the server starts the Codex rollout watcher. Nothing is installed
106
116
  // and no directory is created either way — Codex hooks are not used any more,
107
117
  // so `--codex` only means "watch even though ~/.codex/ is not there yet",
@@ -110,6 +120,25 @@ const wantCodex = flags.noCodex
110
120
  ? false
111
121
  : (flags.codex === true || hasCodexInstalled());
112
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
+
113
142
  const WEB_DIST = join(PKG_ROOT, "dist", "web", "index.html");
114
143
  if (!existsSync(WEB_DIST)) {
115
144
  console.error(`${PRODUCT}: ui not built. run \`npm run build\` (or \`pnpm build\`) first.`);
@@ -229,16 +258,28 @@ async function step(label, work) {
229
258
  * rejection.
230
259
  */
231
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
+
232
270
  // Settings the installer cannot parse are settings it cannot rewrite without
233
271
  // losing them, so it refuses — and that refusal is reported rather than
234
272
  // thrown, because it is the only thing the user can act on.
235
- 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);
236
276
 
237
277
  // claude-swap backs the multi-account panel, and an empty store leaves that
238
278
  // panel useless even when the tool is there — so the account already signed
239
279
  // in is registered once. Bounded inside seedFirstAccount: empty store only,
240
280
  // once ever, never with NO_INSTALL set.
241
281
  const cswap = (async () => {
282
+ if (!wantClaude) return null;
242
283
  const { ensureCswap } = await import(pathToFileURL(join(PKG_ROOT, "src/server/cswap-install.mjs")).href);
243
284
  const cs = await ensureCswap();
244
285
  const usable = cs.state === "present" || cs.state === "installed" || cs.state === "upgrading";
@@ -249,7 +290,10 @@ function startupWork() {
249
290
 
250
291
  // ccusage backs the usage-history modal. Primed at boot rather than on first
251
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.
252
295
  const ccusage = (async () => {
296
+ if (!wantClaude) return null;
253
297
  if (process.env.AGENTS_DECK_NO_INSTALL === "1") return null;
254
298
  const { primeCcusage } = await import(pathToFileURL(join(PKG_ROOT, "src/server/ccusage.mjs")).href);
255
299
  return primeCcusage();
@@ -280,14 +324,22 @@ async function reportStartup(jobs) {
280
324
  }));
281
325
 
282
326
  const hooks = await step(`installing Claude hooks${G.ellipsis}`, jobs.hooks);
283
- 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) {
284
335
  // The file it names is one only the user can repair, and every Claude Code
285
336
  // session on this machine is reading it too.
286
337
  write(row({ mark: G.fail, tone: P.err, label: "Claude hooks", detail: "not installed" }));
287
338
  console.error(`\n ${PRODUCT}: ${hooks.err.message}\n`);
288
339
  process.exit(1);
340
+ } else {
341
+ write(row({ mark: G.ok, label: "Claude hooks", detail: fileLink(hooks.v.hookPath) }));
289
342
  }
290
- write(row({ mark: G.ok, label: "Claude hooks", detail: fileLink(hooks.v.hookPath) }));
291
343
 
292
344
  // Codex CLI hooks never fire on Windows (sandbox refuses to spawn the hook
293
345
  // command). Instead the server tails Codex's rollout JSONL files directly, so
@@ -306,7 +358,13 @@ async function reportStartup(jobs) {
306
358
 
307
359
  const swap = await step(`checking claude-swap${G.ellipsis}`, jobs.cswap);
308
360
  const cs = swap?.cs;
309
- 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") {
310
368
  write(row({ mark: G.ok, label: "claude-swap", detail: `v${cs.version} (accounts panel enabled)` }));
311
369
  } else if (cs?.state === "installed") {
312
370
  write(row({ mark: G.ok, label: "claude-swap", detail: `installed v${cs.version} via ${cs.via}` }));
@@ -450,7 +508,7 @@ function restartTarget() {
450
508
  }
451
509
 
452
510
  const starting = startServer({
453
- port, persist, workspace, codex: wantCodex,
511
+ port, persist, workspace, codex: wantCodex, claude: wantClaude,
454
512
  // Withheld when nothing is supervising us: without a parent, exiting is just
455
513
  // exiting, and /api/restart answers 501 so the UI hides the control.
456
514
  onRestart: SUPERVISED ? requestRestart : null,
@@ -609,6 +667,8 @@ function parseArgs(args) {
609
667
  else if (a === "--history") out.history = args[++i];
610
668
  else if (a === "--codex") out.codex = true;
611
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;
612
672
  }
613
673
  return out;
614
674
  }
@@ -629,8 +689,12 @@ Options:
629
689
  --no-persist Don't write or replay events log (RAM-only)
630
690
  --codex Force-enable Codex capture even if ~/.codex/ missing
631
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
632
694
  --uninstall Remove ${PRODUCT}'s hooks from ~/.claude/settings.json and
633
- ~/.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
634
698
  -h, --help Show this help
635
699
  `);
636
700
  }