@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.
- package/bin/agenthub.cjs +947 -359
- 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,
|
|
1359
|
+
return (0, import_node_path8.join)(cacheDir(env), `wait-${key}.json`);
|
|
638
1360
|
}
|
|
639
1361
|
function pendingFile(env, key) {
|
|
640
|
-
return (0,
|
|
1362
|
+
return (0, import_node_path8.join)(cacheDir(env), `pending-${key}.json`);
|
|
641
1363
|
}
|
|
642
1364
|
function releaseWaiter(env, key) {
|
|
643
|
-
(0,
|
|
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,
|
|
649
|
-
(0,
|
|
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,
|
|
1382
|
+
before = JSON.parse((0, import_node_fs7.readFileSync)(file2, "utf8"));
|
|
661
1383
|
} catch {
|
|
662
1384
|
before = void 0;
|
|
663
1385
|
}
|
|
664
|
-
(0,
|
|
665
|
-
(0,
|
|
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 =
|
|
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,
|
|
693
|
-
(0,
|
|
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,
|
|
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
|
-
${
|
|
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,
|
|
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,
|
|
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
|
-
|
|
777
|
-
|
|
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
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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
|
|
842
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
919
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
981
|
-
(0,
|
|
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,
|
|
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,
|
|
990
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
1006
|
-
(0,
|
|
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,
|
|
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
|
|
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
|
-
|
|
1795
|
+
import_node_child_process5 = require("node:child_process");
|
|
1039
1796
|
import_node_crypto3 = require("node:crypto");
|
|
1040
|
-
|
|
1041
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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/
|
|
56006
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
"
|
|
56416
|
-
"
|
|
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
|
|
56423
|
-
["agenthub wake default --project", "this repo follows your
|
|
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 <
|
|
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: [
|
|
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: [
|
|
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.
|
|
57032
|
+
if (true) return "0.3.3";
|
|
56501
57033
|
try {
|
|
56502
|
-
return JSON.parse((0,
|
|
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
|
-
|
|
56572
|
-
|
|
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
|
|
56596
|
-
const
|
|
56597
|
-
console.log(describe3(mode) +
|
|
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
|
-
|
|
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
|
-
|
|
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")
|
|
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
|
|
56621
|
-
|
|
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
|
|
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
|
}
|