@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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/bin/agenthub.cjs +1364 -297
  3. 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
- return render(opts.format, event, [text, notice].filter(Boolean).join("\n"));
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 = typeof opts.payload?.cwd === "string" ? opts.payload.cwd : deps.cwd ?? process.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 = (0, import_node_crypto2.createHash)("sha256").update(`${hub}|${origin}|${agentName ?? ""}`).digest("hex").slice(0, 24);
480
- const wait = THROTTLE_MS[event];
481
- if (wait && !throttle((0, import_node_path7.join)(cacheDir(env), `notify-${throttleKey}.json`), wait, now())) return "";
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, import_node_path7.join)(cacheDir(env), `waiting-${throttleKey}.json`);
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, import_node_fs6.rmSync)(waitingFile, { force: true });
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
- return out(`Agent Hub: this repo is on the hub. To work with teammates' agents, call the agenthub join_project tool with ${args}.${keep}`);
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
- return out(formatInbox(await res.json()));
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, import_node_crypto2.createHash)("sha256").update(session).digest("hex").slice(0, 8)}`;
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, import_node_fs6.readFileSync)(path);
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, import_node_fs6.mkdirSync)((0, import_node_path7.join)(path, ".."), { recursive: true, mode: 448 });
568
- (0, import_node_fs6.writeFileSync)(path, "{}");
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, import_node_fs6.readFileSync)(path, "utf8")).at;
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, import_node_fs6.mkdirSync)((0, import_node_path7.join)(path, ".."), { recursive: true, mode: 448 });
577
- (0, import_node_fs6.writeFileSync)(path, JSON.stringify({ at: now }));
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, import_node_path7.join)(cacheDir(env), `providers-${(0, import_node_crypto2.createHash)("sha256").update(hub).digest("hex").slice(0, 16)}.json`);
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, import_node_fs6.readFileSync)(path, "utf8"));
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, import_node_fs6.mkdirSync)((0, import_node_path7.join)(path, ".."), { recursive: true, mode: 448 });
593
- (0, import_node_fs6.writeFileSync)(path, JSON.stringify({ at: now(), providers }));
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, import_node_child_process4.execFileSync)("git", ["-C", cwd, "remote", "get-url", name], { stdio: ["ignore", "pipe", "ignore"], timeout: 1e3 }).toString().trim() || void 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 import_node_child_process4, import_node_crypto2, import_node_fs6, import_node_path7, HOOK_EVENTS, EVENT_NAMES, THROTTLE_MS, PROVIDERS_TTL_MS;
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
- import_node_child_process4 = require("node:child_process");
626
- import_node_crypto2 = require("node:crypto");
627
- import_node_fs6 = require("node:fs");
628
- import_node_path7 = require("node:path");
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
- // src/cli/watch.ts
654
- var watch_exports = {};
655
- __export(watch_exports, {
656
- readEvents: () => readEvents,
657
- watch: () => watch
658
- });
659
- async function watch(opts, deps) {
660
- const env = deps.env ?? process.env;
661
- const fetchFn = deps.fetch ?? fetch;
662
- const sleep = deps.sleep ?? defaultSleep;
663
- const hub = hubUrl(env);
664
- if (!hub) {
665
- deps.write("No hub configured. Run agenthub login --hub <url>.");
666
- return 1;
667
- }
668
- const { origin, upstream } = (deps.remotes ?? gitRemotes)(deps.cwd ?? process.cwd());
669
- if (!origin) {
670
- deps.write("Not in a git repo with an origin remote.");
671
- return 1;
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(sessionKey) {
3668
- const ref = this._sessionCache.get(sessionKey);
4114
+ get(sessionKey2) {
4115
+ const ref = this._sessionCache.get(sessionKey2);
3669
4116
  return ref ? ref.deref() : null;
3670
4117
  }
3671
- set(sessionKey, session) {
4118
+ set(sessionKey2, session) {
3672
4119
  if (this._maxCachedSessions === 0) {
3673
4120
  return;
3674
4121
  }
3675
- if (this._sessionCache.has(sessionKey)) {
3676
- this._sessionCache.delete(sessionKey);
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(sessionKey, new WeakRef(session));
3694
- this._sessionRegistry.register(session, sessionKey);
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 sessionKey = servername || hostname3;
3713
- assert2(sessionKey);
3714
- const session = customSession || sessionCache.get(sessionKey) || null;
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(sessionKey, session2);
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 COMMANDS = {
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(COMMANDS.CONNECT, address, port);
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, import_node_fs7.readFileSync)(instance.caBundlePath, "utf8");
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 import_node_fs7, import_node_tls, import_undici, dispatchers;
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
- import_node_fs7 = require("node:fs");
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, import_node_fs8.readFileSync)(p, "utf8"));
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, import_node_fs8.readFileSync)(p, "utf8"));
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 import_node_fs8, import_yaml, secretRef, oauthSchema, providerSchema, configSchema, ConfigError;
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
- import_node_fs8 = require("node:fs");
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 import_node_fs9 = require("node:fs");
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 PACKAGE_HEADER = `// Agent Hub plugin for OpenCode, from the @theagenthub/opencode npm package.
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 PACKAGE_COMMAND = `import { existsSync } from "node:fs";
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" ? PACKAGE_HEADER : `${OPENCODE_MARKER}
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" ? PACKAGE_COMMAND : `const COMMAND = ${JSON.stringify(command)};`}
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") presence = presence.then(() => notify("stop", event.properties.sessionID, directory));
55815
- else if (event.type === "session.deleted") presence = presence.then(() => notify("session-end", event.properties.info.id, directory));
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") started.delete(sessionID);
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 () => controller.abort();
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 COPILOT_EVENTS = [
55866
- ["sessionStart", "session-start"],
55867
- ["userPromptSubmitted", "prompt"],
55868
- ["postToolUse", "post-tool"],
55869
- ["agentStop", "stop"],
55870
- ["sessionEnd", "session-end"]
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 user ? (0, import_node_path3.join)(home, ".codex", "hooks.json") : (0, import_node_path3.join)(cwd, ".codex", "hooks.json");
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
- var OURS = [/^(?:\S*[\\/])?agenthub(?:\.js|\.exe)?\s+notify\b/i, /^"(?:[^"]*[\\/])?agenthub(?:\.js|\.exe)?"\s+notify\b/i];
55887
- var isOurs = (h) => typeof h.command === "string" && OURS.some((r) => r.test(h.command));
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
- if (harness === "opencode") {
55891
- (0, import_node_fs2.writeFileSync)(file2, opencodePlugin(command));
55892
- return ["chat.message", "tool.execute.after", "session.idle", "session.deleted"];
55893
- }
55894
- if (harness === "copilot") {
55895
- const config3 = readJson(file2);
55896
- config3.version = 1;
55897
- const hooks2 = config3.hooks ??= {};
55898
- for (const [event, notifyEvent] of COPILOT_EVENTS) {
55899
- const kept = (hooks2[event] ?? []).filter((h) => !isOurs(h));
55900
- hooks2[event] = [...kept, { type: "command", command: `${command} notify --format copilot --event ${notifyEvent}`, timeoutSec: 10 }];
55901
- }
55902
- writeJson(file2, config3);
55903
- return COPILOT_EVENTS.map(([e]) => e);
55904
- }
55905
- const config2 = readJson(file2);
55906
- const hooks = config2.hooks ??= {};
55907
- for (const event of EVENTS) {
55908
- const groups = (hooks[event] ?? []).map((g) => ({ ...g, hooks: g.hooks.filter((h) => !isOurs(h)) })).filter((g) => g.hooks.length);
55909
- const entry = { hooks: [{ type: "command", command: `${command} notify --format ${harness}`, timeout: TIMEOUT_S[harness][event] ?? 10 }] };
55910
- if (event === "PostToolUse" && harness === "claude-code") entry.matcher = "*";
55911
- hooks[event] = [...groups, entry];
55912
- }
55913
- writeJson(file2, config2);
55914
- return EVENTS;
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
- if (harness === "opencode") {
55919
- if (!(0, import_node_fs2.readFileSync)(file2, "utf8").startsWith(OPENCODE_MARKER)) return false;
55920
- (0, import_node_fs2.rmSync)(file2);
55921
- return true;
55922
- }
55923
- if (harness === "copilot") {
55924
- const config3 = readJson(file2);
55925
- if (!config3.hooks) return false;
55926
- let changed2 = false;
55927
- for (const [event, entries] of Object.entries(config3.hooks)) {
55928
- const kept = entries.filter((h) => !isOurs(h));
55929
- if (kept.length !== entries.length) changed2 = true;
55930
- if (kept.length) config3.hooks[event] = kept;
55931
- else delete config3.hooks[event];
55932
- }
55933
- if (!changed2) return false;
55934
- if (!Object.keys(config3.hooks).length && Object.keys(config3).every((k) => k === "version" || k === "hooks")) (0, import_node_fs2.rmSync)(file2);
55935
- else writeJson(file2, config3);
55936
- return true;
55937
- }
55938
- const config2 = readJson(file2);
55939
- if (!config2.hooks) return false;
55940
- let changed = false;
55941
- for (const [event, groups] of Object.entries(config2.hooks)) {
55942
- const kept = groups.map((g) => ({ ...g, hooks: g.hooks.filter((h) => !isOurs(h)) })).filter((g) => g.hooks.length);
55943
- if (kept.length !== groups.length || kept.some((g, i) => g.hooks.length !== groups[i].hooks.length)) changed = true;
55944
- if (kept.length) config2.hooks[event] = kept;
55945
- else delete config2.hooks[event];
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
- if (changed) writeJson(file2, config2);
55948
- return changed;
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.0";
56985
+ if (true) return "0.3.2";
56014
56986
  try {
56015
- return JSON.parse((0, import_node_fs9.readFileSync)(new URL("../../package.json", import_meta.url), "utf8")).version;
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 notify({ format, event, payload }, { store: credentialStore(), version: version2() });
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(USAGE);
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, USAGE);
57249
+ return admin2(args, commandHelp("admin"));
56183
57250
  }
56184
- console.error(USAGE);
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(