@theagenthub/cli 0.3.1 → 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 (2) hide show
  1. package/bin/agenthub.cjs +685 -160
  2. package/package.json +1 -1
package/bin/agenthub.cjs CHANGED
@@ -463,6 +463,98 @@ var init_update = __esm({
463
463
  }
464
464
  });
465
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
+
466
558
  // src/cli/watch.ts
467
559
  var watch_exports = {};
468
560
  __export(watch_exports, {
@@ -611,15 +703,19 @@ var init_watch = __esm({
611
703
  var wait_exports = {};
612
704
  __export(wait_exports, {
613
705
  DEFAULT_WAIT_MS: () => DEFAULT_WAIT_MS,
706
+ actMode: () => actMode,
707
+ actSetting: () => actSetting,
614
708
  addPending: () => addPending,
615
709
  claimFile: () => claimFile,
710
+ continueNotice: () => continueNotice,
616
711
  mergeInbox: () => mergeInbox,
617
712
  pendingFile: () => pendingFile,
618
713
  releaseWaiter: () => releaseWaiter,
619
714
  takePending: () => takePending,
620
715
  wait: () => wait,
621
716
  wakeMode: () => wakeMode,
622
- wakeSetting: () => wakeSetting
717
+ wakeSetting: () => wakeSetting,
718
+ wokenNotice: () => wokenNotice
623
719
  });
624
720
  function wakeSetting(env, origin) {
625
721
  const settings = readSettings(env);
@@ -630,23 +726,39 @@ function wakeSetting(env, origin) {
630
726
  const mode = ["0", "false", "off", "no"].includes(v) ? "off" : v === "all" ? "all" : "requests";
631
727
  return { mode, from };
632
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
+ }
633
745
  function wakeMode(env, origin) {
634
746
  return wakeSetting(env, origin).mode;
635
747
  }
636
748
  function claimFile(env, key) {
637
- return (0, import_node_path7.join)(cacheDir(env), `wait-${key}.json`);
749
+ return (0, import_node_path8.join)(cacheDir(env), `wait-${key}.json`);
638
750
  }
639
751
  function pendingFile(env, key) {
640
- return (0, import_node_path7.join)(cacheDir(env), `pending-${key}.json`);
752
+ return (0, import_node_path8.join)(cacheDir(env), `pending-${key}.json`);
641
753
  }
642
754
  function releaseWaiter(env, key) {
643
- (0, import_node_fs6.rmSync)(claimFile(env, key), { force: true });
755
+ (0, import_node_fs7.rmSync)(claimFile(env, key), { force: true });
644
756
  }
645
757
  function takePending(env, key) {
646
758
  const file2 = pendingFile(env, key);
647
759
  try {
648
- const pending = JSON.parse((0, import_node_fs6.readFileSync)(file2, "utf8"));
649
- (0, import_node_fs6.rmSync)(file2, { force: true });
760
+ const pending = JSON.parse((0, import_node_fs7.readFileSync)(file2, "utf8"));
761
+ (0, import_node_fs7.rmSync)(file2, { force: true });
650
762
  return pending;
651
763
  } catch {
652
764
  return void 0;
@@ -657,12 +769,12 @@ function addPending(env, key, inbox) {
657
769
  const file2 = pendingFile(env, key);
658
770
  let before;
659
771
  try {
660
- before = JSON.parse((0, import_node_fs6.readFileSync)(file2, "utf8"));
772
+ before = JSON.parse((0, import_node_fs7.readFileSync)(file2, "utf8"));
661
773
  } catch {
662
774
  before = void 0;
663
775
  }
664
- (0, import_node_fs6.mkdirSync)((0, import_node_path7.join)(file2, ".."), { recursive: true, mode: 448 });
665
- (0, import_node_fs6.writeFileSync)(file2, JSON.stringify(mergeInbox(before, inbox)), { mode: 384 });
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 });
666
778
  }
667
779
  function mergeInbox(earlier, later) {
668
780
  if (!earlier) return later;
@@ -677,23 +789,24 @@ async function wait(opts, deps) {
677
789
  const fetchFn = deps.fetch ?? fetch;
678
790
  const now = deps.now ?? Date.now;
679
791
  const sleep = deps.sleep ?? defaultSleep2;
680
- const cwd = typeof opts.payload?.cwd === "string" ? opts.payload.cwd : deps.cwd ?? process.cwd();
792
+ const cwd = payloadCwd(opts.payload) ?? deps.cwd ?? process.cwd();
681
793
  const hub = hubUrl(env);
682
794
  if (!hub) return NOTHING;
683
795
  const { origin, upstream } = (deps.remotes ?? gitRemotes)(cwd);
684
796
  if (!origin) return NOTHING;
685
797
  const mode = wakeMode(env, origin);
686
798
  if (mode === "off") return NOTHING;
799
+ const act = actMode(env, origin);
687
800
  if (!await onHub(hub, [origin, upstream], fetchFn, env, now).catch(() => false)) return NOTHING;
688
801
  const agentName = hookAgentName(opts.payload, env);
689
802
  const key = sessionKey(hub, origin, agentName);
690
803
  const id = (0, import_node_crypto2.randomBytes)(8).toString("hex");
691
804
  const claim2 = claimFile(env, key);
692
- (0, import_node_fs6.mkdirSync)((0, import_node_path7.join)(claim2, ".."), { recursive: true, mode: 448 });
693
- (0, import_node_fs6.writeFileSync)(claim2, JSON.stringify({ id, pid: process.pid, at: now() }));
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() }));
694
807
  const holds = () => {
695
808
  try {
696
- return JSON.parse((0, import_node_fs6.readFileSync)(claim2, "utf8")).id === id;
809
+ return JSON.parse((0, import_node_fs7.readFileSync)(claim2, "utf8")).id === id;
697
810
  } catch {
698
811
  return false;
699
812
  }
@@ -740,7 +853,7 @@ async function wait(opts, deps) {
740
853
  const data = JSON.parse(ev.data);
741
854
  const wakes = data.messages.some((m) => m.new && (mode === "all" || m.type !== "fyi"));
742
855
  if (wakes && holds()) return { code: 2, text: `${formatInbox(mergeInbox(takePending(env, key), data))}
743
- ${WOKEN}` };
856
+ ${wokenNotice(act)}` };
744
857
  addPending(env, key, data);
745
858
  } else if (ev.event === "end") {
746
859
  const { reason } = JSON.parse(ev.data);
@@ -756,7 +869,7 @@ ${WOKEN}` };
756
869
  clearInterval(check2);
757
870
  opts.signal?.removeEventListener("abort", stop);
758
871
  ac.abort();
759
- if (holds()) (0, import_node_fs6.rmSync)(claim2, { force: true });
872
+ if (holds()) (0, import_node_fs7.rmSync)(claim2, { force: true });
760
873
  }
761
874
  }
762
875
  function defaultSleep2(ms, signal) {
@@ -768,13 +881,13 @@ function defaultSleep2(ms, signal) {
768
881
  }, { once: true });
769
882
  });
770
883
  }
771
- var import_node_crypto2, import_node_fs6, import_node_path7, DEFAULT_WAIT_MS, MAX_BACKOFF_MS2, NOTHING, WOKEN;
884
+ var import_node_crypto2, import_node_fs7, import_node_path8, DEFAULT_WAIT_MS, MAX_BACKOFF_MS2, NOTHING, ASK_FIRST, WOKEN;
772
885
  var init_wait = __esm({
773
886
  "src/cli/wait.ts"() {
774
887
  "use strict";
775
888
  import_node_crypto2 = require("node:crypto");
776
- import_node_fs6 = require("node:fs");
777
- import_node_path7 = require("node:path");
889
+ import_node_fs7 = require("node:fs");
890
+ import_node_path8 = require("node:path");
778
891
  init_paths();
779
892
  init_notify();
780
893
  init_session();
@@ -783,6 +896,7 @@ var init_wait = __esm({
783
896
  DEFAULT_WAIT_MS = 12 * 36e5;
784
897
  MAX_BACKOFF_MS2 = 6e4;
785
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.";
786
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.";
787
901
  }
788
902
  });
@@ -791,31 +905,48 @@ var init_wait = __esm({
791
905
  var notify_exports = {};
792
906
  __export(notify_exports, {
793
907
  CARD_NUDGE_MS: () => CARD_NUDGE_MS,
908
+ NOTIFY_FORMATS: () => NOTIFY_FORMATS,
794
909
  TASK_CHECK_MS: () => TASK_CHECK_MS,
795
910
  formatInbox: () => formatInbox,
796
911
  gitBranch: () => gitBranch,
797
912
  gitRemotes: () => gitRemotes,
798
913
  hookAgentName: () => hookAgentName,
799
914
  notify: () => notify,
915
+ notifyHook: () => notifyHook,
800
916
  onHub: () => onHub,
917
+ payloadCwd: () => payloadCwd,
801
918
  sessionKey: () => sessionKey
802
919
  });
803
920
  async function notify(opts, deps) {
921
+ return (await notifyHook(opts, deps)).stdout;
922
+ }
923
+ async function notifyHook(opts, deps) {
804
924
  try {
805
925
  const event = opts.event ?? HOOK_EVENTS[String(opts.payload?.hook_event_name ?? "")] ?? "post-tool";
806
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("");
807
927
  const [text, notice] = await Promise.all([run(opts, event, deps).catch(() => ""), update.catch(() => "")]);
808
- if (typeof text === "object") return text.raw;
809
- return render(opts.format, event, [text, notice].filter(Boolean).join("\n"));
928
+ if (typeof text === "object") return text;
929
+ return { stdout: render(opts.format, event, [text, notice].filter(Boolean).join("\n")) };
810
930
  } catch {
811
- 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;
812
943
  }
813
944
  }
814
945
  async function run(opts, event, deps) {
815
946
  const env = deps.env ?? process.env;
816
947
  const now = deps.now ?? Date.now;
817
948
  const fetchFn = deps.fetch ?? fetch;
818
- const cwd = typeof opts.payload?.cwd === "string" ? opts.payload.cwd : deps.cwd ?? process.cwd();
949
+ const cwd = payloadCwd(opts.payload) ?? deps.cwd ?? process.cwd();
819
950
  const out = (text) => text;
820
951
  const hub = hubUrl(env);
821
952
  if (!hub) return "";
@@ -825,24 +956,34 @@ async function run(opts, event, deps) {
825
956
  const agentName = hookAgentName(opts.payload, env);
826
957
  const throttleKey = sessionKey(hub, origin, agentName);
827
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
+ }
828
966
  const wait2 = THROTTLE_MS[event];
829
- if (wait2 && !throttle((0, import_node_path8.join)(cacheDir(env), `notify-${throttleKey}.json`), wait2, now())) return "";
967
+ if (wait2 && !throttle((0, import_node_path9.join)(cacheDir(env), `notify-${throttleKey}.json`), wait2, now())) return "";
830
968
  const token = await accessToken(hub, { store: deps.store, fetch: fetchFn, env, now });
831
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.`) : "";
832
970
  const who = { project: origin, ...upstream ? { upstream } : {}, ...agentName ? { agent_name: agentName } : {} };
833
971
  const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
834
972
  const signal = AbortSignal.timeout(3e3);
835
- if (event === "stop" && opts.format === "codex" && wakeMode(env, origin) !== "off") {
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") {
836
976
  const res2 = await fetchFn(`${hub}/api/inbox?${new URLSearchParams(who)}`, { headers, signal });
837
977
  if (res2.ok) {
838
978
  const inbox2 = mergeInbox(takePending(env, throttleKey), await res2.json());
839
979
  const mode = wakeMode(env, origin);
840
980
  if (inbox2.messages.some((m) => m.new && (mode === "all" || m.type !== "fyi"))) {
841
- return { raw: JSON.stringify({ decision: "block", reason: `${formatInbox(inbox2)}
842
- Handle these as the Agent Hub rules say before you finish.` }) };
981
+ return block(`${formatInbox(inbox2)}
982
+ ${continueNotice(actMode(env, origin))}`);
843
983
  }
844
984
  addPending(env, throttleKey, inbox2);
845
985
  }
986
+ if (opts.format === "kimi" && kimiWakeable(opts.payload)) (deps.startKimiWaiter ?? startKimiWaiter)(opts.payload, env);
846
987
  }
847
988
  if (event === "stop" || event === "session-end") {
848
989
  await fetchFn(`${hub}/api/presence`, {
@@ -858,14 +999,14 @@ Handle these as the Agent Hub rules say before you finish.` }) };
858
999
  const { membership } = await res.json().catch(() => ({}));
859
1000
  const args = [`remote="${origin}"`, ...upstream ? [`upstream="${upstream}"`] : [], ...agentName ? [`agent_name="${agentName}"`] : []].join(", ");
860
1001
  const keep = agentName ? " Use exactly that agent_name: it's how hub notices find this session." : "";
861
- const waitingFile = (0, import_node_path8.join)(cacheDir(env), `waiting-${throttleKey}.json`);
1002
+ const waitingFile = (0, import_node_path9.join)(cacheDir(env), `waiting-${throttleKey}.json`);
862
1003
  const wasWaiting = readFlag(waitingFile);
863
1004
  if (membership === "pending") {
864
1005
  if (!wasWaiting) writeFlag(waitingFile);
865
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.") : "";
866
1007
  }
867
1008
  if (wasWaiting) {
868
- (0, import_node_fs7.rmSync)(waitingFile, { force: true });
1009
+ (0, import_node_fs8.rmSync)(waitingFile, { force: true });
869
1010
  if (membership === "active") return out(`Agent Hub: a project admin approved you. Call the agenthub join_project tool with ${args} to join.${keep}`);
870
1011
  return out("Agent Hub: a project admin turned down your request to join this project.");
871
1012
  }
@@ -879,7 +1020,7 @@ Handle these as the Agent Hub rules say before you finish.` }) };
879
1020
  const card = await keepCardCurrent(inbox, event, {
880
1021
  // Without a name the hub picks the user's latest agent here, which may be another session's.
881
1022
  branch: agentName ? (deps.branch ?? gitBranch)(cwd) : void 0,
882
- stateFile: (0, import_node_path8.join)(cacheDir(env), `card-${throttleKey}.json`),
1023
+ stateFile: (0, import_node_path9.join)(cacheDir(env), `card-${throttleKey}.json`),
883
1024
  now: now(),
884
1025
  post: (fields) => fetchFn(`${hub}/api/card`, { method: "POST", headers, body: JSON.stringify({ ...who, ...fields }), signal })
885
1026
  }).catch(() => "");
@@ -909,18 +1050,18 @@ async function keepCardCurrent(inbox, event, ctx) {
909
1050
  }
910
1051
  function readState(path) {
911
1052
  try {
912
- return JSON.parse((0, import_node_fs7.readFileSync)(path, "utf8"));
1053
+ return JSON.parse((0, import_node_fs8.readFileSync)(path, "utf8"));
913
1054
  } catch {
914
1055
  return {};
915
1056
  }
916
1057
  }
917
1058
  function writeState(path, state) {
918
- (0, import_node_fs7.mkdirSync)((0, import_node_path8.join)(path, ".."), { recursive: true, mode: 448 });
919
- (0, import_node_fs7.writeFileSync)(path, JSON.stringify(state));
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));
920
1061
  }
921
1062
  function gitBranch(cwd) {
922
1063
  try {
923
- return (0, import_node_child_process4.execFileSync)("git", ["-C", cwd, "branch", "--show-current"], { stdio: ["ignore", "pipe", "ignore"], timeout: 1e3 }).toString().trim() || void 0;
1064
+ return (0, import_node_child_process5.execFileSync)("git", ["-C", cwd, "branch", "--show-current"], { stdio: ["ignore", "pipe", "ignore"], timeout: 1e3 }).toString().trim() || void 0;
924
1065
  } catch {
925
1066
  return void 0;
926
1067
  }
@@ -930,7 +1071,7 @@ function sessionKey(hub, origin, agentName) {
930
1071
  }
931
1072
  function hookAgentName(payload, env) {
932
1073
  if (env.AGENTHUB_AGENT_NAME) return env.AGENTHUB_AGENT_NAME;
933
- const session = payload?.session_id ?? payload?.sessionId;
1074
+ const session = payload?.session_id ?? payload?.sessionId ?? payload?.conversation_id ?? payload?.conversationId;
934
1075
  if (typeof session !== "string" || !session) return void 0;
935
1076
  return `s-${(0, import_node_crypto3.createHash)("sha256").update(session).digest("hex").slice(0, 8)}`;
936
1077
  }
@@ -963,38 +1104,44 @@ function clean(s) {
963
1104
  }
964
1105
  function render(format, event, text) {
965
1106
  if (!text) return "";
966
- 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 } });
967
1114
  if (format === "codex" && event === "session-start") return text;
968
1115
  if (format === "copilot") return JSON.stringify({ additionalContext: text });
969
1116
  return JSON.stringify({ hookSpecificOutput: { hookEventName: EVENT_NAMES[event], additionalContext: text } });
970
1117
  }
971
1118
  function readFlag(path) {
972
1119
  try {
973
- (0, import_node_fs7.readFileSync)(path);
1120
+ (0, import_node_fs8.readFileSync)(path);
974
1121
  return true;
975
1122
  } catch {
976
1123
  return false;
977
1124
  }
978
1125
  }
979
1126
  function writeFlag(path) {
980
- (0, import_node_fs7.mkdirSync)((0, import_node_path8.join)(path, ".."), { recursive: true, mode: 448 });
981
- (0, import_node_fs7.writeFileSync)(path, "{}");
1127
+ (0, import_node_fs8.mkdirSync)((0, import_node_path9.join)(path, ".."), { recursive: true, mode: 448 });
1128
+ (0, import_node_fs8.writeFileSync)(path, "{}");
982
1129
  }
983
1130
  function throttle(path, waitMs, now) {
984
1131
  try {
985
- const last = JSON.parse((0, import_node_fs7.readFileSync)(path, "utf8")).at;
1132
+ const last = JSON.parse((0, import_node_fs8.readFileSync)(path, "utf8")).at;
986
1133
  if (now - last < waitMs) return false;
987
1134
  } catch {
988
1135
  }
989
- (0, import_node_fs7.mkdirSync)((0, import_node_path8.join)(path, ".."), { recursive: true, mode: 448 });
990
- (0, import_node_fs7.writeFileSync)(path, JSON.stringify({ at: now }));
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 }));
991
1138
  return true;
992
1139
  }
993
1140
  async function onHub(hub, remotes, fetchFn, env, now) {
994
- const path = (0, import_node_path8.join)(cacheDir(env), `providers-${(0, import_node_crypto3.createHash)("sha256").update(hub).digest("hex").slice(0, 16)}.json`);
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`);
995
1142
  let providers;
996
1143
  try {
997
- const cached2 = JSON.parse((0, import_node_fs7.readFileSync)(path, "utf8"));
1144
+ const cached2 = JSON.parse((0, import_node_fs8.readFileSync)(path, "utf8"));
998
1145
  if (now() - cached2.at < PROVIDERS_TTL_MS) providers = cached2.providers;
999
1146
  } catch {
1000
1147
  }
@@ -1002,8 +1149,8 @@ async function onHub(hub, remotes, fetchFn, env, now) {
1002
1149
  const res = await fetchFn(`${hub}/api/providers`, { signal: AbortSignal.timeout(3e3) });
1003
1150
  if (!res.ok) return false;
1004
1151
  providers = (await res.json()).providers;
1005
- (0, import_node_fs7.mkdirSync)((0, import_node_path8.join)(path, ".."), { recursive: true, mode: 448 });
1006
- (0, import_node_fs7.writeFileSync)(path, JSON.stringify({ at: now(), providers }));
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 }));
1007
1154
  }
1008
1155
  const hosts = /* @__PURE__ */ new Set();
1009
1156
  for (const p of providers) {
@@ -1022,7 +1169,7 @@ async function onHub(hub, remotes, fetchFn, env, now) {
1022
1169
  function gitRemotes(cwd) {
1023
1170
  const get = (name) => {
1024
1171
  try {
1025
- return (0, import_node_child_process4.execFileSync)("git", ["-C", cwd, "remote", "get-url", name], { stdio: ["ignore", "pipe", "ignore"], timeout: 1e3 }).toString().trim() || void 0;
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;
1026
1173
  } catch {
1027
1174
  return void 0;
1028
1175
  }
@@ -1031,26 +1178,33 @@ function gitRemotes(cwd) {
1031
1178
  const upstream = origin ? get("upstream") : void 0;
1032
1179
  return { ...origin ? { origin } : {}, ...upstream ? { upstream } : {} };
1033
1180
  }
1034
- var import_node_child_process4, import_node_crypto3, import_node_fs7, import_node_path8, HOOK_EVENTS, EVENT_NAMES, THROTTLE_MS, PROVIDERS_TTL_MS, CARD_NUDGE_MS, TASK_CHECK_MS;
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;
1035
1182
  var init_notify = __esm({
1036
1183
  "src/cli/notify.ts"() {
1037
1184
  "use strict";
1038
- import_node_child_process4 = require("node:child_process");
1185
+ import_node_child_process5 = require("node:child_process");
1039
1186
  import_node_crypto3 = require("node:crypto");
1040
- import_node_fs7 = require("node:fs");
1041
- import_node_path8 = require("node:path");
1187
+ import_node_fs8 = require("node:fs");
1188
+ import_node_path9 = require("node:path");
1189
+ import_node_url = require("node:url");
1042
1190
  init_registry();
1043
1191
  init_paths();
1044
1192
  init_session();
1045
1193
  init_settings();
1046
1194
  init_update();
1195
+ init_kimi();
1047
1196
  init_wait();
1197
+ NOTIFY_FORMATS = ["claude-code", "codex", "copilot", "droid", "gemini", "qwen", "cursor", "antigravity", "kimi", "goose", "amp", "roo", "crush", "text"];
1048
1198
  HOOK_EVENTS = {
1049
1199
  SessionStart: "session-start",
1050
1200
  UserPromptSubmit: "prompt",
1051
1201
  PostToolUse: "post-tool",
1052
1202
  Stop: "stop",
1053
- SessionEnd: "session-end"
1203
+ SessionEnd: "session-end",
1204
+ // Gemini CLI
1205
+ BeforeAgent: "prompt",
1206
+ AfterTool: "post-tool",
1207
+ AfterAgent: "stop"
1054
1208
  };
1055
1209
  EVENT_NAMES = {
1056
1210
  "session-start": "SessionStart",
@@ -1059,8 +1213,29 @@ var init_notify = __esm({
1059
1213
  stop: "Stop",
1060
1214
  "session-end": "SessionEnd"
1061
1215
  };
1216
+ GEMINI_EVENT_NAMES = { ...EVENT_NAMES, prompt: "BeforeAgent", "post-tool": "AfterTool", stop: "AfterAgent" };
1062
1217
  THROTTLE_MS = { "post-tool": 3e4, prompt: 1e4 };
1063
1218
  PROVIDERS_TTL_MS = 24 * 36e5;
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" };
1064
1239
  CARD_NUDGE_MS = 10 * 6e4;
1065
1240
  TASK_CHECK_MS = 30 * 6e4;
1066
1241
  }
@@ -27704,7 +27879,7 @@ function dispatcherFor(instance) {
27704
27879
  if (!instance.caBundlePath) return void 0;
27705
27880
  let d = dispatchers.get(instance.id);
27706
27881
  if (!d) {
27707
- const ca = (0, import_node_fs8.readFileSync)(instance.caBundlePath, "utf8");
27882
+ const ca = (0, import_node_fs9.readFileSync)(instance.caBundlePath, "utf8");
27708
27883
  d = new import_undici.Agent({ connect: { ca: [...import_node_tls.rootCertificates, ca] } });
27709
27884
  dispatchers.set(instance.id, d);
27710
27885
  }
@@ -27721,11 +27896,11 @@ function fetchFor(instance, override) {
27721
27896
  signal: AbortSignal.timeout(1e4)
27722
27897
  });
27723
27898
  }
27724
- var import_node_fs8, import_node_tls, import_undici, dispatchers;
27899
+ var import_node_fs9, import_node_tls, import_undici, dispatchers;
27725
27900
  var init_http = __esm({
27726
27901
  "src/providers/http.ts"() {
27727
27902
  "use strict";
27728
- import_node_fs8 = require("node:fs");
27903
+ import_node_fs9 = require("node:fs");
27729
27904
  import_node_tls = require("node:tls");
27730
27905
  import_undici = __toESM(require_undici(), 1);
27731
27906
  dispatchers = /* @__PURE__ */ new Map();
@@ -55604,7 +55779,7 @@ var init_types = __esm({
55604
55779
 
55605
55780
  // src/providers/config.ts
55606
55781
  function loadConfigFile(path, opts = {}) {
55607
- const read = opts.readFile ?? ((p) => (0, import_node_fs9.readFileSync)(p, "utf8"));
55782
+ const read = opts.readFile ?? ((p) => (0, import_node_fs10.readFileSync)(p, "utf8"));
55608
55783
  let text;
55609
55784
  try {
55610
55785
  text = read(path);
@@ -55615,7 +55790,7 @@ function loadConfigFile(path, opts = {}) {
55615
55790
  }
55616
55791
  function buildConfig(raw, opts = {}) {
55617
55792
  const env = opts.env ?? process.env;
55618
- const read = opts.readFile ?? ((p) => (0, import_node_fs9.readFileSync)(p, "utf8"));
55793
+ const read = opts.readFile ?? ((p) => (0, import_node_fs10.readFileSync)(p, "utf8"));
55619
55794
  const parsed = configSchema.safeParse(raw);
55620
55795
  if (!parsed.success) {
55621
55796
  throw new ConfigError(
@@ -55796,11 +55971,11 @@ function checkNoHostCollisions(instances) {
55796
55971
  for (const a of inst.remoteAliases) claim2(a.port ? `${a.host}:${a.port}` : a.host, inst.id);
55797
55972
  }
55798
55973
  }
55799
- var import_node_fs9, import_yaml, secretRef, oauthSchema, providerSchema, configSchema, ConfigError;
55974
+ var import_node_fs10, import_yaml, secretRef, oauthSchema, providerSchema, configSchema, ConfigError;
55800
55975
  var init_config = __esm({
55801
55976
  "src/providers/config.ts"() {
55802
55977
  "use strict";
55803
- import_node_fs9 = require("node:fs");
55978
+ import_node_fs10 = require("node:fs");
55804
55979
  import_yaml = __toESM(require_dist(), 1);
55805
55980
  init_zod();
55806
55981
  init_defaults();
@@ -55901,7 +56076,7 @@ var init_admin = __esm({
55901
56076
  });
55902
56077
 
55903
56078
  // src/cli/index.ts
55904
- var import_node_fs10 = require("node:fs");
56079
+ var import_node_fs11 = require("node:fs");
55905
56080
 
55906
56081
  // src/cli/credentials.ts
55907
56082
  var import_node_child_process = require("node:child_process");
@@ -56007,11 +56182,133 @@ var import_node_fs2 = require("node:fs");
56007
56182
  var import_node_os2 = require("node:os");
56008
56183
  var import_node_path3 = require("node:path");
56009
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
+
56010
56307
  // src/cli/opencode-plugin.ts
56011
56308
  var OPENCODE_MARKER = "// Agent Hub plugin for OpenCode, written by `agenthub hooks install opencode`.";
56012
- var PACKAGE_HEADER = `// Agent Hub plugin for OpenCode, from the @theagenthub/opencode npm package.
56309
+ var PACKAGE_HEADER2 = `// Agent Hub plugin for OpenCode, from the @theagenthub/opencode npm package.
56013
56310
  // Needs the agenthub CLI: curl -fsSL https://get.theagenthub.app | sh`;
56014
- var PACKAGE_COMMAND = `import { existsSync } from "node:fs";
56311
+ var PACKAGE_COMMAND2 = `import { existsSync } from "node:fs";
56015
56312
  import { homedir } from "node:os";
56016
56313
  import { join } from "node:path";
56017
56314
 
@@ -56020,12 +56317,12 @@ const COMMAND =
56020
56317
  [join(homedir(), ".local", "bin", "agenthub"), join(process.env.LOCALAPPDATA || homedir(), "Programs", "agenthub", "agenthub.exe")].find((f) => existsSync(f)) ||
56021
56318
  "agenthub";`;
56022
56319
  function opencodePlugin(command, as = "file") {
56023
- const header = as === "package" ? PACKAGE_HEADER : `${OPENCODE_MARKER}
56320
+ const header = as === "package" ? PACKAGE_HEADER2 : `${OPENCODE_MARKER}
56024
56321
  // Re-run that command to update it, or \`agenthub hooks uninstall opencode\` to remove it.`;
56025
56322
  return `${header}
56026
56323
  import { spawn } from "node:child_process";
56027
56324
 
56028
- ${as === "package" ? PACKAGE_COMMAND : `const COMMAND = ${JSON.stringify(command)};`}
56325
+ ${as === "package" ? PACKAGE_COMMAND2 : `const COMMAND = ${JSON.stringify(command)};`}
56029
56326
  const TIMEOUT_MS = 5000;
56030
56327
 
56031
56328
  function notify(event, sessionID, cwd) {
@@ -56140,11 +56437,23 @@ async function setup(ctx) {
56140
56437
  if (!ctx?.session?.hook || !ctx.tool?.hook || !ctx.event?.subscribe) return;
56141
56438
  const directory = ctx.location?.directory ?? process.cwd();
56142
56439
  const started = new Set();
56440
+ const waiting = new Map();
56143
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
+ };
56144
56452
  const wrap = (text) => "\\n\\n<agenthub>\\n" + text + "\\n</agenthub>";
56145
56453
 
56146
56454
  // V2 has no hidden message parts, so the notice is appended to the prompt's text.
56147
56455
  await ctx.session.hook("prompt", async (event) => {
56456
+ waiting.get(event.sessionID)?.kill();
56148
56457
  const first = !started.has(event.sessionID);
56149
56458
  started.add(event.sessionID);
56150
56459
  const text = await notify(first ? "session-start" : "prompt", event.sessionID, directory);
@@ -56164,11 +56473,21 @@ async function setup(ctx) {
56164
56473
  const sessionID = event.data?.sessionID;
56165
56474
  if (!sessionID) continue;
56166
56475
  const kind = event.type === "session.deleted" ? "session-end" : "stop";
56167
- if (kind === "session-end") started.delete(sessionID);
56476
+ if (kind === "session-end") {
56477
+ started.delete(sessionID);
56478
+ waiting.get(sessionID)?.kill();
56479
+ }
56168
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
+ }
56169
56485
  }
56170
56486
  })().catch(() => {});
56171
- return () => controller.abort();
56487
+ return () => {
56488
+ controller.abort();
56489
+ for (const child of waiting.values()) child.kill();
56490
+ };
56172
56491
  }
56173
56492
 
56174
56493
  export default { id: "agenthub", server: AgentHub, setup };
@@ -56176,97 +56495,248 @@ export default { id: "agenthub", server: AgentHub, setup };
56176
56495
  }
56177
56496
 
56178
56497
  // src/cli/hooks.ts
56179
- 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"]);
56180
56500
  var EVENTS = ["SessionStart", "UserPromptSubmit", "PostToolUse", "Stop", "SessionEnd"];
56181
- var COPILOT_EVENTS = [
56182
- ["sessionStart", "session-start"],
56183
- ["userPromptSubmitted", "prompt"],
56184
- ["postToolUse", "post-tool"],
56185
- ["agentStop", "stop"],
56186
- ["sessionEnd", "session-end"]
56501
+ var CLAUDE_PAIRS = [
56502
+ ["SessionStart", "session-start"],
56503
+ ["UserPromptSubmit", "prompt"],
56504
+ ["PostToolUse", "post-tool"],
56505
+ ["Stop", "stop"],
56506
+ ["SessionEnd", "session-end"]
56187
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
+ };
56188
56553
  var WAIT_MAX_S = 12 * 3600;
56189
56554
  var WAIT_TIMEOUT_S = WAIT_MAX_S + 60;
56190
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" };
56191
56559
  function hookFile(harness, scope, cwd, home = (0, import_node_os2.homedir)(), env = process.env) {
56192
56560
  const user = scope === "user";
56561
+ const base = user ? home : cwd;
56193
56562
  switch (harness) {
56194
56563
  case "claude-code":
56195
56564
  return user ? (0, import_node_path3.join)(home, ".claude", "settings.json") : (0, import_node_path3.join)(cwd, ".claude", "settings.local.json");
56196
56565
  case "codex":
56197
- 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");
56198
56567
  case "copilot":
56199
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");
56200
56569
  case "opencode":
56201
56570
  return user ? (0, import_node_path3.join)(env.XDG_CONFIG_HOME || (0, import_node_path3.join)(home, ".config"), "opencode", "plugins", "agenthub.js") : (0, import_node_path3.join)(cwd, ".opencode", "plugins", "agenthub.js");
56571
+ case "droid":
56572
+ return (0, import_node_path3.join)(base, ".factory", "hooks.json");
56573
+ case "gemini":
56574
+ return (0, import_node_path3.join)(base, ".gemini", "settings.json");
56575
+ case "qwen":
56576
+ return (0, import_node_path3.join)(base, ".qwen", "settings.json");
56577
+ case "cursor":
56578
+ return (0, import_node_path3.join)(base, ".cursor", "hooks.json");
56579
+ case "antigravity":
56580
+ return user ? (0, import_node_path3.join)(home, ".gemini", "config", "hooks.json") : (0, import_node_path3.join)(cwd, ".agents", "hooks.json");
56581
+ case "kimi":
56582
+ return (0, import_node_path3.join)(home, ".kimi-code", "config.toml");
56583
+ case "goose":
56584
+ return (0, import_node_path3.join)(base, ".agents", "plugins", "agenthub", "hooks", "hooks.json");
56585
+ case "crush":
56586
+ return user ? (0, import_node_path3.join)(env.XDG_CONFIG_HOME || (0, import_node_path3.join)(home, ".config"), "crush", "crush.json") : (0, import_node_fs2.existsSync)((0, import_node_path3.join)(cwd, "crush.json")) ? (0, import_node_path3.join)(cwd, "crush.json") : (0, import_node_path3.join)(cwd, ".crush.json");
56587
+ case "amp":
56588
+ return user ? (0, import_node_path3.join)(env.XDG_CONFIG_HOME || (0, import_node_path3.join)(home, ".config"), "amp", "plugins", "agenthub.ts") : (0, import_node_path3.join)(cwd, ".amp", "plugins", "agenthub.ts");
56202
56589
  }
56203
56590
  }
56204
56591
  var OURS = [/^(?:\S*[\\/])?agenthub(?:\.js|\.exe)?\s+(?:notify|wait)\b/i, /^"(?:[^"]*[\\/])?agenthub(?:\.js|\.exe)?"\s+(?:notify|wait)\b/i];
56205
- var isOurs = (h) => typeof h.command === "string" && OURS.some((r) => r.test(h.command));
56592
+ var isOurs = (h) => typeof h?.command === "string" && OURS.some((r) => r.test(h.command));
56206
56593
  function installHooks(harness, file2, command = "agenthub") {
56207
56594
  (0, import_node_fs2.mkdirSync)((0, import_node_path3.dirname)(file2), { recursive: true });
56208
- if (harness === "opencode") {
56209
- (0, import_node_fs2.writeFileSync)(file2, opencodePlugin(command));
56210
- return ["chat.message", "tool.execute.after", "session.idle", "session.deleted"];
56211
- }
56212
- if (harness === "copilot") {
56213
- const config3 = readJson(file2);
56214
- config3.version = 1;
56215
- const hooks2 = config3.hooks ??= {};
56216
- for (const [event, notifyEvent] of COPILOT_EVENTS) {
56217
- const kept = (hooks2[event] ?? []).filter((h) => !isOurs(h));
56218
- hooks2[event] = [...kept, { type: "command", command: `${command} notify --format copilot --event ${notifyEvent}`, timeoutSec: 10 }];
56219
- }
56220
- writeJson(file2, config3);
56221
- return COPILOT_EVENTS.map(([e]) => e);
56222
- }
56223
- const config2 = readJson(file2);
56224
- const hooks = config2.hooks ??= {};
56225
- for (const event of EVENTS) {
56226
- const groups = (hooks[event] ?? []).map((g) => ({ ...g, hooks: g.hooks.filter((h) => !isOurs(h)) })).filter((g) => g.hooks.length);
56227
- const entry = { hooks: [{ type: "command", command: `${command} notify --format ${harness}`, timeout: TIMEOUT_S[harness][event] ?? 10 }] };
56228
- if (event === "PostToolUse" && harness === "claude-code") entry.matcher = "*";
56229
- if (event === "Stop" && harness === "claude-code") {
56230
- entry.hooks.push({ type: "command", command: `${command} wait --max ${WAIT_MAX_S}`, asyncRewake: true, timeout: WAIT_TIMEOUT_S });
56231
- }
56232
- hooks[event] = [...groups, entry];
56233
- }
56234
- writeJson(file2, config2);
56235
- return EVENTS;
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
+ }
56236
56664
  }
56237
56665
  function uninstallHooks(harness, file2) {
56238
56666
  if (!(0, import_node_fs2.existsSync)(file2)) return false;
56239
- if (harness === "opencode") {
56240
- if (!(0, import_node_fs2.readFileSync)(file2, "utf8").startsWith(OPENCODE_MARKER)) return false;
56241
- (0, import_node_fs2.rmSync)(file2);
56242
- return true;
56243
- }
56244
- if (harness === "copilot") {
56245
- const config3 = readJson(file2);
56246
- if (!config3.hooks) return false;
56247
- let changed2 = false;
56248
- for (const [event, entries] of Object.entries(config3.hooks)) {
56249
- const kept = entries.filter((h) => !isOurs(h));
56250
- if (kept.length !== entries.length) changed2 = true;
56251
- if (kept.length) config3.hooks[event] = kept;
56252
- else delete config3.hooks[event];
56253
- }
56254
- if (!changed2) return false;
56255
- if (!Object.keys(config3.hooks).length && Object.keys(config3).every((k) => k === "version" || k === "hooks")) (0, import_node_fs2.rmSync)(file2);
56256
- else writeJson(file2, config3);
56257
- return true;
56258
- }
56259
- const config2 = readJson(file2);
56260
- if (!config2.hooks) return false;
56261
- let changed = false;
56262
- for (const [event, groups] of Object.entries(config2.hooks)) {
56263
- const kept = groups.map((g) => ({ ...g, hooks: g.hooks.filter((h) => !isOurs(h)) })).filter((g) => g.hooks.length);
56264
- if (kept.length !== groups.length || kept.some((g, i) => g.hooks.length !== groups[i].hooks.length)) changed = true;
56265
- if (kept.length) config2.hooks[event] = kept;
56266
- else delete config2.hooks[event];
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
+ }
56267
56729
  }
56268
- if (changed) writeJson(file2, config2);
56269
- 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+$/, "");
56270
56740
  }
56271
56741
  function readJson(file2) {
56272
56742
  return (0, import_node_fs2.existsSync)(file2) ? JSON.parse((0, import_node_fs2.readFileSync)(file2, "utf8")) : {};
@@ -56330,7 +56800,7 @@ function openBrowser(url2) {
56330
56800
  init_notify();
56331
56801
 
56332
56802
  // src/cli/help.ts
56333
- var HARNESS_LIST = "claude-code, codex, copilot or opencode";
56803
+ var HARNESS_LIST = "claude-code, codex, copilot, opencode, amp, droid, gemini, qwen, cursor, antigravity, kimi, goose or crush";
56334
56804
  var COMMANDS = {
56335
56805
  login: {
56336
56806
  short: "sign in to a hub",
@@ -56385,13 +56855,17 @@ var COMMANDS = {
56385
56855
  details: [
56386
56856
  "--scope user (the default) writes your own settings; --scope project writes the repo's local settings.",
56387
56857
  "Claude Code: ~/.claude/settings.json (or .claude/settings.local.json), including the Stop hook that wakes an idle session.",
56388
- "Codex: ~/.codex/hooks.json; then trust the hooks in Codex's /hooks. Copilot CLI: ~/.copilot/hooks/agenthub.json. OpenCode: a plugin in ~/.config/opencode/plugins/.",
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.",
56389
56861
  "Re-running install updates the hooks; uninstall removes only ours."
56390
56862
  ],
56391
56863
  examples: [
56392
56864
  ["agenthub hooks install claude-code", "for all your Claude Code sessions"],
56393
56865
  ["agenthub hooks install codex", "then open /hooks in Codex to trust them"],
56394
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"],
56395
56869
  ["agenthub hooks uninstall claude-code", ""]
56396
56870
  ]
56397
56871
  },
@@ -56407,34 +56881,44 @@ var COMMANDS = {
56407
56881
  ]
56408
56882
  },
56409
56883
  wake: {
56410
- short: "turn waking idle agents on or off, for every repo or one",
56411
- usage: ["agenthub wake [on|off|all] [--project]", "agenthub wake default --project"],
56412
- summary: "Choose whether idle agents are started up when a hub message arrives, in Claude Code, OpenCode and pi.",
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.",
56413
56887
  details: [
56414
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.",
56415
- "--project sets it for the repo you're in only; it wins over your setting for every repo. default --project removes the repo's own setting.",
56416
- "Without on|off|all it shows what applies here and where that comes from. AGENTHUB_WAKE=0|on|all overrides it for one shell."
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."
56417
56893
  ],
56418
56894
  examples: [
56419
56895
  ["agenthub wake", "what applies in this repo"],
56420
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"],
56421
56899
  ["agenthub wake off --project", "not in this repo, still elsewhere"],
56422
- ["agenthub wake all --project", "in this repo, wake for fyi too"],
56423
- ["agenthub wake default --project", "this repo follows your setting again"]
56900
+ ["agenthub wake ask --project", "ask first in this repo only"],
56901
+ ["agenthub wake default --project", "this repo follows your settings again"]
56424
56902
  ]
56425
56903
  },
56426
56904
  notify: {
56427
56905
  short: "print new messages for a hook",
56428
- usage: ["agenthub notify --format <claude-code|codex|copilot|text> [--event <session-start|prompt|post-tool|stop|session-end>]"],
56906
+ usage: ["agenthub notify --format <harness|text> [--event <session-start|prompt|post-tool|stop|session-end>]"],
56429
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.",
56430
- details: ["It never fails the harness: off the hub, signed out or on any error it prints nothing. Stop marks the agent idle and session-end takes it offline."],
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
+ ],
56431
56912
  examples: [[`echo '{"hook_event_name":"UserPromptSubmit","session_id":"abc"}' | agenthub notify --format text`, "see what a hook would print"]]
56432
56913
  },
56433
56914
  wait: {
56434
56915
  short: "wait for a message that should wake an idle agent",
56435
- usage: ["agenthub wait [--max <seconds>]"],
56916
+ usage: ["agenthub wait [--max <seconds>] [--deliver kimi]"],
56436
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.",
56437
- details: ["The next prompt, a newer waiter for the same session, or --max (12 hours by default) stops it. `agenthub wake` decides whether it waits at all."],
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
+ ],
56438
56922
  examples: [[`echo '{"session_id":"abc"}' | agenthub wait --max 60; echo $?`, "wait up to a minute; 2 means it would wake the agent"]]
56439
56923
  },
56440
56924
  admin: {
@@ -56458,6 +56942,7 @@ var ENVIRONMENT = `Environment:
56458
56942
  AGENTHUB_URL the hub (else the one from agenthub login)
56459
56943
  AGENTHUB_AGENT_NAME a fixed agent name for hooks (default: one per harness session)
56460
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
56461
56946
  AGENTHUB_CREDENTIAL_STORE=file|keychain|auto
56462
56947
  AGENTHUB_HOME config directory (default ~/.config/agenthub)
56463
56948
  AGENTHUB_DOWNLOAD_URL where releases come from
@@ -56497,9 +56982,9 @@ function commandHelp(name) {
56497
56982
  init_settings();
56498
56983
  var import_meta = {};
56499
56984
  function version2() {
56500
- if (true) return "0.3.1";
56985
+ if (true) return "0.3.2";
56501
56986
  try {
56502
- return JSON.parse((0, import_node_fs10.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;
56503
56988
  } catch {
56504
56989
  return "unknown";
56505
56990
  }
@@ -56568,8 +57053,10 @@ async function main(argv) {
56568
57053
  const format = flag(args, "--format") ?? "text";
56569
57054
  const event = flag(args, "--event");
56570
57055
  const payload = await readStdin(500);
56571
- const out = await notify({ format, event, payload }, { store: credentialStore(), version: version2() });
56572
- if (out) process.stdout.write(out + "\n");
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;
56573
57060
  } catch {
56574
57061
  }
56575
57062
  return 0;
@@ -56584,20 +57071,25 @@ async function main(argv) {
56584
57071
  return watch2({ json: json2, agentName, signal: ac.signal }, { store: credentialStore(), write: (l) => console.log(l) });
56585
57072
  }
56586
57073
  if (cmd === "wake") {
56587
- const { wakeSetting: wakeSetting2 } = await Promise.resolve().then(() => (init_wait(), wait_exports));
57074
+ const { actSetting: actSetting2, wakeSetting: wakeSetting2 } = await Promise.resolve().then(() => (init_wait(), wait_exports));
56588
57075
  const { gitRemotes: gitRemotes2 } = await Promise.resolve().then(() => (init_notify(), notify_exports));
56589
57076
  const perProject = args.includes("--project");
56590
57077
  const value = args.filter((a) => a !== "--project").shift();
56591
57078
  const origin = gitRemotes2(process.cwd()).origin;
56592
57079
  const key = origin ? projectKey(origin) : void 0;
56593
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 };
56594
57084
  if (value === void 0) {
56595
- const { mode, from } = wakeSetting2(process.env, origin);
56596
- const source = { env: " (from AGENTHUB_WAKE)", project: ` (this repo's setting, ${key})`, user: " (your setting for every repo)", default: " (the default)" }[from];
56597
- console.log(describe3(mode) + source);
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]);
56598
57089
  return 0;
56599
57090
  }
56600
- if (!["on", "off", "all", ...perProject ? ["default"] : []].includes(value)) {
57091
+ const isAct = value === "ask" || value === "auto";
57092
+ if (!["on", "off", "all", "ask", "auto", ...perProject ? ["default"] : []].includes(value)) {
56601
57093
  console.error(commandHelp("wake"));
56602
57094
  return 2;
56603
57095
  }
@@ -56608,28 +57100,52 @@ async function main(argv) {
56608
57100
  return 1;
56609
57101
  }
56610
57102
  const projects = { ...settings.wake_projects };
56611
- if (value === "default") delete projects[key];
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;
56612
57108
  else projects[key] = value;
56613
- writeSettings({ ...settings, wake_projects: projects });
57109
+ writeSettings({ ...settings, wake_projects: projects, wake_act_projects: actProjects });
57110
+ } else if (isAct) {
57111
+ writeSettings({ ...settings, wake_act: value });
56614
57112
  } else {
56615
57113
  writeSettings({ ...settings, wake: value });
56616
57114
  }
56617
57115
  const where = perProject ? `in ${key}` : "in every repo without its own setting";
56618
- if (value === "default") console.log(`${key} now follows your setting for every repo: ${describe3(wakeSetting2({ ...process.env, AGENTHUB_WAKE: void 0 }, origin).mode)}.`);
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.`);
56619
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.`);
56620
- else console.log(`Idle agents will be woken for ${value === "all" ? "every hub message, fyi too" : "requests and replies"} ${where}, in Claude Code, OpenCode and pi.`);
56621
- if (process.env.AGENTHUB_WAKE !== void 0) console.log(`AGENTHUB_WAKE=${process.env.AGENTHUB_WAKE} is set in this shell and wins over this setting.`);
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.`);
56622
57126
  return 0;
56623
57127
  }
56624
57128
  if (cmd === "wait") {
56625
57129
  try {
56626
57130
  const max = Number(flag(args, "--max"));
56627
- const payload = await readStdin(500);
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
+ }
56628
57139
  const { wait: wait2 } = await Promise.resolve().then(() => (init_wait(), wait_exports));
56629
57140
  const ac = new AbortController();
56630
57141
  process.once("SIGINT", () => ac.abort());
56631
57142
  process.once("SIGTERM", () => ac.abort());
56632
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
+ }
56633
57149
  if (result.code === 2 && result.text) {
56634
57150
  process.stdout.write(result.text + "\n");
56635
57151
  return 2;
@@ -56707,13 +57223,22 @@ version: ${version2()}${newer}`);
56707
57223
  return 2;
56708
57224
  }
56709
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
+ }
56710
57230
  const file2 = hookFile(h, scope, process.cwd());
56711
57231
  if (action === "install") {
56712
57232
  const events = installHooks(h, file2);
56713
57233
  console.log(`Added agenthub notify to ${events.join(", ")} in ${file2}`);
56714
57234
  if (h === "codex") console.log("Codex skips new hooks until you trust them: open /hooks in Codex and review them.");
56715
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.");
56716
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.`);
56717
57242
  } else {
56718
57243
  console.log(uninstallHooks(h, file2) ? `Removed agenthub hooks from ${file2}` : `No agenthub hooks in ${file2}`);
56719
57244
  }