agents-deck 1.18.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 +21 -0
- package/README.md +61 -0
- package/bin/agent-dag.js +227 -0
- package/dist/web/assets/index-DGqLVo1U.css +1 -0
- package/dist/web/assets/index-uANyTzBq.js +62 -0
- package/dist/web/index.html +14 -0
- package/hook/hook.js +110 -0
- package/package.json +70 -0
- package/src/server/index.mjs +1109 -0
- package/src/server/installer.mjs +185 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Idempotent hook installer. Supports two providers:
|
|
2
|
+
// - "claude" → ~/.claude/settings.json (Claude Code)
|
|
3
|
+
// - "codex" → ~/.codex/hooks.json (OpenAI Codex CLI)
|
|
4
|
+
// Both providers share the discovery dir at ~/.claude/agent-dag/ so a single
|
|
5
|
+
// running server can receive events from either CLI. Re-runs are safe; entries
|
|
6
|
+
// are tagged with __agent-dag and de-duped.
|
|
7
|
+
import { readFile, writeFile, mkdir, copyFile, unlink } from "node:fs/promises";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join, resolve, dirname } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const PKG_ROOT = resolve(__dirname, "..", "..");
|
|
15
|
+
|
|
16
|
+
const HOME = homedir();
|
|
17
|
+
const CLAUDE_DIR = join(HOME, ".claude");
|
|
18
|
+
const CODEX_DIR = process.env.CODEX_HOME
|
|
19
|
+
? resolve(process.env.CODEX_HOME)
|
|
20
|
+
: join(HOME, ".codex");
|
|
21
|
+
|
|
22
|
+
// Single shared discovery dir — both providers' hook scripts post here so one
|
|
23
|
+
// running agent-dag server can match either ecosystem's events.
|
|
24
|
+
const AGENT_DAG_DIR = join(CLAUDE_DIR, "agent-dag");
|
|
25
|
+
|
|
26
|
+
const CLAUDE_EVENTS = [
|
|
27
|
+
"SessionStart",
|
|
28
|
+
"UserPromptSubmit",
|
|
29
|
+
"PreToolUse",
|
|
30
|
+
"PostToolUse",
|
|
31
|
+
"PostToolUseFailure",
|
|
32
|
+
"SubagentStart",
|
|
33
|
+
"SubagentStop",
|
|
34
|
+
"Stop",
|
|
35
|
+
"SessionEnd",
|
|
36
|
+
"Notification",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// Codex CLI hook events. SubagentStart/Stop exist (multi-agent feature).
|
|
40
|
+
// PostToolUseFailure / SessionEnd / Notification have no Codex equivalent.
|
|
41
|
+
const CODEX_EVENTS = [
|
|
42
|
+
"SessionStart",
|
|
43
|
+
"UserPromptSubmit",
|
|
44
|
+
"PreToolUse",
|
|
45
|
+
"PostToolUse",
|
|
46
|
+
"SubagentStart",
|
|
47
|
+
"SubagentStop",
|
|
48
|
+
"Stop",
|
|
49
|
+
"PreCompact",
|
|
50
|
+
"PostCompact",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const PROVIDERS = {
|
|
54
|
+
claude: {
|
|
55
|
+
settingsPath: join(CLAUDE_DIR, "settings.json"),
|
|
56
|
+
hookInstallDir: join(CLAUDE_DIR, "agent-dag"),
|
|
57
|
+
events: CLAUDE_EVENTS,
|
|
58
|
+
ensureDir: CLAUDE_DIR,
|
|
59
|
+
},
|
|
60
|
+
codex: {
|
|
61
|
+
settingsPath: join(CODEX_DIR, "hooks.json"),
|
|
62
|
+
hookInstallDir: join(CODEX_DIR, "agent-dag"),
|
|
63
|
+
events: CODEX_EVENTS,
|
|
64
|
+
ensureDir: CODEX_DIR,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const MARK_KEY = "__agent-dag";
|
|
69
|
+
// Legacy marks from earlier names — purged on every install/uninstall so
|
|
70
|
+
// duplicate forwarders don't pile up when the project gets renamed.
|
|
71
|
+
const LEGACY_MARKS = ["__ccgraph", "__agent-flow"];
|
|
72
|
+
const LEGACY_DIRS = ["ccgraph", "agent-flow", "agent-dag"];
|
|
73
|
+
|
|
74
|
+
function hookCommand(installedHookPath, provider) {
|
|
75
|
+
const node = process.execPath;
|
|
76
|
+
return `"${node}" "${installedHookPath}" --provider ${provider}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isOurEntry(g) {
|
|
80
|
+
if (!g || typeof g !== "object") return false;
|
|
81
|
+
if (g[MARK_KEY] === true) return true;
|
|
82
|
+
for (const k of LEGACY_MARKS) if (g[k] === true) return true;
|
|
83
|
+
const cmds = Array.isArray(g.hooks) ? g.hooks : [];
|
|
84
|
+
for (const h of cmds) {
|
|
85
|
+
const c = typeof h?.command === "string" ? h.command : "";
|
|
86
|
+
for (const dir of LEGACY_DIRS) {
|
|
87
|
+
if (c.includes(`.claude/${dir}/hook.js`) || c.includes(`.claude\\${dir}\\hook.js`)) return true;
|
|
88
|
+
if (c.includes(`.codex/${dir}/hook.js`) || c.includes(`.codex\\${dir}\\hook.js`)) return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function ensureDir(p) {
|
|
95
|
+
if (!existsSync(p)) await mkdir(p, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function readJsonSafe(p) {
|
|
99
|
+
try { return JSON.parse(await readFile(p, "utf8")); } catch { return null; }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function installHookScript(installDir) {
|
|
103
|
+
await ensureDir(installDir);
|
|
104
|
+
const src = join(PKG_ROOT, "hook", "hook.js");
|
|
105
|
+
const dst = join(installDir, "hook.js");
|
|
106
|
+
await copyFile(src, dst);
|
|
107
|
+
return dst;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function buildHookEntry(command) {
|
|
111
|
+
return {
|
|
112
|
+
[MARK_KEY]: true,
|
|
113
|
+
hooks: [{ type: "command", command, timeout: 2 }],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function dedupeOurEntries(group) {
|
|
118
|
+
if (!Array.isArray(group)) return [];
|
|
119
|
+
return group.filter(g => !isOurEntry(g));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Install hooks for a single provider. Returns {settingsPath, hookPath, events}. */
|
|
123
|
+
export async function installHooks({ provider = "claude" } = {}) {
|
|
124
|
+
const cfg = PROVIDERS[provider];
|
|
125
|
+
if (!cfg) throw new Error(`unknown provider: ${provider}`);
|
|
126
|
+
|
|
127
|
+
const hookPath = await installHookScript(cfg.hookInstallDir);
|
|
128
|
+
const command = hookCommand(hookPath, provider);
|
|
129
|
+
await ensureDir(cfg.ensureDir);
|
|
130
|
+
// Discovery dir is shared across providers — always make sure it exists.
|
|
131
|
+
await ensureDir(AGENT_DAG_DIR);
|
|
132
|
+
|
|
133
|
+
const current = (await readJsonSafe(cfg.settingsPath)) ?? {};
|
|
134
|
+
current.hooks = current.hooks ?? {};
|
|
135
|
+
|
|
136
|
+
for (const evt of cfg.events) {
|
|
137
|
+
const cleaned = dedupeOurEntries(current.hooks[evt]);
|
|
138
|
+
cleaned.push(buildHookEntry(command));
|
|
139
|
+
current.hooks[evt] = cleaned;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
await writeFile(cfg.settingsPath, JSON.stringify(current, null, 2) + "\n", "utf8");
|
|
143
|
+
return { settingsPath: cfg.settingsPath, hookPath, events: cfg.events, provider };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function uninstallHooks({ provider = "claude" } = {}) {
|
|
147
|
+
const cfg = PROVIDERS[provider];
|
|
148
|
+
if (!cfg) throw new Error(`unknown provider: ${provider}`);
|
|
149
|
+
const current = await readJsonSafe(cfg.settingsPath);
|
|
150
|
+
if (!current?.hooks) return { changed: false, provider };
|
|
151
|
+
let changed = false;
|
|
152
|
+
for (const evt of Object.keys(current.hooks)) {
|
|
153
|
+
const cleaned = dedupeOurEntries(current.hooks[evt]);
|
|
154
|
+
if (cleaned.length !== (current.hooks[evt]?.length ?? 0)) changed = true;
|
|
155
|
+
if (cleaned.length === 0) delete current.hooks[evt];
|
|
156
|
+
else current.hooks[evt] = cleaned;
|
|
157
|
+
}
|
|
158
|
+
if (changed) await writeFile(cfg.settingsPath, JSON.stringify(current, null, 2) + "\n", "utf8");
|
|
159
|
+
return { changed, provider, settingsPath: cfg.settingsPath };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** True when ~/.codex/ exists — used by CLI to default-enable Codex hooks. */
|
|
163
|
+
export function hasCodexInstalled() {
|
|
164
|
+
return existsSync(CODEX_DIR);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function writeDiscovery({ port, workspace }) {
|
|
168
|
+
await ensureDir(AGENT_DAG_DIR);
|
|
169
|
+
const file = join(AGENT_DAG_DIR, `${process.pid}.json`);
|
|
170
|
+
const data = {
|
|
171
|
+
pid: process.pid,
|
|
172
|
+
port,
|
|
173
|
+
workspace: workspace ?? "",
|
|
174
|
+
startedAt: new Date().toISOString(),
|
|
175
|
+
};
|
|
176
|
+
await writeFile(file, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
177
|
+
return file;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function removeDiscovery(file) {
|
|
181
|
+
try { await unlink(file); } catch {}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export const HOOK_EVENTS = CLAUDE_EVENTS;
|
|
185
|
+
export { AGENT_DAG_DIR, CLAUDE_DIR, CODEX_DIR, CLAUDE_EVENTS, CODEX_EVENTS };
|