agent-dag 1.30.9 → 1.32.0
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 +39 -4
- package/bin/agent-dag.js +94 -298
- package/bin/deck.js +368 -0
- package/dist/web/assets/index-Bj4ALC2V.js +66 -0
- package/dist/web/assets/index-CAnz1LW4.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/server/index.mjs +49 -2
- package/src/server/self-update.mjs +126 -5
- package/dist/web/assets/index-CEAcuttN.css +0 -1
- package/dist/web/assets/index-bPVurIAI.js +0 -66
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ No config. No install step. Ctrl+C to stop.
|
|
|
28
28
|
- **Workspace filter** — `--scope` limits capture to the current directory; `--workspace <path>` for any subtree
|
|
29
29
|
- **Zero trust step for Codex** — no hook install, no `/hooks` trust prompt; the server tails `~/.codex/sessions/` directly
|
|
30
30
|
- **Version drift warning** — Node caches modules at startup, so a deck upgraded while running keeps executing the old code. The topbar says so, and points at the restart or the upgrade command
|
|
31
|
+
- **One-click update** — when a newer release is on npm, `Update now` installs it in the background and the deck restarts itself as soon as nothing is running. Never runs behind your back, and declines outright where it could do harm
|
|
31
32
|
|
|
32
33
|
## How it works
|
|
33
34
|
|
|
@@ -74,10 +75,44 @@ AGENTS_DECK_NO_UPDATE_CHECK=1 Don't ask npm about releases, but keep everything
|
|
|
74
75
|
AGENTS_DECK_NO_FRESHEN=1 Never nudge claude-swap to collect usage early
|
|
75
76
|
```
|
|
76
77
|
|
|
77
|
-
The update check is one ~20-byte GET to `registry.npmjs.org`, at most once a day
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
The update check is one ~20-byte GET to `registry.npmjs.org`, at most once a day.
|
|
79
|
+
Being told to restart after an upgrade is local only — no network involved — and
|
|
80
|
+
cannot be turned off, because a deck running superseded code is a bug you cannot
|
|
81
|
+
see any other way.
|
|
82
|
+
|
|
83
|
+
### Updating
|
|
84
|
+
|
|
85
|
+
`Update now` runs `npm install -g agents-deck@latest` and nothing else — the
|
|
86
|
+
argument vector is fixed in the server, not taken from the request. When it
|
|
87
|
+
finishes, the newer files on disk trigger the ordinary restart path below.
|
|
88
|
+
|
|
89
|
+
Nothing is ever installed unless you click. The button is replaced by the reason
|
|
90
|
+
when installing would be wrong:
|
|
91
|
+
|
|
92
|
+
| | |
|
|
93
|
+
|---|---|
|
|
94
|
+
| git checkout | your working copy leads npm; pull instead |
|
|
95
|
+
| npx | its cache directory is never upgraded in place |
|
|
96
|
+
| directory not writable | a root-owned global prefix — declined up front rather than failing inside npm |
|
|
97
|
+
| `AGENTS_DECK_NO_INSTALL=1` | you asked for no installs |
|
|
98
|
+
|
|
99
|
+
If npm fails anyway, the banner shows npm's own last line and the command to run
|
|
100
|
+
by hand. The command is always on screen, button or no button.
|
|
101
|
+
|
|
102
|
+
### Restarting
|
|
103
|
+
|
|
104
|
+
`agents-deck` runs as a two-process pair: a supervisor that owns nothing but the
|
|
105
|
+
lifecycle, and the deck itself. When newer code is found on disk, the deck exits
|
|
106
|
+
with code 75 and the supervisor brings it back **on the port it actually bound**,
|
|
107
|
+
which is not always the one it asked for. Ctrl+C, stdout and exit codes behave
|
|
108
|
+
exactly as before — same terminal, same process group.
|
|
109
|
+
|
|
110
|
+
It restarts on its own only once nothing has been running for 30 seconds: hook
|
|
111
|
+
events are fire-and-forget, so anything fired during the gap is lost. Turn that
|
|
112
|
+
off with the toggle in the banner and use the button instead; the preference is
|
|
113
|
+
per-browser. Under `--no-persist` restarting is refused outright, by the server
|
|
114
|
+
and not just by the UI — with no event log there is nothing to replay, and the
|
|
115
|
+
canvas would be gone.
|
|
81
116
|
|
|
82
117
|
## Uninstall
|
|
83
118
|
|
package/bin/agent-dag.js
CHANGED
|
@@ -1,304 +1,100 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
blue: tty ? "\x1b[34m" : "",
|
|
74
|
-
magenta: tty ? "\x1b[35m" : "",
|
|
75
|
-
yellow: tty ? "\x1b[33m" : "",
|
|
76
|
-
green: tty ? "\x1b[32m" : "",
|
|
77
|
-
white: tty ? "\x1b[97m" : "",
|
|
78
|
-
bCyan: tty ? "\x1b[96m" : "",
|
|
79
|
-
bMag: tty ? "\x1b[95m" : "",
|
|
80
|
-
};
|
|
81
|
-
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
82
|
-
|
|
83
|
-
// ── Animated banner ───────────────────────────────────────────────────────────
|
|
84
|
-
async function printBanner() {
|
|
85
|
-
// figlet slant font — hardcoded, no runtime dep
|
|
86
|
-
const ART = [
|
|
87
|
-
' __ __ __ ',
|
|
88
|
-
' ____ _____ ____ ____ / /______ ____/ /__ _____/ /__',
|
|
89
|
-
' / __ `/ __ `/ _ \\/ __ \\/ __/ ___/_____/ __ / _ \\/ ___/ //_/',
|
|
90
|
-
'/ /_/ / /_/ / __/ / / / /_(__ )_____/ /_/ / __/ /__/ ,< ',
|
|
91
|
-
'\\__,_/\\__, /\\___/_/ /_/\\__/____/ \\__,_/\\___/\\___/_/|_| ',
|
|
92
|
-
' /____/ ',
|
|
93
|
-
];
|
|
94
|
-
const COLORS = [C.dim, C.blue, C.cyan, C.bCyan, C.magenta, C.dim];
|
|
95
|
-
|
|
96
|
-
process.stdout.write('\n');
|
|
97
|
-
|
|
98
|
-
if (tty) {
|
|
99
|
-
const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
100
|
-
for (let i = 0; i < 8; i++) {
|
|
101
|
-
process.stdout.write(`\r ${C.bCyan}${frames[i % frames.length]}${C.reset} ${C.dim}loading…${C.reset}`);
|
|
102
|
-
await sleep(70);
|
|
103
|
-
}
|
|
104
|
-
process.stdout.write('\r' + ' '.repeat(28) + '\n');
|
|
105
|
-
await sleep(40);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
for (let i = 0; i < ART.length; i++) {
|
|
109
|
-
process.stdout.write(` ${COLORS[i]}${ART[i]}${C.reset}\n`);
|
|
110
|
-
if (tty) await sleep(38);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
process.stdout.write(`\n ${C.dim}v${PKG_VERSION} · live agent DAG · Claude Code + Codex${C.reset}\n\n`);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ── Spinner ───────────────────────────────────────────────────────────────────
|
|
117
|
-
function spinner(label) {
|
|
118
|
-
if (!tty) { process.stdout.write(` … ${label}\n`); return { stop: (ok, msg) => process.stdout.write(` ${ok ? "✓" : "✗"} ${msg}\n`) }; }
|
|
119
|
-
const frames = ["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"];
|
|
120
|
-
let i = 0;
|
|
121
|
-
const iv = setInterval(() => {
|
|
122
|
-
process.stdout.write(`\r ${C.cyan}${frames[i++ % frames.length]}${C.reset} ${label}`);
|
|
123
|
-
}, 80);
|
|
124
|
-
return {
|
|
125
|
-
stop(ok, msg) {
|
|
126
|
-
clearInterval(iv);
|
|
127
|
-
const icon = ok ? `${C.green}✓${C.reset}` : `${C.yellow}✗${C.reset}`;
|
|
128
|
-
process.stdout.write(`\r ${icon} ${msg}\n`);
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
await printBanner();
|
|
134
|
-
|
|
135
|
-
// ── Startup steps ─────────────────────────────────────────────────────────────
|
|
136
|
-
process.stdout.write(` ${C.dim}workspace :${C.reset} ${workspace === "" ? C.yellow + "(all)" + C.reset : workspace}\n`);
|
|
137
|
-
|
|
138
|
-
let sp = spinner("installing Claude hooks…");
|
|
139
|
-
const claudeInstall = await installHooks({ provider: "claude" });
|
|
140
|
-
sp.stop(true, `Claude hooks ${C.dim}→ ${claudeInstall.hookPath}${C.reset}`);
|
|
141
|
-
|
|
142
|
-
// Codex CLI hooks never fire on Windows (sandbox refuses to spawn the hook
|
|
143
|
-
// command). Instead the server tails Codex's rollout JSONL files directly, so
|
|
144
|
-
// there's nothing to install and no /hooks trust step. We just confirm Codex
|
|
145
|
-
// is present and let the watcher pick up sessions.
|
|
146
|
-
if (wantCodex) {
|
|
147
|
-
process.stdout.write(` ${C.green}✓${C.reset} Codex sessions ${C.dim}→ watching ${join(homedir(), ".codex", "sessions")}${C.reset}\n`);
|
|
148
|
-
} else {
|
|
149
|
-
process.stdout.write(` ${C.dim}Codex watch skipped (no ~/.codex/, or --no-codex)${C.reset}\n`);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// claude-swap backs the multi-account panel. Installing it touches the user's
|
|
153
|
-
// global tool path, so unlike the ccusage install this one announces itself.
|
|
154
|
-
{
|
|
155
|
-
const { ensureCswap } = await import(pathToFileURL(join(PKG_ROOT, "src/server/cswap-install.mjs")).href);
|
|
156
|
-
const csp = spinner("checking claude-swap…");
|
|
157
|
-
const cs = await ensureCswap();
|
|
158
|
-
if (cs.state === "present") {
|
|
159
|
-
csp.stop(true, `claude-swap ${C.dim}→ v${cs.version} (accounts panel enabled)${C.reset}`);
|
|
160
|
-
} else if (cs.state === "installed") {
|
|
161
|
-
csp.stop(true, `claude-swap ${C.dim}→ installed v${cs.version} via ${cs.via}${C.reset}`);
|
|
162
|
-
} else if (cs.state === "upgrading") {
|
|
163
|
-
csp.stop(true, `claude-swap ${C.dim}→ v${cs.version}, upgrading to v${cs.latest} in background${C.reset}`);
|
|
164
|
-
} else if (cs.state === "skipped") {
|
|
165
|
-
csp.stop(true, `claude-swap ${C.dim}not installed (AGENTS_DECK_NO_INSTALL=1)${C.reset}`);
|
|
166
|
-
} else {
|
|
167
|
-
const how = cs.reason === "no_installer"
|
|
168
|
-
? "not installed — the accounts panel needs it"
|
|
169
|
-
: cs.reason === "not_on_path"
|
|
170
|
-
? `installed via ${cs.via} but not on PATH — add ${
|
|
171
|
-
process.platform === "win32" ? "%USERPROFILE%\\.local\\bin" : "~/.local/bin"
|
|
172
|
-
}`
|
|
173
|
-
: `install failed via ${cs.via}`;
|
|
174
|
-
csp.stop(false, `claude-swap ${C.dim}${how}${C.reset}`);
|
|
175
|
-
// A URL is not an answer when someone just wants the panel to work. Print
|
|
176
|
-
// the command for THIS machine, picked from what is already on it.
|
|
177
|
-
if (cs.hint) process.stdout.write(` ${C.dim}${cs.hint}${C.reset}\n`);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// A working claude-swap with an empty store still leaves the panel useless,
|
|
181
|
-
// so the account already signed in is registered once. Bounded inside
|
|
182
|
-
// seedFirstAccount: empty store only, once ever, never with NO_INSTALL set.
|
|
183
|
-
if (cs.state === "present" || cs.state === "installed" || cs.state === "upgrading") {
|
|
184
|
-
const { seedFirstAccount } = await import(pathToFileURL(join(PKG_ROOT, "src/server/claude-accounts.mjs")).href);
|
|
185
|
-
const seed = await seedFirstAccount().catch(() => ({ state: "failed" }));
|
|
186
|
-
if (seed.state === "added") {
|
|
187
|
-
process.stdout.write(` ${C.green}✓${C.reset} accounts ${C.dim}registered the signed-in account (cswap add)${C.reset}\n`);
|
|
188
|
-
} else if (seed.state === "failed" || seed.state === "nothing-to-add") {
|
|
189
|
-
process.stdout.write(` ${C.dim} accounts panel empty — sign in to Claude Code, then run cswap add${C.reset}\n`);
|
|
2
|
+
// Supervisor. Owns one thing: the worker's lifecycle.
|
|
3
|
+
//
|
|
4
|
+
// Why this exists at all: Node caches every module at import, so a deck that is
|
|
5
|
+
// running when an upgrade lands keeps executing the old code until the process
|
|
6
|
+
// is replaced. The deck can now see that (GET /api/version) — this is the half
|
|
7
|
+
// that can act on it.
|
|
8
|
+
//
|
|
9
|
+
// The tempting shortcut is to have the server respawn itself and exit. Every
|
|
10
|
+
// version of that is worse than it looks:
|
|
11
|
+
// • the replacement races the dying listener, and startServer answers
|
|
12
|
+
// EADDRINUSE by binding one of ten RANDOM ports in 4318–4400 — so the tab
|
|
13
|
+
// you are looking at reconnects forever next to a healthy invisible server;
|
|
14
|
+
// • an orphan spawned from a dying parent leaves the shell's foreground
|
|
15
|
+
// process group, so Ctrl+C stops reaching it;
|
|
16
|
+
// • with stdio ignored it also loses the banner, the URL line and every
|
|
17
|
+
// console.error the server writes.
|
|
18
|
+
// A parent that stays alive avoids all three: the child is dead — and its
|
|
19
|
+
// listening socket released — before the next one is spawned, stdio is
|
|
20
|
+
// inherited so the terminal is unchanged, and Ctrl+C keeps working because the
|
|
21
|
+
// process group never changes.
|
|
22
|
+
//
|
|
23
|
+
// Everything else the deck does still lives in bin/deck.js. This file must stay
|
|
24
|
+
// boring: it is the one process that is never replaced.
|
|
25
|
+
import { spawn } from "node:child_process";
|
|
26
|
+
import { dirname, join } from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
|
|
29
|
+
// Chosen because it means nothing else here: the worker exits 0 normally and
|
|
30
|
+
// non-zero on failure, both of which must pass straight through.
|
|
31
|
+
const RESTART_CODE = 75;
|
|
32
|
+
const WORKER = join(dirname(fileURLToPath(import.meta.url)), "deck.js");
|
|
33
|
+
|
|
34
|
+
// The port the worker actually bound, which is not necessarily the one it was
|
|
35
|
+
// asked for — the first launch falls back to a random port when 4317 is taken.
|
|
36
|
+
// Re-launching without this is how a restart silently moves the deck out from
|
|
37
|
+
// under an open tab.
|
|
38
|
+
let boundPort = null;
|
|
39
|
+
let restarts = 0;
|
|
40
|
+
let child = null;
|
|
41
|
+
|
|
42
|
+
function launch(respawn) {
|
|
43
|
+
const args = [WORKER, ...process.argv.slice(2)];
|
|
44
|
+
// Appended last so it wins: the worker's parser keeps the final --port.
|
|
45
|
+
if (respawn && boundPort != null) args.push("--port", String(boundPort));
|
|
46
|
+
|
|
47
|
+
child = spawn(process.execPath, args, {
|
|
48
|
+
// stdio inherited so the child owns the same terminal the user started:
|
|
49
|
+
// same banner, same colours, same Ctrl+C. The fourth slot adds an IPC
|
|
50
|
+
// channel — the only way the worker can tell us which port it got, since
|
|
51
|
+
// parsing its stdout would be guesswork.
|
|
52
|
+
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
|
53
|
+
env: {
|
|
54
|
+
...process.env,
|
|
55
|
+
// Boot did the slow, once-per-session work already (hook install, the
|
|
56
|
+
// claude-swap probe with its 8s timeout, the ccusage prime). Repeating it
|
|
57
|
+
// is what would make a restart feel like a restart.
|
|
58
|
+
AGENTS_DECK_RESPAWN: respawn ? "1" : "",
|
|
59
|
+
AGENTS_DECK_RESTARTS: String(restarts),
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
child.on("message", (m) => {
|
|
64
|
+
if (m && m.type === "listening" && typeof m.port === "number") boundPort = m.port;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
child.on("exit", (code, signal) => {
|
|
68
|
+
child = null;
|
|
69
|
+
if (code === RESTART_CODE) {
|
|
70
|
+
restarts++;
|
|
71
|
+
launch(true);
|
|
72
|
+
return;
|
|
190
73
|
}
|
|
191
|
-
|
|
74
|
+
// Anything else is the worker's own verdict and belongs to whoever started
|
|
75
|
+
// us — including the ccdeck wrapper, which exits with our code in turn.
|
|
76
|
+
if (signal) process.kill(process.pid, signal);
|
|
77
|
+
else process.exit(code ?? 0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
child.on("error", (err) => {
|
|
81
|
+
console.error(`agents-deck: could not start ${WORKER}: ${err.message}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
});
|
|
192
84
|
}
|
|
193
85
|
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
86
|
+
// Ctrl+C already reaches the child directly — it shares this process group — so
|
|
87
|
+
// forwarding would deliver it twice. These handlers exist only to keep the
|
|
88
|
+
// supervisor alive long enough for the child's own graceful shutdown to run and
|
|
89
|
+
// for its exit code to arrive.
|
|
90
|
+
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
91
|
+
process.on(sig, () => {
|
|
92
|
+
// On Windows none of these are delivered to a Node process, which is fine:
|
|
93
|
+
// there the console kills the whole tree and sweepStaleDiscovery cleans up
|
|
94
|
+
// on the next boot.
|
|
95
|
+
if (child) { try { child.kill(sig); } catch { /* already gone */ } }
|
|
96
|
+
else process.exit(0);
|
|
97
|
+
});
|
|
202
98
|
}
|
|
203
99
|
|
|
204
|
-
|
|
205
|
-
// Started here and collected below so the lookup overlaps the rest of boot, and
|
|
206
|
-
// hard-capped so a slow registry cannot delay the server — the answer is
|
|
207
|
-
// usually already cached in ~/.agents-deck/.self-update-check anyway. It has to
|
|
208
|
-
// resolve BEFORE the pulse indicator starts writing over the last line.
|
|
209
|
-
const selfCheck = import(pathToFileURL(join(PKG_ROOT, "src/server/self-update.mjs")).href)
|
|
210
|
-
.then(m => m.versionReport({ running: PKG_VERSION, pkgRoot: PKG_ROOT }))
|
|
211
|
-
.catch(() => null);
|
|
212
|
-
const upgrade = await Promise.race([
|
|
213
|
-
selfCheck.then(r => r?.notice?.kind === "upgrade" ? r : null),
|
|
214
|
-
new Promise(r => setTimeout(() => r(null), 1200)),
|
|
215
|
-
]);
|
|
216
|
-
if (upgrade) {
|
|
217
|
-
process.stdout.write(
|
|
218
|
-
` ${C.yellow}↑${C.reset} update ${C.dim}v${upgrade.notice.to} available — ${C.reset}${C.yellow}${upgrade.command}${C.reset}\n`,
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
sp = spinner("starting server…");
|
|
223
|
-
const server = await startServer({ port, persist, workspace, codex: wantCodex }).catch(err => {
|
|
224
|
-
sp.stop(false, `server failed: ${err.message}`);
|
|
225
|
-
process.exit(1);
|
|
226
|
-
});
|
|
227
|
-
const addr = server.address();
|
|
228
|
-
const realPort = typeof addr === "object" && addr ? addr.port : port;
|
|
229
|
-
const url = `http://127.0.0.1:${realPort}`;
|
|
230
|
-
sp.stop(true, `server ready ${C.dim}→ ${C.reset}${C.bCyan}${C.bold}${url}${C.reset}`);
|
|
231
|
-
|
|
232
|
-
if (persist) process.stdout.write(` ${C.dim}log : ${persist}${C.reset}\n`);
|
|
233
|
-
|
|
234
|
-
process.stdout.write(`\n ${C.green}${C.bold}▶ opening browser…${C.reset}\n\n`);
|
|
235
|
-
|
|
236
|
-
const discoveryFile = await writeDiscovery({ port: realPort, workspace });
|
|
237
|
-
|
|
238
|
-
if (openBrowser) {
|
|
239
|
-
try {
|
|
240
|
-
const { default: open } = await import("open");
|
|
241
|
-
await open(url);
|
|
242
|
-
} catch {}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// ── Pulse indicator ───────────────────────────────────────────────────────────
|
|
246
|
-
if (tty) {
|
|
247
|
-
const pulseFrames = [`${C.green}●${C.reset}`, `${C.dim}●${C.reset}`];
|
|
248
|
-
let pi = 0;
|
|
249
|
-
setInterval(() => {
|
|
250
|
-
process.stdout.write(`\r ${pulseFrames[pi++ % 2]} ${C.dim}listening — Ctrl+C to stop${C.reset} `);
|
|
251
|
-
}, 800).unref();
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const shutdown = async () => {
|
|
255
|
-
if (tty) process.stdout.write(`\n\n ${C.yellow}◉ shutting down…${C.reset}\n`);
|
|
256
|
-
await removeDiscovery(discoveryFile);
|
|
257
|
-
server.close(() => process.exit(0));
|
|
258
|
-
setTimeout(() => process.exit(0), 1500).unref();
|
|
259
|
-
};
|
|
260
|
-
process.on("SIGINT", shutdown);
|
|
261
|
-
process.on("SIGTERM", shutdown);
|
|
262
|
-
process.on("beforeExit", () => removeDiscovery(discoveryFile));
|
|
263
|
-
|
|
264
|
-
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
265
|
-
|
|
266
|
-
function parseArgs(args) {
|
|
267
|
-
const out = {};
|
|
268
|
-
for (let i = 0; i < args.length; i++) {
|
|
269
|
-
const a = args[i];
|
|
270
|
-
if (a === "-h" || a === "--help") out.help = true;
|
|
271
|
-
else if (a === "-p" || a === "--port") out.port = args[++i];
|
|
272
|
-
else if (a === "--no-open") out.noOpen = true;
|
|
273
|
-
else if (a === "--uninstall") out.uninstall = true;
|
|
274
|
-
else if (a === "--workspace") out.workspace = args[++i];
|
|
275
|
-
else if (a === "--scope") out.scope = true;
|
|
276
|
-
else if (a === "--all") out.all = true; // legacy no-op (now default)
|
|
277
|
-
else if (a === "--no-persist") out.noPersist = true;
|
|
278
|
-
else if (a === "--history") out.history = args[++i];
|
|
279
|
-
else if (a === "--codex") out.codex = true;
|
|
280
|
-
else if (a === "--no-codex") out.noCodex = true;
|
|
281
|
-
}
|
|
282
|
-
return out;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function printHelp() {
|
|
286
|
-
process.stdout.write(`agents-deck — live deck of Claude Code + Codex agents
|
|
287
|
-
|
|
288
|
-
Usage:
|
|
289
|
-
agents-deck [options]
|
|
290
|
-
|
|
291
|
-
Options:
|
|
292
|
-
-p, --port <number> Preferred port (default: 4317; falls back to random 4318–4400)
|
|
293
|
-
--no-open Don't open the browser automatically
|
|
294
|
-
--workspace <path> Only capture sessions whose cwd is inside <path>
|
|
295
|
-
--scope Restrict to current working directory
|
|
296
|
-
--all Capture every session (default)
|
|
297
|
-
--history <path> Override events log file (default: ~/.claude/agent-dag/events.jsonl)
|
|
298
|
-
--no-persist Don't write or replay events log (RAM-only)
|
|
299
|
-
--codex Force-enable Codex capture even if ~/.codex/ missing
|
|
300
|
-
--no-codex Skip Codex capture (Claude only)
|
|
301
|
-
--uninstall Remove agents-deck Claude hook entries
|
|
302
|
-
-h, --help Show this help
|
|
303
|
-
`);
|
|
304
|
-
}
|
|
100
|
+
launch(false);
|