@theagenthub/cli 0.3.2 → 0.3.3
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/bin/agenthub.cjs +635 -572
- package/package.json +1 -1
package/bin/agenthub.cjs
CHANGED
|
@@ -59,6 +59,616 @@ var init_paths = __esm({
|
|
|
59
59
|
}
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
+
// src/cli/amp-plugin.ts
|
|
63
|
+
function ampPlugin(command, as = "file") {
|
|
64
|
+
const header = as === "package" ? PACKAGE_HEADER : `${AMP_MARKER}
|
|
65
|
+
// Re-run that command to update it, or \`agenthub hooks uninstall amp\` to remove it.`;
|
|
66
|
+
return `${header}
|
|
67
|
+
import { spawn } from "node:child_process";
|
|
68
|
+
import { existsSync } from "node:fs";
|
|
69
|
+
import { homedir } from "node:os";
|
|
70
|
+
import { join } from "node:path";
|
|
71
|
+
import { fileURLToPath } from "node:url";
|
|
72
|
+
|
|
73
|
+
${as === "package" ? PACKAGE_COMMAND : `const COMMAND = ${JSON.stringify(command)};`}
|
|
74
|
+
const TIMEOUT_MS = 5000;
|
|
75
|
+
|
|
76
|
+
export const description = "Agent Hub: teammates' agents' messages in your threads, and an idle thread wakes for their requests.";
|
|
77
|
+
|
|
78
|
+
/** Runs agenthub; resolves to its exit code and stdout, or code -1 on any failure. */
|
|
79
|
+
function run(args, input, cwd, timeoutMs, onChild) {
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
let out = "";
|
|
82
|
+
let child;
|
|
83
|
+
try {
|
|
84
|
+
child = spawn(COMMAND, args, { cwd, stdio: ["pipe", "pipe", "ignore"], windowsHide: true, shell: process.platform === "win32" });
|
|
85
|
+
} catch {
|
|
86
|
+
return resolve({ code: -1, out: "" });
|
|
87
|
+
}
|
|
88
|
+
onChild?.(child);
|
|
89
|
+
const timer = timeoutMs ? setTimeout(() => child.kill(), timeoutMs) : undefined;
|
|
90
|
+
child.stdout.on("data", (chunk) => (out += chunk));
|
|
91
|
+
child.on("error", () => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
resolve({ code: -1, out: "" });
|
|
94
|
+
});
|
|
95
|
+
child.on("close", (code) => {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
resolve({ code: code ?? -1, out: out.trim() });
|
|
98
|
+
});
|
|
99
|
+
child.stdin.on("error", () => {});
|
|
100
|
+
child.stdin.end(input);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default function agenthub(amp) {
|
|
105
|
+
let cwd = process.cwd();
|
|
106
|
+
try {
|
|
107
|
+
const root = amp.system?.workspaceRoot ? String(amp.system.workspaceRoot) : "";
|
|
108
|
+
if (root) cwd = root.startsWith("file:") ? fileURLToPath(root) : root;
|
|
109
|
+
} catch {}
|
|
110
|
+
const started = new Set();
|
|
111
|
+
const waiting = new Map();
|
|
112
|
+
const stopWaiting = (id) => {
|
|
113
|
+
waiting.get(id)?.kill();
|
|
114
|
+
waiting.delete(id);
|
|
115
|
+
};
|
|
116
|
+
const notify = async (event, id, extra) => {
|
|
117
|
+
const { out } = await run(["notify", "--format", "amp", "--event", event], JSON.stringify({ session_id: id, cwd, ...extra }), cwd, TIMEOUT_MS);
|
|
118
|
+
return out;
|
|
119
|
+
};
|
|
120
|
+
const say = async (id, content, steer) => {
|
|
121
|
+
try {
|
|
122
|
+
await amp.threads.get(id).appendUserMessage({ type: "user-message", content }, steer ? { steer: true } : undefined);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
amp.logger?.log?.("agenthub: couldn't add to thread " + id + ": " + e);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const wake = async (id) => {
|
|
128
|
+
const { code, out } = await run(["wait"], JSON.stringify({ session_id: id, cwd }), cwd, 0, (child) => {
|
|
129
|
+
stopWaiting(id);
|
|
130
|
+
waiting.set(id, child);
|
|
131
|
+
});
|
|
132
|
+
// An agenthub too old to know \`wait\` exits 2 too, but prints nothing on stdout.
|
|
133
|
+
if (code === 2 && out) await say(id, out, false);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
amp.on("agent.start", async (event) => {
|
|
137
|
+
const id = event.thread?.id;
|
|
138
|
+
if (!id) return;
|
|
139
|
+
stopWaiting(id);
|
|
140
|
+
const first = !started.has(id);
|
|
141
|
+
started.add(id);
|
|
142
|
+
const text = await notify(first ? "session-start" : "prompt", id);
|
|
143
|
+
return text ? { message: { content: text, display: false } } : undefined;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
amp.on("tool.result", async (event) => {
|
|
147
|
+
const id = event.thread?.id;
|
|
148
|
+
if (!id) return;
|
|
149
|
+
const text = await notify("post-tool", id);
|
|
150
|
+
if (!text) return;
|
|
151
|
+
if (typeof event.output === "string") return { status: event.status, output: event.output + "\\n\\n" + text };
|
|
152
|
+
await say(id, text, true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
amp.on("agent.end", async (event) => {
|
|
156
|
+
const id = event.thread?.id;
|
|
157
|
+
if (!id) return;
|
|
158
|
+
// A cancelled turn isn't kept going, and doesn't start a waiter.
|
|
159
|
+
const out = await notify("stop", id, { status: event.status });
|
|
160
|
+
if (out) {
|
|
161
|
+
try {
|
|
162
|
+
const r = JSON.parse(out);
|
|
163
|
+
if (r.decision === "block" && r.reason) return { action: "continue", userMessage: r.reason };
|
|
164
|
+
} catch {}
|
|
165
|
+
}
|
|
166
|
+
if (event.status !== "cancelled") void wake(id);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
amp.onDispose?.(() => {
|
|
170
|
+
for (const child of waiting.values()) child.kill();
|
|
171
|
+
waiting.clear();
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
`;
|
|
175
|
+
}
|
|
176
|
+
var AMP_MARKER, PACKAGE_HEADER, PACKAGE_COMMAND;
|
|
177
|
+
var init_amp_plugin = __esm({
|
|
178
|
+
"src/cli/amp-plugin.ts"() {
|
|
179
|
+
"use strict";
|
|
180
|
+
AMP_MARKER = "// Agent Hub plugin for Amp, written by `agenthub hooks install amp`.";
|
|
181
|
+
PACKAGE_HEADER = `// Agent Hub plugin for Amp. Copy it to ~/.config/amp/plugins/ (or a repo's .amp/plugins/).
|
|
182
|
+
// Needs the agenthub CLI: curl -fsSL https://get.theagenthub.app | sh`;
|
|
183
|
+
PACKAGE_COMMAND = `const COMMAND =
|
|
184
|
+
process.env.AGENTHUB_BIN ||
|
|
185
|
+
[join(homedir(), ".local", "bin", "agenthub"), join(process.env.LOCALAPPDATA || homedir(), "Programs", "agenthub", "agenthub.exe")].find((f) => existsSync(f)) ||
|
|
186
|
+
"agenthub";`;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// src/cli/opencode-plugin.ts
|
|
191
|
+
function opencodePlugin(command, as = "file") {
|
|
192
|
+
const header = as === "package" ? PACKAGE_HEADER2 : `${OPENCODE_MARKER}
|
|
193
|
+
// Re-run that command to update it, or \`agenthub hooks uninstall opencode\` to remove it.`;
|
|
194
|
+
return `${header}
|
|
195
|
+
import { spawn } from "node:child_process";
|
|
196
|
+
|
|
197
|
+
${as === "package" ? PACKAGE_COMMAND2 : `const COMMAND = ${JSON.stringify(command)};`}
|
|
198
|
+
const TIMEOUT_MS = 5000;
|
|
199
|
+
|
|
200
|
+
function notify(event, sessionID, cwd) {
|
|
201
|
+
return new Promise((resolve) => {
|
|
202
|
+
let out = "";
|
|
203
|
+
let child;
|
|
204
|
+
try {
|
|
205
|
+
child = spawn(COMMAND, ["notify", "--format", "text", "--event", event], {
|
|
206
|
+
cwd,
|
|
207
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
208
|
+
windowsHide: true,
|
|
209
|
+
shell: process.platform === "win32",
|
|
210
|
+
});
|
|
211
|
+
} catch {
|
|
212
|
+
return resolve("");
|
|
213
|
+
}
|
|
214
|
+
const timer = setTimeout(() => {
|
|
215
|
+
child.kill();
|
|
216
|
+
resolve("");
|
|
217
|
+
}, TIMEOUT_MS);
|
|
218
|
+
child.stdout.on("data", (chunk) => (out += chunk));
|
|
219
|
+
child.on("error", () => {
|
|
220
|
+
clearTimeout(timer);
|
|
221
|
+
resolve("");
|
|
222
|
+
});
|
|
223
|
+
child.on("close", () => {
|
|
224
|
+
clearTimeout(timer);
|
|
225
|
+
resolve(out.trim());
|
|
226
|
+
});
|
|
227
|
+
child.stdin.on("error", () => {});
|
|
228
|
+
child.stdin.end(JSON.stringify({ session_id: sessionID, cwd }));
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Runs \`agenthub wait\`; resolves to the text to wake the session with, or "" when it shouldn't. */
|
|
233
|
+
function waitForMessage(sessionID, cwd, children) {
|
|
234
|
+
return new Promise((resolve) => {
|
|
235
|
+
let out = "";
|
|
236
|
+
let child;
|
|
237
|
+
try {
|
|
238
|
+
child = spawn(COMMAND, ["wait"], { cwd, stdio: ["pipe", "pipe", "ignore"], windowsHide: true, shell: process.platform === "win32" });
|
|
239
|
+
} catch {
|
|
240
|
+
return resolve("");
|
|
241
|
+
}
|
|
242
|
+
children.get(sessionID)?.kill();
|
|
243
|
+
children.set(sessionID, child);
|
|
244
|
+
child.stdout.on("data", (chunk) => (out += chunk));
|
|
245
|
+
child.on("error", () => resolve(""));
|
|
246
|
+
child.on("close", (code) => {
|
|
247
|
+
if (children.get(sessionID) === child) children.delete(sessionID);
|
|
248
|
+
// An agenthub too old to know \`wait\` exits 2 too, but prints nothing on stdout.
|
|
249
|
+
resolve(code === 2 ? out.trim() : "");
|
|
250
|
+
});
|
|
251
|
+
child.stdin.on("error", () => {});
|
|
252
|
+
child.stdin.end(JSON.stringify({ session_id: sessionID, cwd }));
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function partId() {
|
|
257
|
+
return "prt_" + Date.now().toString(16) + Math.random().toString(36).slice(2, 12);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export const AgentHub = async ({ directory, client }) => {
|
|
261
|
+
const started = new Set();
|
|
262
|
+
const waiting = new Map();
|
|
263
|
+
let presence = Promise.resolve();
|
|
264
|
+
const wake = async (sessionID) => {
|
|
265
|
+
const send = client?.session?.promptAsync ?? client?.session?.prompt;
|
|
266
|
+
if (!send) return;
|
|
267
|
+
const text = await waitForMessage(sessionID, directory, waiting);
|
|
268
|
+
if (!text) return;
|
|
269
|
+
try {
|
|
270
|
+
await send.call(client.session, { path: { id: sessionID }, body: { parts: [{ type: "text", text }] } });
|
|
271
|
+
} catch {
|
|
272
|
+
/* the session is gone, or busy again */
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
return {
|
|
276
|
+
"chat.message": async (input, output) => {
|
|
277
|
+
waiting.get(input.sessionID)?.kill();
|
|
278
|
+
const first = !started.has(input.sessionID);
|
|
279
|
+
started.add(input.sessionID);
|
|
280
|
+
const text = await notify(first ? "session-start" : "prompt", input.sessionID, directory);
|
|
281
|
+
const messageID = input.messageID ?? output.message?.id;
|
|
282
|
+
if (!text || !messageID) return;
|
|
283
|
+
output.parts.push({ id: partId(), sessionID: input.sessionID, messageID, type: "text", text, synthetic: true });
|
|
284
|
+
},
|
|
285
|
+
"tool.execute.after": async (input, output) => {
|
|
286
|
+
const text = await notify("post-tool", input.sessionID, directory);
|
|
287
|
+
if (text && typeof output.output === "string") output.output += "\\n\\n<agenthub>\\n" + text + "\\n</agenthub>";
|
|
288
|
+
},
|
|
289
|
+
// Presence updates don't block OpenCode, but run one at a time so "offline" can't be
|
|
290
|
+
// overtaken by an earlier "idle".
|
|
291
|
+
event: async ({ event }) => {
|
|
292
|
+
if (event.type === "session.idle") {
|
|
293
|
+
const sessionID = event.properties.sessionID;
|
|
294
|
+
presence = presence.then(() => notify("stop", sessionID, directory));
|
|
295
|
+
// Not awaited: it waits as long as the session is idle.
|
|
296
|
+
presence.then(() => wake(sessionID)).catch(() => {});
|
|
297
|
+
} else if (event.type === "session.deleted") {
|
|
298
|
+
waiting.get(event.properties.info.id)?.kill();
|
|
299
|
+
presence = presence.then(() => notify("session-end", event.properties.info.id, directory));
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// OpenCode 2 loads the default export's setup() and ignores the hooks above; OpenCode 1
|
|
306
|
+
// reads the named export (and default.server, the same function). setup() returns early
|
|
307
|
+
// when OpenCode 1 calls it too.
|
|
308
|
+
async function setup(ctx) {
|
|
309
|
+
if (!ctx?.session?.hook || !ctx.tool?.hook || !ctx.event?.subscribe) return;
|
|
310
|
+
const directory = ctx.location?.directory ?? process.cwd();
|
|
311
|
+
const started = new Set();
|
|
312
|
+
const waiting = new Map();
|
|
313
|
+
let presence = Promise.resolve();
|
|
314
|
+
const wake = async (sessionID) => {
|
|
315
|
+
if (typeof ctx.session.synthetic !== "function") return;
|
|
316
|
+
const text = await waitForMessage(sessionID, directory, waiting);
|
|
317
|
+
if (!text) return;
|
|
318
|
+
try {
|
|
319
|
+
await ctx.session.synthetic({ sessionID, text, description: "Agent Hub: new message", delivery: "queue", resume: true });
|
|
320
|
+
} catch {
|
|
321
|
+
/* the session is gone, or busy again */
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
const wrap = (text) => "\\n\\n<agenthub>\\n" + text + "\\n</agenthub>";
|
|
325
|
+
|
|
326
|
+
// V2 has no hidden message parts, so the notice is appended to the prompt's text.
|
|
327
|
+
await ctx.session.hook("prompt", async (event) => {
|
|
328
|
+
waiting.get(event.sessionID)?.kill();
|
|
329
|
+
const first = !started.has(event.sessionID);
|
|
330
|
+
started.add(event.sessionID);
|
|
331
|
+
const text = await notify(first ? "session-start" : "prompt", event.sessionID, directory);
|
|
332
|
+
if (text && event.prompt && typeof event.prompt.text === "string") event.prompt.text += wrap(text);
|
|
333
|
+
});
|
|
334
|
+
// The model reads a tool's result.content.
|
|
335
|
+
await ctx.tool.hook("execute.after", async (event) => {
|
|
336
|
+
if (event.status !== "completed" || !Array.isArray(event.result?.content)) return;
|
|
337
|
+
const text = await notify("post-tool", event.sessionID, directory);
|
|
338
|
+
if (text) event.result.content.push({ type: "text", text: wrap(text).trimStart() });
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const controller = new AbortController();
|
|
342
|
+
(async () => {
|
|
343
|
+
const types = ["session.execution.succeeded", "session.execution.failed", "session.execution.interrupted", "session.deleted"];
|
|
344
|
+
for await (const event of ctx.event.subscribe({ types, signal: controller.signal })) {
|
|
345
|
+
const sessionID = event.data?.sessionID;
|
|
346
|
+
if (!sessionID) continue;
|
|
347
|
+
const kind = event.type === "session.deleted" ? "session-end" : "stop";
|
|
348
|
+
if (kind === "session-end") {
|
|
349
|
+
started.delete(sessionID);
|
|
350
|
+
waiting.get(sessionID)?.kill();
|
|
351
|
+
}
|
|
352
|
+
presence = presence.then(() => notify(kind, sessionID, directory));
|
|
353
|
+
// A finished run (not one the user interrupted) waits for a hub message; not awaited.
|
|
354
|
+
if (event.type === "session.execution.succeeded" || event.type === "session.execution.failed") {
|
|
355
|
+
presence.then(() => wake(sessionID)).catch(() => {});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
})().catch(() => {});
|
|
359
|
+
return () => {
|
|
360
|
+
controller.abort();
|
|
361
|
+
for (const child of waiting.values()) child.kill();
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export default { id: "agenthub", server: AgentHub, setup };
|
|
366
|
+
`;
|
|
367
|
+
}
|
|
368
|
+
var OPENCODE_MARKER, PACKAGE_HEADER2, PACKAGE_COMMAND2;
|
|
369
|
+
var init_opencode_plugin = __esm({
|
|
370
|
+
"src/cli/opencode-plugin.ts"() {
|
|
371
|
+
"use strict";
|
|
372
|
+
OPENCODE_MARKER = "// Agent Hub plugin for OpenCode, written by `agenthub hooks install opencode`.";
|
|
373
|
+
PACKAGE_HEADER2 = `// Agent Hub plugin for OpenCode, from the @theagenthub/opencode npm package.
|
|
374
|
+
// Needs the agenthub CLI: curl -fsSL https://get.theagenthub.app | sh`;
|
|
375
|
+
PACKAGE_COMMAND2 = `import { existsSync } from "node:fs";
|
|
376
|
+
import { homedir } from "node:os";
|
|
377
|
+
import { join } from "node:path";
|
|
378
|
+
|
|
379
|
+
const COMMAND =
|
|
380
|
+
process.env.AGENTHUB_BIN ||
|
|
381
|
+
[join(homedir(), ".local", "bin", "agenthub"), join(process.env.LOCALAPPDATA || homedir(), "Programs", "agenthub", "agenthub.exe")].find((f) => existsSync(f)) ||
|
|
382
|
+
"agenthub";`;
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// src/cli/hooks.ts
|
|
387
|
+
var hooks_exports = {};
|
|
388
|
+
__export(hooks_exports, {
|
|
389
|
+
HARNESSES: () => HARNESSES,
|
|
390
|
+
KIMI_BEGIN: () => KIMI_BEGIN,
|
|
391
|
+
KIMI_END: () => KIMI_END,
|
|
392
|
+
USER_SCOPE_ONLY: () => USER_SCOPE_ONLY,
|
|
393
|
+
WAIT_MAX_S: () => WAIT_MAX_S,
|
|
394
|
+
WAIT_TIMEOUT_S: () => WAIT_TIMEOUT_S,
|
|
395
|
+
cliHooksFile: () => cliHooksFile,
|
|
396
|
+
hookFile: () => hookFile,
|
|
397
|
+
installHooks: () => installHooks,
|
|
398
|
+
uninstallHooks: () => uninstallHooks
|
|
399
|
+
});
|
|
400
|
+
function hookFile(harness, scope, cwd, home = (0, import_node_os2.homedir)(), env = process.env) {
|
|
401
|
+
const user = scope === "user";
|
|
402
|
+
const base = user ? home : cwd;
|
|
403
|
+
switch (harness) {
|
|
404
|
+
case "claude-code":
|
|
405
|
+
return user ? (0, import_node_path3.join)(home, ".claude", "settings.json") : (0, import_node_path3.join)(cwd, ".claude", "settings.local.json");
|
|
406
|
+
case "codex":
|
|
407
|
+
return (0, import_node_path3.join)(base, ".codex", "hooks.json");
|
|
408
|
+
case "copilot":
|
|
409
|
+
return user ? (0, import_node_path3.join)(env.COPILOT_HOME || (0, import_node_path3.join)(home, ".copilot"), "hooks", "agenthub.json") : (0, import_node_path3.join)(cwd, ".github", "hooks", "agenthub.json");
|
|
410
|
+
case "opencode":
|
|
411
|
+
return user ? (0, import_node_path3.join)(env.XDG_CONFIG_HOME || (0, import_node_path3.join)(home, ".config"), "opencode", "plugins", "agenthub.js") : (0, import_node_path3.join)(cwd, ".opencode", "plugins", "agenthub.js");
|
|
412
|
+
case "droid":
|
|
413
|
+
return (0, import_node_path3.join)(base, ".factory", "hooks.json");
|
|
414
|
+
case "gemini":
|
|
415
|
+
return (0, import_node_path3.join)(base, ".gemini", "settings.json");
|
|
416
|
+
case "qwen":
|
|
417
|
+
return (0, import_node_path3.join)(base, ".qwen", "settings.json");
|
|
418
|
+
case "cursor":
|
|
419
|
+
return (0, import_node_path3.join)(base, ".cursor", "hooks.json");
|
|
420
|
+
case "antigravity":
|
|
421
|
+
return user ? (0, import_node_path3.join)(home, ".gemini", "config", "hooks.json") : (0, import_node_path3.join)(cwd, ".agents", "hooks.json");
|
|
422
|
+
case "kimi":
|
|
423
|
+
return (0, import_node_path3.join)(home, ".kimi-code", "config.toml");
|
|
424
|
+
case "goose":
|
|
425
|
+
return (0, import_node_path3.join)(base, ".agents", "plugins", "agenthub", "hooks", "hooks.json");
|
|
426
|
+
case "crush":
|
|
427
|
+
return user ? (0, import_node_path3.join)(env.XDG_CONFIG_HOME || (0, import_node_path3.join)(home, ".config"), "crush", "crush.json") : (0, import_node_fs2.existsSync)((0, import_node_path3.join)(cwd, "crush.json")) ? (0, import_node_path3.join)(cwd, "crush.json") : (0, import_node_path3.join)(cwd, ".crush.json");
|
|
428
|
+
case "amp":
|
|
429
|
+
return user ? (0, import_node_path3.join)(env.XDG_CONFIG_HOME || (0, import_node_path3.join)(home, ".config"), "amp", "plugins", "agenthub.ts") : (0, import_node_path3.join)(cwd, ".amp", "plugins", "agenthub.ts");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function cliHooksFile(harness, cwd, home = (0, import_node_os2.homedir)(), env = process.env) {
|
|
433
|
+
const project = (harness === "claude-code" ? env.CLAUDE_PROJECT_DIR : void 0) || cwd;
|
|
434
|
+
const files = harness === "codex" ? [(0, import_node_path3.join)(env.CODEX_HOME || (0, import_node_path3.join)(home, ".codex"), "hooks.json"), (0, import_node_path3.join)(project, ".codex", "hooks.json")] : [(0, import_node_path3.join)(home, ".claude", "settings.json"), (0, import_node_path3.join)(project, ".claude", "settings.local.json"), (0, import_node_path3.join)(project, ".claude", "settings.json")];
|
|
435
|
+
for (const f of files) {
|
|
436
|
+
try {
|
|
437
|
+
if (/agenthub[^"]* notify/.test((0, import_node_fs2.readFileSync)(f, "utf8"))) return f;
|
|
438
|
+
} catch {
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return void 0;
|
|
442
|
+
}
|
|
443
|
+
function installHooks(harness, file2, command = "agenthub") {
|
|
444
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path3.dirname)(file2), { recursive: true });
|
|
445
|
+
const notify2 = (event) => `${command} notify --format ${harness} --event ${event}`;
|
|
446
|
+
const pairs = HARNESS_EVENTS[harness] ?? [];
|
|
447
|
+
switch (harness) {
|
|
448
|
+
case "opencode":
|
|
449
|
+
(0, import_node_fs2.writeFileSync)(file2, opencodePlugin(command));
|
|
450
|
+
return ["chat.message", "tool.execute.after", "session.idle", "session.deleted"];
|
|
451
|
+
case "amp":
|
|
452
|
+
(0, import_node_fs2.writeFileSync)(file2, ampPlugin(command));
|
|
453
|
+
return ["agent.start", "tool.result", "agent.end"];
|
|
454
|
+
case "copilot":
|
|
455
|
+
case "cursor":
|
|
456
|
+
case "crush": {
|
|
457
|
+
const config2 = readJson(file2);
|
|
458
|
+
if (harness !== "crush") config2.version = 1;
|
|
459
|
+
const hooks = config2.hooks ??= {};
|
|
460
|
+
for (const [event, notifyEvent] of pairs) {
|
|
461
|
+
const kept = (hooks[event] ?? []).filter((h) => !isOurs(h));
|
|
462
|
+
const entry = harness === "copilot" ? { type: "command", command: notify2(notifyEvent), timeoutSec: 10 } : { command: notify2(notifyEvent), timeout: 10 };
|
|
463
|
+
hooks[event] = [...kept, entry];
|
|
464
|
+
}
|
|
465
|
+
writeJson(file2, config2);
|
|
466
|
+
return pairs.map(([e]) => e);
|
|
467
|
+
}
|
|
468
|
+
case "antigravity": {
|
|
469
|
+
const config2 = readJson(file2);
|
|
470
|
+
config2.agenthub = Object.fromEntries(pairs.map(([event, n]) => [event, [{ hooks: [{ type: "command", command: notify2(n), timeout: 10 }] }]]));
|
|
471
|
+
writeJson(file2, config2);
|
|
472
|
+
return pairs.map(([e]) => e);
|
|
473
|
+
}
|
|
474
|
+
case "kimi": {
|
|
475
|
+
const before = (0, import_node_fs2.existsSync)(file2) ? stripKimi((0, import_node_fs2.readFileSync)(file2, "utf8")) : "";
|
|
476
|
+
const block = [KIMI_BEGIN, ...pairs.flatMap(([event, n]) => ["[[hooks]]", `event = "${event}"`, `command = ${JSON.stringify(notify2(n))}`, "timeout = 10", ""]), KIMI_END, ""].join("\n");
|
|
477
|
+
(0, import_node_fs2.writeFileSync)(file2, `${before}${before && !before.endsWith("\n") ? "\n" : ""}${before ? "\n" : ""}${block}`);
|
|
478
|
+
return pairs.map(([e]) => e);
|
|
479
|
+
}
|
|
480
|
+
case "goose": {
|
|
481
|
+
const root = (0, import_node_path3.dirname)((0, import_node_path3.dirname)(file2));
|
|
482
|
+
writeJson((0, import_node_path3.join)(root, "plugin.json"), GOOSE_PLUGIN);
|
|
483
|
+
writeJson(file2, { hooks: Object.fromEntries(pairs.map(([event, n]) => [event, [{ hooks: [{ type: "command", command: notify2(n), timeout: 10 }] }]])) });
|
|
484
|
+
return pairs.map(([e]) => e);
|
|
485
|
+
}
|
|
486
|
+
case "droid":
|
|
487
|
+
case "gemini":
|
|
488
|
+
case "qwen": {
|
|
489
|
+
const config2 = readJson(file2);
|
|
490
|
+
const hooks = harness === "droid" ? config2 : config2.hooks ??= {};
|
|
491
|
+
const timeout = harness === "droid" ? 10 : 1e4;
|
|
492
|
+
for (const [event, n] of pairs) {
|
|
493
|
+
hooks[event] = [...keepOthers(hooks[event]), { hooks: [{ type: "command", command: notify2(n), timeout }] }];
|
|
494
|
+
}
|
|
495
|
+
writeJson(file2, config2);
|
|
496
|
+
return pairs.map(([e]) => e);
|
|
497
|
+
}
|
|
498
|
+
case "claude-code":
|
|
499
|
+
case "codex": {
|
|
500
|
+
const config2 = readJson(file2);
|
|
501
|
+
const hooks = config2.hooks ??= {};
|
|
502
|
+
for (const event of EVENTS) {
|
|
503
|
+
const entry = { hooks: [{ type: "command", command: `${command} notify --format ${harness}`, timeout: TIMEOUT_S[harness][event] ?? 10 }] };
|
|
504
|
+
if (event === "PostToolUse" && harness === "claude-code") entry.matcher = "*";
|
|
505
|
+
if (event === "Stop" && harness === "claude-code") {
|
|
506
|
+
entry.hooks.push({ type: "command", command: `${command} wait --max ${WAIT_MAX_S}`, asyncRewake: true, timeout: WAIT_TIMEOUT_S });
|
|
507
|
+
}
|
|
508
|
+
hooks[event] = [...keepOthers(hooks[event]), entry];
|
|
509
|
+
}
|
|
510
|
+
writeJson(file2, config2);
|
|
511
|
+
return EVENTS;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function uninstallHooks(harness, file2) {
|
|
516
|
+
if (!(0, import_node_fs2.existsSync)(file2)) return false;
|
|
517
|
+
switch (harness) {
|
|
518
|
+
case "opencode":
|
|
519
|
+
if (!(0, import_node_fs2.readFileSync)(file2, "utf8").startsWith(OPENCODE_MARKER)) return false;
|
|
520
|
+
(0, import_node_fs2.rmSync)(file2);
|
|
521
|
+
return true;
|
|
522
|
+
case "amp":
|
|
523
|
+
if (!(0, import_node_fs2.readFileSync)(file2, "utf8").startsWith(AMP_MARKER)) return false;
|
|
524
|
+
(0, import_node_fs2.rmSync)(file2);
|
|
525
|
+
return true;
|
|
526
|
+
case "kimi": {
|
|
527
|
+
const text = (0, import_node_fs2.readFileSync)(file2, "utf8");
|
|
528
|
+
const kept = stripKimi(text);
|
|
529
|
+
if (kept === text) return false;
|
|
530
|
+
(0, import_node_fs2.writeFileSync)(file2, kept);
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
case "goose": {
|
|
534
|
+
const root = (0, import_node_path3.dirname)((0, import_node_path3.dirname)(file2));
|
|
535
|
+
if (readJson((0, import_node_path3.join)(root, "plugin.json")).name !== GOOSE_PLUGIN.name) return false;
|
|
536
|
+
(0, import_node_fs2.rmSync)(root, { recursive: true, force: true });
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
case "antigravity": {
|
|
540
|
+
const config2 = readJson(file2);
|
|
541
|
+
if (!("agenthub" in config2)) return false;
|
|
542
|
+
delete config2.agenthub;
|
|
543
|
+
writeJson(file2, config2);
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
case "copilot":
|
|
547
|
+
case "cursor":
|
|
548
|
+
case "crush": {
|
|
549
|
+
const config2 = readJson(file2);
|
|
550
|
+
if (!config2.hooks) return false;
|
|
551
|
+
let changed = false;
|
|
552
|
+
for (const [event, entries] of Object.entries(config2.hooks)) {
|
|
553
|
+
const kept = entries.filter((h) => !isOurs(h));
|
|
554
|
+
if (kept.length !== entries.length) changed = true;
|
|
555
|
+
if (kept.length) config2.hooks[event] = kept;
|
|
556
|
+
else delete config2.hooks[event];
|
|
557
|
+
}
|
|
558
|
+
if (!changed) return false;
|
|
559
|
+
const empty = !Object.keys(config2.hooks).length && Object.keys(config2).every((k) => k === "version" || k === "hooks");
|
|
560
|
+
if (harness === "copilot" && empty) (0, import_node_fs2.rmSync)(file2);
|
|
561
|
+
else writeJson(file2, config2);
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
default: {
|
|
565
|
+
const config2 = readJson(file2);
|
|
566
|
+
const hooks = harness === "droid" ? config2 : config2.hooks;
|
|
567
|
+
if (!hooks || typeof hooks !== "object") return false;
|
|
568
|
+
let changed = false;
|
|
569
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
570
|
+
if (!Array.isArray(groups)) continue;
|
|
571
|
+
const kept = keepOthers(groups);
|
|
572
|
+
if (kept.length !== groups.length || kept.some((g, i) => g.hooks.length !== groups[i].hooks.length)) changed = true;
|
|
573
|
+
if (kept.length) hooks[event] = kept;
|
|
574
|
+
else delete hooks[event];
|
|
575
|
+
}
|
|
576
|
+
if (changed) writeJson(file2, config2);
|
|
577
|
+
return changed;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
function keepOthers(groups) {
|
|
582
|
+
return (groups ?? []).map((g) => ({ ...g, hooks: (g.hooks ?? []).filter((h) => !isOurs(h)) })).filter((g) => g.hooks.length);
|
|
583
|
+
}
|
|
584
|
+
function stripKimi(text) {
|
|
585
|
+
const start = text.indexOf(KIMI_BEGIN);
|
|
586
|
+
if (start < 0) return text;
|
|
587
|
+
const end = text.indexOf(KIMI_END, start);
|
|
588
|
+
const after = end < 0 ? "" : text.slice(end + KIMI_END.length).replace(/^\n/, "");
|
|
589
|
+
return (text.slice(0, start).replace(/\n+$/, "\n") + after).replace(/^\n+$/, "");
|
|
590
|
+
}
|
|
591
|
+
function readJson(file2) {
|
|
592
|
+
return (0, import_node_fs2.existsSync)(file2) ? JSON.parse((0, import_node_fs2.readFileSync)(file2, "utf8")) : {};
|
|
593
|
+
}
|
|
594
|
+
function writeJson(file2, value) {
|
|
595
|
+
(0, import_node_fs2.writeFileSync)(file2, JSON.stringify(value, null, 2) + "\n");
|
|
596
|
+
}
|
|
597
|
+
var import_node_fs2, import_node_os2, import_node_path3, HARNESSES, USER_SCOPE_ONLY, EVENTS, CLAUDE_PAIRS, HARNESS_EVENTS, WAIT_MAX_S, WAIT_TIMEOUT_S, TIMEOUT_S, KIMI_BEGIN, KIMI_END, GOOSE_PLUGIN, OURS, isOurs;
|
|
598
|
+
var init_hooks = __esm({
|
|
599
|
+
"src/cli/hooks.ts"() {
|
|
600
|
+
"use strict";
|
|
601
|
+
import_node_fs2 = require("node:fs");
|
|
602
|
+
import_node_os2 = require("node:os");
|
|
603
|
+
import_node_path3 = require("node:path");
|
|
604
|
+
init_amp_plugin();
|
|
605
|
+
init_opencode_plugin();
|
|
606
|
+
HARNESSES = ["claude-code", "codex", "copilot", "opencode", "droid", "gemini", "qwen", "cursor", "antigravity", "kimi", "goose", "amp", "crush"];
|
|
607
|
+
USER_SCOPE_ONLY = /* @__PURE__ */ new Set(["kimi"]);
|
|
608
|
+
EVENTS = ["SessionStart", "UserPromptSubmit", "PostToolUse", "Stop", "SessionEnd"];
|
|
609
|
+
CLAUDE_PAIRS = [
|
|
610
|
+
["SessionStart", "session-start"],
|
|
611
|
+
["UserPromptSubmit", "prompt"],
|
|
612
|
+
["PostToolUse", "post-tool"],
|
|
613
|
+
["Stop", "stop"],
|
|
614
|
+
["SessionEnd", "session-end"]
|
|
615
|
+
];
|
|
616
|
+
HARNESS_EVENTS = {
|
|
617
|
+
copilot: [
|
|
618
|
+
["sessionStart", "session-start"],
|
|
619
|
+
["userPromptSubmitted", "prompt"],
|
|
620
|
+
["postToolUse", "post-tool"],
|
|
621
|
+
["agentStop", "stop"],
|
|
622
|
+
["sessionEnd", "session-end"]
|
|
623
|
+
],
|
|
624
|
+
droid: CLAUDE_PAIRS,
|
|
625
|
+
qwen: CLAUDE_PAIRS,
|
|
626
|
+
gemini: [
|
|
627
|
+
["SessionStart", "session-start"],
|
|
628
|
+
["BeforeAgent", "prompt"],
|
|
629
|
+
["AfterTool", "post-tool"],
|
|
630
|
+
["AfterAgent", "stop"],
|
|
631
|
+
["SessionEnd", "session-end"]
|
|
632
|
+
],
|
|
633
|
+
// beforeSubmitPrompt can't add context, so prompts are left out.
|
|
634
|
+
cursor: [
|
|
635
|
+
["sessionStart", "session-start"],
|
|
636
|
+
["postToolUse", "post-tool"],
|
|
637
|
+
["stop", "stop"],
|
|
638
|
+
["sessionEnd", "session-end"]
|
|
639
|
+
],
|
|
640
|
+
// PostToolUse can't add context; PreInvocation runs before each model call, so it stands in
|
|
641
|
+
// for both prompts and tool calls. There's no session end.
|
|
642
|
+
antigravity: [
|
|
643
|
+
["SessionStart", "session-start"],
|
|
644
|
+
["PreInvocation", "post-tool"],
|
|
645
|
+
["Stop", "stop"]
|
|
646
|
+
],
|
|
647
|
+
// SessionStart and PostToolUse only observe; the first prompt gets the session-start notice.
|
|
648
|
+
kimi: [
|
|
649
|
+
["UserPromptSubmit", "prompt"],
|
|
650
|
+
["Stop", "stop"],
|
|
651
|
+
["SessionEnd", "session-end"]
|
|
652
|
+
],
|
|
653
|
+
// Crush has only PreToolUse; its first one in a session gets the session-start notice.
|
|
654
|
+
crush: [["PreToolUse", "post-tool"]],
|
|
655
|
+
// Goose hooks can't add context: Stop keeps a turn going for a request, and presence.
|
|
656
|
+
goose: [
|
|
657
|
+
["Stop", "stop"],
|
|
658
|
+
["SessionEnd", "session-end"]
|
|
659
|
+
]
|
|
660
|
+
};
|
|
661
|
+
WAIT_MAX_S = 12 * 3600;
|
|
662
|
+
WAIT_TIMEOUT_S = WAIT_MAX_S + 60;
|
|
663
|
+
TIMEOUT_S = { "claude-code": {}, codex: { SessionEnd: 3 } };
|
|
664
|
+
KIMI_BEGIN = "# >>> agenthub hooks: agenthub hooks uninstall kimi removes this block >>>";
|
|
665
|
+
KIMI_END = "# <<< agenthub hooks <<<";
|
|
666
|
+
GOOSE_PLUGIN = { name: "agenthub", version: "1.0.0", description: "Agent Hub: handle hub requests that arrive during a turn, and presence" };
|
|
667
|
+
OURS = [/^(?:\S*[\\/])?agenthub(?:\.js|\.exe)?\s+(?:notify|wait)\b/i, /^"(?:[^"]*[\\/])?agenthub(?:\.js|\.exe)?"\s+(?:notify|wait)\b/i];
|
|
668
|
+
isOurs = (h) => typeof h?.command === "string" && OURS.some((r) => r.test(h.command));
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
|
|
62
672
|
// src/cli/session.ts
|
|
63
673
|
var session_exports = {};
|
|
64
674
|
__export(session_exports, {
|
|
@@ -56177,573 +56787,8 @@ function credentialStore(env = process.env) {
|
|
|
56177
56787
|
return new FileCredentialStore(configDir(env));
|
|
56178
56788
|
}
|
|
56179
56789
|
|
|
56180
|
-
// src/cli/
|
|
56181
|
-
|
|
56182
|
-
var import_node_os2 = require("node:os");
|
|
56183
|
-
var import_node_path3 = require("node:path");
|
|
56184
|
-
|
|
56185
|
-
// src/cli/amp-plugin.ts
|
|
56186
|
-
var AMP_MARKER = "// Agent Hub plugin for Amp, written by `agenthub hooks install amp`.";
|
|
56187
|
-
var PACKAGE_HEADER = `// Agent Hub plugin for Amp. Copy it to ~/.config/amp/plugins/ (or a repo's .amp/plugins/).
|
|
56188
|
-
// Needs the agenthub CLI: curl -fsSL https://get.theagenthub.app | sh`;
|
|
56189
|
-
var PACKAGE_COMMAND = `const COMMAND =
|
|
56190
|
-
process.env.AGENTHUB_BIN ||
|
|
56191
|
-
[join(homedir(), ".local", "bin", "agenthub"), join(process.env.LOCALAPPDATA || homedir(), "Programs", "agenthub", "agenthub.exe")].find((f) => existsSync(f)) ||
|
|
56192
|
-
"agenthub";`;
|
|
56193
|
-
function ampPlugin(command, as = "file") {
|
|
56194
|
-
const header = as === "package" ? PACKAGE_HEADER : `${AMP_MARKER}
|
|
56195
|
-
// Re-run that command to update it, or \`agenthub hooks uninstall amp\` to remove it.`;
|
|
56196
|
-
return `${header}
|
|
56197
|
-
import { spawn } from "node:child_process";
|
|
56198
|
-
import { existsSync } from "node:fs";
|
|
56199
|
-
import { homedir } from "node:os";
|
|
56200
|
-
import { join } from "node:path";
|
|
56201
|
-
import { fileURLToPath } from "node:url";
|
|
56202
|
-
|
|
56203
|
-
${as === "package" ? PACKAGE_COMMAND : `const COMMAND = ${JSON.stringify(command)};`}
|
|
56204
|
-
const TIMEOUT_MS = 5000;
|
|
56205
|
-
|
|
56206
|
-
export const description = "Agent Hub: teammates' agents' messages in your threads, and an idle thread wakes for their requests.";
|
|
56207
|
-
|
|
56208
|
-
/** Runs agenthub; resolves to its exit code and stdout, or code -1 on any failure. */
|
|
56209
|
-
function run(args, input, cwd, timeoutMs, onChild) {
|
|
56210
|
-
return new Promise((resolve) => {
|
|
56211
|
-
let out = "";
|
|
56212
|
-
let child;
|
|
56213
|
-
try {
|
|
56214
|
-
child = spawn(COMMAND, args, { cwd, stdio: ["pipe", "pipe", "ignore"], windowsHide: true, shell: process.platform === "win32" });
|
|
56215
|
-
} catch {
|
|
56216
|
-
return resolve({ code: -1, out: "" });
|
|
56217
|
-
}
|
|
56218
|
-
onChild?.(child);
|
|
56219
|
-
const timer = timeoutMs ? setTimeout(() => child.kill(), timeoutMs) : undefined;
|
|
56220
|
-
child.stdout.on("data", (chunk) => (out += chunk));
|
|
56221
|
-
child.on("error", () => {
|
|
56222
|
-
clearTimeout(timer);
|
|
56223
|
-
resolve({ code: -1, out: "" });
|
|
56224
|
-
});
|
|
56225
|
-
child.on("close", (code) => {
|
|
56226
|
-
clearTimeout(timer);
|
|
56227
|
-
resolve({ code: code ?? -1, out: out.trim() });
|
|
56228
|
-
});
|
|
56229
|
-
child.stdin.on("error", () => {});
|
|
56230
|
-
child.stdin.end(input);
|
|
56231
|
-
});
|
|
56232
|
-
}
|
|
56233
|
-
|
|
56234
|
-
export default function agenthub(amp) {
|
|
56235
|
-
let cwd = process.cwd();
|
|
56236
|
-
try {
|
|
56237
|
-
const root = amp.system?.workspaceRoot ? String(amp.system.workspaceRoot) : "";
|
|
56238
|
-
if (root) cwd = root.startsWith("file:") ? fileURLToPath(root) : root;
|
|
56239
|
-
} catch {}
|
|
56240
|
-
const started = new Set();
|
|
56241
|
-
const waiting = new Map();
|
|
56242
|
-
const stopWaiting = (id) => {
|
|
56243
|
-
waiting.get(id)?.kill();
|
|
56244
|
-
waiting.delete(id);
|
|
56245
|
-
};
|
|
56246
|
-
const notify = async (event, id, extra) => {
|
|
56247
|
-
const { out } = await run(["notify", "--format", "amp", "--event", event], JSON.stringify({ session_id: id, cwd, ...extra }), cwd, TIMEOUT_MS);
|
|
56248
|
-
return out;
|
|
56249
|
-
};
|
|
56250
|
-
const say = async (id, content, steer) => {
|
|
56251
|
-
try {
|
|
56252
|
-
await amp.threads.get(id).appendUserMessage({ type: "user-message", content }, steer ? { steer: true } : undefined);
|
|
56253
|
-
} catch (e) {
|
|
56254
|
-
amp.logger?.log?.("agenthub: couldn't add to thread " + id + ": " + e);
|
|
56255
|
-
}
|
|
56256
|
-
};
|
|
56257
|
-
const wake = async (id) => {
|
|
56258
|
-
const { code, out } = await run(["wait"], JSON.stringify({ session_id: id, cwd }), cwd, 0, (child) => {
|
|
56259
|
-
stopWaiting(id);
|
|
56260
|
-
waiting.set(id, child);
|
|
56261
|
-
});
|
|
56262
|
-
// An agenthub too old to know \`wait\` exits 2 too, but prints nothing on stdout.
|
|
56263
|
-
if (code === 2 && out) await say(id, out, false);
|
|
56264
|
-
};
|
|
56265
|
-
|
|
56266
|
-
amp.on("agent.start", async (event) => {
|
|
56267
|
-
const id = event.thread?.id;
|
|
56268
|
-
if (!id) return;
|
|
56269
|
-
stopWaiting(id);
|
|
56270
|
-
const first = !started.has(id);
|
|
56271
|
-
started.add(id);
|
|
56272
|
-
const text = await notify(first ? "session-start" : "prompt", id);
|
|
56273
|
-
return text ? { message: { content: text, display: false } } : undefined;
|
|
56274
|
-
});
|
|
56275
|
-
|
|
56276
|
-
amp.on("tool.result", async (event) => {
|
|
56277
|
-
const id = event.thread?.id;
|
|
56278
|
-
if (!id) return;
|
|
56279
|
-
const text = await notify("post-tool", id);
|
|
56280
|
-
if (!text) return;
|
|
56281
|
-
if (typeof event.output === "string") return { status: event.status, output: event.output + "\\n\\n" + text };
|
|
56282
|
-
await say(id, text, true);
|
|
56283
|
-
});
|
|
56284
|
-
|
|
56285
|
-
amp.on("agent.end", async (event) => {
|
|
56286
|
-
const id = event.thread?.id;
|
|
56287
|
-
if (!id) return;
|
|
56288
|
-
// A cancelled turn isn't kept going, and doesn't start a waiter.
|
|
56289
|
-
const out = await notify("stop", id, { status: event.status });
|
|
56290
|
-
if (out) {
|
|
56291
|
-
try {
|
|
56292
|
-
const r = JSON.parse(out);
|
|
56293
|
-
if (r.decision === "block" && r.reason) return { action: "continue", userMessage: r.reason };
|
|
56294
|
-
} catch {}
|
|
56295
|
-
}
|
|
56296
|
-
if (event.status !== "cancelled") void wake(id);
|
|
56297
|
-
});
|
|
56298
|
-
|
|
56299
|
-
amp.onDispose?.(() => {
|
|
56300
|
-
for (const child of waiting.values()) child.kill();
|
|
56301
|
-
waiting.clear();
|
|
56302
|
-
});
|
|
56303
|
-
}
|
|
56304
|
-
`;
|
|
56305
|
-
}
|
|
56306
|
-
|
|
56307
|
-
// src/cli/opencode-plugin.ts
|
|
56308
|
-
var OPENCODE_MARKER = "// Agent Hub plugin for OpenCode, written by `agenthub hooks install opencode`.";
|
|
56309
|
-
var PACKAGE_HEADER2 = `// Agent Hub plugin for OpenCode, from the @theagenthub/opencode npm package.
|
|
56310
|
-
// Needs the agenthub CLI: curl -fsSL https://get.theagenthub.app | sh`;
|
|
56311
|
-
var PACKAGE_COMMAND2 = `import { existsSync } from "node:fs";
|
|
56312
|
-
import { homedir } from "node:os";
|
|
56313
|
-
import { join } from "node:path";
|
|
56314
|
-
|
|
56315
|
-
const COMMAND =
|
|
56316
|
-
process.env.AGENTHUB_BIN ||
|
|
56317
|
-
[join(homedir(), ".local", "bin", "agenthub"), join(process.env.LOCALAPPDATA || homedir(), "Programs", "agenthub", "agenthub.exe")].find((f) => existsSync(f)) ||
|
|
56318
|
-
"agenthub";`;
|
|
56319
|
-
function opencodePlugin(command, as = "file") {
|
|
56320
|
-
const header = as === "package" ? PACKAGE_HEADER2 : `${OPENCODE_MARKER}
|
|
56321
|
-
// Re-run that command to update it, or \`agenthub hooks uninstall opencode\` to remove it.`;
|
|
56322
|
-
return `${header}
|
|
56323
|
-
import { spawn } from "node:child_process";
|
|
56324
|
-
|
|
56325
|
-
${as === "package" ? PACKAGE_COMMAND2 : `const COMMAND = ${JSON.stringify(command)};`}
|
|
56326
|
-
const TIMEOUT_MS = 5000;
|
|
56327
|
-
|
|
56328
|
-
function notify(event, sessionID, cwd) {
|
|
56329
|
-
return new Promise((resolve) => {
|
|
56330
|
-
let out = "";
|
|
56331
|
-
let child;
|
|
56332
|
-
try {
|
|
56333
|
-
child = spawn(COMMAND, ["notify", "--format", "text", "--event", event], {
|
|
56334
|
-
cwd,
|
|
56335
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
56336
|
-
windowsHide: true,
|
|
56337
|
-
shell: process.platform === "win32",
|
|
56338
|
-
});
|
|
56339
|
-
} catch {
|
|
56340
|
-
return resolve("");
|
|
56341
|
-
}
|
|
56342
|
-
const timer = setTimeout(() => {
|
|
56343
|
-
child.kill();
|
|
56344
|
-
resolve("");
|
|
56345
|
-
}, TIMEOUT_MS);
|
|
56346
|
-
child.stdout.on("data", (chunk) => (out += chunk));
|
|
56347
|
-
child.on("error", () => {
|
|
56348
|
-
clearTimeout(timer);
|
|
56349
|
-
resolve("");
|
|
56350
|
-
});
|
|
56351
|
-
child.on("close", () => {
|
|
56352
|
-
clearTimeout(timer);
|
|
56353
|
-
resolve(out.trim());
|
|
56354
|
-
});
|
|
56355
|
-
child.stdin.on("error", () => {});
|
|
56356
|
-
child.stdin.end(JSON.stringify({ session_id: sessionID, cwd }));
|
|
56357
|
-
});
|
|
56358
|
-
}
|
|
56359
|
-
|
|
56360
|
-
/** Runs \`agenthub wait\`; resolves to the text to wake the session with, or "" when it shouldn't. */
|
|
56361
|
-
function waitForMessage(sessionID, cwd, children) {
|
|
56362
|
-
return new Promise((resolve) => {
|
|
56363
|
-
let out = "";
|
|
56364
|
-
let child;
|
|
56365
|
-
try {
|
|
56366
|
-
child = spawn(COMMAND, ["wait"], { cwd, stdio: ["pipe", "pipe", "ignore"], windowsHide: true, shell: process.platform === "win32" });
|
|
56367
|
-
} catch {
|
|
56368
|
-
return resolve("");
|
|
56369
|
-
}
|
|
56370
|
-
children.get(sessionID)?.kill();
|
|
56371
|
-
children.set(sessionID, child);
|
|
56372
|
-
child.stdout.on("data", (chunk) => (out += chunk));
|
|
56373
|
-
child.on("error", () => resolve(""));
|
|
56374
|
-
child.on("close", (code) => {
|
|
56375
|
-
if (children.get(sessionID) === child) children.delete(sessionID);
|
|
56376
|
-
// An agenthub too old to know \`wait\` exits 2 too, but prints nothing on stdout.
|
|
56377
|
-
resolve(code === 2 ? out.trim() : "");
|
|
56378
|
-
});
|
|
56379
|
-
child.stdin.on("error", () => {});
|
|
56380
|
-
child.stdin.end(JSON.stringify({ session_id: sessionID, cwd }));
|
|
56381
|
-
});
|
|
56382
|
-
}
|
|
56383
|
-
|
|
56384
|
-
function partId() {
|
|
56385
|
-
return "prt_" + Date.now().toString(16) + Math.random().toString(36).slice(2, 12);
|
|
56386
|
-
}
|
|
56387
|
-
|
|
56388
|
-
export const AgentHub = async ({ directory, client }) => {
|
|
56389
|
-
const started = new Set();
|
|
56390
|
-
const waiting = new Map();
|
|
56391
|
-
let presence = Promise.resolve();
|
|
56392
|
-
const wake = async (sessionID) => {
|
|
56393
|
-
const send = client?.session?.promptAsync ?? client?.session?.prompt;
|
|
56394
|
-
if (!send) return;
|
|
56395
|
-
const text = await waitForMessage(sessionID, directory, waiting);
|
|
56396
|
-
if (!text) return;
|
|
56397
|
-
try {
|
|
56398
|
-
await send.call(client.session, { path: { id: sessionID }, body: { parts: [{ type: "text", text }] } });
|
|
56399
|
-
} catch {
|
|
56400
|
-
/* the session is gone, or busy again */
|
|
56401
|
-
}
|
|
56402
|
-
};
|
|
56403
|
-
return {
|
|
56404
|
-
"chat.message": async (input, output) => {
|
|
56405
|
-
waiting.get(input.sessionID)?.kill();
|
|
56406
|
-
const first = !started.has(input.sessionID);
|
|
56407
|
-
started.add(input.sessionID);
|
|
56408
|
-
const text = await notify(first ? "session-start" : "prompt", input.sessionID, directory);
|
|
56409
|
-
const messageID = input.messageID ?? output.message?.id;
|
|
56410
|
-
if (!text || !messageID) return;
|
|
56411
|
-
output.parts.push({ id: partId(), sessionID: input.sessionID, messageID, type: "text", text, synthetic: true });
|
|
56412
|
-
},
|
|
56413
|
-
"tool.execute.after": async (input, output) => {
|
|
56414
|
-
const text = await notify("post-tool", input.sessionID, directory);
|
|
56415
|
-
if (text && typeof output.output === "string") output.output += "\\n\\n<agenthub>\\n" + text + "\\n</agenthub>";
|
|
56416
|
-
},
|
|
56417
|
-
// Presence updates don't block OpenCode, but run one at a time so "offline" can't be
|
|
56418
|
-
// overtaken by an earlier "idle".
|
|
56419
|
-
event: async ({ event }) => {
|
|
56420
|
-
if (event.type === "session.idle") {
|
|
56421
|
-
const sessionID = event.properties.sessionID;
|
|
56422
|
-
presence = presence.then(() => notify("stop", sessionID, directory));
|
|
56423
|
-
// Not awaited: it waits as long as the session is idle.
|
|
56424
|
-
presence.then(() => wake(sessionID)).catch(() => {});
|
|
56425
|
-
} else if (event.type === "session.deleted") {
|
|
56426
|
-
waiting.get(event.properties.info.id)?.kill();
|
|
56427
|
-
presence = presence.then(() => notify("session-end", event.properties.info.id, directory));
|
|
56428
|
-
}
|
|
56429
|
-
},
|
|
56430
|
-
};
|
|
56431
|
-
};
|
|
56432
|
-
|
|
56433
|
-
// OpenCode 2 loads the default export's setup() and ignores the hooks above; OpenCode 1
|
|
56434
|
-
// reads the named export (and default.server, the same function). setup() returns early
|
|
56435
|
-
// when OpenCode 1 calls it too.
|
|
56436
|
-
async function setup(ctx) {
|
|
56437
|
-
if (!ctx?.session?.hook || !ctx.tool?.hook || !ctx.event?.subscribe) return;
|
|
56438
|
-
const directory = ctx.location?.directory ?? process.cwd();
|
|
56439
|
-
const started = new Set();
|
|
56440
|
-
const waiting = new Map();
|
|
56441
|
-
let presence = Promise.resolve();
|
|
56442
|
-
const wake = async (sessionID) => {
|
|
56443
|
-
if (typeof ctx.session.synthetic !== "function") return;
|
|
56444
|
-
const text = await waitForMessage(sessionID, directory, waiting);
|
|
56445
|
-
if (!text) return;
|
|
56446
|
-
try {
|
|
56447
|
-
await ctx.session.synthetic({ sessionID, text, description: "Agent Hub: new message", delivery: "queue", resume: true });
|
|
56448
|
-
} catch {
|
|
56449
|
-
/* the session is gone, or busy again */
|
|
56450
|
-
}
|
|
56451
|
-
};
|
|
56452
|
-
const wrap = (text) => "\\n\\n<agenthub>\\n" + text + "\\n</agenthub>";
|
|
56453
|
-
|
|
56454
|
-
// V2 has no hidden message parts, so the notice is appended to the prompt's text.
|
|
56455
|
-
await ctx.session.hook("prompt", async (event) => {
|
|
56456
|
-
waiting.get(event.sessionID)?.kill();
|
|
56457
|
-
const first = !started.has(event.sessionID);
|
|
56458
|
-
started.add(event.sessionID);
|
|
56459
|
-
const text = await notify(first ? "session-start" : "prompt", event.sessionID, directory);
|
|
56460
|
-
if (text && event.prompt && typeof event.prompt.text === "string") event.prompt.text += wrap(text);
|
|
56461
|
-
});
|
|
56462
|
-
// The model reads a tool's result.content.
|
|
56463
|
-
await ctx.tool.hook("execute.after", async (event) => {
|
|
56464
|
-
if (event.status !== "completed" || !Array.isArray(event.result?.content)) return;
|
|
56465
|
-
const text = await notify("post-tool", event.sessionID, directory);
|
|
56466
|
-
if (text) event.result.content.push({ type: "text", text: wrap(text).trimStart() });
|
|
56467
|
-
});
|
|
56468
|
-
|
|
56469
|
-
const controller = new AbortController();
|
|
56470
|
-
(async () => {
|
|
56471
|
-
const types = ["session.execution.succeeded", "session.execution.failed", "session.execution.interrupted", "session.deleted"];
|
|
56472
|
-
for await (const event of ctx.event.subscribe({ types, signal: controller.signal })) {
|
|
56473
|
-
const sessionID = event.data?.sessionID;
|
|
56474
|
-
if (!sessionID) continue;
|
|
56475
|
-
const kind = event.type === "session.deleted" ? "session-end" : "stop";
|
|
56476
|
-
if (kind === "session-end") {
|
|
56477
|
-
started.delete(sessionID);
|
|
56478
|
-
waiting.get(sessionID)?.kill();
|
|
56479
|
-
}
|
|
56480
|
-
presence = presence.then(() => notify(kind, sessionID, directory));
|
|
56481
|
-
// A finished run (not one the user interrupted) waits for a hub message; not awaited.
|
|
56482
|
-
if (event.type === "session.execution.succeeded" || event.type === "session.execution.failed") {
|
|
56483
|
-
presence.then(() => wake(sessionID)).catch(() => {});
|
|
56484
|
-
}
|
|
56485
|
-
}
|
|
56486
|
-
})().catch(() => {});
|
|
56487
|
-
return () => {
|
|
56488
|
-
controller.abort();
|
|
56489
|
-
for (const child of waiting.values()) child.kill();
|
|
56490
|
-
};
|
|
56491
|
-
}
|
|
56492
|
-
|
|
56493
|
-
export default { id: "agenthub", server: AgentHub, setup };
|
|
56494
|
-
`;
|
|
56495
|
-
}
|
|
56496
|
-
|
|
56497
|
-
// src/cli/hooks.ts
|
|
56498
|
-
var HARNESSES = ["claude-code", "codex", "copilot", "opencode", "droid", "gemini", "qwen", "cursor", "antigravity", "kimi", "goose", "amp", "crush"];
|
|
56499
|
-
var USER_SCOPE_ONLY = /* @__PURE__ */ new Set(["kimi"]);
|
|
56500
|
-
var EVENTS = ["SessionStart", "UserPromptSubmit", "PostToolUse", "Stop", "SessionEnd"];
|
|
56501
|
-
var CLAUDE_PAIRS = [
|
|
56502
|
-
["SessionStart", "session-start"],
|
|
56503
|
-
["UserPromptSubmit", "prompt"],
|
|
56504
|
-
["PostToolUse", "post-tool"],
|
|
56505
|
-
["Stop", "stop"],
|
|
56506
|
-
["SessionEnd", "session-end"]
|
|
56507
|
-
];
|
|
56508
|
-
var HARNESS_EVENTS = {
|
|
56509
|
-
copilot: [
|
|
56510
|
-
["sessionStart", "session-start"],
|
|
56511
|
-
["userPromptSubmitted", "prompt"],
|
|
56512
|
-
["postToolUse", "post-tool"],
|
|
56513
|
-
["agentStop", "stop"],
|
|
56514
|
-
["sessionEnd", "session-end"]
|
|
56515
|
-
],
|
|
56516
|
-
droid: CLAUDE_PAIRS,
|
|
56517
|
-
qwen: CLAUDE_PAIRS,
|
|
56518
|
-
gemini: [
|
|
56519
|
-
["SessionStart", "session-start"],
|
|
56520
|
-
["BeforeAgent", "prompt"],
|
|
56521
|
-
["AfterTool", "post-tool"],
|
|
56522
|
-
["AfterAgent", "stop"],
|
|
56523
|
-
["SessionEnd", "session-end"]
|
|
56524
|
-
],
|
|
56525
|
-
// beforeSubmitPrompt can't add context, so prompts are left out.
|
|
56526
|
-
cursor: [
|
|
56527
|
-
["sessionStart", "session-start"],
|
|
56528
|
-
["postToolUse", "post-tool"],
|
|
56529
|
-
["stop", "stop"],
|
|
56530
|
-
["sessionEnd", "session-end"]
|
|
56531
|
-
],
|
|
56532
|
-
// PostToolUse can't add context; PreInvocation runs before each model call, so it stands in
|
|
56533
|
-
// for both prompts and tool calls. There's no session end.
|
|
56534
|
-
antigravity: [
|
|
56535
|
-
["SessionStart", "session-start"],
|
|
56536
|
-
["PreInvocation", "post-tool"],
|
|
56537
|
-
["Stop", "stop"]
|
|
56538
|
-
],
|
|
56539
|
-
// SessionStart and PostToolUse only observe; the first prompt gets the session-start notice.
|
|
56540
|
-
kimi: [
|
|
56541
|
-
["UserPromptSubmit", "prompt"],
|
|
56542
|
-
["Stop", "stop"],
|
|
56543
|
-
["SessionEnd", "session-end"]
|
|
56544
|
-
],
|
|
56545
|
-
// Crush has only PreToolUse; its first one in a session gets the session-start notice.
|
|
56546
|
-
crush: [["PreToolUse", "post-tool"]],
|
|
56547
|
-
// Goose hooks can't add context: Stop keeps a turn going for a request, and presence.
|
|
56548
|
-
goose: [
|
|
56549
|
-
["Stop", "stop"],
|
|
56550
|
-
["SessionEnd", "session-end"]
|
|
56551
|
-
]
|
|
56552
|
-
};
|
|
56553
|
-
var WAIT_MAX_S = 12 * 3600;
|
|
56554
|
-
var WAIT_TIMEOUT_S = WAIT_MAX_S + 60;
|
|
56555
|
-
var TIMEOUT_S = { "claude-code": {}, codex: { SessionEnd: 3 } };
|
|
56556
|
-
var KIMI_BEGIN = "# >>> agenthub hooks: agenthub hooks uninstall kimi removes this block >>>";
|
|
56557
|
-
var KIMI_END = "# <<< agenthub hooks <<<";
|
|
56558
|
-
var GOOSE_PLUGIN = { name: "agenthub", version: "1.0.0", description: "Agent Hub: handle hub requests that arrive during a turn, and presence" };
|
|
56559
|
-
function hookFile(harness, scope, cwd, home = (0, import_node_os2.homedir)(), env = process.env) {
|
|
56560
|
-
const user = scope === "user";
|
|
56561
|
-
const base = user ? home : cwd;
|
|
56562
|
-
switch (harness) {
|
|
56563
|
-
case "claude-code":
|
|
56564
|
-
return user ? (0, import_node_path3.join)(home, ".claude", "settings.json") : (0, import_node_path3.join)(cwd, ".claude", "settings.local.json");
|
|
56565
|
-
case "codex":
|
|
56566
|
-
return (0, import_node_path3.join)(base, ".codex", "hooks.json");
|
|
56567
|
-
case "copilot":
|
|
56568
|
-
return user ? (0, import_node_path3.join)(env.COPILOT_HOME || (0, import_node_path3.join)(home, ".copilot"), "hooks", "agenthub.json") : (0, import_node_path3.join)(cwd, ".github", "hooks", "agenthub.json");
|
|
56569
|
-
case "opencode":
|
|
56570
|
-
return user ? (0, import_node_path3.join)(env.XDG_CONFIG_HOME || (0, import_node_path3.join)(home, ".config"), "opencode", "plugins", "agenthub.js") : (0, import_node_path3.join)(cwd, ".opencode", "plugins", "agenthub.js");
|
|
56571
|
-
case "droid":
|
|
56572
|
-
return (0, import_node_path3.join)(base, ".factory", "hooks.json");
|
|
56573
|
-
case "gemini":
|
|
56574
|
-
return (0, import_node_path3.join)(base, ".gemini", "settings.json");
|
|
56575
|
-
case "qwen":
|
|
56576
|
-
return (0, import_node_path3.join)(base, ".qwen", "settings.json");
|
|
56577
|
-
case "cursor":
|
|
56578
|
-
return (0, import_node_path3.join)(base, ".cursor", "hooks.json");
|
|
56579
|
-
case "antigravity":
|
|
56580
|
-
return user ? (0, import_node_path3.join)(home, ".gemini", "config", "hooks.json") : (0, import_node_path3.join)(cwd, ".agents", "hooks.json");
|
|
56581
|
-
case "kimi":
|
|
56582
|
-
return (0, import_node_path3.join)(home, ".kimi-code", "config.toml");
|
|
56583
|
-
case "goose":
|
|
56584
|
-
return (0, import_node_path3.join)(base, ".agents", "plugins", "agenthub", "hooks", "hooks.json");
|
|
56585
|
-
case "crush":
|
|
56586
|
-
return user ? (0, import_node_path3.join)(env.XDG_CONFIG_HOME || (0, import_node_path3.join)(home, ".config"), "crush", "crush.json") : (0, import_node_fs2.existsSync)((0, import_node_path3.join)(cwd, "crush.json")) ? (0, import_node_path3.join)(cwd, "crush.json") : (0, import_node_path3.join)(cwd, ".crush.json");
|
|
56587
|
-
case "amp":
|
|
56588
|
-
return user ? (0, import_node_path3.join)(env.XDG_CONFIG_HOME || (0, import_node_path3.join)(home, ".config"), "amp", "plugins", "agenthub.ts") : (0, import_node_path3.join)(cwd, ".amp", "plugins", "agenthub.ts");
|
|
56589
|
-
}
|
|
56590
|
-
}
|
|
56591
|
-
var OURS = [/^(?:\S*[\\/])?agenthub(?:\.js|\.exe)?\s+(?:notify|wait)\b/i, /^"(?:[^"]*[\\/])?agenthub(?:\.js|\.exe)?"\s+(?:notify|wait)\b/i];
|
|
56592
|
-
var isOurs = (h) => typeof h?.command === "string" && OURS.some((r) => r.test(h.command));
|
|
56593
|
-
function installHooks(harness, file2, command = "agenthub") {
|
|
56594
|
-
(0, import_node_fs2.mkdirSync)((0, import_node_path3.dirname)(file2), { recursive: true });
|
|
56595
|
-
const notify2 = (event) => `${command} notify --format ${harness} --event ${event}`;
|
|
56596
|
-
const pairs = HARNESS_EVENTS[harness] ?? [];
|
|
56597
|
-
switch (harness) {
|
|
56598
|
-
case "opencode":
|
|
56599
|
-
(0, import_node_fs2.writeFileSync)(file2, opencodePlugin(command));
|
|
56600
|
-
return ["chat.message", "tool.execute.after", "session.idle", "session.deleted"];
|
|
56601
|
-
case "amp":
|
|
56602
|
-
(0, import_node_fs2.writeFileSync)(file2, ampPlugin(command));
|
|
56603
|
-
return ["agent.start", "tool.result", "agent.end"];
|
|
56604
|
-
case "copilot":
|
|
56605
|
-
case "cursor":
|
|
56606
|
-
case "crush": {
|
|
56607
|
-
const config2 = readJson(file2);
|
|
56608
|
-
if (harness !== "crush") config2.version = 1;
|
|
56609
|
-
const hooks = config2.hooks ??= {};
|
|
56610
|
-
for (const [event, notifyEvent] of pairs) {
|
|
56611
|
-
const kept = (hooks[event] ?? []).filter((h) => !isOurs(h));
|
|
56612
|
-
const entry = harness === "copilot" ? { type: "command", command: notify2(notifyEvent), timeoutSec: 10 } : { command: notify2(notifyEvent), timeout: 10 };
|
|
56613
|
-
hooks[event] = [...kept, entry];
|
|
56614
|
-
}
|
|
56615
|
-
writeJson(file2, config2);
|
|
56616
|
-
return pairs.map(([e]) => e);
|
|
56617
|
-
}
|
|
56618
|
-
case "antigravity": {
|
|
56619
|
-
const config2 = readJson(file2);
|
|
56620
|
-
config2.agenthub = Object.fromEntries(pairs.map(([event, n]) => [event, [{ hooks: [{ type: "command", command: notify2(n), timeout: 10 }] }]]));
|
|
56621
|
-
writeJson(file2, config2);
|
|
56622
|
-
return pairs.map(([e]) => e);
|
|
56623
|
-
}
|
|
56624
|
-
case "kimi": {
|
|
56625
|
-
const before = (0, import_node_fs2.existsSync)(file2) ? stripKimi((0, import_node_fs2.readFileSync)(file2, "utf8")) : "";
|
|
56626
|
-
const block = [KIMI_BEGIN, ...pairs.flatMap(([event, n]) => ["[[hooks]]", `event = "${event}"`, `command = ${JSON.stringify(notify2(n))}`, "timeout = 10", ""]), KIMI_END, ""].join("\n");
|
|
56627
|
-
(0, import_node_fs2.writeFileSync)(file2, `${before}${before && !before.endsWith("\n") ? "\n" : ""}${before ? "\n" : ""}${block}`);
|
|
56628
|
-
return pairs.map(([e]) => e);
|
|
56629
|
-
}
|
|
56630
|
-
case "goose": {
|
|
56631
|
-
const root = (0, import_node_path3.dirname)((0, import_node_path3.dirname)(file2));
|
|
56632
|
-
writeJson((0, import_node_path3.join)(root, "plugin.json"), GOOSE_PLUGIN);
|
|
56633
|
-
writeJson(file2, { hooks: Object.fromEntries(pairs.map(([event, n]) => [event, [{ hooks: [{ type: "command", command: notify2(n), timeout: 10 }] }]])) });
|
|
56634
|
-
return pairs.map(([e]) => e);
|
|
56635
|
-
}
|
|
56636
|
-
case "droid":
|
|
56637
|
-
case "gemini":
|
|
56638
|
-
case "qwen": {
|
|
56639
|
-
const config2 = readJson(file2);
|
|
56640
|
-
const hooks = harness === "droid" ? config2 : config2.hooks ??= {};
|
|
56641
|
-
const timeout = harness === "droid" ? 10 : 1e4;
|
|
56642
|
-
for (const [event, n] of pairs) {
|
|
56643
|
-
hooks[event] = [...keepOthers(hooks[event]), { hooks: [{ type: "command", command: notify2(n), timeout }] }];
|
|
56644
|
-
}
|
|
56645
|
-
writeJson(file2, config2);
|
|
56646
|
-
return pairs.map(([e]) => e);
|
|
56647
|
-
}
|
|
56648
|
-
case "claude-code":
|
|
56649
|
-
case "codex": {
|
|
56650
|
-
const config2 = readJson(file2);
|
|
56651
|
-
const hooks = config2.hooks ??= {};
|
|
56652
|
-
for (const event of EVENTS) {
|
|
56653
|
-
const entry = { hooks: [{ type: "command", command: `${command} notify --format ${harness}`, timeout: TIMEOUT_S[harness][event] ?? 10 }] };
|
|
56654
|
-
if (event === "PostToolUse" && harness === "claude-code") entry.matcher = "*";
|
|
56655
|
-
if (event === "Stop" && harness === "claude-code") {
|
|
56656
|
-
entry.hooks.push({ type: "command", command: `${command} wait --max ${WAIT_MAX_S}`, asyncRewake: true, timeout: WAIT_TIMEOUT_S });
|
|
56657
|
-
}
|
|
56658
|
-
hooks[event] = [...keepOthers(hooks[event]), entry];
|
|
56659
|
-
}
|
|
56660
|
-
writeJson(file2, config2);
|
|
56661
|
-
return EVENTS;
|
|
56662
|
-
}
|
|
56663
|
-
}
|
|
56664
|
-
}
|
|
56665
|
-
function uninstallHooks(harness, file2) {
|
|
56666
|
-
if (!(0, import_node_fs2.existsSync)(file2)) return false;
|
|
56667
|
-
switch (harness) {
|
|
56668
|
-
case "opencode":
|
|
56669
|
-
if (!(0, import_node_fs2.readFileSync)(file2, "utf8").startsWith(OPENCODE_MARKER)) return false;
|
|
56670
|
-
(0, import_node_fs2.rmSync)(file2);
|
|
56671
|
-
return true;
|
|
56672
|
-
case "amp":
|
|
56673
|
-
if (!(0, import_node_fs2.readFileSync)(file2, "utf8").startsWith(AMP_MARKER)) return false;
|
|
56674
|
-
(0, import_node_fs2.rmSync)(file2);
|
|
56675
|
-
return true;
|
|
56676
|
-
case "kimi": {
|
|
56677
|
-
const text = (0, import_node_fs2.readFileSync)(file2, "utf8");
|
|
56678
|
-
const kept = stripKimi(text);
|
|
56679
|
-
if (kept === text) return false;
|
|
56680
|
-
(0, import_node_fs2.writeFileSync)(file2, kept);
|
|
56681
|
-
return true;
|
|
56682
|
-
}
|
|
56683
|
-
case "goose": {
|
|
56684
|
-
const root = (0, import_node_path3.dirname)((0, import_node_path3.dirname)(file2));
|
|
56685
|
-
if (readJson((0, import_node_path3.join)(root, "plugin.json")).name !== GOOSE_PLUGIN.name) return false;
|
|
56686
|
-
(0, import_node_fs2.rmSync)(root, { recursive: true, force: true });
|
|
56687
|
-
return true;
|
|
56688
|
-
}
|
|
56689
|
-
case "antigravity": {
|
|
56690
|
-
const config2 = readJson(file2);
|
|
56691
|
-
if (!("agenthub" in config2)) return false;
|
|
56692
|
-
delete config2.agenthub;
|
|
56693
|
-
writeJson(file2, config2);
|
|
56694
|
-
return true;
|
|
56695
|
-
}
|
|
56696
|
-
case "copilot":
|
|
56697
|
-
case "cursor":
|
|
56698
|
-
case "crush": {
|
|
56699
|
-
const config2 = readJson(file2);
|
|
56700
|
-
if (!config2.hooks) return false;
|
|
56701
|
-
let changed = false;
|
|
56702
|
-
for (const [event, entries] of Object.entries(config2.hooks)) {
|
|
56703
|
-
const kept = entries.filter((h) => !isOurs(h));
|
|
56704
|
-
if (kept.length !== entries.length) changed = true;
|
|
56705
|
-
if (kept.length) config2.hooks[event] = kept;
|
|
56706
|
-
else delete config2.hooks[event];
|
|
56707
|
-
}
|
|
56708
|
-
if (!changed) return false;
|
|
56709
|
-
const empty = !Object.keys(config2.hooks).length && Object.keys(config2).every((k) => k === "version" || k === "hooks");
|
|
56710
|
-
if (harness === "copilot" && empty) (0, import_node_fs2.rmSync)(file2);
|
|
56711
|
-
else writeJson(file2, config2);
|
|
56712
|
-
return true;
|
|
56713
|
-
}
|
|
56714
|
-
default: {
|
|
56715
|
-
const config2 = readJson(file2);
|
|
56716
|
-
const hooks = harness === "droid" ? config2 : config2.hooks;
|
|
56717
|
-
if (!hooks || typeof hooks !== "object") return false;
|
|
56718
|
-
let changed = false;
|
|
56719
|
-
for (const [event, groups] of Object.entries(hooks)) {
|
|
56720
|
-
if (!Array.isArray(groups)) continue;
|
|
56721
|
-
const kept = keepOthers(groups);
|
|
56722
|
-
if (kept.length !== groups.length || kept.some((g, i) => g.hooks.length !== groups[i].hooks.length)) changed = true;
|
|
56723
|
-
if (kept.length) hooks[event] = kept;
|
|
56724
|
-
else delete hooks[event];
|
|
56725
|
-
}
|
|
56726
|
-
if (changed) writeJson(file2, config2);
|
|
56727
|
-
return changed;
|
|
56728
|
-
}
|
|
56729
|
-
}
|
|
56730
|
-
}
|
|
56731
|
-
function keepOthers(groups) {
|
|
56732
|
-
return (groups ?? []).map((g) => ({ ...g, hooks: (g.hooks ?? []).filter((h) => !isOurs(h)) })).filter((g) => g.hooks.length);
|
|
56733
|
-
}
|
|
56734
|
-
function stripKimi(text) {
|
|
56735
|
-
const start = text.indexOf(KIMI_BEGIN);
|
|
56736
|
-
if (start < 0) return text;
|
|
56737
|
-
const end = text.indexOf(KIMI_END, start);
|
|
56738
|
-
const after = end < 0 ? "" : text.slice(end + KIMI_END.length).replace(/^\n/, "");
|
|
56739
|
-
return (text.slice(0, start).replace(/\n+$/, "\n") + after).replace(/^\n+$/, "");
|
|
56740
|
-
}
|
|
56741
|
-
function readJson(file2) {
|
|
56742
|
-
return (0, import_node_fs2.existsSync)(file2) ? JSON.parse((0, import_node_fs2.readFileSync)(file2, "utf8")) : {};
|
|
56743
|
-
}
|
|
56744
|
-
function writeJson(file2, value) {
|
|
56745
|
-
(0, import_node_fs2.writeFileSync)(file2, JSON.stringify(value, null, 2) + "\n");
|
|
56746
|
-
}
|
|
56790
|
+
// src/cli/index.ts
|
|
56791
|
+
init_hooks();
|
|
56747
56792
|
|
|
56748
56793
|
// src/cli/login.ts
|
|
56749
56794
|
var import_node_child_process2 = require("node:child_process");
|
|
@@ -56903,21 +56948,23 @@ var COMMANDS = {
|
|
|
56903
56948
|
},
|
|
56904
56949
|
notify: {
|
|
56905
56950
|
short: "print new messages for a hook",
|
|
56906
|
-
usage: ["agenthub notify --format <harness|text> [--event <session-start|prompt|post-tool|stop|session-end>]"],
|
|
56951
|
+
usage: ["agenthub notify --format <harness|text> [--event <session-start|prompt|post-tool|stop|session-end>] [--plugin]"],
|
|
56907
56952
|
summary: "What the harness hooks run: reads the hook's JSON on stdin and prints new hub messages for the session, in the harness's format. You don't run it yourself.",
|
|
56908
56953
|
details: [
|
|
56909
56954
|
"--format is the harness: claude-code, codex, copilot, amp, droid, gemini, qwen, cursor, antigravity, kimi, goose, crush or roo; or text for plain text (the OpenCode and pi plugins use it).",
|
|
56910
|
-
"It never fails the harness: off the hub, signed out or on any error it prints nothing. Stop marks the agent idle and session-end takes it offline. Only Kimi Code's Stop hook exits 2, to keep a turn going."
|
|
56955
|
+
"It never fails the harness: off the hub, signed out or on any error it prints nothing. Stop marks the agent idle and session-end takes it offline. Only Kimi Code's Stop hook exits 2, to keep a turn going.",
|
|
56956
|
+
"--plugin is what the Claude Code and Codex plugin passes: if `agenthub hooks install` already put hooks in that harness's settings, it prints nothing (at session start, a note on removing them), so notices don't show twice."
|
|
56911
56957
|
],
|
|
56912
56958
|
examples: [[`echo '{"hook_event_name":"UserPromptSubmit","session_id":"abc"}' | agenthub notify --format text`, "see what a hook would print"]]
|
|
56913
56959
|
},
|
|
56914
56960
|
wait: {
|
|
56915
56961
|
short: "wait for a message that should wake an idle agent",
|
|
56916
|
-
usage: ["agenthub wait [--max <seconds>] [--deliver kimi]"],
|
|
56962
|
+
usage: ["agenthub wait [--max <seconds>] [--deliver kimi] [--plugin]"],
|
|
56917
56963
|
summary: "What harnesses run when their agent goes idle: waits for a hub message that should wake it, then exits 2 and prints the summaries (else 0). You don't run it yourself.",
|
|
56918
56964
|
details: [
|
|
56919
56965
|
"The next prompt, a newer waiter for the same session, or --max (12 hours by default) stops it. `agenthub wake` decides whether it waits at all, and whether its text tells the agent to ask you first.",
|
|
56920
|
-
"--deliver kimi is what Kimi Code's Stop hook starts in the background: instead of printing, it sends the summaries to the session through the local kimi web server."
|
|
56966
|
+
"--deliver kimi is what Kimi Code's Stop hook starts in the background: instead of printing, it sends the summaries to the session through the local kimi web server.",
|
|
56967
|
+
"--plugin is what the Claude Code plugin's wake hook passes: it exits 0 right away when `agenthub hooks install claude-code` already added a wake hook."
|
|
56921
56968
|
],
|
|
56922
56969
|
examples: [[`echo '{"session_id":"abc"}' | agenthub wait --max 60; echo $?`, "wait up to a minute; 2 means it would wake the agent"]]
|
|
56923
56970
|
},
|
|
@@ -56982,7 +57029,7 @@ function commandHelp(name) {
|
|
|
56982
57029
|
init_settings();
|
|
56983
57030
|
var import_meta = {};
|
|
56984
57031
|
function version2() {
|
|
56985
|
-
if (true) return "0.3.
|
|
57032
|
+
if (true) return "0.3.3";
|
|
56986
57033
|
try {
|
|
56987
57034
|
return JSON.parse((0, import_node_fs11.readFileSync)(new URL("../../package.json", import_meta.url), "utf8")).version;
|
|
56988
57035
|
} catch {
|
|
@@ -57053,6 +57100,18 @@ async function main(argv) {
|
|
|
57053
57100
|
const format = flag(args, "--format") ?? "text";
|
|
57054
57101
|
const event = flag(args, "--event");
|
|
57055
57102
|
const payload = await readStdin(500);
|
|
57103
|
+
if (args.includes("--plugin") && (format === "claude-code" || format === "codex")) {
|
|
57104
|
+
const { cliHooksFile: cliHooksFile2 } = await Promise.resolve().then(() => (init_hooks(), hooks_exports));
|
|
57105
|
+
const file2 = cliHooksFile2(format, process.cwd());
|
|
57106
|
+
if (file2) {
|
|
57107
|
+
if (event === "session-start" || payload?.hook_event_name === "SessionStart")
|
|
57108
|
+
process.stdout.write(
|
|
57109
|
+
`Agent Hub: ${file2} also runs agenthub notify, so the agenthub plugin's hooks stay out of the way. To use only the plugin, ask the user to run \`agenthub hooks uninstall ${format}\` (with --scope project for a repo's settings).
|
|
57110
|
+
`
|
|
57111
|
+
);
|
|
57112
|
+
return 0;
|
|
57113
|
+
}
|
|
57114
|
+
}
|
|
57056
57115
|
const out = await notifyHook({ format, event, payload }, { store: credentialStore(), version: version2() });
|
|
57057
57116
|
if (out.stdout) process.stdout.write(out.stdout + "\n");
|
|
57058
57117
|
if (out.stderr) process.stderr.write(out.stderr + "\n");
|
|
@@ -57136,6 +57195,10 @@ async function main(argv) {
|
|
|
57136
57195
|
} catch {
|
|
57137
57196
|
payload = void 0;
|
|
57138
57197
|
}
|
|
57198
|
+
if (args.includes("--plugin")) {
|
|
57199
|
+
const { cliHooksFile: cliHooksFile2 } = await Promise.resolve().then(() => (init_hooks(), hooks_exports));
|
|
57200
|
+
if (cliHooksFile2("claude-code", process.cwd())) return 0;
|
|
57201
|
+
}
|
|
57139
57202
|
const { wait: wait2 } = await Promise.resolve().then(() => (init_wait(), wait_exports));
|
|
57140
57203
|
const ac = new AbortController();
|
|
57141
57204
|
process.once("SIGINT", () => ac.abort());
|
package/package.json
CHANGED