agent-dag 1.15.6 → 1.17.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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Bargan Constantin
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bargan Constantin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,52 +1,59 @@
1
- # agent-dag
2
-
3
- Live DAG of Claude Code agents. Watch parallel subagents fork, call tools, and return — all on one calm canvas.
4
-
5
- ## Run
6
-
7
- ```bash
8
- npx agent-dag
9
- ```
10
-
11
- Opens http://127.0.0.1:4317 (or a random port in 4318–4400 if 4317 is taken). Start a Claude Code session in any directory and watch the graph fill in.
12
-
13
- ## Options
14
-
15
- ```
16
- -p, --port <number> Preferred port (default: 4317; falls back to random 4318–4400)
17
- --no-open Don't open the browser automatically
18
- --workspace <path> Workspace root to filter events (default: cwd)
19
- --all Capture sessions from all workspaces (machine-wide)
20
- --history <path> Override events log file (default: ~/.claude/ccgraph/events.jsonl)
21
- --no-persist RAM-only mode, no log file
22
- --uninstall Remove agent-dag hooks from ~/.claude/settings.json
23
- -h, --help Show help
24
- ```
25
-
26
- ## Design
27
-
28
- - One canvas. No tabs. No kanban.
29
- - Node = agent (root session, subagent).
30
- - Edge = parent → child (spawn) or agent → tool (call).
31
- - In-flight edges animate; settled edges fade.
32
- - Click a node for details.
33
-
34
- ## How it works
35
-
36
- `agent-dag` registers a hook script in `~/.claude/settings.json` for these Claude Code events:
37
-
38
- `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `SubagentStart`, `SubagentStop`, `Stop`, `SessionEnd`, `Notification`.
39
-
40
- The hook forwards the event JSON to the running server, which streams it to the browser via SSE.
41
-
42
- ## Uninstall
43
-
44
- ```bash
45
- npx agent-dag --uninstall
46
- ```
47
-
48
- Removes all hooks from `~/.claude/settings.json`.
49
-
50
- ## Status
51
-
52
- Pre-alpha. Names, ports, and event shapes may change.
1
+ # agent-dag
2
+
3
+ Live DAG of Claude Code **and OpenAI Codex** agents. Watch parallel subagents fork, call tools, and return — all on one calm canvas.
4
+
5
+ ## Run
6
+
7
+ ```bash
8
+ npx agent-dag
9
+ ```
10
+
11
+ Opens http://127.0.0.1:4317 (or a random port in 4318–4400 if 4317 is taken). Start a Claude Code **or Codex** session in any directory and watch the graph fill in. Both providers render side by side on the same canvas; the model chip on each node tells them apart (e.g. `Opus 4.8`, `GPT-5.5`).
12
+
13
+ ## Options
14
+
15
+ ```
16
+ -p, --port <number> Preferred port (default: 4317; falls back to random 4318–4400)
17
+ --no-open Don't open the browser automatically
18
+ --workspace <path> Only capture sessions whose cwd is inside <path>
19
+ --scope Restrict to the current working directory
20
+ --all Capture sessions from all workspaces (machine-wide; default)
21
+ --codex Force-enable Codex capture even if ~/.codex/ is missing
22
+ --no-codex Skip Codex capture (Claude only)
23
+ --history <path> Override events log file (default: ~/.claude/agent-dag/events.jsonl)
24
+ --no-persist RAM-only mode, no log file
25
+ --uninstall Remove agent-dag hooks from ~/.claude/settings.json
26
+ -h, --help Show help
27
+ ```
28
+
29
+ ## Design
30
+
31
+ - One canvas. No tabs. No kanban.
32
+ - Node = agent (root session, subagent).
33
+ - Edge = parent → child (spawn) or agent → tool (call).
34
+ - In-flight edges animate; settled edges fade.
35
+ - Click a node for details.
36
+
37
+ ## How it works
38
+
39
+ Two providers, two capture paths — both stream to the same server, which pushes events to the browser over SSE.
40
+
41
+ **Claude Code** — `agent-dag` registers a hook script in `~/.claude/settings.json` for these events:
42
+
43
+ `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `SubagentStart`, `SubagentStop`, `Stop`, `SessionEnd`, `Notification`.
44
+
45
+ The hook forwards the event JSON to the running server.
46
+
47
+ **OpenAI Codex** — Codex CLI hooks don't fire reliably on Windows (the sandbox refuses to spawn the hook command), so instead the server tails Codex's session rollout files at `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl` and reconstructs the same event stream (session start, prompts, tool calls, token usage, model). No hook install and no `/hooks` trust step is needed — just run a Codex session and it shows up. Honors `CODEX_HOME` if set.
48
+
49
+ ## Uninstall
50
+
51
+ ```bash
52
+ npx agent-dag --uninstall
53
+ ```
54
+
55
+ Removes all hooks from `~/.claude/settings.json`.
56
+
57
+ ## Status
58
+
59
+ Pre-alpha. Names, ports, and event shapes may change.
package/bin/agent-dag.js CHANGED
@@ -1,199 +1,227 @@
1
- #!/usr/bin/env node
2
- // agent-dag CLI entrypoint. Registers hooks, starts server, opens browser.
3
- import { resolve, dirname, join } from "node:path";
4
- import { homedir } from "node:os";
5
- import { fileURLToPath, pathToFileURL } from "node:url";
6
- import { existsSync, readFileSync } from "node:fs";
7
-
8
- const __dirname = dirname(fileURLToPath(import.meta.url));
9
- const PKG_ROOT = resolve(__dirname, "..");
10
- const PKG_VERSION = (() => {
11
- try { return JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8")).version ?? "0.0.0"; }
12
- catch { return "0.0.0"; }
13
- })();
14
-
15
- const argv = process.argv.slice(2);
16
- const flags = parseArgs(argv);
17
-
18
- if (flags.help) {
19
- printHelp();
20
- process.exit(0);
21
- }
22
-
23
- if (flags.uninstall) {
24
- const { uninstallHooks } = await import(pathToFileURL(join(PKG_ROOT, "src/server/installer.mjs")).href);
25
- const r = await uninstallHooks();
26
- console.log(r.changed ? "agent-dag: hooks removed from ~/.claude/settings.json" : "agent-dag: no hooks to remove");
27
- process.exit(0);
28
- }
29
-
30
- const port = Number(flags.port ?? process.env.AGENT_DAG_PORT ?? 4317);
31
- // Default = machine-wide (capture every CC session on this box). Pass
32
- // `--workspace <path>` (or `--scope`) to restrict to a single tree.
33
- const workspace = flags.workspace != null
34
- ? flags.workspace
35
- : (flags.scope ? process.cwd() : "");
36
- const openBrowser = flags.noOpen !== true;
37
- const persist = flags.noPersist
38
- ? null
39
- : (flags.history ?? join(homedir(), ".claude", "agent-dag", "events.jsonl"));
40
-
41
- const { installHooks, writeDiscovery, removeDiscovery } =
42
- await import(pathToFileURL(join(PKG_ROOT, "src/server/installer.mjs")).href);
43
- const { startServer } =
44
- await import(pathToFileURL(join(PKG_ROOT, "src/server/index.mjs")).href);
45
-
46
- const WEB_DIST = join(PKG_ROOT, "dist", "web", "index.html");
47
- if (!existsSync(WEB_DIST)) {
48
- console.error("agent-dag: ui not built. run `npm run build` (or `pnpm build`) first.");
49
- process.exit(1);
50
- }
51
-
52
- // ── ANSI helpers ──────────────────────────────────────────────────────────────
53
- const tty = process.stdout.isTTY;
54
- const C = {
55
- reset: tty ? "\x1b[0m" : "",
56
- bold: tty ? "\x1b[1m" : "",
57
- dim: tty ? "\x1b[2m" : "",
58
- cyan: tty ? "\x1b[36m" : "",
59
- blue: tty ? "\x1b[34m" : "",
60
- magenta: tty ? "\x1b[35m" : "",
61
- yellow: tty ? "\x1b[33m" : "",
62
- green: tty ? "\x1b[32m" : "",
63
- white: tty ? "\x1b[97m" : "",
64
- bCyan: tty ? "\x1b[96m" : "",
65
- bMag: tty ? "\x1b[95m" : "",
66
- };
67
- const sleep = ms => new Promise(r => setTimeout(r, ms));
68
-
69
- // ── Animated banner ───────────────────────────────────────────────────────────
70
- async function printBanner() {
71
- const IW = 38; // inner visible width between ║ chars
72
- const pad = (visLen) => " ".repeat(Math.max(0, IW - visLen));
73
- const HR = "".repeat(IW);
74
-
75
- // Each entry: [rendered string with ANSI, visible char count, left-border color, right-border color]
76
- const rows = [
77
- { l: ` ${C.bCyan}${C.bold}◉${C.reset} ${C.white}${C.bold}agent-dag${C.reset} ${C.dim}v${PKG_VERSION}${C.reset}`, vis: 17 + PKG_VERSION.length, lc: C.blue, rc: C.blue },
78
- { l: ` ${C.dim}live DAG · Claude Code agents${C.reset}`, vis: 31, lc: C.magenta, rc: C.magenta },
79
- { l: ` ${C.yellow}watch agents fork ${C.cyan}→${C.reset} ${C.green}tools fire${C.reset}`, vis: 34, lc: C.bMag, rc: C.bMag },
80
- ];
81
-
82
- const lines = [
83
- "",
84
- ` ${C.cyan}${C.bold}╔${HR}╗${C.reset}`,
85
- ...rows.map(r => ` ${r.lc}${C.bold}║${C.reset}${r.l}${pad(r.vis)}${r.rc}${C.bold}║${C.reset}`),
86
- ` ${C.cyan}${C.bold}╚${HR}╝${C.reset}`,
87
- "",
88
- ];
89
- for (const line of lines) {
90
- process.stdout.write(line + "\n");
91
- if (tty) await sleep(45);
92
- }
93
- }
94
-
95
- // ── Spinner ───────────────────────────────────────────────────────────────────
96
- function spinner(label) {
97
- if (!tty) { process.stdout.write(` … ${label}\n`); return { stop: (ok, msg) => process.stdout.write(` ${ok ? "" : "✗"} ${msg}\n`) }; }
98
- const frames = ["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"];
99
- let i = 0;
100
- const iv = setInterval(() => {
101
- process.stdout.write(`\r ${C.cyan}${frames[i++ % frames.length]}${C.reset} ${label}`);
102
- }, 80);
103
- return {
104
- stop(ok, msg) {
105
- clearInterval(iv);
106
- const icon = ok ? `${C.green}✓${C.reset}` : `${C.yellow}✗${C.reset}`;
107
- process.stdout.write(`\r ${icon} ${msg}\n`);
108
- }
109
- };
110
- }
111
-
112
- await printBanner();
113
-
114
- // ── Startup steps ─────────────────────────────────────────────────────────────
115
- process.stdout.write(` ${C.dim}workspace :${C.reset} ${workspace === "" ? C.yellow + "(all)" + C.reset : workspace}\n`);
116
-
117
- let sp = spinner("installing hooks…");
118
- const { settingsPath, hookPath } = await installHooks();
119
- sp.stop(true, `hooks installed ${C.dim}→ ${hookPath}${C.reset}`);
120
-
121
- sp = spinner("starting server…");
122
- const server = await startServer({ port, persist }).catch(err => {
123
- sp.stop(false, `server failed: ${err.message}`);
124
- process.exit(1);
125
- });
126
- const addr = server.address();
127
- const realPort = typeof addr === "object" && addr ? addr.port : port;
128
- const url = `http://127.0.0.1:${realPort}`;
129
- sp.stop(true, `server ready ${C.dim} ${C.reset}${C.bCyan}${C.bold}${url}${C.reset}`);
130
-
131
- if (persist) process.stdout.write(` ${C.dim}log : ${persist}${C.reset}\n`);
132
-
133
- process.stdout.write(`\n ${C.green}${C.bold}▶ opening browser…${C.reset}\n\n`);
134
-
135
- const discoveryFile = await writeDiscovery({ port: realPort, workspace });
136
-
137
- if (openBrowser) {
138
- try {
139
- const { default: open } = await import("open");
140
- await open(url);
141
- } catch {}
142
- }
143
-
144
- // ── Pulse indicator ───────────────────────────────────────────────────────────
145
- if (tty) {
146
- const pulseFrames = [`${C.green}●${C.reset}`, `${C.dim}●${C.reset}`];
147
- let pi = 0;
148
- setInterval(() => {
149
- process.stdout.write(`\r ${pulseFrames[pi++ % 2]} ${C.dim}listening — Ctrl+C to stop${C.reset} `);
150
- }, 800).unref();
151
- }
152
-
153
- const shutdown = async () => {
154
- if (tty) process.stdout.write(`\n\n ${C.yellow}◉ shutting down…${C.reset}\n`);
155
- await removeDiscovery(discoveryFile);
156
- server.close(() => process.exit(0));
157
- setTimeout(() => process.exit(0), 1500).unref();
158
- };
159
- process.on("SIGINT", shutdown);
160
- process.on("SIGTERM", shutdown);
161
- process.on("beforeExit", () => removeDiscovery(discoveryFile));
162
-
163
- // ── helpers ───────────────────────────────────────────────────────────────────
164
-
165
- function parseArgs(args) {
166
- const out = {};
167
- for (let i = 0; i < args.length; i++) {
168
- const a = args[i];
169
- if (a === "-h" || a === "--help") out.help = true;
170
- else if (a === "-p" || a === "--port") out.port = args[++i];
171
- else if (a === "--no-open") out.noOpen = true;
172
- else if (a === "--uninstall") out.uninstall = true;
173
- else if (a === "--workspace") out.workspace = args[++i];
174
- else if (a === "--scope") out.scope = true;
175
- else if (a === "--all") out.all = true; // legacy no-op (now default)
176
- else if (a === "--no-persist") out.noPersist = true;
177
- else if (a === "--history") out.history = args[++i];
178
- }
179
- return out;
180
- }
181
-
182
- function printHelp() {
183
- process.stdout.write(`agent-dag — live DAG of Claude Code agents
184
-
185
- Usage:
186
- agent-dag [options]
187
-
188
- Options:
189
- -p, --port <number> Preferred port (default: 4317; falls back to random 4318–4400)
190
- --no-open Don't open the browser automatically
191
- --workspace <path> Only capture sessions whose cwd is inside <path>
192
- --scope Restrict to current working directory
193
- --all Capture every Claude Code session (default)
194
- --history <path> Override events log file (default: ~/.claude/agent-dag/events.jsonl)
195
- --no-persist Don't write or replay events log (RAM-only)
196
- --uninstall Remove agent-dag hook entries from ~/.claude/settings.json
197
- -h, --help Show this help
198
- `);
199
- }
1
+ #!/usr/bin/env node
2
+ // agent-dag CLI entrypoint. Registers hooks, starts server, opens browser.
3
+ import { resolve, dirname, join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { existsSync, readFileSync } from "node:fs";
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const PKG_ROOT = resolve(__dirname, "..");
10
+ const PKG_VERSION = (() => {
11
+ try { return JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8")).version ?? "0.0.0"; }
12
+ catch { return "0.0.0"; }
13
+ })();
14
+
15
+ const argv = process.argv.slice(2);
16
+ const flags = parseArgs(argv);
17
+
18
+ if (flags.help) {
19
+ printHelp();
20
+ process.exit(0);
21
+ }
22
+
23
+ if (flags.uninstall) {
24
+ const { uninstallHooks, hasCodexInstalled } = await import(pathToFileURL(join(PKG_ROOT, "src/server/installer.mjs")).href);
25
+ const claude = await uninstallHooks({ provider: "claude" });
26
+ console.log(claude.changed
27
+ ? `agent-dag: hooks removed from ${claude.settingsPath}`
28
+ : "agent-dag: no Claude hooks to remove");
29
+ if (hasCodexInstalled()) {
30
+ const codex = await uninstallHooks({ provider: "codex" });
31
+ console.log(codex.changed
32
+ ? `agent-dag: hooks removed from ${codex.settingsPath}`
33
+ : "agent-dag: no Codex hooks to remove");
34
+ }
35
+ process.exit(0);
36
+ }
37
+
38
+ const port = Number(flags.port ?? process.env.AGENT_DAG_PORT ?? 4317);
39
+ // Default = machine-wide (capture every CC session on this box). Pass
40
+ // `--workspace <path>` (or `--scope`) to restrict to a single tree.
41
+ const workspace = flags.workspace != null
42
+ ? flags.workspace
43
+ : (flags.scope ? process.cwd() : "");
44
+ const openBrowser = flags.noOpen !== true;
45
+ const persist = flags.noPersist
46
+ ? null
47
+ : (flags.history ?? join(homedir(), ".claude", "agent-dag", "events.jsonl"));
48
+
49
+ const { installHooks, writeDiscovery, removeDiscovery, hasCodexInstalled } =
50
+ await import(pathToFileURL(join(PKG_ROOT, "src/server/installer.mjs")).href);
51
+ const { startServer } =
52
+ await import(pathToFileURL(join(PKG_ROOT, "src/server/index.mjs")).href);
53
+
54
+ // Codex hooks install when ~/.codex/ exists, unless --no-codex was passed.
55
+ // --codex forces install even if the dir is missing (creates it).
56
+ const wantCodex = flags.noCodex
57
+ ? false
58
+ : (flags.codex === true || hasCodexInstalled());
59
+
60
+ const WEB_DIST = join(PKG_ROOT, "dist", "web", "index.html");
61
+ if (!existsSync(WEB_DIST)) {
62
+ console.error("agent-dag: ui not built. run `npm run build` (or `pnpm build`) first.");
63
+ process.exit(1);
64
+ }
65
+
66
+ // ── ANSI helpers ──────────────────────────────────────────────────────────────
67
+ const tty = process.stdout.isTTY;
68
+ const C = {
69
+ reset: tty ? "\x1b[0m" : "",
70
+ bold: tty ? "\x1b[1m" : "",
71
+ dim: tty ? "\x1b[2m" : "",
72
+ cyan: tty ? "\x1b[36m" : "",
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
+ const IW = 38; // inner visible width between ║ chars
86
+ const pad = (visLen) => " ".repeat(Math.max(0, IW - visLen));
87
+ const HR = "".repeat(IW);
88
+
89
+ // Each entry: [rendered string with ANSI, visible char count, left-border color, right-border color]
90
+ const rows = [
91
+ { l: ` ${C.bCyan}${C.bold}◉${C.reset} ${C.white}${C.bold}agent-dag${C.reset} ${C.dim}v${PKG_VERSION}${C.reset}`, vis: 17 + PKG_VERSION.length, lc: C.blue, rc: C.blue },
92
+ { l: ` ${C.dim}live DAG · Claude Code + Codex${C.reset}`, vis: 32, lc: C.magenta, rc: C.magenta },
93
+ { l: ` ${C.yellow}watch agents fork ${C.cyan}→${C.reset} ${C.green}tools fire${C.reset}`, vis: 34, lc: C.bMag, rc: C.bMag },
94
+ ];
95
+
96
+ const lines = [
97
+ "",
98
+ ` ${C.cyan}${C.bold}╔${HR}╗${C.reset}`,
99
+ ...rows.map(r => ` ${r.lc}${C.bold}║${C.reset}${r.l}${pad(r.vis)}${r.rc}${C.bold}║${C.reset}`),
100
+ ` ${C.cyan}${C.bold}╚${HR}╝${C.reset}`,
101
+ "",
102
+ ];
103
+ for (const line of lines) {
104
+ process.stdout.write(line + "\n");
105
+ if (tty) await sleep(45);
106
+ }
107
+ }
108
+
109
+ // ── Spinner ───────────────────────────────────────────────────────────────────
110
+ function spinner(label) {
111
+ if (!tty) { process.stdout.write(` … ${label}\n`); return { stop: (ok, msg) => process.stdout.write(` ${ok ? "✓" : "✗"} ${msg}\n`) }; }
112
+ const frames = ["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"];
113
+ let i = 0;
114
+ const iv = setInterval(() => {
115
+ process.stdout.write(`\r ${C.cyan}${frames[i++ % frames.length]}${C.reset} ${label}`);
116
+ }, 80);
117
+ return {
118
+ stop(ok, msg) {
119
+ clearInterval(iv);
120
+ const icon = ok ? `${C.green}✓${C.reset}` : `${C.yellow}✗${C.reset}`;
121
+ process.stdout.write(`\r ${icon} ${msg}\n`);
122
+ }
123
+ };
124
+ }
125
+
126
+ await printBanner();
127
+
128
+ // ── Startup steps ─────────────────────────────────────────────────────────────
129
+ process.stdout.write(` ${C.dim}workspace :${C.reset} ${workspace === "" ? C.yellow + "(all)" + C.reset : workspace}\n`);
130
+
131
+ let sp = spinner("installing Claude hooks…");
132
+ const claudeInstall = await installHooks({ provider: "claude" });
133
+ sp.stop(true, `Claude hooks ${C.dim}${claudeInstall.hookPath}${C.reset}`);
134
+
135
+ // Codex CLI hooks never fire on Windows (sandbox refuses to spawn the hook
136
+ // command). Instead the server tails Codex's rollout JSONL files directly, so
137
+ // there's nothing to install and no /hooks trust step. We just confirm Codex
138
+ // is present and let the watcher pick up sessions.
139
+ if (wantCodex) {
140
+ process.stdout.write(` ${C.green}✓${C.reset} Codex sessions ${C.dim}→ watching ${join(homedir(), ".codex", "sessions")}${C.reset}\n`);
141
+ } else {
142
+ process.stdout.write(` ${C.dim}Codex watch skipped (no ~/.codex/, or --no-codex)${C.reset}\n`);
143
+ }
144
+
145
+ sp = spinner("starting server…");
146
+ const server = await startServer({ port, persist, workspace, codex: wantCodex }).catch(err => {
147
+ sp.stop(false, `server failed: ${err.message}`);
148
+ process.exit(1);
149
+ });
150
+ const addr = server.address();
151
+ const realPort = typeof addr === "object" && addr ? addr.port : port;
152
+ const url = `http://127.0.0.1:${realPort}`;
153
+ sp.stop(true, `server ready ${C.dim}→ ${C.reset}${C.bCyan}${C.bold}${url}${C.reset}`);
154
+
155
+ if (persist) process.stdout.write(` ${C.dim}log : ${persist}${C.reset}\n`);
156
+
157
+ process.stdout.write(`\n ${C.green}${C.bold}▶ opening browser…${C.reset}\n\n`);
158
+
159
+ const discoveryFile = await writeDiscovery({ port: realPort, workspace });
160
+
161
+ if (openBrowser) {
162
+ try {
163
+ const { default: open } = await import("open");
164
+ await open(url);
165
+ } catch {}
166
+ }
167
+
168
+ // ── Pulse indicator ───────────────────────────────────────────────────────────
169
+ if (tty) {
170
+ const pulseFrames = [`${C.green}●${C.reset}`, `${C.dim}●${C.reset}`];
171
+ let pi = 0;
172
+ setInterval(() => {
173
+ process.stdout.write(`\r ${pulseFrames[pi++ % 2]} ${C.dim}listening Ctrl+C to stop${C.reset} `);
174
+ }, 800).unref();
175
+ }
176
+
177
+ const shutdown = async () => {
178
+ if (tty) process.stdout.write(`\n\n ${C.yellow}◉ shutting down…${C.reset}\n`);
179
+ await removeDiscovery(discoveryFile);
180
+ server.close(() => process.exit(0));
181
+ setTimeout(() => process.exit(0), 1500).unref();
182
+ };
183
+ process.on("SIGINT", shutdown);
184
+ process.on("SIGTERM", shutdown);
185
+ process.on("beforeExit", () => removeDiscovery(discoveryFile));
186
+
187
+ // ── helpers ───────────────────────────────────────────────────────────────────
188
+
189
+ function parseArgs(args) {
190
+ const out = {};
191
+ for (let i = 0; i < args.length; i++) {
192
+ const a = args[i];
193
+ if (a === "-h" || a === "--help") out.help = true;
194
+ else if (a === "-p" || a === "--port") out.port = args[++i];
195
+ else if (a === "--no-open") out.noOpen = true;
196
+ else if (a === "--uninstall") out.uninstall = true;
197
+ else if (a === "--workspace") out.workspace = args[++i];
198
+ else if (a === "--scope") out.scope = true;
199
+ else if (a === "--all") out.all = true; // legacy no-op (now default)
200
+ else if (a === "--no-persist") out.noPersist = true;
201
+ else if (a === "--history") out.history = args[++i];
202
+ else if (a === "--codex") out.codex = true;
203
+ else if (a === "--no-codex") out.noCodex = true;
204
+ }
205
+ return out;
206
+ }
207
+
208
+ function printHelp() {
209
+ process.stdout.write(`agent-dag — live DAG of Claude Code + Codex agents
210
+
211
+ Usage:
212
+ agent-dag [options]
213
+
214
+ Options:
215
+ -p, --port <number> Preferred port (default: 4317; falls back to random 4318–4400)
216
+ --no-open Don't open the browser automatically
217
+ --workspace <path> Only capture sessions whose cwd is inside <path>
218
+ --scope Restrict to current working directory
219
+ --all Capture every session (default)
220
+ --history <path> Override events log file (default: ~/.claude/agent-dag/events.jsonl)
221
+ --no-persist Don't write or replay events log (RAM-only)
222
+ --codex Force-enable Codex capture even if ~/.codex/ missing
223
+ --no-codex Skip Codex capture (Claude only)
224
+ --uninstall Remove agent-dag Claude hook entries
225
+ -h, --help Show this help
226
+ `);
227
+ }