agent-dag 1.15.0 → 1.15.4
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 +21 -21
- package/README.md +52 -52
- package/bin/agent-dag.js +199 -199
- package/dist/web/assets/index-BPyjn8Kd.js +62 -0
- package/dist/web/index.html +13 -13
- package/hook/hook.js +96 -96
- package/package.json +68 -66
- package/src/server/index.mjs +643 -627
- package/src/server/installer.mjs +137 -137
- package/dist/web/assets/index-DZeh3MHD.js +0 -62
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,52 @@
|
|
|
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 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.
|
package/bin/agent-dag.js
CHANGED
|
@@ -1,199 +1,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 } = 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 } = 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
|
+
}
|