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 +22 -1
- package/bin/deck.js +74 -10
- package/dist/web/assets/index-BWurW9Rz.js +70 -0
- package/dist/web/index.html +1 -1
- package/hook/hook.js +64 -21
- package/package.json +1 -1
- package/src/server/claude-dir.mjs +191 -7
- package/src/server/index.mjs +52 -2
- package/src/server/log-writer.mjs +34 -19
- package/src/server/quota.mjs +7 -39
- package/dist/web/assets/index-G1Xt6iC6.js +0 -70
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
|
-
|
|
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 =
|
|
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 (
|
|
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 (
|
|
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
|
}
|