@theagenthub/cli 0.3.1 → 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.
Files changed (2) hide show
  1. package/bin/agenthub.cjs +947 -359
  2. 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, {
@@ -463,6 +1073,98 @@ var init_update = __esm({
463
1073
  }
464
1074
  });
465
1075
 
1076
+ // src/cli/kimi.ts
1077
+ var kimi_exports = {};
1078
+ __export(kimi_exports, {
1079
+ KIMI_DEFAULT_PORT: () => KIMI_DEFAULT_PORT,
1080
+ deliverToKimi: () => deliverToKimi,
1081
+ kimiHome: () => kimiHome,
1082
+ kimiServers: () => kimiServers,
1083
+ kimiWakeable: () => kimiWakeable,
1084
+ startKimiWaiter: () => startKimiWaiter
1085
+ });
1086
+ function kimiHome(env, home = (0, import_node_os3.homedir)()) {
1087
+ return env.KIMI_CODE_HOME || (0, import_node_path7.join)(home, ".kimi-code");
1088
+ }
1089
+ function kimiWakeable(payload) {
1090
+ const client = payload?.client_type;
1091
+ return typeof payload?.session_id === "string" && typeof client === "string" && client !== TUI_CLIENT;
1092
+ }
1093
+ function kimiServers(env, home = (0, import_node_os3.homedir)()) {
1094
+ const dir = (0, import_node_path7.join)(kimiHome(env, home), "server", "instances");
1095
+ const urls = [];
1096
+ let files = [];
1097
+ try {
1098
+ files = (0, import_node_fs6.readdirSync)(dir);
1099
+ } catch {
1100
+ }
1101
+ for (const f of files) {
1102
+ try {
1103
+ const entry = JSON.parse((0, import_node_fs6.readFileSync)((0, import_node_path7.join)(dir, f), "utf8"));
1104
+ if (typeof entry.url === "string" && /^https?:\/\//.test(entry.url)) urls.push(entry.url.replace(/\/+$/, ""));
1105
+ else if (typeof entry.port === "number") {
1106
+ const host = typeof entry.host === "string" && entry.host && entry.host !== "0.0.0.0" ? entry.host : "127.0.0.1";
1107
+ urls.push(`http://${host}:${entry.port}`);
1108
+ }
1109
+ } catch {
1110
+ }
1111
+ }
1112
+ urls.push(`http://127.0.0.1:${KIMI_DEFAULT_PORT}`);
1113
+ return [...new Set(urls)];
1114
+ }
1115
+ async function deliverToKimi(sessionId, text, deps = {}) {
1116
+ const env = deps.env ?? process.env;
1117
+ const fetchFn = deps.fetch ?? fetch;
1118
+ let token = "";
1119
+ try {
1120
+ token = (0, import_node_fs6.readFileSync)((0, import_node_path7.join)(kimiHome(env, deps.home), "server.token"), "utf8").trim();
1121
+ } catch {
1122
+ }
1123
+ for (const base of kimiServers(env, deps.home)) {
1124
+ try {
1125
+ const res = await fetchFn(`${base}/api/v1/sessions/${encodeURIComponent(sessionId)}/prompts`, {
1126
+ method: "POST",
1127
+ headers: { "Content-Type": "application/json", ...token ? { Authorization: `Bearer ${token}` } : {} },
1128
+ body: JSON.stringify({ content: [{ type: "text", text }] }),
1129
+ signal: AbortSignal.timeout(1e4)
1130
+ });
1131
+ await res.body?.cancel().catch(() => {
1132
+ });
1133
+ if (res.ok) return true;
1134
+ } catch {
1135
+ }
1136
+ }
1137
+ return false;
1138
+ }
1139
+ function startKimiWaiter(payload, env = process.env) {
1140
+ const script = process.argv[1];
1141
+ const args = script && /\.[cm]?[jt]s$/.test(script) ? [script, "wait", "--deliver", "kimi"] : ["wait", "--deliver", "kimi"];
1142
+ try {
1143
+ const child = (0, import_node_child_process4.spawn)(process.execPath, args, {
1144
+ detached: true,
1145
+ stdio: "ignore",
1146
+ windowsHide: true,
1147
+ env: { ...env, AGENTHUB_WAIT_PAYLOAD: JSON.stringify(payload) }
1148
+ });
1149
+ child.on("error", () => {
1150
+ });
1151
+ child.unref();
1152
+ } catch {
1153
+ }
1154
+ }
1155
+ var import_node_child_process4, import_node_fs6, import_node_os3, import_node_path7, KIMI_DEFAULT_PORT, TUI_CLIENT;
1156
+ var init_kimi = __esm({
1157
+ "src/cli/kimi.ts"() {
1158
+ "use strict";
1159
+ import_node_child_process4 = require("node:child_process");
1160
+ import_node_fs6 = require("node:fs");
1161
+ import_node_os3 = require("node:os");
1162
+ import_node_path7 = require("node:path");
1163
+ KIMI_DEFAULT_PORT = 58627;
1164
+ TUI_CLIENT = "kimi_code_cli";
1165
+ }
1166
+ });
1167
+
466
1168
  // src/cli/watch.ts
467
1169
  var watch_exports = {};
468
1170
  __export(watch_exports, {
@@ -611,15 +1313,19 @@ var init_watch = __esm({
611
1313
  var wait_exports = {};
612
1314
  __export(wait_exports, {
613
1315
  DEFAULT_WAIT_MS: () => DEFAULT_WAIT_MS,
1316
+ actMode: () => actMode,
1317
+ actSetting: () => actSetting,
614
1318
  addPending: () => addPending,
615
1319
  claimFile: () => claimFile,
1320
+ continueNotice: () => continueNotice,
616
1321
  mergeInbox: () => mergeInbox,
617
1322
  pendingFile: () => pendingFile,
618
1323
  releaseWaiter: () => releaseWaiter,
619
1324
  takePending: () => takePending,
620
1325
  wait: () => wait,
621
1326
  wakeMode: () => wakeMode,
622
- wakeSetting: () => wakeSetting
1327
+ wakeSetting: () => wakeSetting,
1328
+ wokenNotice: () => wokenNotice
623
1329
  });
624
1330
  function wakeSetting(env, origin) {
625
1331
  const settings = readSettings(env);
@@ -630,23 +1336,39 @@ function wakeSetting(env, origin) {
630
1336
  const mode = ["0", "false", "off", "no"].includes(v) ? "off" : v === "all" ? "all" : "requests";
631
1337
  return { mode, from };
632
1338
  }
1339
+ function actSetting(env, origin) {
1340
+ const settings = readSettings(env);
1341
+ const key = origin ? projectKey(origin) : void 0;
1342
+ const project = key ? settings.wake_act_projects?.[key] : void 0;
1343
+ const [raw, from] = env.AGENTHUB_WAKE_ACT !== void 0 ? [env.AGENTHUB_WAKE_ACT, "env"] : project ? [project, "project"] : settings.wake_act ? [settings.wake_act, "user"] : ["", "default"];
1344
+ return { mode: raw.trim().toLowerCase() === "ask" ? "ask" : "auto", from };
1345
+ }
1346
+ function actMode(env, origin) {
1347
+ return actSetting(env, origin).mode;
1348
+ }
1349
+ function wokenNotice(act) {
1350
+ return act === "ask" ? `Agent Hub woke this session while it was idle, for new hub messages. ${ASK_FIRST}` : WOKEN;
1351
+ }
1352
+ function continueNotice(act) {
1353
+ return act === "ask" ? `These came in during the turn. ${ASK_FIRST}` : "Handle these as the Agent Hub rules say before you finish.";
1354
+ }
633
1355
  function wakeMode(env, origin) {
634
1356
  return wakeSetting(env, origin).mode;
635
1357
  }
636
1358
  function claimFile(env, key) {
637
- return (0, import_node_path7.join)(cacheDir(env), `wait-${key}.json`);
1359
+ return (0, import_node_path8.join)(cacheDir(env), `wait-${key}.json`);
638
1360
  }
639
1361
  function pendingFile(env, key) {
640
- return (0, import_node_path7.join)(cacheDir(env), `pending-${key}.json`);
1362
+ return (0, import_node_path8.join)(cacheDir(env), `pending-${key}.json`);
641
1363
  }
642
1364
  function releaseWaiter(env, key) {
643
- (0, import_node_fs6.rmSync)(claimFile(env, key), { force: true });
1365
+ (0, import_node_fs7.rmSync)(claimFile(env, key), { force: true });
644
1366
  }
645
1367
  function takePending(env, key) {
646
1368
  const file2 = pendingFile(env, key);
647
1369
  try {
648
- const pending = JSON.parse((0, import_node_fs6.readFileSync)(file2, "utf8"));
649
- (0, import_node_fs6.rmSync)(file2, { force: true });
1370
+ const pending = JSON.parse((0, import_node_fs7.readFileSync)(file2, "utf8"));
1371
+ (0, import_node_fs7.rmSync)(file2, { force: true });
650
1372
  return pending;
651
1373
  } catch {
652
1374
  return void 0;
@@ -657,12 +1379,12 @@ function addPending(env, key, inbox) {
657
1379
  const file2 = pendingFile(env, key);
658
1380
  let before;
659
1381
  try {
660
- before = JSON.parse((0, import_node_fs6.readFileSync)(file2, "utf8"));
1382
+ before = JSON.parse((0, import_node_fs7.readFileSync)(file2, "utf8"));
661
1383
  } catch {
662
1384
  before = void 0;
663
1385
  }
664
- (0, import_node_fs6.mkdirSync)((0, import_node_path7.join)(file2, ".."), { recursive: true, mode: 448 });
665
- (0, import_node_fs6.writeFileSync)(file2, JSON.stringify(mergeInbox(before, inbox)), { mode: 384 });
1386
+ (0, import_node_fs7.mkdirSync)((0, import_node_path8.join)(file2, ".."), { recursive: true, mode: 448 });
1387
+ (0, import_node_fs7.writeFileSync)(file2, JSON.stringify(mergeInbox(before, inbox)), { mode: 384 });
666
1388
  }
667
1389
  function mergeInbox(earlier, later) {
668
1390
  if (!earlier) return later;
@@ -677,23 +1399,24 @@ async function wait(opts, deps) {
677
1399
  const fetchFn = deps.fetch ?? fetch;
678
1400
  const now = deps.now ?? Date.now;
679
1401
  const sleep = deps.sleep ?? defaultSleep2;
680
- const cwd = typeof opts.payload?.cwd === "string" ? opts.payload.cwd : deps.cwd ?? process.cwd();
1402
+ const cwd = payloadCwd(opts.payload) ?? deps.cwd ?? process.cwd();
681
1403
  const hub = hubUrl(env);
682
1404
  if (!hub) return NOTHING;
683
1405
  const { origin, upstream } = (deps.remotes ?? gitRemotes)(cwd);
684
1406
  if (!origin) return NOTHING;
685
1407
  const mode = wakeMode(env, origin);
686
1408
  if (mode === "off") return NOTHING;
1409
+ const act = actMode(env, origin);
687
1410
  if (!await onHub(hub, [origin, upstream], fetchFn, env, now).catch(() => false)) return NOTHING;
688
1411
  const agentName = hookAgentName(opts.payload, env);
689
1412
  const key = sessionKey(hub, origin, agentName);
690
1413
  const id = (0, import_node_crypto2.randomBytes)(8).toString("hex");
691
1414
  const claim2 = claimFile(env, key);
692
- (0, import_node_fs6.mkdirSync)((0, import_node_path7.join)(claim2, ".."), { recursive: true, mode: 448 });
693
- (0, import_node_fs6.writeFileSync)(claim2, JSON.stringify({ id, pid: process.pid, at: now() }));
1415
+ (0, import_node_fs7.mkdirSync)((0, import_node_path8.join)(claim2, ".."), { recursive: true, mode: 448 });
1416
+ (0, import_node_fs7.writeFileSync)(claim2, JSON.stringify({ id, pid: process.pid, at: now() }));
694
1417
  const holds = () => {
695
1418
  try {
696
- return JSON.parse((0, import_node_fs6.readFileSync)(claim2, "utf8")).id === id;
1419
+ return JSON.parse((0, import_node_fs7.readFileSync)(claim2, "utf8")).id === id;
697
1420
  } catch {
698
1421
  return false;
699
1422
  }
@@ -740,7 +1463,7 @@ async function wait(opts, deps) {
740
1463
  const data = JSON.parse(ev.data);
741
1464
  const wakes = data.messages.some((m) => m.new && (mode === "all" || m.type !== "fyi"));
742
1465
  if (wakes && holds()) return { code: 2, text: `${formatInbox(mergeInbox(takePending(env, key), data))}
743
- ${WOKEN}` };
1466
+ ${wokenNotice(act)}` };
744
1467
  addPending(env, key, data);
745
1468
  } else if (ev.event === "end") {
746
1469
  const { reason } = JSON.parse(ev.data);
@@ -756,7 +1479,7 @@ ${WOKEN}` };
756
1479
  clearInterval(check2);
757
1480
  opts.signal?.removeEventListener("abort", stop);
758
1481
  ac.abort();
759
- if (holds()) (0, import_node_fs6.rmSync)(claim2, { force: true });
1482
+ if (holds()) (0, import_node_fs7.rmSync)(claim2, { force: true });
760
1483
  }
761
1484
  }
762
1485
  function defaultSleep2(ms, signal) {
@@ -768,13 +1491,13 @@ function defaultSleep2(ms, signal) {
768
1491
  }, { once: true });
769
1492
  });
770
1493
  }
771
- var import_node_crypto2, import_node_fs6, import_node_path7, DEFAULT_WAIT_MS, MAX_BACKOFF_MS2, NOTHING, WOKEN;
1494
+ var import_node_crypto2, import_node_fs7, import_node_path8, DEFAULT_WAIT_MS, MAX_BACKOFF_MS2, NOTHING, ASK_FIRST, WOKEN;
772
1495
  var init_wait = __esm({
773
1496
  "src/cli/wait.ts"() {
774
1497
  "use strict";
775
1498
  import_node_crypto2 = require("node:crypto");
776
- import_node_fs6 = require("node:fs");
777
- import_node_path7 = require("node:path");
1499
+ import_node_fs7 = require("node:fs");
1500
+ import_node_path8 = require("node:path");
778
1501
  init_paths();
779
1502
  init_notify();
780
1503
  init_session();
@@ -783,6 +1506,7 @@ var init_wait = __esm({
783
1506
  DEFAULT_WAIT_MS = 12 * 36e5;
784
1507
  MAX_BACKOFF_MS2 = 6e4;
785
1508
  NOTHING = { code: 0, text: "" };
1509
+ ASK_FIRST = "The user asked to be asked first (agenthub wake ask): don't act on these yet. Tell the user in a few lines who sent what, ask which ones to handle, and stop. Don't claim, answer, complete or decline any of them until the user says so.";
786
1510
  WOKEN = "Agent Hub woke this session while it was idle, so the user may be away. Handle these as the Agent Hub rules say: answer questions and do reviews read-only, and complete them; leave a handoff, or anything that would run commands, push, merge or touch secrets, for the user and say so.";
787
1511
  }
788
1512
  });
@@ -791,31 +1515,48 @@ var init_wait = __esm({
791
1515
  var notify_exports = {};
792
1516
  __export(notify_exports, {
793
1517
  CARD_NUDGE_MS: () => CARD_NUDGE_MS,
1518
+ NOTIFY_FORMATS: () => NOTIFY_FORMATS,
794
1519
  TASK_CHECK_MS: () => TASK_CHECK_MS,
795
1520
  formatInbox: () => formatInbox,
796
1521
  gitBranch: () => gitBranch,
797
1522
  gitRemotes: () => gitRemotes,
798
1523
  hookAgentName: () => hookAgentName,
799
1524
  notify: () => notify,
1525
+ notifyHook: () => notifyHook,
800
1526
  onHub: () => onHub,
1527
+ payloadCwd: () => payloadCwd,
801
1528
  sessionKey: () => sessionKey
802
1529
  });
803
1530
  async function notify(opts, deps) {
1531
+ return (await notifyHook(opts, deps)).stdout;
1532
+ }
1533
+ async function notifyHook(opts, deps) {
804
1534
  try {
805
1535
  const event = opts.event ?? HOOK_EVENTS[String(opts.payload?.hook_event_name ?? "")] ?? "post-tool";
806
1536
  const update = event === "session-start" && deps.version ? updateNotice(deps.version, { env: deps.env ?? process.env, fetch: deps.fetch ?? fetch, ...deps.now ? { now: deps.now } : {} }) : Promise.resolve("");
807
1537
  const [text, notice] = await Promise.all([run(opts, event, deps).catch(() => ""), update.catch(() => "")]);
808
- if (typeof text === "object") return text.raw;
809
- return render(opts.format, event, [text, notice].filter(Boolean).join("\n"));
1538
+ if (typeof text === "object") return text;
1539
+ return { stdout: render(opts.format, event, [text, notice].filter(Boolean).join("\n")) };
810
1540
  } catch {
811
- return "";
1541
+ return { stdout: "" };
1542
+ }
1543
+ }
1544
+ function payloadCwd(payload) {
1545
+ const first = (v) => Array.isArray(v) ? v[0] : void 0;
1546
+ const dir = payload?.cwd ?? payload?.working_dir ?? first(payload?.workspace_roots) ?? first(payload?.workspacePaths);
1547
+ if (typeof dir !== "string" || !dir) return void 0;
1548
+ if (!dir.startsWith("file:")) return dir;
1549
+ try {
1550
+ return (0, import_node_url.fileURLToPath)(dir);
1551
+ } catch {
1552
+ return void 0;
812
1553
  }
813
1554
  }
814
1555
  async function run(opts, event, deps) {
815
1556
  const env = deps.env ?? process.env;
816
1557
  const now = deps.now ?? Date.now;
817
1558
  const fetchFn = deps.fetch ?? fetch;
818
- const cwd = typeof opts.payload?.cwd === "string" ? opts.payload.cwd : deps.cwd ?? process.cwd();
1559
+ const cwd = payloadCwd(opts.payload) ?? deps.cwd ?? process.cwd();
819
1560
  const out = (text) => text;
820
1561
  const hub = hubUrl(env);
821
1562
  if (!hub) return "";
@@ -825,24 +1566,34 @@ async function run(opts, event, deps) {
825
1566
  const agentName = hookAgentName(opts.payload, env);
826
1567
  const throttleKey = sessionKey(hub, origin, agentName);
827
1568
  if (event !== "stop") releaseWaiter(env, throttleKey);
1569
+ if (event === FIRST_EVENT_STARTS[opts.format]) {
1570
+ const started = (0, import_node_path9.join)(cacheDir(env), `started-${throttleKey}.json`);
1571
+ if (!readFlag(started)) {
1572
+ writeFlag(started);
1573
+ event = "session-start";
1574
+ }
1575
+ }
828
1576
  const wait2 = THROTTLE_MS[event];
829
- if (wait2 && !throttle((0, import_node_path8.join)(cacheDir(env), `notify-${throttleKey}.json`), wait2, now())) return "";
1577
+ if (wait2 && !throttle((0, import_node_path9.join)(cacheDir(env), `notify-${throttleKey}.json`), wait2, now())) return "";
830
1578
  const token = await accessToken(hub, { store: deps.store, fetch: fetchFn, env, now });
831
1579
  if (!token) return event === "session-start" ? out(`Agent Hub: not signed in. Ask the user to run \`agenthub login --hub ${hub}\` to get teammates' messages here.`) : "";
832
1580
  const who = { project: origin, ...upstream ? { upstream } : {}, ...agentName ? { agent_name: agentName } : {} };
833
1581
  const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
834
1582
  const signal = AbortSignal.timeout(3e3);
835
- if (event === "stop" && opts.format === "codex" && wakeMode(env, origin) !== "off") {
1583
+ const block = STOP_BLOCK[opts.format];
1584
+ const stopped = [opts.payload?.status, opts.payload?.terminationReason].some((v) => typeof v === "string" && /abort|cancel|interrupt/i.test(v));
1585
+ if (event === "stop" && block && !stopped && wakeMode(env, origin) !== "off") {
836
1586
  const res2 = await fetchFn(`${hub}/api/inbox?${new URLSearchParams(who)}`, { headers, signal });
837
1587
  if (res2.ok) {
838
1588
  const inbox2 = mergeInbox(takePending(env, throttleKey), await res2.json());
839
1589
  const mode = wakeMode(env, origin);
840
1590
  if (inbox2.messages.some((m) => m.new && (mode === "all" || m.type !== "fyi"))) {
841
- return { raw: JSON.stringify({ decision: "block", reason: `${formatInbox(inbox2)}
842
- Handle these as the Agent Hub rules say before you finish.` }) };
1591
+ return block(`${formatInbox(inbox2)}
1592
+ ${continueNotice(actMode(env, origin))}`);
843
1593
  }
844
1594
  addPending(env, throttleKey, inbox2);
845
1595
  }
1596
+ if (opts.format === "kimi" && kimiWakeable(opts.payload)) (deps.startKimiWaiter ?? startKimiWaiter)(opts.payload, env);
846
1597
  }
847
1598
  if (event === "stop" || event === "session-end") {
848
1599
  await fetchFn(`${hub}/api/presence`, {
@@ -858,14 +1609,14 @@ Handle these as the Agent Hub rules say before you finish.` }) };
858
1609
  const { membership } = await res.json().catch(() => ({}));
859
1610
  const args = [`remote="${origin}"`, ...upstream ? [`upstream="${upstream}"`] : [], ...agentName ? [`agent_name="${agentName}"`] : []].join(", ");
860
1611
  const keep = agentName ? " Use exactly that agent_name: it's how hub notices find this session." : "";
861
- const waitingFile = (0, import_node_path8.join)(cacheDir(env), `waiting-${throttleKey}.json`);
1612
+ const waitingFile = (0, import_node_path9.join)(cacheDir(env), `waiting-${throttleKey}.json`);
862
1613
  const wasWaiting = readFlag(waitingFile);
863
1614
  if (membership === "pending") {
864
1615
  if (!wasWaiting) writeFlag(waitingFile);
865
1616
  return event === "session-start" || !wasWaiting ? out("Agent Hub: your request to join this project is waiting for a project admin's approval. You'll be told here once it's approved.") : "";
866
1617
  }
867
1618
  if (wasWaiting) {
868
- (0, import_node_fs7.rmSync)(waitingFile, { force: true });
1619
+ (0, import_node_fs8.rmSync)(waitingFile, { force: true });
869
1620
  if (membership === "active") return out(`Agent Hub: a project admin approved you. Call the agenthub join_project tool with ${args} to join.${keep}`);
870
1621
  return out("Agent Hub: a project admin turned down your request to join this project.");
871
1622
  }
@@ -879,7 +1630,7 @@ Handle these as the Agent Hub rules say before you finish.` }) };
879
1630
  const card = await keepCardCurrent(inbox, event, {
880
1631
  // Without a name the hub picks the user's latest agent here, which may be another session's.
881
1632
  branch: agentName ? (deps.branch ?? gitBranch)(cwd) : void 0,
882
- stateFile: (0, import_node_path8.join)(cacheDir(env), `card-${throttleKey}.json`),
1633
+ stateFile: (0, import_node_path9.join)(cacheDir(env), `card-${throttleKey}.json`),
883
1634
  now: now(),
884
1635
  post: (fields) => fetchFn(`${hub}/api/card`, { method: "POST", headers, body: JSON.stringify({ ...who, ...fields }), signal })
885
1636
  }).catch(() => "");
@@ -909,18 +1660,18 @@ async function keepCardCurrent(inbox, event, ctx) {
909
1660
  }
910
1661
  function readState(path) {
911
1662
  try {
912
- return JSON.parse((0, import_node_fs7.readFileSync)(path, "utf8"));
1663
+ return JSON.parse((0, import_node_fs8.readFileSync)(path, "utf8"));
913
1664
  } catch {
914
1665
  return {};
915
1666
  }
916
1667
  }
917
1668
  function writeState(path, state) {
918
- (0, import_node_fs7.mkdirSync)((0, import_node_path8.join)(path, ".."), { recursive: true, mode: 448 });
919
- (0, import_node_fs7.writeFileSync)(path, JSON.stringify(state));
1669
+ (0, import_node_fs8.mkdirSync)((0, import_node_path9.join)(path, ".."), { recursive: true, mode: 448 });
1670
+ (0, import_node_fs8.writeFileSync)(path, JSON.stringify(state));
920
1671
  }
921
1672
  function gitBranch(cwd) {
922
1673
  try {
923
- return (0, import_node_child_process4.execFileSync)("git", ["-C", cwd, "branch", "--show-current"], { stdio: ["ignore", "pipe", "ignore"], timeout: 1e3 }).toString().trim() || void 0;
1674
+ return (0, import_node_child_process5.execFileSync)("git", ["-C", cwd, "branch", "--show-current"], { stdio: ["ignore", "pipe", "ignore"], timeout: 1e3 }).toString().trim() || void 0;
924
1675
  } catch {
925
1676
  return void 0;
926
1677
  }
@@ -930,7 +1681,7 @@ function sessionKey(hub, origin, agentName) {
930
1681
  }
931
1682
  function hookAgentName(payload, env) {
932
1683
  if (env.AGENTHUB_AGENT_NAME) return env.AGENTHUB_AGENT_NAME;
933
- const session = payload?.session_id ?? payload?.sessionId;
1684
+ const session = payload?.session_id ?? payload?.sessionId ?? payload?.conversation_id ?? payload?.conversationId;
934
1685
  if (typeof session !== "string" || !session) return void 0;
935
1686
  return `s-${(0, import_node_crypto3.createHash)("sha256").update(session).digest("hex").slice(0, 8)}`;
936
1687
  }
@@ -963,38 +1714,44 @@ function clean(s) {
963
1714
  }
964
1715
  function render(format, event, text) {
965
1716
  if (!text) return "";
966
- if (format === "text") return text;
1717
+ if (format === "text" || format === "kimi" || format === "amp" || format === "roo") return text;
1718
+ if (format === "goose") return "";
1719
+ if (event === "stop" || event === "session-end") return "";
1720
+ if (format === "crush") return JSON.stringify({ context: text });
1721
+ if (format === "cursor") return JSON.stringify({ additional_context: text });
1722
+ if (format === "antigravity") return JSON.stringify({ injectSteps: [{ systemMessage: text }] });
1723
+ if (format === "gemini") return JSON.stringify({ hookSpecificOutput: { hookEventName: GEMINI_EVENT_NAMES[event], additionalContext: text } });
967
1724
  if (format === "codex" && event === "session-start") return text;
968
1725
  if (format === "copilot") return JSON.stringify({ additionalContext: text });
969
1726
  return JSON.stringify({ hookSpecificOutput: { hookEventName: EVENT_NAMES[event], additionalContext: text } });
970
1727
  }
971
1728
  function readFlag(path) {
972
1729
  try {
973
- (0, import_node_fs7.readFileSync)(path);
1730
+ (0, import_node_fs8.readFileSync)(path);
974
1731
  return true;
975
1732
  } catch {
976
1733
  return false;
977
1734
  }
978
1735
  }
979
1736
  function writeFlag(path) {
980
- (0, import_node_fs7.mkdirSync)((0, import_node_path8.join)(path, ".."), { recursive: true, mode: 448 });
981
- (0, import_node_fs7.writeFileSync)(path, "{}");
1737
+ (0, import_node_fs8.mkdirSync)((0, import_node_path9.join)(path, ".."), { recursive: true, mode: 448 });
1738
+ (0, import_node_fs8.writeFileSync)(path, "{}");
982
1739
  }
983
1740
  function throttle(path, waitMs, now) {
984
1741
  try {
985
- const last = JSON.parse((0, import_node_fs7.readFileSync)(path, "utf8")).at;
1742
+ const last = JSON.parse((0, import_node_fs8.readFileSync)(path, "utf8")).at;
986
1743
  if (now - last < waitMs) return false;
987
1744
  } catch {
988
1745
  }
989
- (0, import_node_fs7.mkdirSync)((0, import_node_path8.join)(path, ".."), { recursive: true, mode: 448 });
990
- (0, import_node_fs7.writeFileSync)(path, JSON.stringify({ at: now }));
1746
+ (0, import_node_fs8.mkdirSync)((0, import_node_path9.join)(path, ".."), { recursive: true, mode: 448 });
1747
+ (0, import_node_fs8.writeFileSync)(path, JSON.stringify({ at: now }));
991
1748
  return true;
992
1749
  }
993
1750
  async function onHub(hub, remotes, fetchFn, env, now) {
994
- const path = (0, import_node_path8.join)(cacheDir(env), `providers-${(0, import_node_crypto3.createHash)("sha256").update(hub).digest("hex").slice(0, 16)}.json`);
1751
+ const path = (0, import_node_path9.join)(cacheDir(env), `providers-${(0, import_node_crypto3.createHash)("sha256").update(hub).digest("hex").slice(0, 16)}.json`);
995
1752
  let providers;
996
1753
  try {
997
- const cached2 = JSON.parse((0, import_node_fs7.readFileSync)(path, "utf8"));
1754
+ const cached2 = JSON.parse((0, import_node_fs8.readFileSync)(path, "utf8"));
998
1755
  if (now() - cached2.at < PROVIDERS_TTL_MS) providers = cached2.providers;
999
1756
  } catch {
1000
1757
  }
@@ -1002,8 +1759,8 @@ async function onHub(hub, remotes, fetchFn, env, now) {
1002
1759
  const res = await fetchFn(`${hub}/api/providers`, { signal: AbortSignal.timeout(3e3) });
1003
1760
  if (!res.ok) return false;
1004
1761
  providers = (await res.json()).providers;
1005
- (0, import_node_fs7.mkdirSync)((0, import_node_path8.join)(path, ".."), { recursive: true, mode: 448 });
1006
- (0, import_node_fs7.writeFileSync)(path, JSON.stringify({ at: now(), providers }));
1762
+ (0, import_node_fs8.mkdirSync)((0, import_node_path9.join)(path, ".."), { recursive: true, mode: 448 });
1763
+ (0, import_node_fs8.writeFileSync)(path, JSON.stringify({ at: now(), providers }));
1007
1764
  }
1008
1765
  const hosts = /* @__PURE__ */ new Set();
1009
1766
  for (const p of providers) {
@@ -1022,7 +1779,7 @@ async function onHub(hub, remotes, fetchFn, env, now) {
1022
1779
  function gitRemotes(cwd) {
1023
1780
  const get = (name) => {
1024
1781
  try {
1025
- return (0, import_node_child_process4.execFileSync)("git", ["-C", cwd, "remote", "get-url", name], { stdio: ["ignore", "pipe", "ignore"], timeout: 1e3 }).toString().trim() || void 0;
1782
+ return (0, import_node_child_process5.execFileSync)("git", ["-C", cwd, "remote", "get-url", name], { stdio: ["ignore", "pipe", "ignore"], timeout: 1e3 }).toString().trim() || void 0;
1026
1783
  } catch {
1027
1784
  return void 0;
1028
1785
  }
@@ -1031,26 +1788,33 @@ function gitRemotes(cwd) {
1031
1788
  const upstream = origin ? get("upstream") : void 0;
1032
1789
  return { ...origin ? { origin } : {}, ...upstream ? { upstream } : {} };
1033
1790
  }
1034
- var import_node_child_process4, import_node_crypto3, import_node_fs7, import_node_path8, HOOK_EVENTS, EVENT_NAMES, THROTTLE_MS, PROVIDERS_TTL_MS, CARD_NUDGE_MS, TASK_CHECK_MS;
1791
+ var import_node_child_process5, import_node_crypto3, import_node_fs8, import_node_path9, import_node_url, NOTIFY_FORMATS, HOOK_EVENTS, EVENT_NAMES, GEMINI_EVENT_NAMES, THROTTLE_MS, PROVIDERS_TTL_MS, STOP_BLOCK, FIRST_EVENT_STARTS, CARD_NUDGE_MS, TASK_CHECK_MS;
1035
1792
  var init_notify = __esm({
1036
1793
  "src/cli/notify.ts"() {
1037
1794
  "use strict";
1038
- import_node_child_process4 = require("node:child_process");
1795
+ import_node_child_process5 = require("node:child_process");
1039
1796
  import_node_crypto3 = require("node:crypto");
1040
- import_node_fs7 = require("node:fs");
1041
- import_node_path8 = require("node:path");
1797
+ import_node_fs8 = require("node:fs");
1798
+ import_node_path9 = require("node:path");
1799
+ import_node_url = require("node:url");
1042
1800
  init_registry();
1043
1801
  init_paths();
1044
1802
  init_session();
1045
1803
  init_settings();
1046
1804
  init_update();
1805
+ init_kimi();
1047
1806
  init_wait();
1807
+ NOTIFY_FORMATS = ["claude-code", "codex", "copilot", "droid", "gemini", "qwen", "cursor", "antigravity", "kimi", "goose", "amp", "roo", "crush", "text"];
1048
1808
  HOOK_EVENTS = {
1049
1809
  SessionStart: "session-start",
1050
1810
  UserPromptSubmit: "prompt",
1051
1811
  PostToolUse: "post-tool",
1052
1812
  Stop: "stop",
1053
- SessionEnd: "session-end"
1813
+ SessionEnd: "session-end",
1814
+ // Gemini CLI
1815
+ BeforeAgent: "prompt",
1816
+ AfterTool: "post-tool",
1817
+ AfterAgent: "stop"
1054
1818
  };
1055
1819
  EVENT_NAMES = {
1056
1820
  "session-start": "SessionStart",
@@ -1059,8 +1823,29 @@ var init_notify = __esm({
1059
1823
  stop: "Stop",
1060
1824
  "session-end": "SessionEnd"
1061
1825
  };
1826
+ GEMINI_EVENT_NAMES = { ...EVENT_NAMES, prompt: "BeforeAgent", "post-tool": "AfterTool", stop: "AfterAgent" };
1062
1827
  THROTTLE_MS = { "post-tool": 3e4, prompt: 1e4 };
1063
1828
  PROVIDERS_TTL_MS = 24 * 36e5;
1829
+ STOP_BLOCK = {
1830
+ codex: (reason) => ({ stdout: JSON.stringify({ decision: "block", reason }) }),
1831
+ copilot: (reason) => ({ stdout: JSON.stringify({ decision: "block", reason }) }),
1832
+ droid: (reason) => ({ stdout: JSON.stringify({ decision: "block", reason }) }),
1833
+ qwen: (reason) => ({ stdout: JSON.stringify({ decision: "block", reason }) }),
1834
+ goose: (reason) => ({ stdout: JSON.stringify({ decision: "block", reason }) }),
1835
+ // The Amp plugin returns agent.end's `continue` with the reason as the next user message.
1836
+ amp: (reason) => ({ stdout: JSON.stringify({ decision: "block", reason }) }),
1837
+ // The Roo Code extension sends the reason to the task with sendMessage.
1838
+ roo: (reason) => ({ stdout: JSON.stringify({ decision: "block", reason }) }),
1839
+ // AfterAgent: deny sends the reason to the agent as a new prompt.
1840
+ gemini: (reason) => ({ stdout: JSON.stringify({ decision: "deny", reason }) }),
1841
+ // stop: Cursor submits followup_message as the next user message.
1842
+ cursor: (reason) => ({ stdout: JSON.stringify({ followup_message: reason }) }),
1843
+ // Stop: the reason is injected as a system message and the agent goes on.
1844
+ antigravity: (reason) => ({ stdout: JSON.stringify({ decision: "continue", reason }) }),
1845
+ // Stop: exit 2 blocks it, with stderr as the reason.
1846
+ kimi: (reason) => ({ stdout: "", stderr: reason, code: 2 })
1847
+ };
1848
+ FIRST_EVENT_STARTS = { kimi: "prompt", crush: "post-tool" };
1064
1849
  CARD_NUDGE_MS = 10 * 6e4;
1065
1850
  TASK_CHECK_MS = 30 * 6e4;
1066
1851
  }
@@ -27704,7 +28489,7 @@ function dispatcherFor(instance) {
27704
28489
  if (!instance.caBundlePath) return void 0;
27705
28490
  let d = dispatchers.get(instance.id);
27706
28491
  if (!d) {
27707
- const ca = (0, import_node_fs8.readFileSync)(instance.caBundlePath, "utf8");
28492
+ const ca = (0, import_node_fs9.readFileSync)(instance.caBundlePath, "utf8");
27708
28493
  d = new import_undici.Agent({ connect: { ca: [...import_node_tls.rootCertificates, ca] } });
27709
28494
  dispatchers.set(instance.id, d);
27710
28495
  }
@@ -27721,11 +28506,11 @@ function fetchFor(instance, override) {
27721
28506
  signal: AbortSignal.timeout(1e4)
27722
28507
  });
27723
28508
  }
27724
- var import_node_fs8, import_node_tls, import_undici, dispatchers;
28509
+ var import_node_fs9, import_node_tls, import_undici, dispatchers;
27725
28510
  var init_http = __esm({
27726
28511
  "src/providers/http.ts"() {
27727
28512
  "use strict";
27728
- import_node_fs8 = require("node:fs");
28513
+ import_node_fs9 = require("node:fs");
27729
28514
  import_node_tls = require("node:tls");
27730
28515
  import_undici = __toESM(require_undici(), 1);
27731
28516
  dispatchers = /* @__PURE__ */ new Map();
@@ -55604,7 +56389,7 @@ var init_types = __esm({
55604
56389
 
55605
56390
  // src/providers/config.ts
55606
56391
  function loadConfigFile(path, opts = {}) {
55607
- const read = opts.readFile ?? ((p) => (0, import_node_fs9.readFileSync)(p, "utf8"));
56392
+ const read = opts.readFile ?? ((p) => (0, import_node_fs10.readFileSync)(p, "utf8"));
55608
56393
  let text;
55609
56394
  try {
55610
56395
  text = read(path);
@@ -55615,7 +56400,7 @@ function loadConfigFile(path, opts = {}) {
55615
56400
  }
55616
56401
  function buildConfig(raw, opts = {}) {
55617
56402
  const env = opts.env ?? process.env;
55618
- const read = opts.readFile ?? ((p) => (0, import_node_fs9.readFileSync)(p, "utf8"));
56403
+ const read = opts.readFile ?? ((p) => (0, import_node_fs10.readFileSync)(p, "utf8"));
55619
56404
  const parsed = configSchema.safeParse(raw);
55620
56405
  if (!parsed.success) {
55621
56406
  throw new ConfigError(
@@ -55796,11 +56581,11 @@ function checkNoHostCollisions(instances) {
55796
56581
  for (const a of inst.remoteAliases) claim2(a.port ? `${a.host}:${a.port}` : a.host, inst.id);
55797
56582
  }
55798
56583
  }
55799
- var import_node_fs9, import_yaml, secretRef, oauthSchema, providerSchema, configSchema, ConfigError;
56584
+ var import_node_fs10, import_yaml, secretRef, oauthSchema, providerSchema, configSchema, ConfigError;
55800
56585
  var init_config = __esm({
55801
56586
  "src/providers/config.ts"() {
55802
56587
  "use strict";
55803
- import_node_fs9 = require("node:fs");
56588
+ import_node_fs10 = require("node:fs");
55804
56589
  import_yaml = __toESM(require_dist(), 1);
55805
56590
  init_zod();
55806
56591
  init_defaults();
@@ -55901,7 +56686,7 @@ var init_admin = __esm({
55901
56686
  });
55902
56687
 
55903
56688
  // src/cli/index.ts
55904
- var import_node_fs10 = require("node:fs");
56689
+ var import_node_fs11 = require("node:fs");
55905
56690
 
55906
56691
  // src/cli/credentials.ts
55907
56692
  var import_node_child_process = require("node:child_process");
@@ -56002,278 +56787,8 @@ function credentialStore(env = process.env) {
56002
56787
  return new FileCredentialStore(configDir(env));
56003
56788
  }
56004
56789
 
56005
- // src/cli/hooks.ts
56006
- var import_node_fs2 = require("node:fs");
56007
- var import_node_os2 = require("node:os");
56008
- var import_node_path3 = require("node:path");
56009
-
56010
- // src/cli/opencode-plugin.ts
56011
- var OPENCODE_MARKER = "// Agent Hub plugin for OpenCode, written by `agenthub hooks install opencode`.";
56012
- var PACKAGE_HEADER = `// Agent Hub plugin for OpenCode, from the @theagenthub/opencode npm package.
56013
- // Needs the agenthub CLI: curl -fsSL https://get.theagenthub.app | sh`;
56014
- var PACKAGE_COMMAND = `import { existsSync } from "node:fs";
56015
- import { homedir } from "node:os";
56016
- import { join } from "node:path";
56017
-
56018
- const COMMAND =
56019
- process.env.AGENTHUB_BIN ||
56020
- [join(homedir(), ".local", "bin", "agenthub"), join(process.env.LOCALAPPDATA || homedir(), "Programs", "agenthub", "agenthub.exe")].find((f) => existsSync(f)) ||
56021
- "agenthub";`;
56022
- function opencodePlugin(command, as = "file") {
56023
- const header = as === "package" ? PACKAGE_HEADER : `${OPENCODE_MARKER}
56024
- // Re-run that command to update it, or \`agenthub hooks uninstall opencode\` to remove it.`;
56025
- return `${header}
56026
- import { spawn } from "node:child_process";
56027
-
56028
- ${as === "package" ? PACKAGE_COMMAND : `const COMMAND = ${JSON.stringify(command)};`}
56029
- const TIMEOUT_MS = 5000;
56030
-
56031
- function notify(event, sessionID, cwd) {
56032
- return new Promise((resolve) => {
56033
- let out = "";
56034
- let child;
56035
- try {
56036
- child = spawn(COMMAND, ["notify", "--format", "text", "--event", event], {
56037
- cwd,
56038
- stdio: ["pipe", "pipe", "ignore"],
56039
- windowsHide: true,
56040
- shell: process.platform === "win32",
56041
- });
56042
- } catch {
56043
- return resolve("");
56044
- }
56045
- const timer = setTimeout(() => {
56046
- child.kill();
56047
- resolve("");
56048
- }, TIMEOUT_MS);
56049
- child.stdout.on("data", (chunk) => (out += chunk));
56050
- child.on("error", () => {
56051
- clearTimeout(timer);
56052
- resolve("");
56053
- });
56054
- child.on("close", () => {
56055
- clearTimeout(timer);
56056
- resolve(out.trim());
56057
- });
56058
- child.stdin.on("error", () => {});
56059
- child.stdin.end(JSON.stringify({ session_id: sessionID, cwd }));
56060
- });
56061
- }
56062
-
56063
- /** Runs \`agenthub wait\`; resolves to the text to wake the session with, or "" when it shouldn't. */
56064
- function waitForMessage(sessionID, cwd, children) {
56065
- return new Promise((resolve) => {
56066
- let out = "";
56067
- let child;
56068
- try {
56069
- child = spawn(COMMAND, ["wait"], { cwd, stdio: ["pipe", "pipe", "ignore"], windowsHide: true, shell: process.platform === "win32" });
56070
- } catch {
56071
- return resolve("");
56072
- }
56073
- children.get(sessionID)?.kill();
56074
- children.set(sessionID, child);
56075
- child.stdout.on("data", (chunk) => (out += chunk));
56076
- child.on("error", () => resolve(""));
56077
- child.on("close", (code) => {
56078
- if (children.get(sessionID) === child) children.delete(sessionID);
56079
- // An agenthub too old to know \`wait\` exits 2 too, but prints nothing on stdout.
56080
- resolve(code === 2 ? out.trim() : "");
56081
- });
56082
- child.stdin.on("error", () => {});
56083
- child.stdin.end(JSON.stringify({ session_id: sessionID, cwd }));
56084
- });
56085
- }
56086
-
56087
- function partId() {
56088
- return "prt_" + Date.now().toString(16) + Math.random().toString(36).slice(2, 12);
56089
- }
56090
-
56091
- export const AgentHub = async ({ directory, client }) => {
56092
- const started = new Set();
56093
- const waiting = new Map();
56094
- let presence = Promise.resolve();
56095
- const wake = async (sessionID) => {
56096
- const send = client?.session?.promptAsync ?? client?.session?.prompt;
56097
- if (!send) return;
56098
- const text = await waitForMessage(sessionID, directory, waiting);
56099
- if (!text) return;
56100
- try {
56101
- await send.call(client.session, { path: { id: sessionID }, body: { parts: [{ type: "text", text }] } });
56102
- } catch {
56103
- /* the session is gone, or busy again */
56104
- }
56105
- };
56106
- return {
56107
- "chat.message": async (input, output) => {
56108
- waiting.get(input.sessionID)?.kill();
56109
- const first = !started.has(input.sessionID);
56110
- started.add(input.sessionID);
56111
- const text = await notify(first ? "session-start" : "prompt", input.sessionID, directory);
56112
- const messageID = input.messageID ?? output.message?.id;
56113
- if (!text || !messageID) return;
56114
- output.parts.push({ id: partId(), sessionID: input.sessionID, messageID, type: "text", text, synthetic: true });
56115
- },
56116
- "tool.execute.after": async (input, output) => {
56117
- const text = await notify("post-tool", input.sessionID, directory);
56118
- if (text && typeof output.output === "string") output.output += "\\n\\n<agenthub>\\n" + text + "\\n</agenthub>";
56119
- },
56120
- // Presence updates don't block OpenCode, but run one at a time so "offline" can't be
56121
- // overtaken by an earlier "idle".
56122
- event: async ({ event }) => {
56123
- if (event.type === "session.idle") {
56124
- const sessionID = event.properties.sessionID;
56125
- presence = presence.then(() => notify("stop", sessionID, directory));
56126
- // Not awaited: it waits as long as the session is idle.
56127
- presence.then(() => wake(sessionID)).catch(() => {});
56128
- } else if (event.type === "session.deleted") {
56129
- waiting.get(event.properties.info.id)?.kill();
56130
- presence = presence.then(() => notify("session-end", event.properties.info.id, directory));
56131
- }
56132
- },
56133
- };
56134
- };
56135
-
56136
- // OpenCode 2 loads the default export's setup() and ignores the hooks above; OpenCode 1
56137
- // reads the named export (and default.server, the same function). setup() returns early
56138
- // when OpenCode 1 calls it too.
56139
- async function setup(ctx) {
56140
- if (!ctx?.session?.hook || !ctx.tool?.hook || !ctx.event?.subscribe) return;
56141
- const directory = ctx.location?.directory ?? process.cwd();
56142
- const started = new Set();
56143
- let presence = Promise.resolve();
56144
- const wrap = (text) => "\\n\\n<agenthub>\\n" + text + "\\n</agenthub>";
56145
-
56146
- // V2 has no hidden message parts, so the notice is appended to the prompt's text.
56147
- await ctx.session.hook("prompt", async (event) => {
56148
- const first = !started.has(event.sessionID);
56149
- started.add(event.sessionID);
56150
- const text = await notify(first ? "session-start" : "prompt", event.sessionID, directory);
56151
- if (text && event.prompt && typeof event.prompt.text === "string") event.prompt.text += wrap(text);
56152
- });
56153
- // The model reads a tool's result.content.
56154
- await ctx.tool.hook("execute.after", async (event) => {
56155
- if (event.status !== "completed" || !Array.isArray(event.result?.content)) return;
56156
- const text = await notify("post-tool", event.sessionID, directory);
56157
- if (text) event.result.content.push({ type: "text", text: wrap(text).trimStart() });
56158
- });
56159
-
56160
- const controller = new AbortController();
56161
- (async () => {
56162
- const types = ["session.execution.succeeded", "session.execution.failed", "session.execution.interrupted", "session.deleted"];
56163
- for await (const event of ctx.event.subscribe({ types, signal: controller.signal })) {
56164
- const sessionID = event.data?.sessionID;
56165
- if (!sessionID) continue;
56166
- const kind = event.type === "session.deleted" ? "session-end" : "stop";
56167
- if (kind === "session-end") started.delete(sessionID);
56168
- presence = presence.then(() => notify(kind, sessionID, directory));
56169
- }
56170
- })().catch(() => {});
56171
- return () => controller.abort();
56172
- }
56173
-
56174
- export default { id: "agenthub", server: AgentHub, setup };
56175
- `;
56176
- }
56177
-
56178
- // src/cli/hooks.ts
56179
- var HARNESSES = ["claude-code", "codex", "copilot", "opencode"];
56180
- var EVENTS = ["SessionStart", "UserPromptSubmit", "PostToolUse", "Stop", "SessionEnd"];
56181
- var COPILOT_EVENTS = [
56182
- ["sessionStart", "session-start"],
56183
- ["userPromptSubmitted", "prompt"],
56184
- ["postToolUse", "post-tool"],
56185
- ["agentStop", "stop"],
56186
- ["sessionEnd", "session-end"]
56187
- ];
56188
- var WAIT_MAX_S = 12 * 3600;
56189
- var WAIT_TIMEOUT_S = WAIT_MAX_S + 60;
56190
- var TIMEOUT_S = { "claude-code": {}, codex: { SessionEnd: 3 } };
56191
- function hookFile(harness, scope, cwd, home = (0, import_node_os2.homedir)(), env = process.env) {
56192
- const user = scope === "user";
56193
- switch (harness) {
56194
- case "claude-code":
56195
- return user ? (0, import_node_path3.join)(home, ".claude", "settings.json") : (0, import_node_path3.join)(cwd, ".claude", "settings.local.json");
56196
- case "codex":
56197
- return user ? (0, import_node_path3.join)(home, ".codex", "hooks.json") : (0, import_node_path3.join)(cwd, ".codex", "hooks.json");
56198
- case "copilot":
56199
- 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");
56200
- case "opencode":
56201
- 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");
56202
- }
56203
- }
56204
- var OURS = [/^(?:\S*[\\/])?agenthub(?:\.js|\.exe)?\s+(?:notify|wait)\b/i, /^"(?:[^"]*[\\/])?agenthub(?:\.js|\.exe)?"\s+(?:notify|wait)\b/i];
56205
- var isOurs = (h) => typeof h.command === "string" && OURS.some((r) => r.test(h.command));
56206
- function installHooks(harness, file2, command = "agenthub") {
56207
- (0, import_node_fs2.mkdirSync)((0, import_node_path3.dirname)(file2), { recursive: true });
56208
- if (harness === "opencode") {
56209
- (0, import_node_fs2.writeFileSync)(file2, opencodePlugin(command));
56210
- return ["chat.message", "tool.execute.after", "session.idle", "session.deleted"];
56211
- }
56212
- if (harness === "copilot") {
56213
- const config3 = readJson(file2);
56214
- config3.version = 1;
56215
- const hooks2 = config3.hooks ??= {};
56216
- for (const [event, notifyEvent] of COPILOT_EVENTS) {
56217
- const kept = (hooks2[event] ?? []).filter((h) => !isOurs(h));
56218
- hooks2[event] = [...kept, { type: "command", command: `${command} notify --format copilot --event ${notifyEvent}`, timeoutSec: 10 }];
56219
- }
56220
- writeJson(file2, config3);
56221
- return COPILOT_EVENTS.map(([e]) => e);
56222
- }
56223
- const config2 = readJson(file2);
56224
- const hooks = config2.hooks ??= {};
56225
- for (const event of EVENTS) {
56226
- const groups = (hooks[event] ?? []).map((g) => ({ ...g, hooks: g.hooks.filter((h) => !isOurs(h)) })).filter((g) => g.hooks.length);
56227
- const entry = { hooks: [{ type: "command", command: `${command} notify --format ${harness}`, timeout: TIMEOUT_S[harness][event] ?? 10 }] };
56228
- if (event === "PostToolUse" && harness === "claude-code") entry.matcher = "*";
56229
- if (event === "Stop" && harness === "claude-code") {
56230
- entry.hooks.push({ type: "command", command: `${command} wait --max ${WAIT_MAX_S}`, asyncRewake: true, timeout: WAIT_TIMEOUT_S });
56231
- }
56232
- hooks[event] = [...groups, entry];
56233
- }
56234
- writeJson(file2, config2);
56235
- return EVENTS;
56236
- }
56237
- function uninstallHooks(harness, file2) {
56238
- if (!(0, import_node_fs2.existsSync)(file2)) return false;
56239
- if (harness === "opencode") {
56240
- if (!(0, import_node_fs2.readFileSync)(file2, "utf8").startsWith(OPENCODE_MARKER)) return false;
56241
- (0, import_node_fs2.rmSync)(file2);
56242
- return true;
56243
- }
56244
- if (harness === "copilot") {
56245
- const config3 = readJson(file2);
56246
- if (!config3.hooks) return false;
56247
- let changed2 = false;
56248
- for (const [event, entries] of Object.entries(config3.hooks)) {
56249
- const kept = entries.filter((h) => !isOurs(h));
56250
- if (kept.length !== entries.length) changed2 = true;
56251
- if (kept.length) config3.hooks[event] = kept;
56252
- else delete config3.hooks[event];
56253
- }
56254
- if (!changed2) return false;
56255
- if (!Object.keys(config3.hooks).length && Object.keys(config3).every((k) => k === "version" || k === "hooks")) (0, import_node_fs2.rmSync)(file2);
56256
- else writeJson(file2, config3);
56257
- return true;
56258
- }
56259
- const config2 = readJson(file2);
56260
- if (!config2.hooks) return false;
56261
- let changed = false;
56262
- for (const [event, groups] of Object.entries(config2.hooks)) {
56263
- const kept = groups.map((g) => ({ ...g, hooks: g.hooks.filter((h) => !isOurs(h)) })).filter((g) => g.hooks.length);
56264
- if (kept.length !== groups.length || kept.some((g, i) => g.hooks.length !== groups[i].hooks.length)) changed = true;
56265
- if (kept.length) config2.hooks[event] = kept;
56266
- else delete config2.hooks[event];
56267
- }
56268
- if (changed) writeJson(file2, config2);
56269
- return changed;
56270
- }
56271
- function readJson(file2) {
56272
- return (0, import_node_fs2.existsSync)(file2) ? JSON.parse((0, import_node_fs2.readFileSync)(file2, "utf8")) : {};
56273
- }
56274
- function writeJson(file2, value) {
56275
- (0, import_node_fs2.writeFileSync)(file2, JSON.stringify(value, null, 2) + "\n");
56276
- }
56790
+ // src/cli/index.ts
56791
+ init_hooks();
56277
56792
 
56278
56793
  // src/cli/login.ts
56279
56794
  var import_node_child_process2 = require("node:child_process");
@@ -56330,7 +56845,7 @@ function openBrowser(url2) {
56330
56845
  init_notify();
56331
56846
 
56332
56847
  // src/cli/help.ts
56333
- var HARNESS_LIST = "claude-code, codex, copilot or opencode";
56848
+ var HARNESS_LIST = "claude-code, codex, copilot, opencode, amp, droid, gemini, qwen, cursor, antigravity, kimi, goose or crush";
56334
56849
  var COMMANDS = {
56335
56850
  login: {
56336
56851
  short: "sign in to a hub",
@@ -56385,13 +56900,17 @@ var COMMANDS = {
56385
56900
  details: [
56386
56901
  "--scope user (the default) writes your own settings; --scope project writes the repo's local settings.",
56387
56902
  "Claude Code: ~/.claude/settings.json (or .claude/settings.local.json), including the Stop hook that wakes an idle session.",
56388
- "Codex: ~/.codex/hooks.json; then trust the hooks in Codex's /hooks. Copilot CLI: ~/.copilot/hooks/agenthub.json. OpenCode: a plugin in ~/.config/opencode/plugins/.",
56903
+ "Codex: ~/.codex/hooks.json; then trust the hooks in Codex's /hooks. Copilot CLI: ~/.copilot/hooks/agenthub.json. OpenCode: a plugin in ~/.config/opencode/plugins/. Amp: a plugin in ~/.config/amp/plugins/.",
56904
+ "Factory Droid: ~/.factory/hooks.json. Gemini CLI: ~/.gemini/settings.json. Qwen Code: ~/.qwen/settings.json. Cursor: ~/.cursor/hooks.json. Antigravity: ~/.gemini/config/hooks.json (.agents/hooks.json in a repo). Kimi Code: a block in ~/.kimi-code/config.toml (user scope only). Goose: a plugin in ~/.agents/plugins/agenthub/. Crush: ~/.config/crush/crush.json (PreToolUse only).",
56905
+ "Where a harness can't be woken, its Stop hook hands the agent any request that came in during the turn before it goes idle.",
56389
56906
  "Re-running install updates the hooks; uninstall removes only ours."
56390
56907
  ],
56391
56908
  examples: [
56392
56909
  ["agenthub hooks install claude-code", "for all your Claude Code sessions"],
56393
56910
  ["agenthub hooks install codex", "then open /hooks in Codex to trust them"],
56394
56911
  ["agenthub hooks install opencode --scope project", "only in this repo"],
56912
+ ["agenthub hooks install amp", "an Amp plugin that also wakes idle threads"],
56913
+ ["agenthub hooks install gemini", "Gemini CLI; also droid, qwen, cursor, antigravity, kimi, goose or crush"],
56395
56914
  ["agenthub hooks uninstall claude-code", ""]
56396
56915
  ]
56397
56916
  },
@@ -56407,34 +56926,46 @@ var COMMANDS = {
56407
56926
  ]
56408
56927
  },
56409
56928
  wake: {
56410
- short: "turn waking idle agents on or off, for every repo or one",
56411
- usage: ["agenthub wake [on|off|all] [--project]", "agenthub wake default --project"],
56412
- summary: "Choose whether idle agents are started up when a hub message arrives, in Claude Code, OpenCode and pi.",
56929
+ short: "turn waking idle agents on or off, and choose whether they ask you first",
56930
+ usage: ["agenthub wake [on|off|all|ask|auto] [--project]", "agenthub wake default --project"],
56931
+ summary: "Choose whether idle agents are started up when a hub message arrives, in Claude Code, OpenCode, Amp, pi, Roo Code (with its extension) and Kimi Code (in kimi web), and what they do once woken.",
56413
56932
  details: [
56414
56933
  "on (the default): requests and replies wake an idle agent; fyi waits for its next turn. all: fyi wakes it too. off: nothing wakes it.",
56415
- "--project sets it for the repo you're in only; it wins over your setting for every repo. default --project removes the repo's own setting.",
56416
- "Without on|off|all it shows what applies here and where that comes from. AGENTHUB_WAKE=0|on|all overrides it for one shell."
56934
+ "auto (the default): a woken agent handles the messages by itself, within the Agent Hub rules (answers and reviews read-only; handoffs, pushes and merges still wait for you). ask: it tells you what came in and waits for your go-ahead. The same applies when a Stop hook keeps a turn going for a message that came in during it.",
56935
+ "on|off|all and ask|auto are separate settings: set one of each.",
56936
+ "--project sets it for the repo you're in only; it wins over your setting for every repo. default --project removes both of the repo's own settings.",
56937
+ "Without a value it shows what applies here and where that comes from. AGENTHUB_WAKE=0|on|all and AGENTHUB_WAKE_ACT=ask|auto override them for one shell."
56417
56938
  ],
56418
56939
  examples: [
56419
56940
  ["agenthub wake", "what applies in this repo"],
56420
56941
  ["agenthub wake off", "never wake idle agents"],
56942
+ ["agenthub wake ask", "wake them, but have them ask you before acting"],
56943
+ ["agenthub wake auto", "back to the default: act without asking, within the rules"],
56421
56944
  ["agenthub wake off --project", "not in this repo, still elsewhere"],
56422
- ["agenthub wake all --project", "in this repo, wake for fyi too"],
56423
- ["agenthub wake default --project", "this repo follows your setting again"]
56945
+ ["agenthub wake ask --project", "ask first in this repo only"],
56946
+ ["agenthub wake default --project", "this repo follows your settings again"]
56424
56947
  ]
56425
56948
  },
56426
56949
  notify: {
56427
56950
  short: "print new messages for a hook",
56428
- usage: ["agenthub notify --format <claude-code|codex|copilot|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]"],
56429
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.",
56430
- details: ["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."],
56953
+ details: [
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).",
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."
56957
+ ],
56431
56958
  examples: [[`echo '{"hook_event_name":"UserPromptSubmit","session_id":"abc"}' | agenthub notify --format text`, "see what a hook would print"]]
56432
56959
  },
56433
56960
  wait: {
56434
56961
  short: "wait for a message that should wake an idle agent",
56435
- usage: ["agenthub wait [--max <seconds>]"],
56962
+ usage: ["agenthub wait [--max <seconds>] [--deliver kimi] [--plugin]"],
56436
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.",
56437
- details: ["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."],
56964
+ details: [
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.",
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."
56968
+ ],
56438
56969
  examples: [[`echo '{"session_id":"abc"}' | agenthub wait --max 60; echo $?`, "wait up to a minute; 2 means it would wake the agent"]]
56439
56970
  },
56440
56971
  admin: {
@@ -56458,6 +56989,7 @@ var ENVIRONMENT = `Environment:
56458
56989
  AGENTHUB_URL the hub (else the one from agenthub login)
56459
56990
  AGENTHUB_AGENT_NAME a fixed agent name for hooks (default: one per harness session)
56460
56991
  AGENTHUB_WAKE=0|on|all whether idle agents are woken; overrides agenthub wake
56992
+ AGENTHUB_WAKE_ACT=ask|auto whether a woken agent asks you first; overrides agenthub wake ask|auto
56461
56993
  AGENTHUB_CREDENTIAL_STORE=file|keychain|auto
56462
56994
  AGENTHUB_HOME config directory (default ~/.config/agenthub)
56463
56995
  AGENTHUB_DOWNLOAD_URL where releases come from
@@ -56497,9 +57029,9 @@ function commandHelp(name) {
56497
57029
  init_settings();
56498
57030
  var import_meta = {};
56499
57031
  function version2() {
56500
- if (true) return "0.3.1";
57032
+ if (true) return "0.3.3";
56501
57033
  try {
56502
- return JSON.parse((0, import_node_fs10.readFileSync)(new URL("../../package.json", import_meta.url), "utf8")).version;
57034
+ return JSON.parse((0, import_node_fs11.readFileSync)(new URL("../../package.json", import_meta.url), "utf8")).version;
56503
57035
  } catch {
56504
57036
  return "unknown";
56505
57037
  }
@@ -56568,8 +57100,22 @@ async function main(argv) {
56568
57100
  const format = flag(args, "--format") ?? "text";
56569
57101
  const event = flag(args, "--event");
56570
57102
  const payload = await readStdin(500);
56571
- const out = await notify({ format, event, payload }, { store: credentialStore(), version: version2() });
56572
- if (out) process.stdout.write(out + "\n");
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
+ }
57115
+ const out = await notifyHook({ format, event, payload }, { store: credentialStore(), version: version2() });
57116
+ if (out.stdout) process.stdout.write(out.stdout + "\n");
57117
+ if (out.stderr) process.stderr.write(out.stderr + "\n");
57118
+ return out.code ?? 0;
56573
57119
  } catch {
56574
57120
  }
56575
57121
  return 0;
@@ -56584,20 +57130,25 @@ async function main(argv) {
56584
57130
  return watch2({ json: json2, agentName, signal: ac.signal }, { store: credentialStore(), write: (l) => console.log(l) });
56585
57131
  }
56586
57132
  if (cmd === "wake") {
56587
- const { wakeSetting: wakeSetting2 } = await Promise.resolve().then(() => (init_wait(), wait_exports));
57133
+ const { actSetting: actSetting2, wakeSetting: wakeSetting2 } = await Promise.resolve().then(() => (init_wait(), wait_exports));
56588
57134
  const { gitRemotes: gitRemotes2 } = await Promise.resolve().then(() => (init_notify(), notify_exports));
56589
57135
  const perProject = args.includes("--project");
56590
57136
  const value = args.filter((a) => a !== "--project").shift();
56591
57137
  const origin = gitRemotes2(process.cwd()).origin;
56592
57138
  const key = origin ? projectKey(origin) : void 0;
56593
57139
  const describe3 = (mode) => mode === "off" ? "off: idle agents aren't woken" : mode === "all" ? "all: idle agents are woken for every message, fyi too" : "on: idle agents are woken for requests and replies, not fyi";
57140
+ const describeAct = (mode) => mode === "ask" ? "ask: a woken agent tells you what came in and asks before acting on it" : "auto: a woken agent handles messages by itself, within the Agent Hub rules";
57141
+ const sources = (name) => ({ env: ` (from ${name})`, project: ` (this repo's setting, ${key})`, user: " (your setting for every repo)", default: " (the default)" });
57142
+ const noEnv = { ...process.env, AGENTHUB_WAKE: void 0, AGENTHUB_WAKE_ACT: void 0 };
56594
57143
  if (value === void 0) {
56595
- const { mode, from } = wakeSetting2(process.env, origin);
56596
- const source = { env: " (from AGENTHUB_WAKE)", project: ` (this repo's setting, ${key})`, user: " (your setting for every repo)", default: " (the default)" }[from];
56597
- console.log(describe3(mode) + source);
57144
+ const wake = wakeSetting2(process.env, origin);
57145
+ const act = actSetting2(process.env, origin);
57146
+ console.log(describe3(wake.mode) + sources("AGENTHUB_WAKE")[wake.from]);
57147
+ console.log(describeAct(act.mode) + sources("AGENTHUB_WAKE_ACT")[act.from]);
56598
57148
  return 0;
56599
57149
  }
56600
- if (!["on", "off", "all", ...perProject ? ["default"] : []].includes(value)) {
57150
+ const isAct = value === "ask" || value === "auto";
57151
+ if (!["on", "off", "all", "ask", "auto", ...perProject ? ["default"] : []].includes(value)) {
56601
57152
  console.error(commandHelp("wake"));
56602
57153
  return 2;
56603
57154
  }
@@ -56608,28 +57159,56 @@ async function main(argv) {
56608
57159
  return 1;
56609
57160
  }
56610
57161
  const projects = { ...settings.wake_projects };
56611
- if (value === "default") delete projects[key];
57162
+ const actProjects = { ...settings.wake_act_projects };
57163
+ if (value === "default") {
57164
+ delete projects[key];
57165
+ delete actProjects[key];
57166
+ } else if (isAct) actProjects[key] = value;
56612
57167
  else projects[key] = value;
56613
- writeSettings({ ...settings, wake_projects: projects });
57168
+ writeSettings({ ...settings, wake_projects: projects, wake_act_projects: actProjects });
57169
+ } else if (isAct) {
57170
+ writeSettings({ ...settings, wake_act: value });
56614
57171
  } else {
56615
57172
  writeSettings({ ...settings, wake: value });
56616
57173
  }
56617
57174
  const where = perProject ? `in ${key}` : "in every repo without its own setting";
56618
- if (value === "default") console.log(`${key} now follows your setting for every repo: ${describe3(wakeSetting2({ ...process.env, AGENTHUB_WAKE: void 0 }, origin).mode)}.`);
57175
+ if (value === "default") {
57176
+ console.log(`${key} now follows your settings for every repo:`);
57177
+ console.log(` ${describe3(wakeSetting2(noEnv, origin).mode)}`);
57178
+ console.log(` ${describeAct(actSetting2(noEnv, origin).mode)}`);
57179
+ } else if (value === "ask") console.log(`A woken agent will tell you what came in and ask before acting on it, ${where}. This also applies when a Stop hook keeps a turn going for a new message.`);
57180
+ else if (value === "auto") console.log(`A woken agent will handle hub messages by itself, within the Agent Hub rules, ${where}. Handoffs and anything that runs commands, pushes or merges still wait for you.`);
56619
57181
  else if (value === "off") console.log(`Idle agents won't be woken for hub messages ${where}. They still see them on their next turn.`);
56620
- else console.log(`Idle agents will be woken for ${value === "all" ? "every hub message, fyi too" : "requests and replies"} ${where}, in Claude Code, OpenCode and pi.`);
56621
- if (process.env.AGENTHUB_WAKE !== void 0) console.log(`AGENTHUB_WAKE=${process.env.AGENTHUB_WAKE} is set in this shell and wins over this setting.`);
57182
+ else console.log(`Idle agents will be woken for ${value === "all" ? "every hub message, fyi too" : "requests and replies"} ${where}, in Claude Code, OpenCode, Amp, pi, Roo Code and Kimi Code (kimi web).`);
57183
+ const envName = isAct ? "AGENTHUB_WAKE_ACT" : "AGENTHUB_WAKE";
57184
+ if (value !== "default" && process.env[envName] !== void 0) console.log(`${envName}=${process.env[envName]} is set in this shell and wins over this setting.`);
56622
57185
  return 0;
56623
57186
  }
56624
57187
  if (cmd === "wait") {
56625
57188
  try {
56626
57189
  const max = Number(flag(args, "--max"));
56627
- const payload = await readStdin(500);
57190
+ const deliver = flag(args, "--deliver");
57191
+ const fromEnv = process.env.AGENTHUB_WAIT_PAYLOAD;
57192
+ let payload;
57193
+ try {
57194
+ payload = fromEnv ? JSON.parse(fromEnv) : await readStdin(500);
57195
+ } catch {
57196
+ payload = void 0;
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
+ }
56628
57202
  const { wait: wait2 } = await Promise.resolve().then(() => (init_wait(), wait_exports));
56629
57203
  const ac = new AbortController();
56630
57204
  process.once("SIGINT", () => ac.abort());
56631
57205
  process.once("SIGTERM", () => ac.abort());
56632
57206
  const result = await wait2({ payload, signal: ac.signal, ...max > 0 ? { maxMs: max * 1e3 } : {} }, { store: credentialStore() });
57207
+ if (deliver === "kimi") {
57208
+ const { deliverToKimi: deliverToKimi2 } = await Promise.resolve().then(() => (init_kimi(), kimi_exports));
57209
+ if (result.code === 2 && result.text && typeof payload?.session_id === "string") await deliverToKimi2(payload.session_id, result.text);
57210
+ return 0;
57211
+ }
56633
57212
  if (result.code === 2 && result.text) {
56634
57213
  process.stdout.write(result.text + "\n");
56635
57214
  return 2;
@@ -56707,13 +57286,22 @@ version: ${version2()}${newer}`);
56707
57286
  return 2;
56708
57287
  }
56709
57288
  const h = harness;
57289
+ if (scope === "project" && USER_SCOPE_ONLY.has(h)) {
57290
+ console.error(`${h} reads hooks only from your own config, so it has no --scope project.`);
57291
+ return 2;
57292
+ }
56710
57293
  const file2 = hookFile(h, scope, process.cwd());
56711
57294
  if (action === "install") {
56712
57295
  const events = installHooks(h, file2);
56713
57296
  console.log(`Added agenthub notify to ${events.join(", ")} in ${file2}`);
56714
57297
  if (h === "codex") console.log("Codex skips new hooks until you trust them: open /hooks in Codex and review them.");
56715
57298
  if (h === "opencode") console.log("OpenCode loads the plugin when it next starts.");
57299
+ if (h === "amp") console.log("Amp loads the plugin in new sessions, or now with plugins: reload from the command palette.");
56716
57300
  if (h === "copilot" && scope === "project") console.log("This file is in the repo: commit it only if everyone has agenthub, or add it to .git/info/exclude.");
57301
+ if (scope === "project" && h !== "copilot" && h !== "claude-code") console.log("This file is in the repo: commit it only if everyone has agenthub, or add it to .git/info/exclude.");
57302
+ if (h === "qwen" && scope === "project") console.log("Qwen Code runs a repo's hooks only in a trusted folder.");
57303
+ if (h === "goose") console.log("Goose loads the plugin when it next starts. Its hooks can't add context, so it gets a request that arrives during a turn before it stops, and presence; run agenthub watch beside it for the rest.");
57304
+ if (h === "cursor" || h === "antigravity" || h === "kimi") console.log(`${h === "cursor" ? "Cursor" : h === "kimi" ? "Kimi Code" : "Antigravity"} picks up the hooks in new sessions.`);
56717
57305
  } else {
56718
57306
  console.log(uninstallHooks(h, file2) ? `Removed agenthub hooks from ${file2}` : `No agenthub hooks in ${file2}`);
56719
57307
  }