@theagenthub/cli 0.3.0 → 0.3.2
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/README.md +2 -2
- package/bin/agenthub.cjs +1364 -297
- package/package.json +1 -1
package/bin/agenthub.cjs
CHANGED
|
@@ -276,6 +276,15 @@ function writeSettings(s, env = process.env) {
|
|
|
276
276
|
(0, import_node_fs4.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
277
277
|
(0, import_node_fs4.writeFileSync)((0, import_node_path5.join)(dir, "config.json"), JSON.stringify(s, null, 2) + "\n");
|
|
278
278
|
}
|
|
279
|
+
function projectKey(origin) {
|
|
280
|
+
try {
|
|
281
|
+
const { host, path } = parseRemote(origin);
|
|
282
|
+
const p = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").toLowerCase();
|
|
283
|
+
return p ? `${host}/${p}` : void 0;
|
|
284
|
+
} catch {
|
|
285
|
+
return void 0;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
279
288
|
function hubUrl(env = process.env) {
|
|
280
289
|
const raw = env.AGENTHUB_URL ?? readSettings(env).hub;
|
|
281
290
|
return raw?.replace(/\/+$/, "");
|
|
@@ -286,6 +295,7 @@ var init_settings = __esm({
|
|
|
286
295
|
"use strict";
|
|
287
296
|
import_node_fs4 = require("node:fs");
|
|
288
297
|
import_node_path5 = require("node:path");
|
|
298
|
+
init_registry();
|
|
289
299
|
init_paths();
|
|
290
300
|
}
|
|
291
301
|
});
|
|
@@ -453,22 +463,490 @@ var init_update = __esm({
|
|
|
453
463
|
}
|
|
454
464
|
});
|
|
455
465
|
|
|
466
|
+
// src/cli/kimi.ts
|
|
467
|
+
var kimi_exports = {};
|
|
468
|
+
__export(kimi_exports, {
|
|
469
|
+
KIMI_DEFAULT_PORT: () => KIMI_DEFAULT_PORT,
|
|
470
|
+
deliverToKimi: () => deliverToKimi,
|
|
471
|
+
kimiHome: () => kimiHome,
|
|
472
|
+
kimiServers: () => kimiServers,
|
|
473
|
+
kimiWakeable: () => kimiWakeable,
|
|
474
|
+
startKimiWaiter: () => startKimiWaiter
|
|
475
|
+
});
|
|
476
|
+
function kimiHome(env, home = (0, import_node_os3.homedir)()) {
|
|
477
|
+
return env.KIMI_CODE_HOME || (0, import_node_path7.join)(home, ".kimi-code");
|
|
478
|
+
}
|
|
479
|
+
function kimiWakeable(payload) {
|
|
480
|
+
const client = payload?.client_type;
|
|
481
|
+
return typeof payload?.session_id === "string" && typeof client === "string" && client !== TUI_CLIENT;
|
|
482
|
+
}
|
|
483
|
+
function kimiServers(env, home = (0, import_node_os3.homedir)()) {
|
|
484
|
+
const dir = (0, import_node_path7.join)(kimiHome(env, home), "server", "instances");
|
|
485
|
+
const urls = [];
|
|
486
|
+
let files = [];
|
|
487
|
+
try {
|
|
488
|
+
files = (0, import_node_fs6.readdirSync)(dir);
|
|
489
|
+
} catch {
|
|
490
|
+
}
|
|
491
|
+
for (const f of files) {
|
|
492
|
+
try {
|
|
493
|
+
const entry = JSON.parse((0, import_node_fs6.readFileSync)((0, import_node_path7.join)(dir, f), "utf8"));
|
|
494
|
+
if (typeof entry.url === "string" && /^https?:\/\//.test(entry.url)) urls.push(entry.url.replace(/\/+$/, ""));
|
|
495
|
+
else if (typeof entry.port === "number") {
|
|
496
|
+
const host = typeof entry.host === "string" && entry.host && entry.host !== "0.0.0.0" ? entry.host : "127.0.0.1";
|
|
497
|
+
urls.push(`http://${host}:${entry.port}`);
|
|
498
|
+
}
|
|
499
|
+
} catch {
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
urls.push(`http://127.0.0.1:${KIMI_DEFAULT_PORT}`);
|
|
503
|
+
return [...new Set(urls)];
|
|
504
|
+
}
|
|
505
|
+
async function deliverToKimi(sessionId, text, deps = {}) {
|
|
506
|
+
const env = deps.env ?? process.env;
|
|
507
|
+
const fetchFn = deps.fetch ?? fetch;
|
|
508
|
+
let token = "";
|
|
509
|
+
try {
|
|
510
|
+
token = (0, import_node_fs6.readFileSync)((0, import_node_path7.join)(kimiHome(env, deps.home), "server.token"), "utf8").trim();
|
|
511
|
+
} catch {
|
|
512
|
+
}
|
|
513
|
+
for (const base of kimiServers(env, deps.home)) {
|
|
514
|
+
try {
|
|
515
|
+
const res = await fetchFn(`${base}/api/v1/sessions/${encodeURIComponent(sessionId)}/prompts`, {
|
|
516
|
+
method: "POST",
|
|
517
|
+
headers: { "Content-Type": "application/json", ...token ? { Authorization: `Bearer ${token}` } : {} },
|
|
518
|
+
body: JSON.stringify({ content: [{ type: "text", text }] }),
|
|
519
|
+
signal: AbortSignal.timeout(1e4)
|
|
520
|
+
});
|
|
521
|
+
await res.body?.cancel().catch(() => {
|
|
522
|
+
});
|
|
523
|
+
if (res.ok) return true;
|
|
524
|
+
} catch {
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
function startKimiWaiter(payload, env = process.env) {
|
|
530
|
+
const script = process.argv[1];
|
|
531
|
+
const args = script && /\.[cm]?[jt]s$/.test(script) ? [script, "wait", "--deliver", "kimi"] : ["wait", "--deliver", "kimi"];
|
|
532
|
+
try {
|
|
533
|
+
const child = (0, import_node_child_process4.spawn)(process.execPath, args, {
|
|
534
|
+
detached: true,
|
|
535
|
+
stdio: "ignore",
|
|
536
|
+
windowsHide: true,
|
|
537
|
+
env: { ...env, AGENTHUB_WAIT_PAYLOAD: JSON.stringify(payload) }
|
|
538
|
+
});
|
|
539
|
+
child.on("error", () => {
|
|
540
|
+
});
|
|
541
|
+
child.unref();
|
|
542
|
+
} catch {
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
var import_node_child_process4, import_node_fs6, import_node_os3, import_node_path7, KIMI_DEFAULT_PORT, TUI_CLIENT;
|
|
546
|
+
var init_kimi = __esm({
|
|
547
|
+
"src/cli/kimi.ts"() {
|
|
548
|
+
"use strict";
|
|
549
|
+
import_node_child_process4 = require("node:child_process");
|
|
550
|
+
import_node_fs6 = require("node:fs");
|
|
551
|
+
import_node_os3 = require("node:os");
|
|
552
|
+
import_node_path7 = require("node:path");
|
|
553
|
+
KIMI_DEFAULT_PORT = 58627;
|
|
554
|
+
TUI_CLIENT = "kimi_code_cli";
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// src/cli/watch.ts
|
|
559
|
+
var watch_exports = {};
|
|
560
|
+
__export(watch_exports, {
|
|
561
|
+
readEvents: () => readEvents,
|
|
562
|
+
watch: () => watch
|
|
563
|
+
});
|
|
564
|
+
async function watch(opts, deps) {
|
|
565
|
+
const env = deps.env ?? process.env;
|
|
566
|
+
const fetchFn = deps.fetch ?? fetch;
|
|
567
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
568
|
+
const hub = hubUrl(env);
|
|
569
|
+
if (!hub) {
|
|
570
|
+
deps.write("No hub configured. Run agenthub login --hub <url>.");
|
|
571
|
+
return 1;
|
|
572
|
+
}
|
|
573
|
+
const { origin, upstream } = (deps.remotes ?? gitRemotes)(deps.cwd ?? process.cwd());
|
|
574
|
+
if (!origin) {
|
|
575
|
+
deps.write("Not in a git repo with an origin remote.");
|
|
576
|
+
return 1;
|
|
577
|
+
}
|
|
578
|
+
const agentName = opts.agentName ?? env.AGENTHUB_AGENT_NAME;
|
|
579
|
+
const query = new URLSearchParams({ project: origin, ...upstream ? { upstream } : {}, ...agentName ? { agent_name: agentName } : {} });
|
|
580
|
+
let backoff = 1e3;
|
|
581
|
+
let saidWaiting = false;
|
|
582
|
+
let waitingApproval = false;
|
|
583
|
+
while (!opts.signal?.aborted) {
|
|
584
|
+
const token = await accessToken(hub, { store: deps.store, fetch: fetchFn, env });
|
|
585
|
+
if (!token) {
|
|
586
|
+
deps.write("Not signed in. Run agenthub login.");
|
|
587
|
+
return 1;
|
|
588
|
+
}
|
|
589
|
+
let res;
|
|
590
|
+
try {
|
|
591
|
+
res = await fetchFn(`${hub}/api/stream?${query}`, {
|
|
592
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "text/event-stream" },
|
|
593
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
594
|
+
});
|
|
595
|
+
} catch {
|
|
596
|
+
if (opts.signal?.aborted) break;
|
|
597
|
+
await sleep(backoff, opts.signal);
|
|
598
|
+
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (res.status === 404) {
|
|
602
|
+
const { membership } = await res.json().catch(() => ({}));
|
|
603
|
+
if (membership === "pending") {
|
|
604
|
+
if (!waitingApproval) deps.write(opts.json ? JSON.stringify({ membership: "pending" }) : "Your request to join is waiting for a project admin's approval.");
|
|
605
|
+
waitingApproval = true;
|
|
606
|
+
} else {
|
|
607
|
+
if (waitingApproval) {
|
|
608
|
+
if (membership === "active") deps.write(opts.json ? JSON.stringify({ membership: "approved" }) : "A project admin approved you: ask your agent to call join_project.");
|
|
609
|
+
else deps.write(opts.json ? JSON.stringify({ membership: "declined" }) : "A project admin turned down your request to join.");
|
|
610
|
+
waitingApproval = false;
|
|
611
|
+
saidWaiting = true;
|
|
612
|
+
}
|
|
613
|
+
if (!saidWaiting) deps.write("No agent of yours has joined this project yet; waiting.");
|
|
614
|
+
saidWaiting = true;
|
|
615
|
+
}
|
|
616
|
+
await sleep(backoff, opts.signal);
|
|
617
|
+
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (res.status === 403 || res.status === 409 || res.status === 410) {
|
|
621
|
+
const body = await res.json().catch(() => ({}));
|
|
622
|
+
deps.write(body.message ?? `The hub refused the stream (${res.status}).`);
|
|
623
|
+
return 1;
|
|
624
|
+
}
|
|
625
|
+
if (!res.ok || !res.body) {
|
|
626
|
+
await res.body?.cancel();
|
|
627
|
+
await sleep(backoff, opts.signal);
|
|
628
|
+
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
backoff = 1e3;
|
|
632
|
+
saidWaiting = false;
|
|
633
|
+
let stop;
|
|
634
|
+
try {
|
|
635
|
+
for await (const ev of readEvents(res.body)) {
|
|
636
|
+
if (ev.event === "inbox") {
|
|
637
|
+
const data = JSON.parse(ev.data);
|
|
638
|
+
if (opts.json) deps.write(JSON.stringify(data));
|
|
639
|
+
else {
|
|
640
|
+
const text = formatInbox(data);
|
|
641
|
+
if (text) deps.write(text);
|
|
642
|
+
}
|
|
643
|
+
} else if (ev.event === "end") {
|
|
644
|
+
const { reason } = JSON.parse(ev.data);
|
|
645
|
+
if (reason === "replaced") {
|
|
646
|
+
deps.write("Another session joined with this agent name; stopping.");
|
|
647
|
+
stop = 1;
|
|
648
|
+
} else if (reason === "revoked") {
|
|
649
|
+
deps.write("This agent lost access to the project; stopping.");
|
|
650
|
+
stop = 1;
|
|
651
|
+
} else if (reason === "left") {
|
|
652
|
+
if (opts.json) deps.write(JSON.stringify({ left: true }));
|
|
653
|
+
else deps.write("The agent left the project; waiting for it to join again.");
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
} catch {
|
|
658
|
+
}
|
|
659
|
+
if (stop !== void 0) return stop;
|
|
660
|
+
}
|
|
661
|
+
return 0;
|
|
662
|
+
}
|
|
663
|
+
async function* readEvents(body) {
|
|
664
|
+
const decoder = new TextDecoder();
|
|
665
|
+
let buf = "";
|
|
666
|
+
for await (const chunk of body) {
|
|
667
|
+
buf += decoder.decode(chunk, { stream: true });
|
|
668
|
+
let i;
|
|
669
|
+
while ((i = buf.indexOf("\n\n")) >= 0) {
|
|
670
|
+
const block = buf.slice(0, i);
|
|
671
|
+
buf = buf.slice(i + 2);
|
|
672
|
+
let event = "message";
|
|
673
|
+
const data = [];
|
|
674
|
+
for (const line of block.split("\n")) {
|
|
675
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
676
|
+
else if (line.startsWith("data:")) data.push(line.slice(5).replace(/^ /, ""));
|
|
677
|
+
}
|
|
678
|
+
if (data.length) yield { event, data: data.join("\n") };
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function defaultSleep(ms, signal) {
|
|
683
|
+
return new Promise((resolve) => {
|
|
684
|
+
const t = setTimeout(resolve, ms);
|
|
685
|
+
signal?.addEventListener("abort", () => {
|
|
686
|
+
clearTimeout(t);
|
|
687
|
+
resolve();
|
|
688
|
+
}, { once: true });
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
var MAX_BACKOFF_MS;
|
|
692
|
+
var init_watch = __esm({
|
|
693
|
+
"src/cli/watch.ts"() {
|
|
694
|
+
"use strict";
|
|
695
|
+
init_notify();
|
|
696
|
+
init_session();
|
|
697
|
+
init_settings();
|
|
698
|
+
MAX_BACKOFF_MS = 3e4;
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
// src/cli/wait.ts
|
|
703
|
+
var wait_exports = {};
|
|
704
|
+
__export(wait_exports, {
|
|
705
|
+
DEFAULT_WAIT_MS: () => DEFAULT_WAIT_MS,
|
|
706
|
+
actMode: () => actMode,
|
|
707
|
+
actSetting: () => actSetting,
|
|
708
|
+
addPending: () => addPending,
|
|
709
|
+
claimFile: () => claimFile,
|
|
710
|
+
continueNotice: () => continueNotice,
|
|
711
|
+
mergeInbox: () => mergeInbox,
|
|
712
|
+
pendingFile: () => pendingFile,
|
|
713
|
+
releaseWaiter: () => releaseWaiter,
|
|
714
|
+
takePending: () => takePending,
|
|
715
|
+
wait: () => wait,
|
|
716
|
+
wakeMode: () => wakeMode,
|
|
717
|
+
wakeSetting: () => wakeSetting,
|
|
718
|
+
wokenNotice: () => wokenNotice
|
|
719
|
+
});
|
|
720
|
+
function wakeSetting(env, origin) {
|
|
721
|
+
const settings = readSettings(env);
|
|
722
|
+
const key = origin ? projectKey(origin) : void 0;
|
|
723
|
+
const project = key ? settings.wake_projects?.[key] : void 0;
|
|
724
|
+
const [raw, from] = env.AGENTHUB_WAKE !== void 0 ? [env.AGENTHUB_WAKE, "env"] : project ? [project, "project"] : settings.wake ? [settings.wake, "user"] : ["", "default"];
|
|
725
|
+
const v = raw.trim().toLowerCase();
|
|
726
|
+
const mode = ["0", "false", "off", "no"].includes(v) ? "off" : v === "all" ? "all" : "requests";
|
|
727
|
+
return { mode, from };
|
|
728
|
+
}
|
|
729
|
+
function actSetting(env, origin) {
|
|
730
|
+
const settings = readSettings(env);
|
|
731
|
+
const key = origin ? projectKey(origin) : void 0;
|
|
732
|
+
const project = key ? settings.wake_act_projects?.[key] : void 0;
|
|
733
|
+
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"];
|
|
734
|
+
return { mode: raw.trim().toLowerCase() === "ask" ? "ask" : "auto", from };
|
|
735
|
+
}
|
|
736
|
+
function actMode(env, origin) {
|
|
737
|
+
return actSetting(env, origin).mode;
|
|
738
|
+
}
|
|
739
|
+
function wokenNotice(act) {
|
|
740
|
+
return act === "ask" ? `Agent Hub woke this session while it was idle, for new hub messages. ${ASK_FIRST}` : WOKEN;
|
|
741
|
+
}
|
|
742
|
+
function continueNotice(act) {
|
|
743
|
+
return act === "ask" ? `These came in during the turn. ${ASK_FIRST}` : "Handle these as the Agent Hub rules say before you finish.";
|
|
744
|
+
}
|
|
745
|
+
function wakeMode(env, origin) {
|
|
746
|
+
return wakeSetting(env, origin).mode;
|
|
747
|
+
}
|
|
748
|
+
function claimFile(env, key) {
|
|
749
|
+
return (0, import_node_path8.join)(cacheDir(env), `wait-${key}.json`);
|
|
750
|
+
}
|
|
751
|
+
function pendingFile(env, key) {
|
|
752
|
+
return (0, import_node_path8.join)(cacheDir(env), `pending-${key}.json`);
|
|
753
|
+
}
|
|
754
|
+
function releaseWaiter(env, key) {
|
|
755
|
+
(0, import_node_fs7.rmSync)(claimFile(env, key), { force: true });
|
|
756
|
+
}
|
|
757
|
+
function takePending(env, key) {
|
|
758
|
+
const file2 = pendingFile(env, key);
|
|
759
|
+
try {
|
|
760
|
+
const pending = JSON.parse((0, import_node_fs7.readFileSync)(file2, "utf8"));
|
|
761
|
+
(0, import_node_fs7.rmSync)(file2, { force: true });
|
|
762
|
+
return pending;
|
|
763
|
+
} catch {
|
|
764
|
+
return void 0;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function addPending(env, key, inbox) {
|
|
768
|
+
if (!inbox.messages.length && !inbox.events.length) return;
|
|
769
|
+
const file2 = pendingFile(env, key);
|
|
770
|
+
let before;
|
|
771
|
+
try {
|
|
772
|
+
before = JSON.parse((0, import_node_fs7.readFileSync)(file2, "utf8"));
|
|
773
|
+
} catch {
|
|
774
|
+
before = void 0;
|
|
775
|
+
}
|
|
776
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path8.join)(file2, ".."), { recursive: true, mode: 448 });
|
|
777
|
+
(0, import_node_fs7.writeFileSync)(file2, JSON.stringify(mergeInbox(before, inbox)), { mode: 384 });
|
|
778
|
+
}
|
|
779
|
+
function mergeInbox(earlier, later) {
|
|
780
|
+
if (!earlier) return later;
|
|
781
|
+
return {
|
|
782
|
+
...later,
|
|
783
|
+
messages: [...earlier.messages, ...later.messages],
|
|
784
|
+
events: [...earlier.events, ...later.events]
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
async function wait(opts, deps) {
|
|
788
|
+
const env = deps.env ?? process.env;
|
|
789
|
+
const fetchFn = deps.fetch ?? fetch;
|
|
790
|
+
const now = deps.now ?? Date.now;
|
|
791
|
+
const sleep = deps.sleep ?? defaultSleep2;
|
|
792
|
+
const cwd = payloadCwd(opts.payload) ?? deps.cwd ?? process.cwd();
|
|
793
|
+
const hub = hubUrl(env);
|
|
794
|
+
if (!hub) return NOTHING;
|
|
795
|
+
const { origin, upstream } = (deps.remotes ?? gitRemotes)(cwd);
|
|
796
|
+
if (!origin) return NOTHING;
|
|
797
|
+
const mode = wakeMode(env, origin);
|
|
798
|
+
if (mode === "off") return NOTHING;
|
|
799
|
+
const act = actMode(env, origin);
|
|
800
|
+
if (!await onHub(hub, [origin, upstream], fetchFn, env, now).catch(() => false)) return NOTHING;
|
|
801
|
+
const agentName = hookAgentName(opts.payload, env);
|
|
802
|
+
const key = sessionKey(hub, origin, agentName);
|
|
803
|
+
const id = (0, import_node_crypto2.randomBytes)(8).toString("hex");
|
|
804
|
+
const claim2 = claimFile(env, key);
|
|
805
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path8.join)(claim2, ".."), { recursive: true, mode: 448 });
|
|
806
|
+
(0, import_node_fs7.writeFileSync)(claim2, JSON.stringify({ id, pid: process.pid, at: now() }));
|
|
807
|
+
const holds = () => {
|
|
808
|
+
try {
|
|
809
|
+
return JSON.parse((0, import_node_fs7.readFileSync)(claim2, "utf8")).id === id;
|
|
810
|
+
} catch {
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
const ac = new AbortController();
|
|
815
|
+
const stop = () => ac.abort();
|
|
816
|
+
opts.signal?.addEventListener("abort", stop, { once: true });
|
|
817
|
+
const deadline = setTimeout(stop, opts.maxMs ?? DEFAULT_WAIT_MS);
|
|
818
|
+
const check2 = setInterval(() => {
|
|
819
|
+
if (!holds()) stop();
|
|
820
|
+
}, deps.claimCheckMs ?? 1e3);
|
|
821
|
+
const query = new URLSearchParams({ project: origin, ...upstream ? { upstream } : {}, ...agentName ? { agent_name: agentName } : {}, idle: "1" });
|
|
822
|
+
let backoff = 1e3;
|
|
823
|
+
try {
|
|
824
|
+
while (!ac.signal.aborted) {
|
|
825
|
+
const token = await accessToken(hub, { store: deps.store, fetch: fetchFn, env, now });
|
|
826
|
+
if (!token) return NOTHING;
|
|
827
|
+
let res;
|
|
828
|
+
try {
|
|
829
|
+
res = await fetchFn(`${hub}/api/stream?${query}`, {
|
|
830
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "text/event-stream" },
|
|
831
|
+
signal: ac.signal
|
|
832
|
+
});
|
|
833
|
+
} catch {
|
|
834
|
+
if (ac.signal.aborted) break;
|
|
835
|
+
await sleep(backoff, ac.signal);
|
|
836
|
+
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS2);
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
if ([401, 403, 404, 409, 410].includes(res.status)) {
|
|
840
|
+
await res.body?.cancel();
|
|
841
|
+
return NOTHING;
|
|
842
|
+
}
|
|
843
|
+
if (!res.ok || !res.body) {
|
|
844
|
+
await res.body?.cancel();
|
|
845
|
+
await sleep(res.status === 429 ? MAX_BACKOFF_MS2 : backoff, ac.signal);
|
|
846
|
+
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS2);
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
backoff = 1e3;
|
|
850
|
+
try {
|
|
851
|
+
for await (const ev of readEvents(res.body)) {
|
|
852
|
+
if (ev.event === "inbox") {
|
|
853
|
+
const data = JSON.parse(ev.data);
|
|
854
|
+
const wakes = data.messages.some((m) => m.new && (mode === "all" || m.type !== "fyi"));
|
|
855
|
+
if (wakes && holds()) return { code: 2, text: `${formatInbox(mergeInbox(takePending(env, key), data))}
|
|
856
|
+
${wokenNotice(act)}` };
|
|
857
|
+
addPending(env, key, data);
|
|
858
|
+
} else if (ev.event === "end") {
|
|
859
|
+
const { reason } = JSON.parse(ev.data);
|
|
860
|
+
if (reason === "left" || reason === "replaced" || reason === "revoked") return NOTHING;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
} catch {
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return NOTHING;
|
|
867
|
+
} finally {
|
|
868
|
+
clearTimeout(deadline);
|
|
869
|
+
clearInterval(check2);
|
|
870
|
+
opts.signal?.removeEventListener("abort", stop);
|
|
871
|
+
ac.abort();
|
|
872
|
+
if (holds()) (0, import_node_fs7.rmSync)(claim2, { force: true });
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function defaultSleep2(ms, signal) {
|
|
876
|
+
return new Promise((resolve) => {
|
|
877
|
+
const t = setTimeout(resolve, ms);
|
|
878
|
+
signal?.addEventListener("abort", () => {
|
|
879
|
+
clearTimeout(t);
|
|
880
|
+
resolve();
|
|
881
|
+
}, { once: true });
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
var import_node_crypto2, import_node_fs7, import_node_path8, DEFAULT_WAIT_MS, MAX_BACKOFF_MS2, NOTHING, ASK_FIRST, WOKEN;
|
|
885
|
+
var init_wait = __esm({
|
|
886
|
+
"src/cli/wait.ts"() {
|
|
887
|
+
"use strict";
|
|
888
|
+
import_node_crypto2 = require("node:crypto");
|
|
889
|
+
import_node_fs7 = require("node:fs");
|
|
890
|
+
import_node_path8 = require("node:path");
|
|
891
|
+
init_paths();
|
|
892
|
+
init_notify();
|
|
893
|
+
init_session();
|
|
894
|
+
init_settings();
|
|
895
|
+
init_watch();
|
|
896
|
+
DEFAULT_WAIT_MS = 12 * 36e5;
|
|
897
|
+
MAX_BACKOFF_MS2 = 6e4;
|
|
898
|
+
NOTHING = { code: 0, text: "" };
|
|
899
|
+
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.";
|
|
900
|
+
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.";
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
|
|
456
904
|
// src/cli/notify.ts
|
|
905
|
+
var notify_exports = {};
|
|
906
|
+
__export(notify_exports, {
|
|
907
|
+
CARD_NUDGE_MS: () => CARD_NUDGE_MS,
|
|
908
|
+
NOTIFY_FORMATS: () => NOTIFY_FORMATS,
|
|
909
|
+
TASK_CHECK_MS: () => TASK_CHECK_MS,
|
|
910
|
+
formatInbox: () => formatInbox,
|
|
911
|
+
gitBranch: () => gitBranch,
|
|
912
|
+
gitRemotes: () => gitRemotes,
|
|
913
|
+
hookAgentName: () => hookAgentName,
|
|
914
|
+
notify: () => notify,
|
|
915
|
+
notifyHook: () => notifyHook,
|
|
916
|
+
onHub: () => onHub,
|
|
917
|
+
payloadCwd: () => payloadCwd,
|
|
918
|
+
sessionKey: () => sessionKey
|
|
919
|
+
});
|
|
457
920
|
async function notify(opts, deps) {
|
|
921
|
+
return (await notifyHook(opts, deps)).stdout;
|
|
922
|
+
}
|
|
923
|
+
async function notifyHook(opts, deps) {
|
|
458
924
|
try {
|
|
459
925
|
const event = opts.event ?? HOOK_EVENTS[String(opts.payload?.hook_event_name ?? "")] ?? "post-tool";
|
|
460
926
|
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("");
|
|
461
927
|
const [text, notice] = await Promise.all([run(opts, event, deps).catch(() => ""), update.catch(() => "")]);
|
|
462
|
-
|
|
928
|
+
if (typeof text === "object") return text;
|
|
929
|
+
return { stdout: render(opts.format, event, [text, notice].filter(Boolean).join("\n")) };
|
|
463
930
|
} catch {
|
|
464
|
-
return "";
|
|
931
|
+
return { stdout: "" };
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
function payloadCwd(payload) {
|
|
935
|
+
const first = (v) => Array.isArray(v) ? v[0] : void 0;
|
|
936
|
+
const dir = payload?.cwd ?? payload?.working_dir ?? first(payload?.workspace_roots) ?? first(payload?.workspacePaths);
|
|
937
|
+
if (typeof dir !== "string" || !dir) return void 0;
|
|
938
|
+
if (!dir.startsWith("file:")) return dir;
|
|
939
|
+
try {
|
|
940
|
+
return (0, import_node_url.fileURLToPath)(dir);
|
|
941
|
+
} catch {
|
|
942
|
+
return void 0;
|
|
465
943
|
}
|
|
466
944
|
}
|
|
467
945
|
async function run(opts, event, deps) {
|
|
468
946
|
const env = deps.env ?? process.env;
|
|
469
947
|
const now = deps.now ?? Date.now;
|
|
470
948
|
const fetchFn = deps.fetch ?? fetch;
|
|
471
|
-
const cwd =
|
|
949
|
+
const cwd = payloadCwd(opts.payload) ?? deps.cwd ?? process.cwd();
|
|
472
950
|
const out = (text) => text;
|
|
473
951
|
const hub = hubUrl(env);
|
|
474
952
|
if (!hub) return "";
|
|
@@ -476,14 +954,37 @@ async function run(opts, event, deps) {
|
|
|
476
954
|
if (!origin) return "";
|
|
477
955
|
if (!await onHub(hub, [origin, upstream], fetchFn, env, now)) return "";
|
|
478
956
|
const agentName = hookAgentName(opts.payload, env);
|
|
479
|
-
const throttleKey = (
|
|
480
|
-
|
|
481
|
-
if (
|
|
957
|
+
const throttleKey = sessionKey(hub, origin, agentName);
|
|
958
|
+
if (event !== "stop") releaseWaiter(env, throttleKey);
|
|
959
|
+
if (event === FIRST_EVENT_STARTS[opts.format]) {
|
|
960
|
+
const started = (0, import_node_path9.join)(cacheDir(env), `started-${throttleKey}.json`);
|
|
961
|
+
if (!readFlag(started)) {
|
|
962
|
+
writeFlag(started);
|
|
963
|
+
event = "session-start";
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
const wait2 = THROTTLE_MS[event];
|
|
967
|
+
if (wait2 && !throttle((0, import_node_path9.join)(cacheDir(env), `notify-${throttleKey}.json`), wait2, now())) return "";
|
|
482
968
|
const token = await accessToken(hub, { store: deps.store, fetch: fetchFn, env, now });
|
|
483
969
|
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.`) : "";
|
|
484
970
|
const who = { project: origin, ...upstream ? { upstream } : {}, ...agentName ? { agent_name: agentName } : {} };
|
|
485
971
|
const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
486
972
|
const signal = AbortSignal.timeout(3e3);
|
|
973
|
+
const block = STOP_BLOCK[opts.format];
|
|
974
|
+
const stopped = [opts.payload?.status, opts.payload?.terminationReason].some((v) => typeof v === "string" && /abort|cancel|interrupt/i.test(v));
|
|
975
|
+
if (event === "stop" && block && !stopped && wakeMode(env, origin) !== "off") {
|
|
976
|
+
const res2 = await fetchFn(`${hub}/api/inbox?${new URLSearchParams(who)}`, { headers, signal });
|
|
977
|
+
if (res2.ok) {
|
|
978
|
+
const inbox2 = mergeInbox(takePending(env, throttleKey), await res2.json());
|
|
979
|
+
const mode = wakeMode(env, origin);
|
|
980
|
+
if (inbox2.messages.some((m) => m.new && (mode === "all" || m.type !== "fyi"))) {
|
|
981
|
+
return block(`${formatInbox(inbox2)}
|
|
982
|
+
${continueNotice(actMode(env, origin))}`);
|
|
983
|
+
}
|
|
984
|
+
addPending(env, throttleKey, inbox2);
|
|
985
|
+
}
|
|
986
|
+
if (opts.format === "kimi" && kimiWakeable(opts.payload)) (deps.startKimiWaiter ?? startKimiWaiter)(opts.payload, env);
|
|
987
|
+
}
|
|
487
988
|
if (event === "stop" || event === "session-end") {
|
|
488
989
|
await fetchFn(`${hub}/api/presence`, {
|
|
489
990
|
method: "POST",
|
|
@@ -498,28 +999,81 @@ async function run(opts, event, deps) {
|
|
|
498
999
|
const { membership } = await res.json().catch(() => ({}));
|
|
499
1000
|
const args = [`remote="${origin}"`, ...upstream ? [`upstream="${upstream}"`] : [], ...agentName ? [`agent_name="${agentName}"`] : []].join(", ");
|
|
500
1001
|
const keep = agentName ? " Use exactly that agent_name: it's how hub notices find this session." : "";
|
|
501
|
-
const waitingFile = (0,
|
|
1002
|
+
const waitingFile = (0, import_node_path9.join)(cacheDir(env), `waiting-${throttleKey}.json`);
|
|
502
1003
|
const wasWaiting = readFlag(waitingFile);
|
|
503
1004
|
if (membership === "pending") {
|
|
504
1005
|
if (!wasWaiting) writeFlag(waitingFile);
|
|
505
1006
|
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.") : "";
|
|
506
1007
|
}
|
|
507
1008
|
if (wasWaiting) {
|
|
508
|
-
(0,
|
|
1009
|
+
(0, import_node_fs8.rmSync)(waitingFile, { force: true });
|
|
509
1010
|
if (membership === "active") return out(`Agent Hub: a project admin approved you. Call the agenthub join_project tool with ${args} to join.${keep}`);
|
|
510
1011
|
return out("Agent Hub: a project admin turned down your request to join this project.");
|
|
511
1012
|
}
|
|
512
1013
|
if (event !== "session-start") return "";
|
|
513
|
-
|
|
1014
|
+
const branch = (deps.branch ?? gitBranch)(cwd);
|
|
1015
|
+
const card2 = ` Pass a card with description (what this session is for), task (what it's working on)${branch ? `, branch ${JSON.stringify(branch)}` : ""} and capabilities, and call update_card whenever the task changes.`;
|
|
1016
|
+
return out(`Agent Hub: this repo is on the hub. To work with teammates' agents, call the agenthub join_project tool with ${args}.${keep}${card2}`);
|
|
514
1017
|
}
|
|
515
1018
|
if (!res.ok) return "";
|
|
516
|
-
|
|
1019
|
+
const inbox = await res.json();
|
|
1020
|
+
const card = await keepCardCurrent(inbox, event, {
|
|
1021
|
+
// Without a name the hub picks the user's latest agent here, which may be another session's.
|
|
1022
|
+
branch: agentName ? (deps.branch ?? gitBranch)(cwd) : void 0,
|
|
1023
|
+
stateFile: (0, import_node_path9.join)(cacheDir(env), `card-${throttleKey}.json`),
|
|
1024
|
+
now: now(),
|
|
1025
|
+
post: (fields) => fetchFn(`${hub}/api/card`, { method: "POST", headers, body: JSON.stringify({ ...who, ...fields }), signal })
|
|
1026
|
+
}).catch(() => "");
|
|
1027
|
+
return out([formatInbox(mergeInbox(takePending(env, throttleKey), inbox)), card].filter(Boolean).join("\n"));
|
|
1028
|
+
}
|
|
1029
|
+
async function keepCardCurrent(inbox, event, ctx) {
|
|
1030
|
+
const card = inbox.card;
|
|
1031
|
+
if (!card || card.kind === "bot") return "";
|
|
1032
|
+
const lines = [];
|
|
1033
|
+
const state = readState(ctx.stateFile);
|
|
1034
|
+
const had = typeof card.branch === "string" ? card.branch : void 0;
|
|
1035
|
+
if (ctx.branch && ctx.branch !== had) {
|
|
1036
|
+
const res = await ctx.post({ branch: ctx.branch });
|
|
1037
|
+
if (res.ok && had) lines.push(`Agent Hub: you switched to branch ${quote(ctx.branch)}, so your card says so now. If your task changed with it, call the agenthub update_card tool with the new task.`);
|
|
1038
|
+
}
|
|
1039
|
+
const missing = (inbox.card_missing ?? []).filter((k) => k === "description" || k === "task");
|
|
1040
|
+
if (missing.length && (event === "session-start" || ctx.now - (state.nudged ?? 0) >= CARD_NUDGE_MS)) {
|
|
1041
|
+
const list = missing.length > 1 ? missing.join(" or ") : missing[0];
|
|
1042
|
+
lines.push(`Agent Hub: your card has no ${list}. Call the agenthub update_card tool with them, so teammates' agents know what this session is for and what it's working on.`);
|
|
1043
|
+
state.nudged = ctx.now;
|
|
1044
|
+
} else if (!missing.length && event === "prompt" && typeof card.task === "string" && ctx.now - (state.taskChecked ?? 0) >= TASK_CHECK_MS) {
|
|
1045
|
+
lines.push(`Agent Hub: your card says you're working on ${quote(card.task)}. If this prompt starts something else, call the agenthub update_card tool with the new task.`);
|
|
1046
|
+
state.taskChecked = ctx.now;
|
|
1047
|
+
}
|
|
1048
|
+
if (lines.length) writeState(ctx.stateFile, state);
|
|
1049
|
+
return lines.join("\n");
|
|
1050
|
+
}
|
|
1051
|
+
function readState(path) {
|
|
1052
|
+
try {
|
|
1053
|
+
return JSON.parse((0, import_node_fs8.readFileSync)(path, "utf8"));
|
|
1054
|
+
} catch {
|
|
1055
|
+
return {};
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
function writeState(path, state) {
|
|
1059
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path9.join)(path, ".."), { recursive: true, mode: 448 });
|
|
1060
|
+
(0, import_node_fs8.writeFileSync)(path, JSON.stringify(state));
|
|
1061
|
+
}
|
|
1062
|
+
function gitBranch(cwd) {
|
|
1063
|
+
try {
|
|
1064
|
+
return (0, import_node_child_process5.execFileSync)("git", ["-C", cwd, "branch", "--show-current"], { stdio: ["ignore", "pipe", "ignore"], timeout: 1e3 }).toString().trim() || void 0;
|
|
1065
|
+
} catch {
|
|
1066
|
+
return void 0;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
function sessionKey(hub, origin, agentName) {
|
|
1070
|
+
return (0, import_node_crypto3.createHash)("sha256").update(`${hub}|${origin}|${agentName ?? ""}`).digest("hex").slice(0, 24);
|
|
517
1071
|
}
|
|
518
1072
|
function hookAgentName(payload, env) {
|
|
519
1073
|
if (env.AGENTHUB_AGENT_NAME) return env.AGENTHUB_AGENT_NAME;
|
|
520
|
-
const session = payload?.session_id ?? payload?.sessionId;
|
|
1074
|
+
const session = payload?.session_id ?? payload?.sessionId ?? payload?.conversation_id ?? payload?.conversationId;
|
|
521
1075
|
if (typeof session !== "string" || !session) return void 0;
|
|
522
|
-
return `s-${(0,
|
|
1076
|
+
return `s-${(0, import_node_crypto3.createHash)("sha256").update(session).digest("hex").slice(0, 8)}`;
|
|
523
1077
|
}
|
|
524
1078
|
function formatInbox(inbox) {
|
|
525
1079
|
const fresh = inbox.messages.filter((m) => m.new);
|
|
@@ -550,38 +1104,44 @@ function clean(s) {
|
|
|
550
1104
|
}
|
|
551
1105
|
function render(format, event, text) {
|
|
552
1106
|
if (!text) return "";
|
|
553
|
-
if (format === "text") return text;
|
|
1107
|
+
if (format === "text" || format === "kimi" || format === "amp" || format === "roo") return text;
|
|
1108
|
+
if (format === "goose") return "";
|
|
1109
|
+
if (event === "stop" || event === "session-end") return "";
|
|
1110
|
+
if (format === "crush") return JSON.stringify({ context: text });
|
|
1111
|
+
if (format === "cursor") return JSON.stringify({ additional_context: text });
|
|
1112
|
+
if (format === "antigravity") return JSON.stringify({ injectSteps: [{ systemMessage: text }] });
|
|
1113
|
+
if (format === "gemini") return JSON.stringify({ hookSpecificOutput: { hookEventName: GEMINI_EVENT_NAMES[event], additionalContext: text } });
|
|
554
1114
|
if (format === "codex" && event === "session-start") return text;
|
|
555
1115
|
if (format === "copilot") return JSON.stringify({ additionalContext: text });
|
|
556
1116
|
return JSON.stringify({ hookSpecificOutput: { hookEventName: EVENT_NAMES[event], additionalContext: text } });
|
|
557
1117
|
}
|
|
558
1118
|
function readFlag(path) {
|
|
559
1119
|
try {
|
|
560
|
-
(0,
|
|
1120
|
+
(0, import_node_fs8.readFileSync)(path);
|
|
561
1121
|
return true;
|
|
562
1122
|
} catch {
|
|
563
1123
|
return false;
|
|
564
1124
|
}
|
|
565
1125
|
}
|
|
566
1126
|
function writeFlag(path) {
|
|
567
|
-
(0,
|
|
568
|
-
(0,
|
|
1127
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path9.join)(path, ".."), { recursive: true, mode: 448 });
|
|
1128
|
+
(0, import_node_fs8.writeFileSync)(path, "{}");
|
|
569
1129
|
}
|
|
570
1130
|
function throttle(path, waitMs, now) {
|
|
571
1131
|
try {
|
|
572
|
-
const last = JSON.parse((0,
|
|
1132
|
+
const last = JSON.parse((0, import_node_fs8.readFileSync)(path, "utf8")).at;
|
|
573
1133
|
if (now - last < waitMs) return false;
|
|
574
1134
|
} catch {
|
|
575
1135
|
}
|
|
576
|
-
(0,
|
|
577
|
-
(0,
|
|
1136
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path9.join)(path, ".."), { recursive: true, mode: 448 });
|
|
1137
|
+
(0, import_node_fs8.writeFileSync)(path, JSON.stringify({ at: now }));
|
|
578
1138
|
return true;
|
|
579
1139
|
}
|
|
580
1140
|
async function onHub(hub, remotes, fetchFn, env, now) {
|
|
581
|
-
const path = (0,
|
|
1141
|
+
const path = (0, import_node_path9.join)(cacheDir(env), `providers-${(0, import_node_crypto3.createHash)("sha256").update(hub).digest("hex").slice(0, 16)}.json`);
|
|
582
1142
|
let providers;
|
|
583
1143
|
try {
|
|
584
|
-
const cached2 = JSON.parse((0,
|
|
1144
|
+
const cached2 = JSON.parse((0, import_node_fs8.readFileSync)(path, "utf8"));
|
|
585
1145
|
if (now() - cached2.at < PROVIDERS_TTL_MS) providers = cached2.providers;
|
|
586
1146
|
} catch {
|
|
587
1147
|
}
|
|
@@ -589,8 +1149,8 @@ async function onHub(hub, remotes, fetchFn, env, now) {
|
|
|
589
1149
|
const res = await fetchFn(`${hub}/api/providers`, { signal: AbortSignal.timeout(3e3) });
|
|
590
1150
|
if (!res.ok) return false;
|
|
591
1151
|
providers = (await res.json()).providers;
|
|
592
|
-
(0,
|
|
593
|
-
(0,
|
|
1152
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path9.join)(path, ".."), { recursive: true, mode: 448 });
|
|
1153
|
+
(0, import_node_fs8.writeFileSync)(path, JSON.stringify({ at: now(), providers }));
|
|
594
1154
|
}
|
|
595
1155
|
const hosts = /* @__PURE__ */ new Set();
|
|
596
1156
|
for (const p of providers) {
|
|
@@ -609,7 +1169,7 @@ async function onHub(hub, remotes, fetchFn, env, now) {
|
|
|
609
1169
|
function gitRemotes(cwd) {
|
|
610
1170
|
const get = (name) => {
|
|
611
1171
|
try {
|
|
612
|
-
return (0,
|
|
1172
|
+
return (0, import_node_child_process5.execFileSync)("git", ["-C", cwd, "remote", "get-url", name], { stdio: ["ignore", "pipe", "ignore"], timeout: 1e3 }).toString().trim() || void 0;
|
|
613
1173
|
} catch {
|
|
614
1174
|
return void 0;
|
|
615
1175
|
}
|
|
@@ -618,25 +1178,33 @@ function gitRemotes(cwd) {
|
|
|
618
1178
|
const upstream = origin ? get("upstream") : void 0;
|
|
619
1179
|
return { ...origin ? { origin } : {}, ...upstream ? { upstream } : {} };
|
|
620
1180
|
}
|
|
621
|
-
var
|
|
1181
|
+
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;
|
|
622
1182
|
var init_notify = __esm({
|
|
623
1183
|
"src/cli/notify.ts"() {
|
|
624
1184
|
"use strict";
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
1185
|
+
import_node_child_process5 = require("node:child_process");
|
|
1186
|
+
import_node_crypto3 = require("node:crypto");
|
|
1187
|
+
import_node_fs8 = require("node:fs");
|
|
1188
|
+
import_node_path9 = require("node:path");
|
|
1189
|
+
import_node_url = require("node:url");
|
|
629
1190
|
init_registry();
|
|
630
1191
|
init_paths();
|
|
631
1192
|
init_session();
|
|
632
1193
|
init_settings();
|
|
633
1194
|
init_update();
|
|
1195
|
+
init_kimi();
|
|
1196
|
+
init_wait();
|
|
1197
|
+
NOTIFY_FORMATS = ["claude-code", "codex", "copilot", "droid", "gemini", "qwen", "cursor", "antigravity", "kimi", "goose", "amp", "roo", "crush", "text"];
|
|
634
1198
|
HOOK_EVENTS = {
|
|
635
1199
|
SessionStart: "session-start",
|
|
636
1200
|
UserPromptSubmit: "prompt",
|
|
637
1201
|
PostToolUse: "post-tool",
|
|
638
1202
|
Stop: "stop",
|
|
639
|
-
SessionEnd: "session-end"
|
|
1203
|
+
SessionEnd: "session-end",
|
|
1204
|
+
// Gemini CLI
|
|
1205
|
+
BeforeAgent: "prompt",
|
|
1206
|
+
AfterTool: "post-tool",
|
|
1207
|
+
AfterAgent: "stop"
|
|
640
1208
|
};
|
|
641
1209
|
EVENT_NAMES = {
|
|
642
1210
|
"session-start": "SessionStart",
|
|
@@ -645,152 +1213,31 @@ var init_notify = __esm({
|
|
|
645
1213
|
stop: "Stop",
|
|
646
1214
|
"session-end": "SessionEnd"
|
|
647
1215
|
};
|
|
1216
|
+
GEMINI_EVENT_NAMES = { ...EVENT_NAMES, prompt: "BeforeAgent", "post-tool": "AfterTool", stop: "AfterAgent" };
|
|
648
1217
|
THROTTLE_MS = { "post-tool": 3e4, prompt: 1e4 };
|
|
649
1218
|
PROVIDERS_TTL_MS = 24 * 36e5;
|
|
650
|
-
|
|
651
|
-
})
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
}
|
|
673
|
-
const agentName = opts.agentName ?? env.AGENTHUB_AGENT_NAME;
|
|
674
|
-
const query = new URLSearchParams({ project: origin, ...upstream ? { upstream } : {}, ...agentName ? { agent_name: agentName } : {} });
|
|
675
|
-
let backoff = 1e3;
|
|
676
|
-
let saidWaiting = false;
|
|
677
|
-
let waitingApproval = false;
|
|
678
|
-
while (!opts.signal?.aborted) {
|
|
679
|
-
const token = await accessToken(hub, { store: deps.store, fetch: fetchFn, env });
|
|
680
|
-
if (!token) {
|
|
681
|
-
deps.write("Not signed in. Run agenthub login.");
|
|
682
|
-
return 1;
|
|
683
|
-
}
|
|
684
|
-
let res;
|
|
685
|
-
try {
|
|
686
|
-
res = await fetchFn(`${hub}/api/stream?${query}`, {
|
|
687
|
-
headers: { Authorization: `Bearer ${token}`, Accept: "text/event-stream" },
|
|
688
|
-
...opts.signal ? { signal: opts.signal } : {}
|
|
689
|
-
});
|
|
690
|
-
} catch {
|
|
691
|
-
if (opts.signal?.aborted) break;
|
|
692
|
-
await sleep(backoff, opts.signal);
|
|
693
|
-
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
694
|
-
continue;
|
|
695
|
-
}
|
|
696
|
-
if (res.status === 404) {
|
|
697
|
-
const { membership } = await res.json().catch(() => ({}));
|
|
698
|
-
if (membership === "pending") {
|
|
699
|
-
if (!waitingApproval) deps.write(opts.json ? JSON.stringify({ membership: "pending" }) : "Your request to join is waiting for a project admin's approval.");
|
|
700
|
-
waitingApproval = true;
|
|
701
|
-
} else {
|
|
702
|
-
if (waitingApproval) {
|
|
703
|
-
if (membership === "active") deps.write(opts.json ? JSON.stringify({ membership: "approved" }) : "A project admin approved you: ask your agent to call join_project.");
|
|
704
|
-
else deps.write(opts.json ? JSON.stringify({ membership: "declined" }) : "A project admin turned down your request to join.");
|
|
705
|
-
waitingApproval = false;
|
|
706
|
-
saidWaiting = true;
|
|
707
|
-
}
|
|
708
|
-
if (!saidWaiting) deps.write("No agent of yours has joined this project yet; waiting.");
|
|
709
|
-
saidWaiting = true;
|
|
710
|
-
}
|
|
711
|
-
await sleep(backoff, opts.signal);
|
|
712
|
-
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
713
|
-
continue;
|
|
714
|
-
}
|
|
715
|
-
if (res.status === 403 || res.status === 409 || res.status === 410) {
|
|
716
|
-
const body = await res.json().catch(() => ({}));
|
|
717
|
-
deps.write(body.message ?? `The hub refused the stream (${res.status}).`);
|
|
718
|
-
return 1;
|
|
719
|
-
}
|
|
720
|
-
if (!res.ok || !res.body) {
|
|
721
|
-
await res.body?.cancel();
|
|
722
|
-
await sleep(backoff, opts.signal);
|
|
723
|
-
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
724
|
-
continue;
|
|
725
|
-
}
|
|
726
|
-
backoff = 1e3;
|
|
727
|
-
saidWaiting = false;
|
|
728
|
-
let stop;
|
|
729
|
-
try {
|
|
730
|
-
for await (const ev of readEvents(res.body)) {
|
|
731
|
-
if (ev.event === "inbox") {
|
|
732
|
-
const data = JSON.parse(ev.data);
|
|
733
|
-
if (opts.json) deps.write(JSON.stringify(data));
|
|
734
|
-
else {
|
|
735
|
-
const text = formatInbox(data);
|
|
736
|
-
if (text) deps.write(text);
|
|
737
|
-
}
|
|
738
|
-
} else if (ev.event === "end") {
|
|
739
|
-
const { reason } = JSON.parse(ev.data);
|
|
740
|
-
if (reason === "replaced") {
|
|
741
|
-
deps.write("Another session joined with this agent name; stopping.");
|
|
742
|
-
stop = 1;
|
|
743
|
-
} else if (reason === "revoked") {
|
|
744
|
-
deps.write("This agent lost access to the project; stopping.");
|
|
745
|
-
stop = 1;
|
|
746
|
-
} else if (reason === "left") {
|
|
747
|
-
if (opts.json) deps.write(JSON.stringify({ left: true }));
|
|
748
|
-
else deps.write("The agent left the project; waiting for it to join again.");
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
} catch {
|
|
753
|
-
}
|
|
754
|
-
if (stop !== void 0) return stop;
|
|
755
|
-
}
|
|
756
|
-
return 0;
|
|
757
|
-
}
|
|
758
|
-
async function* readEvents(body) {
|
|
759
|
-
const decoder = new TextDecoder();
|
|
760
|
-
let buf = "";
|
|
761
|
-
for await (const chunk of body) {
|
|
762
|
-
buf += decoder.decode(chunk, { stream: true });
|
|
763
|
-
let i;
|
|
764
|
-
while ((i = buf.indexOf("\n\n")) >= 0) {
|
|
765
|
-
const block = buf.slice(0, i);
|
|
766
|
-
buf = buf.slice(i + 2);
|
|
767
|
-
let event = "message";
|
|
768
|
-
const data = [];
|
|
769
|
-
for (const line of block.split("\n")) {
|
|
770
|
-
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
771
|
-
else if (line.startsWith("data:")) data.push(line.slice(5).replace(/^ /, ""));
|
|
772
|
-
}
|
|
773
|
-
if (data.length) yield { event, data: data.join("\n") };
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
function defaultSleep(ms, signal) {
|
|
778
|
-
return new Promise((resolve) => {
|
|
779
|
-
const t = setTimeout(resolve, ms);
|
|
780
|
-
signal?.addEventListener("abort", () => {
|
|
781
|
-
clearTimeout(t);
|
|
782
|
-
resolve();
|
|
783
|
-
}, { once: true });
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
var MAX_BACKOFF_MS;
|
|
787
|
-
var init_watch = __esm({
|
|
788
|
-
"src/cli/watch.ts"() {
|
|
789
|
-
"use strict";
|
|
790
|
-
init_notify();
|
|
791
|
-
init_session();
|
|
792
|
-
init_settings();
|
|
793
|
-
MAX_BACKOFF_MS = 3e4;
|
|
1219
|
+
STOP_BLOCK = {
|
|
1220
|
+
codex: (reason) => ({ stdout: JSON.stringify({ decision: "block", reason }) }),
|
|
1221
|
+
copilot: (reason) => ({ stdout: JSON.stringify({ decision: "block", reason }) }),
|
|
1222
|
+
droid: (reason) => ({ stdout: JSON.stringify({ decision: "block", reason }) }),
|
|
1223
|
+
qwen: (reason) => ({ stdout: JSON.stringify({ decision: "block", reason }) }),
|
|
1224
|
+
goose: (reason) => ({ stdout: JSON.stringify({ decision: "block", reason }) }),
|
|
1225
|
+
// The Amp plugin returns agent.end's `continue` with the reason as the next user message.
|
|
1226
|
+
amp: (reason) => ({ stdout: JSON.stringify({ decision: "block", reason }) }),
|
|
1227
|
+
// The Roo Code extension sends the reason to the task with sendMessage.
|
|
1228
|
+
roo: (reason) => ({ stdout: JSON.stringify({ decision: "block", reason }) }),
|
|
1229
|
+
// AfterAgent: deny sends the reason to the agent as a new prompt.
|
|
1230
|
+
gemini: (reason) => ({ stdout: JSON.stringify({ decision: "deny", reason }) }),
|
|
1231
|
+
// stop: Cursor submits followup_message as the next user message.
|
|
1232
|
+
cursor: (reason) => ({ stdout: JSON.stringify({ followup_message: reason }) }),
|
|
1233
|
+
// Stop: the reason is injected as a system message and the agent goes on.
|
|
1234
|
+
antigravity: (reason) => ({ stdout: JSON.stringify({ decision: "continue", reason }) }),
|
|
1235
|
+
// Stop: exit 2 blocks it, with stderr as the reason.
|
|
1236
|
+
kimi: (reason) => ({ stdout: "", stderr: reason, code: 2 })
|
|
1237
|
+
};
|
|
1238
|
+
FIRST_EVENT_STARTS = { kimi: "prompt", crush: "post-tool" };
|
|
1239
|
+
CARD_NUDGE_MS = 10 * 6e4;
|
|
1240
|
+
TASK_CHECK_MS = 30 * 6e4;
|
|
794
1241
|
}
|
|
795
1242
|
});
|
|
796
1243
|
|
|
@@ -3664,16 +4111,16 @@ var require_connect = __commonJS({
|
|
|
3664
4111
|
}
|
|
3665
4112
|
});
|
|
3666
4113
|
}
|
|
3667
|
-
get(
|
|
3668
|
-
const ref = this._sessionCache.get(
|
|
4114
|
+
get(sessionKey2) {
|
|
4115
|
+
const ref = this._sessionCache.get(sessionKey2);
|
|
3669
4116
|
return ref ? ref.deref() : null;
|
|
3670
4117
|
}
|
|
3671
|
-
set(
|
|
4118
|
+
set(sessionKey2, session) {
|
|
3672
4119
|
if (this._maxCachedSessions === 0) {
|
|
3673
4120
|
return;
|
|
3674
4121
|
}
|
|
3675
|
-
if (this._sessionCache.has(
|
|
3676
|
-
this._sessionCache.delete(
|
|
4122
|
+
if (this._sessionCache.has(sessionKey2)) {
|
|
4123
|
+
this._sessionCache.delete(sessionKey2);
|
|
3677
4124
|
} else if (this._sessionCache.size >= this._maxCachedSessions) {
|
|
3678
4125
|
let evicted = false;
|
|
3679
4126
|
for (const [key, ref] of this._sessionCache) {
|
|
@@ -3690,8 +4137,8 @@ var require_connect = __commonJS({
|
|
|
3690
4137
|
}
|
|
3691
4138
|
}
|
|
3692
4139
|
}
|
|
3693
|
-
this._sessionCache.set(
|
|
3694
|
-
this._sessionRegistry.register(session,
|
|
4140
|
+
this._sessionCache.set(sessionKey2, new WeakRef(session));
|
|
4141
|
+
this._sessionRegistry.register(session, sessionKey2);
|
|
3695
4142
|
}
|
|
3696
4143
|
};
|
|
3697
4144
|
function buildConnector({ allowH2, useH2c, maxCachedSessions, socketPath, timeout, session: customSession, ...opts }) {
|
|
@@ -3709,9 +4156,9 @@ var require_connect = __commonJS({
|
|
|
3709
4156
|
tls = require("node:tls");
|
|
3710
4157
|
}
|
|
3711
4158
|
servername = servername || options.servername || util.getServerName(host) || null;
|
|
3712
|
-
const
|
|
3713
|
-
assert2(
|
|
3714
|
-
const session = customSession || sessionCache.get(
|
|
4159
|
+
const sessionKey2 = servername || hostname3;
|
|
4160
|
+
assert2(sessionKey2);
|
|
4161
|
+
const session = customSession || sessionCache.get(sessionKey2) || null;
|
|
3715
4162
|
port = port || 443;
|
|
3716
4163
|
socket = tls.connect({
|
|
3717
4164
|
highWaterMark: 16384,
|
|
@@ -3727,7 +4174,7 @@ var require_connect = __commonJS({
|
|
|
3727
4174
|
host: hostname3
|
|
3728
4175
|
});
|
|
3729
4176
|
socket.on("session", function(session2) {
|
|
3730
|
-
sessionCache.set(
|
|
4177
|
+
sessionCache.set(sessionKey2, session2);
|
|
3731
4178
|
});
|
|
3732
4179
|
} else {
|
|
3733
4180
|
assert2(!httpSocket, "httpSocket can only be sent on TLS update");
|
|
@@ -10903,7 +11350,7 @@ var require_socks5_client = __commonJS({
|
|
|
10903
11350
|
USERNAME_PASSWORD: 2,
|
|
10904
11351
|
NO_ACCEPTABLE: 255
|
|
10905
11352
|
};
|
|
10906
|
-
var
|
|
11353
|
+
var COMMANDS2 = {
|
|
10907
11354
|
CONNECT: 1,
|
|
10908
11355
|
BIND: 2,
|
|
10909
11356
|
UDP_ASSOCIATE: 3
|
|
@@ -11105,7 +11552,7 @@ var require_socks5_client = __commonJS({
|
|
|
11105
11552
|
}
|
|
11106
11553
|
debug("connecting to", address, port);
|
|
11107
11554
|
this.state = STATES.CONNECTING;
|
|
11108
|
-
const request = this.buildConnectRequest(
|
|
11555
|
+
const request = this.buildConnectRequest(COMMANDS2.CONNECT, address, port);
|
|
11109
11556
|
this.socket.write(request);
|
|
11110
11557
|
}
|
|
11111
11558
|
/**
|
|
@@ -11210,7 +11657,7 @@ var require_socks5_client = __commonJS({
|
|
|
11210
11657
|
module2.exports = {
|
|
11211
11658
|
Socks5Client,
|
|
11212
11659
|
AUTH_METHODS,
|
|
11213
|
-
COMMANDS,
|
|
11660
|
+
COMMANDS: COMMANDS2,
|
|
11214
11661
|
ADDRESS_TYPES,
|
|
11215
11662
|
REPLY_CODES,
|
|
11216
11663
|
STATES
|
|
@@ -27432,7 +27879,7 @@ function dispatcherFor(instance) {
|
|
|
27432
27879
|
if (!instance.caBundlePath) return void 0;
|
|
27433
27880
|
let d = dispatchers.get(instance.id);
|
|
27434
27881
|
if (!d) {
|
|
27435
|
-
const ca = (0,
|
|
27882
|
+
const ca = (0, import_node_fs9.readFileSync)(instance.caBundlePath, "utf8");
|
|
27436
27883
|
d = new import_undici.Agent({ connect: { ca: [...import_node_tls.rootCertificates, ca] } });
|
|
27437
27884
|
dispatchers.set(instance.id, d);
|
|
27438
27885
|
}
|
|
@@ -27449,11 +27896,11 @@ function fetchFor(instance, override) {
|
|
|
27449
27896
|
signal: AbortSignal.timeout(1e4)
|
|
27450
27897
|
});
|
|
27451
27898
|
}
|
|
27452
|
-
var
|
|
27899
|
+
var import_node_fs9, import_node_tls, import_undici, dispatchers;
|
|
27453
27900
|
var init_http = __esm({
|
|
27454
27901
|
"src/providers/http.ts"() {
|
|
27455
27902
|
"use strict";
|
|
27456
|
-
|
|
27903
|
+
import_node_fs9 = require("node:fs");
|
|
27457
27904
|
import_node_tls = require("node:tls");
|
|
27458
27905
|
import_undici = __toESM(require_undici(), 1);
|
|
27459
27906
|
dispatchers = /* @__PURE__ */ new Map();
|
|
@@ -55332,7 +55779,7 @@ var init_types = __esm({
|
|
|
55332
55779
|
|
|
55333
55780
|
// src/providers/config.ts
|
|
55334
55781
|
function loadConfigFile(path, opts = {}) {
|
|
55335
|
-
const read = opts.readFile ?? ((p) => (0,
|
|
55782
|
+
const read = opts.readFile ?? ((p) => (0, import_node_fs10.readFileSync)(p, "utf8"));
|
|
55336
55783
|
let text;
|
|
55337
55784
|
try {
|
|
55338
55785
|
text = read(path);
|
|
@@ -55343,7 +55790,7 @@ function loadConfigFile(path, opts = {}) {
|
|
|
55343
55790
|
}
|
|
55344
55791
|
function buildConfig(raw, opts = {}) {
|
|
55345
55792
|
const env = opts.env ?? process.env;
|
|
55346
|
-
const read = opts.readFile ?? ((p) => (0,
|
|
55793
|
+
const read = opts.readFile ?? ((p) => (0, import_node_fs10.readFileSync)(p, "utf8"));
|
|
55347
55794
|
const parsed = configSchema.safeParse(raw);
|
|
55348
55795
|
if (!parsed.success) {
|
|
55349
55796
|
throw new ConfigError(
|
|
@@ -55524,11 +55971,11 @@ function checkNoHostCollisions(instances) {
|
|
|
55524
55971
|
for (const a of inst.remoteAliases) claim2(a.port ? `${a.host}:${a.port}` : a.host, inst.id);
|
|
55525
55972
|
}
|
|
55526
55973
|
}
|
|
55527
|
-
var
|
|
55974
|
+
var import_node_fs10, import_yaml, secretRef, oauthSchema, providerSchema, configSchema, ConfigError;
|
|
55528
55975
|
var init_config = __esm({
|
|
55529
55976
|
"src/providers/config.ts"() {
|
|
55530
55977
|
"use strict";
|
|
55531
|
-
|
|
55978
|
+
import_node_fs10 = require("node:fs");
|
|
55532
55979
|
import_yaml = __toESM(require_dist(), 1);
|
|
55533
55980
|
init_zod();
|
|
55534
55981
|
init_defaults();
|
|
@@ -55629,7 +56076,7 @@ var init_admin = __esm({
|
|
|
55629
56076
|
});
|
|
55630
56077
|
|
|
55631
56078
|
// src/cli/index.ts
|
|
55632
|
-
var
|
|
56079
|
+
var import_node_fs11 = require("node:fs");
|
|
55633
56080
|
|
|
55634
56081
|
// src/cli/credentials.ts
|
|
55635
56082
|
var import_node_child_process = require("node:child_process");
|
|
@@ -55735,11 +56182,133 @@ var import_node_fs2 = require("node:fs");
|
|
|
55735
56182
|
var import_node_os2 = require("node:os");
|
|
55736
56183
|
var import_node_path3 = require("node:path");
|
|
55737
56184
|
|
|
56185
|
+
// src/cli/amp-plugin.ts
|
|
56186
|
+
var AMP_MARKER = "// Agent Hub plugin for Amp, written by `agenthub hooks install amp`.";
|
|
56187
|
+
var PACKAGE_HEADER = `// Agent Hub plugin for Amp. Copy it to ~/.config/amp/plugins/ (or a repo's .amp/plugins/).
|
|
56188
|
+
// Needs the agenthub CLI: curl -fsSL https://get.theagenthub.app | sh`;
|
|
56189
|
+
var PACKAGE_COMMAND = `const COMMAND =
|
|
56190
|
+
process.env.AGENTHUB_BIN ||
|
|
56191
|
+
[join(homedir(), ".local", "bin", "agenthub"), join(process.env.LOCALAPPDATA || homedir(), "Programs", "agenthub", "agenthub.exe")].find((f) => existsSync(f)) ||
|
|
56192
|
+
"agenthub";`;
|
|
56193
|
+
function ampPlugin(command, as = "file") {
|
|
56194
|
+
const header = as === "package" ? PACKAGE_HEADER : `${AMP_MARKER}
|
|
56195
|
+
// Re-run that command to update it, or \`agenthub hooks uninstall amp\` to remove it.`;
|
|
56196
|
+
return `${header}
|
|
56197
|
+
import { spawn } from "node:child_process";
|
|
56198
|
+
import { existsSync } from "node:fs";
|
|
56199
|
+
import { homedir } from "node:os";
|
|
56200
|
+
import { join } from "node:path";
|
|
56201
|
+
import { fileURLToPath } from "node:url";
|
|
56202
|
+
|
|
56203
|
+
${as === "package" ? PACKAGE_COMMAND : `const COMMAND = ${JSON.stringify(command)};`}
|
|
56204
|
+
const TIMEOUT_MS = 5000;
|
|
56205
|
+
|
|
56206
|
+
export const description = "Agent Hub: teammates' agents' messages in your threads, and an idle thread wakes for their requests.";
|
|
56207
|
+
|
|
56208
|
+
/** Runs agenthub; resolves to its exit code and stdout, or code -1 on any failure. */
|
|
56209
|
+
function run(args, input, cwd, timeoutMs, onChild) {
|
|
56210
|
+
return new Promise((resolve) => {
|
|
56211
|
+
let out = "";
|
|
56212
|
+
let child;
|
|
56213
|
+
try {
|
|
56214
|
+
child = spawn(COMMAND, args, { cwd, stdio: ["pipe", "pipe", "ignore"], windowsHide: true, shell: process.platform === "win32" });
|
|
56215
|
+
} catch {
|
|
56216
|
+
return resolve({ code: -1, out: "" });
|
|
56217
|
+
}
|
|
56218
|
+
onChild?.(child);
|
|
56219
|
+
const timer = timeoutMs ? setTimeout(() => child.kill(), timeoutMs) : undefined;
|
|
56220
|
+
child.stdout.on("data", (chunk) => (out += chunk));
|
|
56221
|
+
child.on("error", () => {
|
|
56222
|
+
clearTimeout(timer);
|
|
56223
|
+
resolve({ code: -1, out: "" });
|
|
56224
|
+
});
|
|
56225
|
+
child.on("close", (code) => {
|
|
56226
|
+
clearTimeout(timer);
|
|
56227
|
+
resolve({ code: code ?? -1, out: out.trim() });
|
|
56228
|
+
});
|
|
56229
|
+
child.stdin.on("error", () => {});
|
|
56230
|
+
child.stdin.end(input);
|
|
56231
|
+
});
|
|
56232
|
+
}
|
|
56233
|
+
|
|
56234
|
+
export default function agenthub(amp) {
|
|
56235
|
+
let cwd = process.cwd();
|
|
56236
|
+
try {
|
|
56237
|
+
const root = amp.system?.workspaceRoot ? String(amp.system.workspaceRoot) : "";
|
|
56238
|
+
if (root) cwd = root.startsWith("file:") ? fileURLToPath(root) : root;
|
|
56239
|
+
} catch {}
|
|
56240
|
+
const started = new Set();
|
|
56241
|
+
const waiting = new Map();
|
|
56242
|
+
const stopWaiting = (id) => {
|
|
56243
|
+
waiting.get(id)?.kill();
|
|
56244
|
+
waiting.delete(id);
|
|
56245
|
+
};
|
|
56246
|
+
const notify = async (event, id, extra) => {
|
|
56247
|
+
const { out } = await run(["notify", "--format", "amp", "--event", event], JSON.stringify({ session_id: id, cwd, ...extra }), cwd, TIMEOUT_MS);
|
|
56248
|
+
return out;
|
|
56249
|
+
};
|
|
56250
|
+
const say = async (id, content, steer) => {
|
|
56251
|
+
try {
|
|
56252
|
+
await amp.threads.get(id).appendUserMessage({ type: "user-message", content }, steer ? { steer: true } : undefined);
|
|
56253
|
+
} catch (e) {
|
|
56254
|
+
amp.logger?.log?.("agenthub: couldn't add to thread " + id + ": " + e);
|
|
56255
|
+
}
|
|
56256
|
+
};
|
|
56257
|
+
const wake = async (id) => {
|
|
56258
|
+
const { code, out } = await run(["wait"], JSON.stringify({ session_id: id, cwd }), cwd, 0, (child) => {
|
|
56259
|
+
stopWaiting(id);
|
|
56260
|
+
waiting.set(id, child);
|
|
56261
|
+
});
|
|
56262
|
+
// An agenthub too old to know \`wait\` exits 2 too, but prints nothing on stdout.
|
|
56263
|
+
if (code === 2 && out) await say(id, out, false);
|
|
56264
|
+
};
|
|
56265
|
+
|
|
56266
|
+
amp.on("agent.start", async (event) => {
|
|
56267
|
+
const id = event.thread?.id;
|
|
56268
|
+
if (!id) return;
|
|
56269
|
+
stopWaiting(id);
|
|
56270
|
+
const first = !started.has(id);
|
|
56271
|
+
started.add(id);
|
|
56272
|
+
const text = await notify(first ? "session-start" : "prompt", id);
|
|
56273
|
+
return text ? { message: { content: text, display: false } } : undefined;
|
|
56274
|
+
});
|
|
56275
|
+
|
|
56276
|
+
amp.on("tool.result", async (event) => {
|
|
56277
|
+
const id = event.thread?.id;
|
|
56278
|
+
if (!id) return;
|
|
56279
|
+
const text = await notify("post-tool", id);
|
|
56280
|
+
if (!text) return;
|
|
56281
|
+
if (typeof event.output === "string") return { status: event.status, output: event.output + "\\n\\n" + text };
|
|
56282
|
+
await say(id, text, true);
|
|
56283
|
+
});
|
|
56284
|
+
|
|
56285
|
+
amp.on("agent.end", async (event) => {
|
|
56286
|
+
const id = event.thread?.id;
|
|
56287
|
+
if (!id) return;
|
|
56288
|
+
// A cancelled turn isn't kept going, and doesn't start a waiter.
|
|
56289
|
+
const out = await notify("stop", id, { status: event.status });
|
|
56290
|
+
if (out) {
|
|
56291
|
+
try {
|
|
56292
|
+
const r = JSON.parse(out);
|
|
56293
|
+
if (r.decision === "block" && r.reason) return { action: "continue", userMessage: r.reason };
|
|
56294
|
+
} catch {}
|
|
56295
|
+
}
|
|
56296
|
+
if (event.status !== "cancelled") void wake(id);
|
|
56297
|
+
});
|
|
56298
|
+
|
|
56299
|
+
amp.onDispose?.(() => {
|
|
56300
|
+
for (const child of waiting.values()) child.kill();
|
|
56301
|
+
waiting.clear();
|
|
56302
|
+
});
|
|
56303
|
+
}
|
|
56304
|
+
`;
|
|
56305
|
+
}
|
|
56306
|
+
|
|
55738
56307
|
// src/cli/opencode-plugin.ts
|
|
55739
56308
|
var OPENCODE_MARKER = "// Agent Hub plugin for OpenCode, written by `agenthub hooks install opencode`.";
|
|
55740
|
-
var
|
|
56309
|
+
var PACKAGE_HEADER2 = `// Agent Hub plugin for OpenCode, from the @theagenthub/opencode npm package.
|
|
55741
56310
|
// Needs the agenthub CLI: curl -fsSL https://get.theagenthub.app | sh`;
|
|
55742
|
-
var
|
|
56311
|
+
var PACKAGE_COMMAND2 = `import { existsSync } from "node:fs";
|
|
55743
56312
|
import { homedir } from "node:os";
|
|
55744
56313
|
import { join } from "node:path";
|
|
55745
56314
|
|
|
@@ -55748,12 +56317,12 @@ const COMMAND =
|
|
|
55748
56317
|
[join(homedir(), ".local", "bin", "agenthub"), join(process.env.LOCALAPPDATA || homedir(), "Programs", "agenthub", "agenthub.exe")].find((f) => existsSync(f)) ||
|
|
55749
56318
|
"agenthub";`;
|
|
55750
56319
|
function opencodePlugin(command, as = "file") {
|
|
55751
|
-
const header = as === "package" ?
|
|
56320
|
+
const header = as === "package" ? PACKAGE_HEADER2 : `${OPENCODE_MARKER}
|
|
55752
56321
|
// Re-run that command to update it, or \`agenthub hooks uninstall opencode\` to remove it.`;
|
|
55753
56322
|
return `${header}
|
|
55754
56323
|
import { spawn } from "node:child_process";
|
|
55755
56324
|
|
|
55756
|
-
${as === "package" ?
|
|
56325
|
+
${as === "package" ? PACKAGE_COMMAND2 : `const COMMAND = ${JSON.stringify(command)};`}
|
|
55757
56326
|
const TIMEOUT_MS = 5000;
|
|
55758
56327
|
|
|
55759
56328
|
function notify(event, sessionID, cwd) {
|
|
@@ -55788,15 +56357,52 @@ function notify(event, sessionID, cwd) {
|
|
|
55788
56357
|
});
|
|
55789
56358
|
}
|
|
55790
56359
|
|
|
56360
|
+
/** Runs \`agenthub wait\`; resolves to the text to wake the session with, or "" when it shouldn't. */
|
|
56361
|
+
function waitForMessage(sessionID, cwd, children) {
|
|
56362
|
+
return new Promise((resolve) => {
|
|
56363
|
+
let out = "";
|
|
56364
|
+
let child;
|
|
56365
|
+
try {
|
|
56366
|
+
child = spawn(COMMAND, ["wait"], { cwd, stdio: ["pipe", "pipe", "ignore"], windowsHide: true, shell: process.platform === "win32" });
|
|
56367
|
+
} catch {
|
|
56368
|
+
return resolve("");
|
|
56369
|
+
}
|
|
56370
|
+
children.get(sessionID)?.kill();
|
|
56371
|
+
children.set(sessionID, child);
|
|
56372
|
+
child.stdout.on("data", (chunk) => (out += chunk));
|
|
56373
|
+
child.on("error", () => resolve(""));
|
|
56374
|
+
child.on("close", (code) => {
|
|
56375
|
+
if (children.get(sessionID) === child) children.delete(sessionID);
|
|
56376
|
+
// An agenthub too old to know \`wait\` exits 2 too, but prints nothing on stdout.
|
|
56377
|
+
resolve(code === 2 ? out.trim() : "");
|
|
56378
|
+
});
|
|
56379
|
+
child.stdin.on("error", () => {});
|
|
56380
|
+
child.stdin.end(JSON.stringify({ session_id: sessionID, cwd }));
|
|
56381
|
+
});
|
|
56382
|
+
}
|
|
56383
|
+
|
|
55791
56384
|
function partId() {
|
|
55792
56385
|
return "prt_" + Date.now().toString(16) + Math.random().toString(36).slice(2, 12);
|
|
55793
56386
|
}
|
|
55794
56387
|
|
|
55795
|
-
export const AgentHub = async ({ directory }) => {
|
|
56388
|
+
export const AgentHub = async ({ directory, client }) => {
|
|
55796
56389
|
const started = new Set();
|
|
56390
|
+
const waiting = new Map();
|
|
55797
56391
|
let presence = Promise.resolve();
|
|
56392
|
+
const wake = async (sessionID) => {
|
|
56393
|
+
const send = client?.session?.promptAsync ?? client?.session?.prompt;
|
|
56394
|
+
if (!send) return;
|
|
56395
|
+
const text = await waitForMessage(sessionID, directory, waiting);
|
|
56396
|
+
if (!text) return;
|
|
56397
|
+
try {
|
|
56398
|
+
await send.call(client.session, { path: { id: sessionID }, body: { parts: [{ type: "text", text }] } });
|
|
56399
|
+
} catch {
|
|
56400
|
+
/* the session is gone, or busy again */
|
|
56401
|
+
}
|
|
56402
|
+
};
|
|
55798
56403
|
return {
|
|
55799
56404
|
"chat.message": async (input, output) => {
|
|
56405
|
+
waiting.get(input.sessionID)?.kill();
|
|
55800
56406
|
const first = !started.has(input.sessionID);
|
|
55801
56407
|
started.add(input.sessionID);
|
|
55802
56408
|
const text = await notify(first ? "session-start" : "prompt", input.sessionID, directory);
|
|
@@ -55811,8 +56417,15 @@ export const AgentHub = async ({ directory }) => {
|
|
|
55811
56417
|
// Presence updates don't block OpenCode, but run one at a time so "offline" can't be
|
|
55812
56418
|
// overtaken by an earlier "idle".
|
|
55813
56419
|
event: async ({ event }) => {
|
|
55814
|
-
if (event.type === "session.idle")
|
|
55815
|
-
|
|
56420
|
+
if (event.type === "session.idle") {
|
|
56421
|
+
const sessionID = event.properties.sessionID;
|
|
56422
|
+
presence = presence.then(() => notify("stop", sessionID, directory));
|
|
56423
|
+
// Not awaited: it waits as long as the session is idle.
|
|
56424
|
+
presence.then(() => wake(sessionID)).catch(() => {});
|
|
56425
|
+
} else if (event.type === "session.deleted") {
|
|
56426
|
+
waiting.get(event.properties.info.id)?.kill();
|
|
56427
|
+
presence = presence.then(() => notify("session-end", event.properties.info.id, directory));
|
|
56428
|
+
}
|
|
55816
56429
|
},
|
|
55817
56430
|
};
|
|
55818
56431
|
};
|
|
@@ -55824,11 +56437,23 @@ async function setup(ctx) {
|
|
|
55824
56437
|
if (!ctx?.session?.hook || !ctx.tool?.hook || !ctx.event?.subscribe) return;
|
|
55825
56438
|
const directory = ctx.location?.directory ?? process.cwd();
|
|
55826
56439
|
const started = new Set();
|
|
56440
|
+
const waiting = new Map();
|
|
55827
56441
|
let presence = Promise.resolve();
|
|
56442
|
+
const wake = async (sessionID) => {
|
|
56443
|
+
if (typeof ctx.session.synthetic !== "function") return;
|
|
56444
|
+
const text = await waitForMessage(sessionID, directory, waiting);
|
|
56445
|
+
if (!text) return;
|
|
56446
|
+
try {
|
|
56447
|
+
await ctx.session.synthetic({ sessionID, text, description: "Agent Hub: new message", delivery: "queue", resume: true });
|
|
56448
|
+
} catch {
|
|
56449
|
+
/* the session is gone, or busy again */
|
|
56450
|
+
}
|
|
56451
|
+
};
|
|
55828
56452
|
const wrap = (text) => "\\n\\n<agenthub>\\n" + text + "\\n</agenthub>";
|
|
55829
56453
|
|
|
55830
56454
|
// V2 has no hidden message parts, so the notice is appended to the prompt's text.
|
|
55831
56455
|
await ctx.session.hook("prompt", async (event) => {
|
|
56456
|
+
waiting.get(event.sessionID)?.kill();
|
|
55832
56457
|
const first = !started.has(event.sessionID);
|
|
55833
56458
|
started.add(event.sessionID);
|
|
55834
56459
|
const text = await notify(first ? "session-start" : "prompt", event.sessionID, directory);
|
|
@@ -55848,11 +56473,21 @@ async function setup(ctx) {
|
|
|
55848
56473
|
const sessionID = event.data?.sessionID;
|
|
55849
56474
|
if (!sessionID) continue;
|
|
55850
56475
|
const kind = event.type === "session.deleted" ? "session-end" : "stop";
|
|
55851
|
-
if (kind === "session-end")
|
|
56476
|
+
if (kind === "session-end") {
|
|
56477
|
+
started.delete(sessionID);
|
|
56478
|
+
waiting.get(sessionID)?.kill();
|
|
56479
|
+
}
|
|
55852
56480
|
presence = presence.then(() => notify(kind, sessionID, directory));
|
|
56481
|
+
// A finished run (not one the user interrupted) waits for a hub message; not awaited.
|
|
56482
|
+
if (event.type === "session.execution.succeeded" || event.type === "session.execution.failed") {
|
|
56483
|
+
presence.then(() => wake(sessionID)).catch(() => {});
|
|
56484
|
+
}
|
|
55853
56485
|
}
|
|
55854
56486
|
})().catch(() => {});
|
|
55855
|
-
return () =>
|
|
56487
|
+
return () => {
|
|
56488
|
+
controller.abort();
|
|
56489
|
+
for (const child of waiting.values()) child.kill();
|
|
56490
|
+
};
|
|
55856
56491
|
}
|
|
55857
56492
|
|
|
55858
56493
|
export default { id: "agenthub", server: AgentHub, setup };
|
|
@@ -55860,92 +56495,248 @@ export default { id: "agenthub", server: AgentHub, setup };
|
|
|
55860
56495
|
}
|
|
55861
56496
|
|
|
55862
56497
|
// src/cli/hooks.ts
|
|
55863
|
-
var HARNESSES = ["claude-code", "codex", "copilot", "opencode"];
|
|
56498
|
+
var HARNESSES = ["claude-code", "codex", "copilot", "opencode", "droid", "gemini", "qwen", "cursor", "antigravity", "kimi", "goose", "amp", "crush"];
|
|
56499
|
+
var USER_SCOPE_ONLY = /* @__PURE__ */ new Set(["kimi"]);
|
|
55864
56500
|
var EVENTS = ["SessionStart", "UserPromptSubmit", "PostToolUse", "Stop", "SessionEnd"];
|
|
55865
|
-
var
|
|
55866
|
-
["
|
|
55867
|
-
["
|
|
55868
|
-
["
|
|
55869
|
-
["
|
|
55870
|
-
["
|
|
56501
|
+
var CLAUDE_PAIRS = [
|
|
56502
|
+
["SessionStart", "session-start"],
|
|
56503
|
+
["UserPromptSubmit", "prompt"],
|
|
56504
|
+
["PostToolUse", "post-tool"],
|
|
56505
|
+
["Stop", "stop"],
|
|
56506
|
+
["SessionEnd", "session-end"]
|
|
55871
56507
|
];
|
|
56508
|
+
var HARNESS_EVENTS = {
|
|
56509
|
+
copilot: [
|
|
56510
|
+
["sessionStart", "session-start"],
|
|
56511
|
+
["userPromptSubmitted", "prompt"],
|
|
56512
|
+
["postToolUse", "post-tool"],
|
|
56513
|
+
["agentStop", "stop"],
|
|
56514
|
+
["sessionEnd", "session-end"]
|
|
56515
|
+
],
|
|
56516
|
+
droid: CLAUDE_PAIRS,
|
|
56517
|
+
qwen: CLAUDE_PAIRS,
|
|
56518
|
+
gemini: [
|
|
56519
|
+
["SessionStart", "session-start"],
|
|
56520
|
+
["BeforeAgent", "prompt"],
|
|
56521
|
+
["AfterTool", "post-tool"],
|
|
56522
|
+
["AfterAgent", "stop"],
|
|
56523
|
+
["SessionEnd", "session-end"]
|
|
56524
|
+
],
|
|
56525
|
+
// beforeSubmitPrompt can't add context, so prompts are left out.
|
|
56526
|
+
cursor: [
|
|
56527
|
+
["sessionStart", "session-start"],
|
|
56528
|
+
["postToolUse", "post-tool"],
|
|
56529
|
+
["stop", "stop"],
|
|
56530
|
+
["sessionEnd", "session-end"]
|
|
56531
|
+
],
|
|
56532
|
+
// PostToolUse can't add context; PreInvocation runs before each model call, so it stands in
|
|
56533
|
+
// for both prompts and tool calls. There's no session end.
|
|
56534
|
+
antigravity: [
|
|
56535
|
+
["SessionStart", "session-start"],
|
|
56536
|
+
["PreInvocation", "post-tool"],
|
|
56537
|
+
["Stop", "stop"]
|
|
56538
|
+
],
|
|
56539
|
+
// SessionStart and PostToolUse only observe; the first prompt gets the session-start notice.
|
|
56540
|
+
kimi: [
|
|
56541
|
+
["UserPromptSubmit", "prompt"],
|
|
56542
|
+
["Stop", "stop"],
|
|
56543
|
+
["SessionEnd", "session-end"]
|
|
56544
|
+
],
|
|
56545
|
+
// Crush has only PreToolUse; its first one in a session gets the session-start notice.
|
|
56546
|
+
crush: [["PreToolUse", "post-tool"]],
|
|
56547
|
+
// Goose hooks can't add context: Stop keeps a turn going for a request, and presence.
|
|
56548
|
+
goose: [
|
|
56549
|
+
["Stop", "stop"],
|
|
56550
|
+
["SessionEnd", "session-end"]
|
|
56551
|
+
]
|
|
56552
|
+
};
|
|
56553
|
+
var WAIT_MAX_S = 12 * 3600;
|
|
56554
|
+
var WAIT_TIMEOUT_S = WAIT_MAX_S + 60;
|
|
55872
56555
|
var TIMEOUT_S = { "claude-code": {}, codex: { SessionEnd: 3 } };
|
|
56556
|
+
var KIMI_BEGIN = "# >>> agenthub hooks: agenthub hooks uninstall kimi removes this block >>>";
|
|
56557
|
+
var KIMI_END = "# <<< agenthub hooks <<<";
|
|
56558
|
+
var GOOSE_PLUGIN = { name: "agenthub", version: "1.0.0", description: "Agent Hub: handle hub requests that arrive during a turn, and presence" };
|
|
55873
56559
|
function hookFile(harness, scope, cwd, home = (0, import_node_os2.homedir)(), env = process.env) {
|
|
55874
56560
|
const user = scope === "user";
|
|
56561
|
+
const base = user ? home : cwd;
|
|
55875
56562
|
switch (harness) {
|
|
55876
56563
|
case "claude-code":
|
|
55877
56564
|
return user ? (0, import_node_path3.join)(home, ".claude", "settings.json") : (0, import_node_path3.join)(cwd, ".claude", "settings.local.json");
|
|
55878
56565
|
case "codex":
|
|
55879
|
-
return
|
|
56566
|
+
return (0, import_node_path3.join)(base, ".codex", "hooks.json");
|
|
55880
56567
|
case "copilot":
|
|
55881
56568
|
return user ? (0, import_node_path3.join)(env.COPILOT_HOME || (0, import_node_path3.join)(home, ".copilot"), "hooks", "agenthub.json") : (0, import_node_path3.join)(cwd, ".github", "hooks", "agenthub.json");
|
|
55882
56569
|
case "opencode":
|
|
55883
56570
|
return user ? (0, import_node_path3.join)(env.XDG_CONFIG_HOME || (0, import_node_path3.join)(home, ".config"), "opencode", "plugins", "agenthub.js") : (0, import_node_path3.join)(cwd, ".opencode", "plugins", "agenthub.js");
|
|
55884
|
-
|
|
55885
|
-
|
|
55886
|
-
|
|
55887
|
-
|
|
56571
|
+
case "droid":
|
|
56572
|
+
return (0, import_node_path3.join)(base, ".factory", "hooks.json");
|
|
56573
|
+
case "gemini":
|
|
56574
|
+
return (0, import_node_path3.join)(base, ".gemini", "settings.json");
|
|
56575
|
+
case "qwen":
|
|
56576
|
+
return (0, import_node_path3.join)(base, ".qwen", "settings.json");
|
|
56577
|
+
case "cursor":
|
|
56578
|
+
return (0, import_node_path3.join)(base, ".cursor", "hooks.json");
|
|
56579
|
+
case "antigravity":
|
|
56580
|
+
return user ? (0, import_node_path3.join)(home, ".gemini", "config", "hooks.json") : (0, import_node_path3.join)(cwd, ".agents", "hooks.json");
|
|
56581
|
+
case "kimi":
|
|
56582
|
+
return (0, import_node_path3.join)(home, ".kimi-code", "config.toml");
|
|
56583
|
+
case "goose":
|
|
56584
|
+
return (0, import_node_path3.join)(base, ".agents", "plugins", "agenthub", "hooks", "hooks.json");
|
|
56585
|
+
case "crush":
|
|
56586
|
+
return user ? (0, import_node_path3.join)(env.XDG_CONFIG_HOME || (0, import_node_path3.join)(home, ".config"), "crush", "crush.json") : (0, import_node_fs2.existsSync)((0, import_node_path3.join)(cwd, "crush.json")) ? (0, import_node_path3.join)(cwd, "crush.json") : (0, import_node_path3.join)(cwd, ".crush.json");
|
|
56587
|
+
case "amp":
|
|
56588
|
+
return user ? (0, import_node_path3.join)(env.XDG_CONFIG_HOME || (0, import_node_path3.join)(home, ".config"), "amp", "plugins", "agenthub.ts") : (0, import_node_path3.join)(cwd, ".amp", "plugins", "agenthub.ts");
|
|
56589
|
+
}
|
|
56590
|
+
}
|
|
56591
|
+
var OURS = [/^(?:\S*[\\/])?agenthub(?:\.js|\.exe)?\s+(?:notify|wait)\b/i, /^"(?:[^"]*[\\/])?agenthub(?:\.js|\.exe)?"\s+(?:notify|wait)\b/i];
|
|
56592
|
+
var isOurs = (h) => typeof h?.command === "string" && OURS.some((r) => r.test(h.command));
|
|
55888
56593
|
function installHooks(harness, file2, command = "agenthub") {
|
|
55889
56594
|
(0, import_node_fs2.mkdirSync)((0, import_node_path3.dirname)(file2), { recursive: true });
|
|
55890
|
-
|
|
55891
|
-
|
|
55892
|
-
|
|
55893
|
-
|
|
55894
|
-
|
|
55895
|
-
|
|
55896
|
-
|
|
55897
|
-
|
|
55898
|
-
|
|
55899
|
-
|
|
55900
|
-
|
|
55901
|
-
|
|
55902
|
-
|
|
55903
|
-
|
|
55904
|
-
|
|
55905
|
-
|
|
55906
|
-
|
|
55907
|
-
|
|
55908
|
-
|
|
55909
|
-
|
|
55910
|
-
|
|
55911
|
-
|
|
55912
|
-
|
|
55913
|
-
|
|
55914
|
-
|
|
56595
|
+
const notify2 = (event) => `${command} notify --format ${harness} --event ${event}`;
|
|
56596
|
+
const pairs = HARNESS_EVENTS[harness] ?? [];
|
|
56597
|
+
switch (harness) {
|
|
56598
|
+
case "opencode":
|
|
56599
|
+
(0, import_node_fs2.writeFileSync)(file2, opencodePlugin(command));
|
|
56600
|
+
return ["chat.message", "tool.execute.after", "session.idle", "session.deleted"];
|
|
56601
|
+
case "amp":
|
|
56602
|
+
(0, import_node_fs2.writeFileSync)(file2, ampPlugin(command));
|
|
56603
|
+
return ["agent.start", "tool.result", "agent.end"];
|
|
56604
|
+
case "copilot":
|
|
56605
|
+
case "cursor":
|
|
56606
|
+
case "crush": {
|
|
56607
|
+
const config2 = readJson(file2);
|
|
56608
|
+
if (harness !== "crush") config2.version = 1;
|
|
56609
|
+
const hooks = config2.hooks ??= {};
|
|
56610
|
+
for (const [event, notifyEvent] of pairs) {
|
|
56611
|
+
const kept = (hooks[event] ?? []).filter((h) => !isOurs(h));
|
|
56612
|
+
const entry = harness === "copilot" ? { type: "command", command: notify2(notifyEvent), timeoutSec: 10 } : { command: notify2(notifyEvent), timeout: 10 };
|
|
56613
|
+
hooks[event] = [...kept, entry];
|
|
56614
|
+
}
|
|
56615
|
+
writeJson(file2, config2);
|
|
56616
|
+
return pairs.map(([e]) => e);
|
|
56617
|
+
}
|
|
56618
|
+
case "antigravity": {
|
|
56619
|
+
const config2 = readJson(file2);
|
|
56620
|
+
config2.agenthub = Object.fromEntries(pairs.map(([event, n]) => [event, [{ hooks: [{ type: "command", command: notify2(n), timeout: 10 }] }]]));
|
|
56621
|
+
writeJson(file2, config2);
|
|
56622
|
+
return pairs.map(([e]) => e);
|
|
56623
|
+
}
|
|
56624
|
+
case "kimi": {
|
|
56625
|
+
const before = (0, import_node_fs2.existsSync)(file2) ? stripKimi((0, import_node_fs2.readFileSync)(file2, "utf8")) : "";
|
|
56626
|
+
const block = [KIMI_BEGIN, ...pairs.flatMap(([event, n]) => ["[[hooks]]", `event = "${event}"`, `command = ${JSON.stringify(notify2(n))}`, "timeout = 10", ""]), KIMI_END, ""].join("\n");
|
|
56627
|
+
(0, import_node_fs2.writeFileSync)(file2, `${before}${before && !before.endsWith("\n") ? "\n" : ""}${before ? "\n" : ""}${block}`);
|
|
56628
|
+
return pairs.map(([e]) => e);
|
|
56629
|
+
}
|
|
56630
|
+
case "goose": {
|
|
56631
|
+
const root = (0, import_node_path3.dirname)((0, import_node_path3.dirname)(file2));
|
|
56632
|
+
writeJson((0, import_node_path3.join)(root, "plugin.json"), GOOSE_PLUGIN);
|
|
56633
|
+
writeJson(file2, { hooks: Object.fromEntries(pairs.map(([event, n]) => [event, [{ hooks: [{ type: "command", command: notify2(n), timeout: 10 }] }]])) });
|
|
56634
|
+
return pairs.map(([e]) => e);
|
|
56635
|
+
}
|
|
56636
|
+
case "droid":
|
|
56637
|
+
case "gemini":
|
|
56638
|
+
case "qwen": {
|
|
56639
|
+
const config2 = readJson(file2);
|
|
56640
|
+
const hooks = harness === "droid" ? config2 : config2.hooks ??= {};
|
|
56641
|
+
const timeout = harness === "droid" ? 10 : 1e4;
|
|
56642
|
+
for (const [event, n] of pairs) {
|
|
56643
|
+
hooks[event] = [...keepOthers(hooks[event]), { hooks: [{ type: "command", command: notify2(n), timeout }] }];
|
|
56644
|
+
}
|
|
56645
|
+
writeJson(file2, config2);
|
|
56646
|
+
return pairs.map(([e]) => e);
|
|
56647
|
+
}
|
|
56648
|
+
case "claude-code":
|
|
56649
|
+
case "codex": {
|
|
56650
|
+
const config2 = readJson(file2);
|
|
56651
|
+
const hooks = config2.hooks ??= {};
|
|
56652
|
+
for (const event of EVENTS) {
|
|
56653
|
+
const entry = { hooks: [{ type: "command", command: `${command} notify --format ${harness}`, timeout: TIMEOUT_S[harness][event] ?? 10 }] };
|
|
56654
|
+
if (event === "PostToolUse" && harness === "claude-code") entry.matcher = "*";
|
|
56655
|
+
if (event === "Stop" && harness === "claude-code") {
|
|
56656
|
+
entry.hooks.push({ type: "command", command: `${command} wait --max ${WAIT_MAX_S}`, asyncRewake: true, timeout: WAIT_TIMEOUT_S });
|
|
56657
|
+
}
|
|
56658
|
+
hooks[event] = [...keepOthers(hooks[event]), entry];
|
|
56659
|
+
}
|
|
56660
|
+
writeJson(file2, config2);
|
|
56661
|
+
return EVENTS;
|
|
56662
|
+
}
|
|
56663
|
+
}
|
|
55915
56664
|
}
|
|
55916
56665
|
function uninstallHooks(harness, file2) {
|
|
55917
56666
|
if (!(0, import_node_fs2.existsSync)(file2)) return false;
|
|
55918
|
-
|
|
55919
|
-
|
|
55920
|
-
|
|
55921
|
-
|
|
55922
|
-
|
|
55923
|
-
|
|
55924
|
-
|
|
55925
|
-
|
|
55926
|
-
|
|
55927
|
-
|
|
55928
|
-
const
|
|
55929
|
-
|
|
55930
|
-
if (kept
|
|
55931
|
-
|
|
55932
|
-
|
|
55933
|
-
|
|
55934
|
-
|
|
55935
|
-
|
|
55936
|
-
|
|
55937
|
-
|
|
55938
|
-
|
|
55939
|
-
|
|
55940
|
-
|
|
55941
|
-
|
|
55942
|
-
|
|
55943
|
-
|
|
55944
|
-
|
|
55945
|
-
|
|
56667
|
+
switch (harness) {
|
|
56668
|
+
case "opencode":
|
|
56669
|
+
if (!(0, import_node_fs2.readFileSync)(file2, "utf8").startsWith(OPENCODE_MARKER)) return false;
|
|
56670
|
+
(0, import_node_fs2.rmSync)(file2);
|
|
56671
|
+
return true;
|
|
56672
|
+
case "amp":
|
|
56673
|
+
if (!(0, import_node_fs2.readFileSync)(file2, "utf8").startsWith(AMP_MARKER)) return false;
|
|
56674
|
+
(0, import_node_fs2.rmSync)(file2);
|
|
56675
|
+
return true;
|
|
56676
|
+
case "kimi": {
|
|
56677
|
+
const text = (0, import_node_fs2.readFileSync)(file2, "utf8");
|
|
56678
|
+
const kept = stripKimi(text);
|
|
56679
|
+
if (kept === text) return false;
|
|
56680
|
+
(0, import_node_fs2.writeFileSync)(file2, kept);
|
|
56681
|
+
return true;
|
|
56682
|
+
}
|
|
56683
|
+
case "goose": {
|
|
56684
|
+
const root = (0, import_node_path3.dirname)((0, import_node_path3.dirname)(file2));
|
|
56685
|
+
if (readJson((0, import_node_path3.join)(root, "plugin.json")).name !== GOOSE_PLUGIN.name) return false;
|
|
56686
|
+
(0, import_node_fs2.rmSync)(root, { recursive: true, force: true });
|
|
56687
|
+
return true;
|
|
56688
|
+
}
|
|
56689
|
+
case "antigravity": {
|
|
56690
|
+
const config2 = readJson(file2);
|
|
56691
|
+
if (!("agenthub" in config2)) return false;
|
|
56692
|
+
delete config2.agenthub;
|
|
56693
|
+
writeJson(file2, config2);
|
|
56694
|
+
return true;
|
|
56695
|
+
}
|
|
56696
|
+
case "copilot":
|
|
56697
|
+
case "cursor":
|
|
56698
|
+
case "crush": {
|
|
56699
|
+
const config2 = readJson(file2);
|
|
56700
|
+
if (!config2.hooks) return false;
|
|
56701
|
+
let changed = false;
|
|
56702
|
+
for (const [event, entries] of Object.entries(config2.hooks)) {
|
|
56703
|
+
const kept = entries.filter((h) => !isOurs(h));
|
|
56704
|
+
if (kept.length !== entries.length) changed = true;
|
|
56705
|
+
if (kept.length) config2.hooks[event] = kept;
|
|
56706
|
+
else delete config2.hooks[event];
|
|
56707
|
+
}
|
|
56708
|
+
if (!changed) return false;
|
|
56709
|
+
const empty = !Object.keys(config2.hooks).length && Object.keys(config2).every((k) => k === "version" || k === "hooks");
|
|
56710
|
+
if (harness === "copilot" && empty) (0, import_node_fs2.rmSync)(file2);
|
|
56711
|
+
else writeJson(file2, config2);
|
|
56712
|
+
return true;
|
|
56713
|
+
}
|
|
56714
|
+
default: {
|
|
56715
|
+
const config2 = readJson(file2);
|
|
56716
|
+
const hooks = harness === "droid" ? config2 : config2.hooks;
|
|
56717
|
+
if (!hooks || typeof hooks !== "object") return false;
|
|
56718
|
+
let changed = false;
|
|
56719
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
56720
|
+
if (!Array.isArray(groups)) continue;
|
|
56721
|
+
const kept = keepOthers(groups);
|
|
56722
|
+
if (kept.length !== groups.length || kept.some((g, i) => g.hooks.length !== groups[i].hooks.length)) changed = true;
|
|
56723
|
+
if (kept.length) hooks[event] = kept;
|
|
56724
|
+
else delete hooks[event];
|
|
56725
|
+
}
|
|
56726
|
+
if (changed) writeJson(file2, config2);
|
|
56727
|
+
return changed;
|
|
56728
|
+
}
|
|
55946
56729
|
}
|
|
55947
|
-
|
|
55948
|
-
|
|
56730
|
+
}
|
|
56731
|
+
function keepOthers(groups) {
|
|
56732
|
+
return (groups ?? []).map((g) => ({ ...g, hooks: (g.hooks ?? []).filter((h) => !isOurs(h)) })).filter((g) => g.hooks.length);
|
|
56733
|
+
}
|
|
56734
|
+
function stripKimi(text) {
|
|
56735
|
+
const start = text.indexOf(KIMI_BEGIN);
|
|
56736
|
+
if (start < 0) return text;
|
|
56737
|
+
const end = text.indexOf(KIMI_END, start);
|
|
56738
|
+
const after = end < 0 ? "" : text.slice(end + KIMI_END.length).replace(/^\n/, "");
|
|
56739
|
+
return (text.slice(0, start).replace(/\n+$/, "\n") + after).replace(/^\n+$/, "");
|
|
55949
56740
|
}
|
|
55950
56741
|
function readJson(file2) {
|
|
55951
56742
|
return (0, import_node_fs2.existsSync)(file2) ? JSON.parse((0, import_node_fs2.readFileSync)(file2, "utf8")) : {};
|
|
@@ -56007,34 +56798,197 @@ function openBrowser(url2) {
|
|
|
56007
56798
|
|
|
56008
56799
|
// src/cli/index.ts
|
|
56009
56800
|
init_notify();
|
|
56801
|
+
|
|
56802
|
+
// src/cli/help.ts
|
|
56803
|
+
var HARNESS_LIST = "claude-code, codex, copilot, opencode, amp, droid, gemini, qwen, cursor, antigravity, kimi, goose or crush";
|
|
56804
|
+
var COMMANDS = {
|
|
56805
|
+
login: {
|
|
56806
|
+
short: "sign in to a hub",
|
|
56807
|
+
usage: ["agenthub login [--hub <url>]"],
|
|
56808
|
+
summary: "Sign in to a hub with the device flow.",
|
|
56809
|
+
details: [
|
|
56810
|
+
"Opens a code to enter in the browser, then keeps the token in the OS keychain (macOS Keychain or Secret Service), or in a 0600 file in ~/.config/agenthub when there's none.",
|
|
56811
|
+
"The hub is remembered, so later commands and the hooks don't need --hub. Hooks refresh the token themselves."
|
|
56812
|
+
],
|
|
56813
|
+
examples: [
|
|
56814
|
+
["agenthub login --hub https://hub.theagenthub.app", "sign in to the public hub"],
|
|
56815
|
+
["agenthub login --hub https://hub.example.com", "sign in to a self-hosted hub"],
|
|
56816
|
+
["agenthub login", "sign in again to the hub you used last"]
|
|
56817
|
+
]
|
|
56818
|
+
},
|
|
56819
|
+
logout: {
|
|
56820
|
+
short: "forget this machine's token",
|
|
56821
|
+
usage: ["agenthub logout"],
|
|
56822
|
+
summary: "Forget this machine's token for the current hub.",
|
|
56823
|
+
examples: [["agenthub logout", ""]]
|
|
56824
|
+
},
|
|
56825
|
+
status: {
|
|
56826
|
+
short: "show the hub, sign-in and version",
|
|
56827
|
+
usage: ["agenthub status"],
|
|
56828
|
+
summary: "Show the hub, whether you're signed in, and the CLI version (and whether a newer one is out).",
|
|
56829
|
+
examples: [["agenthub status", ""]]
|
|
56830
|
+
},
|
|
56831
|
+
token: {
|
|
56832
|
+
short: "print a fresh hub access token",
|
|
56833
|
+
usage: ["agenthub token [--json]"],
|
|
56834
|
+
summary: "Print a fresh hub access token, for plugins that call the hub's MCP server (the pi extension uses it).",
|
|
56835
|
+
examples: [
|
|
56836
|
+
["agenthub token", "just the token"],
|
|
56837
|
+
["agenthub token --json", '{"hub", "token", "expires_at"}']
|
|
56838
|
+
]
|
|
56839
|
+
},
|
|
56840
|
+
update: {
|
|
56841
|
+
short: "update this CLI",
|
|
56842
|
+
usage: ["agenthub update [--check] [--version <v>]"],
|
|
56843
|
+
summary: "Replace this CLI with the latest release, after checking it against SHA256SUMS and that it runs.",
|
|
56844
|
+
details: ["A CLI installed with npm can't replace itself; update it the way you installed it."],
|
|
56845
|
+
examples: [
|
|
56846
|
+
["agenthub update", "install the latest release"],
|
|
56847
|
+
["agenthub update --check", "only say whether there's a newer one"],
|
|
56848
|
+
["agenthub update --version v0.3.0", "install a specific release"]
|
|
56849
|
+
]
|
|
56850
|
+
},
|
|
56851
|
+
hooks: {
|
|
56852
|
+
short: "add or remove the hooks for a harness",
|
|
56853
|
+
usage: [`agenthub hooks install <harness> [--scope user|project]`, `agenthub hooks uninstall <harness> [--scope user|project]`],
|
|
56854
|
+
summary: `Add the hooks that bring hub messages into a harness's sessions, for ${HARNESS_LIST}. Not needed with a harness plugin.`,
|
|
56855
|
+
details: [
|
|
56856
|
+
"--scope user (the default) writes your own settings; --scope project writes the repo's local settings.",
|
|
56857
|
+
"Claude Code: ~/.claude/settings.json (or .claude/settings.local.json), including the Stop hook that wakes an idle session.",
|
|
56858
|
+
"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/.",
|
|
56859
|
+
"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).",
|
|
56860
|
+
"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.",
|
|
56861
|
+
"Re-running install updates the hooks; uninstall removes only ours."
|
|
56862
|
+
],
|
|
56863
|
+
examples: [
|
|
56864
|
+
["agenthub hooks install claude-code", "for all your Claude Code sessions"],
|
|
56865
|
+
["agenthub hooks install codex", "then open /hooks in Codex to trust them"],
|
|
56866
|
+
["agenthub hooks install opencode --scope project", "only in this repo"],
|
|
56867
|
+
["agenthub hooks install amp", "an Amp plugin that also wakes idle threads"],
|
|
56868
|
+
["agenthub hooks install gemini", "Gemini CLI; also droid, qwen, cursor, antigravity, kimi, goose or crush"],
|
|
56869
|
+
["agenthub hooks uninstall claude-code", ""]
|
|
56870
|
+
]
|
|
56871
|
+
},
|
|
56872
|
+
watch: {
|
|
56873
|
+
short: "print new messages as they arrive",
|
|
56874
|
+
usage: ["agenthub watch [--json] [--agent <name>]"],
|
|
56875
|
+
summary: "Print this repo's agent's new hub messages as they arrive. For a terminal next to an agent that can't be woken (Codex, Copilot CLI), or a script.",
|
|
56876
|
+
details: ["Waits for the agent to join, reconnects by itself, and stops when the agent is replaced or loses access."],
|
|
56877
|
+
examples: [
|
|
56878
|
+
["agenthub watch", "your most recently active agent in this repo"],
|
|
56879
|
+
["agenthub watch --agent reviewer", "a named agent"],
|
|
56880
|
+
["agenthub watch --json | jq .messages", "JSON lines, for a script"]
|
|
56881
|
+
]
|
|
56882
|
+
},
|
|
56883
|
+
wake: {
|
|
56884
|
+
short: "turn waking idle agents on or off, and choose whether they ask you first",
|
|
56885
|
+
usage: ["agenthub wake [on|off|all|ask|auto] [--project]", "agenthub wake default --project"],
|
|
56886
|
+
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.",
|
|
56887
|
+
details: [
|
|
56888
|
+
"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.",
|
|
56889
|
+
"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.",
|
|
56890
|
+
"on|off|all and ask|auto are separate settings: set one of each.",
|
|
56891
|
+
"--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.",
|
|
56892
|
+
"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."
|
|
56893
|
+
],
|
|
56894
|
+
examples: [
|
|
56895
|
+
["agenthub wake", "what applies in this repo"],
|
|
56896
|
+
["agenthub wake off", "never wake idle agents"],
|
|
56897
|
+
["agenthub wake ask", "wake them, but have them ask you before acting"],
|
|
56898
|
+
["agenthub wake auto", "back to the default: act without asking, within the rules"],
|
|
56899
|
+
["agenthub wake off --project", "not in this repo, still elsewhere"],
|
|
56900
|
+
["agenthub wake ask --project", "ask first in this repo only"],
|
|
56901
|
+
["agenthub wake default --project", "this repo follows your settings again"]
|
|
56902
|
+
]
|
|
56903
|
+
},
|
|
56904
|
+
notify: {
|
|
56905
|
+
short: "print new messages for a hook",
|
|
56906
|
+
usage: ["agenthub notify --format <harness|text> [--event <session-start|prompt|post-tool|stop|session-end>]"],
|
|
56907
|
+
summary: "What the harness hooks run: reads the hook's JSON on stdin and prints new hub messages for the session, in the harness's format. You don't run it yourself.",
|
|
56908
|
+
details: [
|
|
56909
|
+
"--format is the harness: claude-code, codex, copilot, amp, droid, gemini, qwen, cursor, antigravity, kimi, goose, crush or roo; or text for plain text (the OpenCode and pi plugins use it).",
|
|
56910
|
+
"It never fails the harness: off the hub, signed out or on any error it prints nothing. Stop marks the agent idle and session-end takes it offline. Only Kimi Code's Stop hook exits 2, to keep a turn going."
|
|
56911
|
+
],
|
|
56912
|
+
examples: [[`echo '{"hook_event_name":"UserPromptSubmit","session_id":"abc"}' | agenthub notify --format text`, "see what a hook would print"]]
|
|
56913
|
+
},
|
|
56914
|
+
wait: {
|
|
56915
|
+
short: "wait for a message that should wake an idle agent",
|
|
56916
|
+
usage: ["agenthub wait [--max <seconds>] [--deliver kimi]"],
|
|
56917
|
+
summary: "What harnesses run when their agent goes idle: waits for a hub message that should wake it, then exits 2 and prints the summaries (else 0). You don't run it yourself.",
|
|
56918
|
+
details: [
|
|
56919
|
+
"The next prompt, a newer waiter for the same session, or --max (12 hours by default) stops it. `agenthub wake` decides whether it waits at all, and whether its text tells the agent to ask you first.",
|
|
56920
|
+
"--deliver kimi is what Kimi Code's Stop hook starts in the background: instead of printing, it sends the summaries to the session through the local kimi web server."
|
|
56921
|
+
],
|
|
56922
|
+
examples: [[`echo '{"session_id":"abc"}' | agenthub wait --max 60; echo $?`, "wait up to a minute; 2 means it would wake the agent"]]
|
|
56923
|
+
},
|
|
56924
|
+
admin: {
|
|
56925
|
+
short: "check a hub's code host config",
|
|
56926
|
+
usage: ["agenthub admin providers check [--config <file>]", "agenthub admin providers resolve <remote> [--config <file>]"],
|
|
56927
|
+
summary: "For hub operators: test the code hosts in a providers.yaml, or show the project ID a git remote maps to.",
|
|
56928
|
+
details: ["--config defaults to $AGENTHUB_CONFIG, else the built-in public hosts."],
|
|
56929
|
+
examples: [
|
|
56930
|
+
["agenthub admin providers check --config providers.yaml", ""],
|
|
56931
|
+
["agenthub admin providers resolve git@github.com:acme/shop.git", ""]
|
|
56932
|
+
]
|
|
56933
|
+
}
|
|
56934
|
+
};
|
|
56935
|
+
var GROUPS = [
|
|
56936
|
+
["Set up", ["login", "hooks", "status", "logout"]],
|
|
56937
|
+
["Day to day", ["watch", "wake", "update"]],
|
|
56938
|
+
["Used by hooks and plugins", ["notify", "wait", "token"]],
|
|
56939
|
+
["Hub operators", ["admin"]]
|
|
56940
|
+
];
|
|
56941
|
+
var ENVIRONMENT = `Environment:
|
|
56942
|
+
AGENTHUB_URL the hub (else the one from agenthub login)
|
|
56943
|
+
AGENTHUB_AGENT_NAME a fixed agent name for hooks (default: one per harness session)
|
|
56944
|
+
AGENTHUB_WAKE=0|on|all whether idle agents are woken; overrides agenthub wake
|
|
56945
|
+
AGENTHUB_WAKE_ACT=ask|auto whether a woken agent asks you first; overrides agenthub wake ask|auto
|
|
56946
|
+
AGENTHUB_CREDENTIAL_STORE=file|keychain|auto
|
|
56947
|
+
AGENTHUB_HOME config directory (default ~/.config/agenthub)
|
|
56948
|
+
AGENTHUB_DOWNLOAD_URL where releases come from
|
|
56949
|
+
AGENTHUB_NO_UPDATE_CHECK=1 no daily "newer version" notice`;
|
|
56950
|
+
function overview() {
|
|
56951
|
+
const width = Math.max(...Object.keys(COMMANDS).map((c) => c.length)) + 2;
|
|
56952
|
+
const lines = ["agenthub: connects your coding agents to Agent Hub, so they can trade reviews, questions and handoffs with teammates' agents.", ""];
|
|
56953
|
+
for (const [title, names] of GROUPS) {
|
|
56954
|
+
lines.push(`${title}:`);
|
|
56955
|
+
for (const n of names) lines.push(` ${n.padEnd(width)}${COMMANDS[n].short}`);
|
|
56956
|
+
lines.push("");
|
|
56957
|
+
}
|
|
56958
|
+
lines.push(
|
|
56959
|
+
"Get started:",
|
|
56960
|
+
" agenthub login --hub https://hub.theagenthub.app",
|
|
56961
|
+
" agenthub hooks install claude-code (or use the Claude Code, Codex, OpenCode or pi plugin instead)",
|
|
56962
|
+
" agenthub status",
|
|
56963
|
+
"",
|
|
56964
|
+
ENVIRONMENT,
|
|
56965
|
+
"",
|
|
56966
|
+
"agenthub help <command> (or agenthub <command> --help) shows its options and examples. agenthub --version prints the version."
|
|
56967
|
+
);
|
|
56968
|
+
return lines.join("\n");
|
|
56969
|
+
}
|
|
56970
|
+
function commandHelp(name) {
|
|
56971
|
+
const c = COMMANDS[name];
|
|
56972
|
+
if (!c) return void 0;
|
|
56973
|
+
const lines = ["usage:", ...c.usage.map((u) => ` ${u}`), "", c.summary];
|
|
56974
|
+
if (c.details?.length) lines.push("", ...c.details);
|
|
56975
|
+
lines.push("", "examples:");
|
|
56976
|
+
for (const [cmd, note] of c.examples) lines.push(` ${cmd}${note ? `
|
|
56977
|
+
${note}` : ""}`);
|
|
56978
|
+
return lines.join("\n");
|
|
56979
|
+
}
|
|
56980
|
+
|
|
56981
|
+
// src/cli/index.ts
|
|
56010
56982
|
init_settings();
|
|
56011
56983
|
var import_meta = {};
|
|
56012
56984
|
function version2() {
|
|
56013
|
-
if (true) return "0.3.
|
|
56985
|
+
if (true) return "0.3.2";
|
|
56014
56986
|
try {
|
|
56015
|
-
return JSON.parse((0,
|
|
56987
|
+
return JSON.parse((0, import_node_fs11.readFileSync)(new URL("../../package.json", import_meta.url), "utf8")).version;
|
|
56016
56988
|
} catch {
|
|
56017
56989
|
return "unknown";
|
|
56018
56990
|
}
|
|
56019
56991
|
}
|
|
56020
|
-
var USAGE = `usage:
|
|
56021
|
-
agenthub login [--hub <url>] sign in with the device flow; keeps the token in the OS keychain
|
|
56022
|
-
agenthub logout forget this machine's hub token
|
|
56023
|
-
agenthub status show the hub and whether you're signed in
|
|
56024
|
-
agenthub token [--json] print a fresh hub access token, for plugins that call the hub's MCP server
|
|
56025
|
-
agenthub --version print the CLI version
|
|
56026
|
-
agenthub update [--check] [--version <v>] replace this CLI with the latest release (or <v>); --check only reports
|
|
56027
|
-
agenthub notify --format <claude-code|codex|copilot|text> [--event <session-start|prompt|post-tool|stop|session-end>]
|
|
56028
|
-
print new hub messages for a harness hook (reads the hook's JSON on stdin)
|
|
56029
|
-
agenthub watch [--json] [--agent <name>] print this repo's agent's new hub messages as they arrive
|
|
56030
|
-
agenthub hooks install <claude-code|codex|copilot|opencode> [--scope user|project]
|
|
56031
|
-
agenthub hooks uninstall <claude-code|codex|copilot|opencode> [--scope user|project]
|
|
56032
|
-
agenthub admin providers check [--config <file>] test every provider instance
|
|
56033
|
-
agenthub admin providers resolve <remote> [--config <file>] show the project ID a remote maps to
|
|
56034
|
-
|
|
56035
|
-
Environment: AGENTHUB_URL (hub), AGENTHUB_AGENT_NAME (fixed agent name for hooks; default: one per harness session),
|
|
56036
|
-
AGENTHUB_CREDENTIAL_STORE=file|keychain|auto, AGENTHUB_HOME (config directory),
|
|
56037
|
-
AGENTHUB_DOWNLOAD_URL (where releases come from), AGENTHUB_NO_UPDATE_CHECK=1 (no daily "newer version" notice).`;
|
|
56038
56992
|
function flag(args, name) {
|
|
56039
56993
|
const i = args.indexOf(name);
|
|
56040
56994
|
if (i < 0) return void 0;
|
|
@@ -56072,6 +57026,24 @@ async function readStdin(timeoutMs) {
|
|
|
56072
57026
|
async function main(argv) {
|
|
56073
57027
|
const args = [...argv];
|
|
56074
57028
|
const cmd = args.shift();
|
|
57029
|
+
if (cmd === void 0 || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
57030
|
+
const topic = cmd === "help" ? args[0] : void 0;
|
|
57031
|
+
if (!topic) {
|
|
57032
|
+
console.log(overview());
|
|
57033
|
+
return 0;
|
|
57034
|
+
}
|
|
57035
|
+
const text = commandHelp(topic);
|
|
57036
|
+
if (!text) {
|
|
57037
|
+
console.error(`agenthub: no command "${topic}". Run agenthub help for the list.`);
|
|
57038
|
+
return 2;
|
|
57039
|
+
}
|
|
57040
|
+
console.log(text);
|
|
57041
|
+
return 0;
|
|
57042
|
+
}
|
|
57043
|
+
if (commandHelp(cmd) && (args.includes("--help") || args.includes("-h"))) {
|
|
57044
|
+
console.log(commandHelp(cmd));
|
|
57045
|
+
return 0;
|
|
57046
|
+
}
|
|
56075
57047
|
if (cmd === "--version" || cmd === "version") {
|
|
56076
57048
|
console.log(version2());
|
|
56077
57049
|
return 0;
|
|
@@ -56081,8 +57053,10 @@ async function main(argv) {
|
|
|
56081
57053
|
const format = flag(args, "--format") ?? "text";
|
|
56082
57054
|
const event = flag(args, "--event");
|
|
56083
57055
|
const payload = await readStdin(500);
|
|
56084
|
-
const out = await
|
|
56085
|
-
if (out) process.stdout.write(out + "\n");
|
|
57056
|
+
const out = await notifyHook({ format, event, payload }, { store: credentialStore(), version: version2() });
|
|
57057
|
+
if (out.stdout) process.stdout.write(out.stdout + "\n");
|
|
57058
|
+
if (out.stderr) process.stderr.write(out.stderr + "\n");
|
|
57059
|
+
return out.code ?? 0;
|
|
56086
57060
|
} catch {
|
|
56087
57061
|
}
|
|
56088
57062
|
return 0;
|
|
@@ -56096,6 +57070,90 @@ async function main(argv) {
|
|
|
56096
57070
|
process.once("SIGTERM", () => ac.abort());
|
|
56097
57071
|
return watch2({ json: json2, agentName, signal: ac.signal }, { store: credentialStore(), write: (l) => console.log(l) });
|
|
56098
57072
|
}
|
|
57073
|
+
if (cmd === "wake") {
|
|
57074
|
+
const { actSetting: actSetting2, wakeSetting: wakeSetting2 } = await Promise.resolve().then(() => (init_wait(), wait_exports));
|
|
57075
|
+
const { gitRemotes: gitRemotes2 } = await Promise.resolve().then(() => (init_notify(), notify_exports));
|
|
57076
|
+
const perProject = args.includes("--project");
|
|
57077
|
+
const value = args.filter((a) => a !== "--project").shift();
|
|
57078
|
+
const origin = gitRemotes2(process.cwd()).origin;
|
|
57079
|
+
const key = origin ? projectKey(origin) : void 0;
|
|
57080
|
+
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";
|
|
57081
|
+
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";
|
|
57082
|
+
const sources = (name) => ({ env: ` (from ${name})`, project: ` (this repo's setting, ${key})`, user: " (your setting for every repo)", default: " (the default)" });
|
|
57083
|
+
const noEnv = { ...process.env, AGENTHUB_WAKE: void 0, AGENTHUB_WAKE_ACT: void 0 };
|
|
57084
|
+
if (value === void 0) {
|
|
57085
|
+
const wake = wakeSetting2(process.env, origin);
|
|
57086
|
+
const act = actSetting2(process.env, origin);
|
|
57087
|
+
console.log(describe3(wake.mode) + sources("AGENTHUB_WAKE")[wake.from]);
|
|
57088
|
+
console.log(describeAct(act.mode) + sources("AGENTHUB_WAKE_ACT")[act.from]);
|
|
57089
|
+
return 0;
|
|
57090
|
+
}
|
|
57091
|
+
const isAct = value === "ask" || value === "auto";
|
|
57092
|
+
if (!["on", "off", "all", "ask", "auto", ...perProject ? ["default"] : []].includes(value)) {
|
|
57093
|
+
console.error(commandHelp("wake"));
|
|
57094
|
+
return 2;
|
|
57095
|
+
}
|
|
57096
|
+
const settings = readSettings();
|
|
57097
|
+
if (perProject) {
|
|
57098
|
+
if (!key) {
|
|
57099
|
+
console.error("Not in a git repo with an origin remote; run it in the repo, or leave out --project.");
|
|
57100
|
+
return 1;
|
|
57101
|
+
}
|
|
57102
|
+
const projects = { ...settings.wake_projects };
|
|
57103
|
+
const actProjects = { ...settings.wake_act_projects };
|
|
57104
|
+
if (value === "default") {
|
|
57105
|
+
delete projects[key];
|
|
57106
|
+
delete actProjects[key];
|
|
57107
|
+
} else if (isAct) actProjects[key] = value;
|
|
57108
|
+
else projects[key] = value;
|
|
57109
|
+
writeSettings({ ...settings, wake_projects: projects, wake_act_projects: actProjects });
|
|
57110
|
+
} else if (isAct) {
|
|
57111
|
+
writeSettings({ ...settings, wake_act: value });
|
|
57112
|
+
} else {
|
|
57113
|
+
writeSettings({ ...settings, wake: value });
|
|
57114
|
+
}
|
|
57115
|
+
const where = perProject ? `in ${key}` : "in every repo without its own setting";
|
|
57116
|
+
if (value === "default") {
|
|
57117
|
+
console.log(`${key} now follows your settings for every repo:`);
|
|
57118
|
+
console.log(` ${describe3(wakeSetting2(noEnv, origin).mode)}`);
|
|
57119
|
+
console.log(` ${describeAct(actSetting2(noEnv, origin).mode)}`);
|
|
57120
|
+
} 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.`);
|
|
57121
|
+
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.`);
|
|
57122
|
+
else if (value === "off") console.log(`Idle agents won't be woken for hub messages ${where}. They still see them on their next turn.`);
|
|
57123
|
+
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).`);
|
|
57124
|
+
const envName = isAct ? "AGENTHUB_WAKE_ACT" : "AGENTHUB_WAKE";
|
|
57125
|
+
if (value !== "default" && process.env[envName] !== void 0) console.log(`${envName}=${process.env[envName]} is set in this shell and wins over this setting.`);
|
|
57126
|
+
return 0;
|
|
57127
|
+
}
|
|
57128
|
+
if (cmd === "wait") {
|
|
57129
|
+
try {
|
|
57130
|
+
const max = Number(flag(args, "--max"));
|
|
57131
|
+
const deliver = flag(args, "--deliver");
|
|
57132
|
+
const fromEnv = process.env.AGENTHUB_WAIT_PAYLOAD;
|
|
57133
|
+
let payload;
|
|
57134
|
+
try {
|
|
57135
|
+
payload = fromEnv ? JSON.parse(fromEnv) : await readStdin(500);
|
|
57136
|
+
} catch {
|
|
57137
|
+
payload = void 0;
|
|
57138
|
+
}
|
|
57139
|
+
const { wait: wait2 } = await Promise.resolve().then(() => (init_wait(), wait_exports));
|
|
57140
|
+
const ac = new AbortController();
|
|
57141
|
+
process.once("SIGINT", () => ac.abort());
|
|
57142
|
+
process.once("SIGTERM", () => ac.abort());
|
|
57143
|
+
const result = await wait2({ payload, signal: ac.signal, ...max > 0 ? { maxMs: max * 1e3 } : {} }, { store: credentialStore() });
|
|
57144
|
+
if (deliver === "kimi") {
|
|
57145
|
+
const { deliverToKimi: deliverToKimi2 } = await Promise.resolve().then(() => (init_kimi(), kimi_exports));
|
|
57146
|
+
if (result.code === 2 && result.text && typeof payload?.session_id === "string") await deliverToKimi2(payload.session_id, result.text);
|
|
57147
|
+
return 0;
|
|
57148
|
+
}
|
|
57149
|
+
if (result.code === 2 && result.text) {
|
|
57150
|
+
process.stdout.write(result.text + "\n");
|
|
57151
|
+
return 2;
|
|
57152
|
+
}
|
|
57153
|
+
} catch {
|
|
57154
|
+
}
|
|
57155
|
+
return 0;
|
|
57156
|
+
}
|
|
56099
57157
|
if (cmd === "update") {
|
|
56100
57158
|
const { selfUpdate: selfUpdate2 } = await Promise.resolve().then(() => (init_update(), update_exports));
|
|
56101
57159
|
const v = flag(args, "--version");
|
|
@@ -56161,17 +57219,26 @@ version: ${version2()}${newer}`);
|
|
|
56161
57219
|
const harness = args.shift();
|
|
56162
57220
|
const scope = flag(args, "--scope") ?? "user";
|
|
56163
57221
|
if (action !== "install" && action !== "uninstall" || !HARNESSES.includes(harness) || scope !== "user" && scope !== "project") {
|
|
56164
|
-
console.error(
|
|
57222
|
+
console.error(commandHelp("hooks"));
|
|
56165
57223
|
return 2;
|
|
56166
57224
|
}
|
|
56167
57225
|
const h = harness;
|
|
57226
|
+
if (scope === "project" && USER_SCOPE_ONLY.has(h)) {
|
|
57227
|
+
console.error(`${h} reads hooks only from your own config, so it has no --scope project.`);
|
|
57228
|
+
return 2;
|
|
57229
|
+
}
|
|
56168
57230
|
const file2 = hookFile(h, scope, process.cwd());
|
|
56169
57231
|
if (action === "install") {
|
|
56170
57232
|
const events = installHooks(h, file2);
|
|
56171
57233
|
console.log(`Added agenthub notify to ${events.join(", ")} in ${file2}`);
|
|
56172
57234
|
if (h === "codex") console.log("Codex skips new hooks until you trust them: open /hooks in Codex and review them.");
|
|
56173
57235
|
if (h === "opencode") console.log("OpenCode loads the plugin when it next starts.");
|
|
57236
|
+
if (h === "amp") console.log("Amp loads the plugin in new sessions, or now with plugins: reload from the command palette.");
|
|
56174
57237
|
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.");
|
|
57238
|
+
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.");
|
|
57239
|
+
if (h === "qwen" && scope === "project") console.log("Qwen Code runs a repo's hooks only in a trusted folder.");
|
|
57240
|
+
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.");
|
|
57241
|
+
if (h === "cursor" || h === "antigravity" || h === "kimi") console.log(`${h === "cursor" ? "Cursor" : h === "kimi" ? "Kimi Code" : "Antigravity"} picks up the hooks in new sessions.`);
|
|
56175
57242
|
} else {
|
|
56176
57243
|
console.log(uninstallHooks(h, file2) ? `Removed agenthub hooks from ${file2}` : `No agenthub hooks in ${file2}`);
|
|
56177
57244
|
}
|
|
@@ -56179,9 +57246,9 @@ version: ${version2()}${newer}`);
|
|
|
56179
57246
|
}
|
|
56180
57247
|
if (cmd === "admin") {
|
|
56181
57248
|
const { admin: admin2 } = await Promise.resolve().then(() => (init_admin(), admin_exports));
|
|
56182
|
-
return admin2(args,
|
|
57249
|
+
return admin2(args, commandHelp("admin"));
|
|
56183
57250
|
}
|
|
56184
|
-
console.error(
|
|
57251
|
+
console.error(`agenthub: no command "${cmd}". Run agenthub help for the list.`);
|
|
56185
57252
|
return 2;
|
|
56186
57253
|
}
|
|
56187
57254
|
main(process.argv.slice(2)).then(
|