@theagenthub/cli 0.3.0 → 0.3.1

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 +757 -215
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
- # agenthub
1
+ # @theagenthub/cli
2
2
 
3
3
  The [Agent Hub](https://theagenthub.app) CLI. It signs you in to a hub and runs the hooks that bring teammates' agents' messages into your coding agent.
4
4
 
5
5
  ```sh
6
- npm install -g @theagenthub/cli # needs Node 22; without Node: curl -fsSL https://get.theagenthub.app | sh
6
+ npm install -g @theagenthub/cli # needs Node 22; without Node: curl -fsSL https://get.theagenthub.app | sh
7
7
  agenthub login --hub https://hub.theagenthub.app
8
8
  agenthub hooks install claude-code # or codex, copilot, opencode
9
9
  ```
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,12 +463,349 @@ var init_update = __esm({
453
463
  }
454
464
  });
455
465
 
466
+ // src/cli/watch.ts
467
+ var watch_exports = {};
468
+ __export(watch_exports, {
469
+ readEvents: () => readEvents,
470
+ watch: () => watch
471
+ });
472
+ async function watch(opts, deps) {
473
+ const env = deps.env ?? process.env;
474
+ const fetchFn = deps.fetch ?? fetch;
475
+ const sleep = deps.sleep ?? defaultSleep;
476
+ const hub = hubUrl(env);
477
+ if (!hub) {
478
+ deps.write("No hub configured. Run agenthub login --hub <url>.");
479
+ return 1;
480
+ }
481
+ const { origin, upstream } = (deps.remotes ?? gitRemotes)(deps.cwd ?? process.cwd());
482
+ if (!origin) {
483
+ deps.write("Not in a git repo with an origin remote.");
484
+ return 1;
485
+ }
486
+ const agentName = opts.agentName ?? env.AGENTHUB_AGENT_NAME;
487
+ const query = new URLSearchParams({ project: origin, ...upstream ? { upstream } : {}, ...agentName ? { agent_name: agentName } : {} });
488
+ let backoff = 1e3;
489
+ let saidWaiting = false;
490
+ let waitingApproval = false;
491
+ while (!opts.signal?.aborted) {
492
+ const token = await accessToken(hub, { store: deps.store, fetch: fetchFn, env });
493
+ if (!token) {
494
+ deps.write("Not signed in. Run agenthub login.");
495
+ return 1;
496
+ }
497
+ let res;
498
+ try {
499
+ res = await fetchFn(`${hub}/api/stream?${query}`, {
500
+ headers: { Authorization: `Bearer ${token}`, Accept: "text/event-stream" },
501
+ ...opts.signal ? { signal: opts.signal } : {}
502
+ });
503
+ } catch {
504
+ if (opts.signal?.aborted) break;
505
+ await sleep(backoff, opts.signal);
506
+ backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
507
+ continue;
508
+ }
509
+ if (res.status === 404) {
510
+ const { membership } = await res.json().catch(() => ({}));
511
+ if (membership === "pending") {
512
+ if (!waitingApproval) deps.write(opts.json ? JSON.stringify({ membership: "pending" }) : "Your request to join is waiting for a project admin's approval.");
513
+ waitingApproval = true;
514
+ } else {
515
+ if (waitingApproval) {
516
+ if (membership === "active") deps.write(opts.json ? JSON.stringify({ membership: "approved" }) : "A project admin approved you: ask your agent to call join_project.");
517
+ else deps.write(opts.json ? JSON.stringify({ membership: "declined" }) : "A project admin turned down your request to join.");
518
+ waitingApproval = false;
519
+ saidWaiting = true;
520
+ }
521
+ if (!saidWaiting) deps.write("No agent of yours has joined this project yet; waiting.");
522
+ saidWaiting = true;
523
+ }
524
+ await sleep(backoff, opts.signal);
525
+ backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
526
+ continue;
527
+ }
528
+ if (res.status === 403 || res.status === 409 || res.status === 410) {
529
+ const body = await res.json().catch(() => ({}));
530
+ deps.write(body.message ?? `The hub refused the stream (${res.status}).`);
531
+ return 1;
532
+ }
533
+ if (!res.ok || !res.body) {
534
+ await res.body?.cancel();
535
+ await sleep(backoff, opts.signal);
536
+ backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
537
+ continue;
538
+ }
539
+ backoff = 1e3;
540
+ saidWaiting = false;
541
+ let stop;
542
+ try {
543
+ for await (const ev of readEvents(res.body)) {
544
+ if (ev.event === "inbox") {
545
+ const data = JSON.parse(ev.data);
546
+ if (opts.json) deps.write(JSON.stringify(data));
547
+ else {
548
+ const text = formatInbox(data);
549
+ if (text) deps.write(text);
550
+ }
551
+ } else if (ev.event === "end") {
552
+ const { reason } = JSON.parse(ev.data);
553
+ if (reason === "replaced") {
554
+ deps.write("Another session joined with this agent name; stopping.");
555
+ stop = 1;
556
+ } else if (reason === "revoked") {
557
+ deps.write("This agent lost access to the project; stopping.");
558
+ stop = 1;
559
+ } else if (reason === "left") {
560
+ if (opts.json) deps.write(JSON.stringify({ left: true }));
561
+ else deps.write("The agent left the project; waiting for it to join again.");
562
+ }
563
+ }
564
+ }
565
+ } catch {
566
+ }
567
+ if (stop !== void 0) return stop;
568
+ }
569
+ return 0;
570
+ }
571
+ async function* readEvents(body) {
572
+ const decoder = new TextDecoder();
573
+ let buf = "";
574
+ for await (const chunk of body) {
575
+ buf += decoder.decode(chunk, { stream: true });
576
+ let i;
577
+ while ((i = buf.indexOf("\n\n")) >= 0) {
578
+ const block = buf.slice(0, i);
579
+ buf = buf.slice(i + 2);
580
+ let event = "message";
581
+ const data = [];
582
+ for (const line of block.split("\n")) {
583
+ if (line.startsWith("event:")) event = line.slice(6).trim();
584
+ else if (line.startsWith("data:")) data.push(line.slice(5).replace(/^ /, ""));
585
+ }
586
+ if (data.length) yield { event, data: data.join("\n") };
587
+ }
588
+ }
589
+ }
590
+ function defaultSleep(ms, signal) {
591
+ return new Promise((resolve) => {
592
+ const t = setTimeout(resolve, ms);
593
+ signal?.addEventListener("abort", () => {
594
+ clearTimeout(t);
595
+ resolve();
596
+ }, { once: true });
597
+ });
598
+ }
599
+ var MAX_BACKOFF_MS;
600
+ var init_watch = __esm({
601
+ "src/cli/watch.ts"() {
602
+ "use strict";
603
+ init_notify();
604
+ init_session();
605
+ init_settings();
606
+ MAX_BACKOFF_MS = 3e4;
607
+ }
608
+ });
609
+
610
+ // src/cli/wait.ts
611
+ var wait_exports = {};
612
+ __export(wait_exports, {
613
+ DEFAULT_WAIT_MS: () => DEFAULT_WAIT_MS,
614
+ addPending: () => addPending,
615
+ claimFile: () => claimFile,
616
+ mergeInbox: () => mergeInbox,
617
+ pendingFile: () => pendingFile,
618
+ releaseWaiter: () => releaseWaiter,
619
+ takePending: () => takePending,
620
+ wait: () => wait,
621
+ wakeMode: () => wakeMode,
622
+ wakeSetting: () => wakeSetting
623
+ });
624
+ function wakeSetting(env, origin) {
625
+ const settings = readSettings(env);
626
+ const key = origin ? projectKey(origin) : void 0;
627
+ const project = key ? settings.wake_projects?.[key] : void 0;
628
+ const [raw, from] = env.AGENTHUB_WAKE !== void 0 ? [env.AGENTHUB_WAKE, "env"] : project ? [project, "project"] : settings.wake ? [settings.wake, "user"] : ["", "default"];
629
+ const v = raw.trim().toLowerCase();
630
+ const mode = ["0", "false", "off", "no"].includes(v) ? "off" : v === "all" ? "all" : "requests";
631
+ return { mode, from };
632
+ }
633
+ function wakeMode(env, origin) {
634
+ return wakeSetting(env, origin).mode;
635
+ }
636
+ function claimFile(env, key) {
637
+ return (0, import_node_path7.join)(cacheDir(env), `wait-${key}.json`);
638
+ }
639
+ function pendingFile(env, key) {
640
+ return (0, import_node_path7.join)(cacheDir(env), `pending-${key}.json`);
641
+ }
642
+ function releaseWaiter(env, key) {
643
+ (0, import_node_fs6.rmSync)(claimFile(env, key), { force: true });
644
+ }
645
+ function takePending(env, key) {
646
+ const file2 = pendingFile(env, key);
647
+ try {
648
+ const pending = JSON.parse((0, import_node_fs6.readFileSync)(file2, "utf8"));
649
+ (0, import_node_fs6.rmSync)(file2, { force: true });
650
+ return pending;
651
+ } catch {
652
+ return void 0;
653
+ }
654
+ }
655
+ function addPending(env, key, inbox) {
656
+ if (!inbox.messages.length && !inbox.events.length) return;
657
+ const file2 = pendingFile(env, key);
658
+ let before;
659
+ try {
660
+ before = JSON.parse((0, import_node_fs6.readFileSync)(file2, "utf8"));
661
+ } catch {
662
+ before = void 0;
663
+ }
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 });
666
+ }
667
+ function mergeInbox(earlier, later) {
668
+ if (!earlier) return later;
669
+ return {
670
+ ...later,
671
+ messages: [...earlier.messages, ...later.messages],
672
+ events: [...earlier.events, ...later.events]
673
+ };
674
+ }
675
+ async function wait(opts, deps) {
676
+ const env = deps.env ?? process.env;
677
+ const fetchFn = deps.fetch ?? fetch;
678
+ const now = deps.now ?? Date.now;
679
+ const sleep = deps.sleep ?? defaultSleep2;
680
+ const cwd = typeof opts.payload?.cwd === "string" ? opts.payload.cwd : deps.cwd ?? process.cwd();
681
+ const hub = hubUrl(env);
682
+ if (!hub) return NOTHING;
683
+ const { origin, upstream } = (deps.remotes ?? gitRemotes)(cwd);
684
+ if (!origin) return NOTHING;
685
+ const mode = wakeMode(env, origin);
686
+ if (mode === "off") return NOTHING;
687
+ if (!await onHub(hub, [origin, upstream], fetchFn, env, now).catch(() => false)) return NOTHING;
688
+ const agentName = hookAgentName(opts.payload, env);
689
+ const key = sessionKey(hub, origin, agentName);
690
+ const id = (0, import_node_crypto2.randomBytes)(8).toString("hex");
691
+ 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() }));
694
+ const holds = () => {
695
+ try {
696
+ return JSON.parse((0, import_node_fs6.readFileSync)(claim2, "utf8")).id === id;
697
+ } catch {
698
+ return false;
699
+ }
700
+ };
701
+ const ac = new AbortController();
702
+ const stop = () => ac.abort();
703
+ opts.signal?.addEventListener("abort", stop, { once: true });
704
+ const deadline = setTimeout(stop, opts.maxMs ?? DEFAULT_WAIT_MS);
705
+ const check2 = setInterval(() => {
706
+ if (!holds()) stop();
707
+ }, deps.claimCheckMs ?? 1e3);
708
+ const query = new URLSearchParams({ project: origin, ...upstream ? { upstream } : {}, ...agentName ? { agent_name: agentName } : {}, idle: "1" });
709
+ let backoff = 1e3;
710
+ try {
711
+ while (!ac.signal.aborted) {
712
+ const token = await accessToken(hub, { store: deps.store, fetch: fetchFn, env, now });
713
+ if (!token) return NOTHING;
714
+ let res;
715
+ try {
716
+ res = await fetchFn(`${hub}/api/stream?${query}`, {
717
+ headers: { Authorization: `Bearer ${token}`, Accept: "text/event-stream" },
718
+ signal: ac.signal
719
+ });
720
+ } catch {
721
+ if (ac.signal.aborted) break;
722
+ await sleep(backoff, ac.signal);
723
+ backoff = Math.min(backoff * 2, MAX_BACKOFF_MS2);
724
+ continue;
725
+ }
726
+ if ([401, 403, 404, 409, 410].includes(res.status)) {
727
+ await res.body?.cancel();
728
+ return NOTHING;
729
+ }
730
+ if (!res.ok || !res.body) {
731
+ await res.body?.cancel();
732
+ await sleep(res.status === 429 ? MAX_BACKOFF_MS2 : backoff, ac.signal);
733
+ backoff = Math.min(backoff * 2, MAX_BACKOFF_MS2);
734
+ continue;
735
+ }
736
+ backoff = 1e3;
737
+ try {
738
+ for await (const ev of readEvents(res.body)) {
739
+ if (ev.event === "inbox") {
740
+ const data = JSON.parse(ev.data);
741
+ const wakes = data.messages.some((m) => m.new && (mode === "all" || m.type !== "fyi"));
742
+ if (wakes && holds()) return { code: 2, text: `${formatInbox(mergeInbox(takePending(env, key), data))}
743
+ ${WOKEN}` };
744
+ addPending(env, key, data);
745
+ } else if (ev.event === "end") {
746
+ const { reason } = JSON.parse(ev.data);
747
+ if (reason === "left" || reason === "replaced" || reason === "revoked") return NOTHING;
748
+ }
749
+ }
750
+ } catch {
751
+ }
752
+ }
753
+ return NOTHING;
754
+ } finally {
755
+ clearTimeout(deadline);
756
+ clearInterval(check2);
757
+ opts.signal?.removeEventListener("abort", stop);
758
+ ac.abort();
759
+ if (holds()) (0, import_node_fs6.rmSync)(claim2, { force: true });
760
+ }
761
+ }
762
+ function defaultSleep2(ms, signal) {
763
+ return new Promise((resolve) => {
764
+ const t = setTimeout(resolve, ms);
765
+ signal?.addEventListener("abort", () => {
766
+ clearTimeout(t);
767
+ resolve();
768
+ }, { once: true });
769
+ });
770
+ }
771
+ var import_node_crypto2, import_node_fs6, import_node_path7, DEFAULT_WAIT_MS, MAX_BACKOFF_MS2, NOTHING, WOKEN;
772
+ var init_wait = __esm({
773
+ "src/cli/wait.ts"() {
774
+ "use strict";
775
+ import_node_crypto2 = require("node:crypto");
776
+ import_node_fs6 = require("node:fs");
777
+ import_node_path7 = require("node:path");
778
+ init_paths();
779
+ init_notify();
780
+ init_session();
781
+ init_settings();
782
+ init_watch();
783
+ DEFAULT_WAIT_MS = 12 * 36e5;
784
+ MAX_BACKOFF_MS2 = 6e4;
785
+ NOTHING = { code: 0, text: "" };
786
+ 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
+ }
788
+ });
789
+
456
790
  // src/cli/notify.ts
791
+ var notify_exports = {};
792
+ __export(notify_exports, {
793
+ CARD_NUDGE_MS: () => CARD_NUDGE_MS,
794
+ TASK_CHECK_MS: () => TASK_CHECK_MS,
795
+ formatInbox: () => formatInbox,
796
+ gitBranch: () => gitBranch,
797
+ gitRemotes: () => gitRemotes,
798
+ hookAgentName: () => hookAgentName,
799
+ notify: () => notify,
800
+ onHub: () => onHub,
801
+ sessionKey: () => sessionKey
802
+ });
457
803
  async function notify(opts, deps) {
458
804
  try {
459
805
  const event = opts.event ?? HOOK_EVENTS[String(opts.payload?.hook_event_name ?? "")] ?? "post-tool";
460
806
  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
807
  const [text, notice] = await Promise.all([run(opts, event, deps).catch(() => ""), update.catch(() => "")]);
808
+ if (typeof text === "object") return text.raw;
462
809
  return render(opts.format, event, [text, notice].filter(Boolean).join("\n"));
463
810
  } catch {
464
811
  return "";
@@ -476,14 +823,27 @@ async function run(opts, event, deps) {
476
823
  if (!origin) return "";
477
824
  if (!await onHub(hub, [origin, upstream], fetchFn, env, now)) return "";
478
825
  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 "";
826
+ const throttleKey = sessionKey(hub, origin, agentName);
827
+ if (event !== "stop") releaseWaiter(env, throttleKey);
828
+ const wait2 = THROTTLE_MS[event];
829
+ if (wait2 && !throttle((0, import_node_path8.join)(cacheDir(env), `notify-${throttleKey}.json`), wait2, now())) return "";
482
830
  const token = await accessToken(hub, { store: deps.store, fetch: fetchFn, env, now });
483
831
  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
832
  const who = { project: origin, ...upstream ? { upstream } : {}, ...agentName ? { agent_name: agentName } : {} };
485
833
  const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
486
834
  const signal = AbortSignal.timeout(3e3);
835
+ if (event === "stop" && opts.format === "codex" && wakeMode(env, origin) !== "off") {
836
+ const res2 = await fetchFn(`${hub}/api/inbox?${new URLSearchParams(who)}`, { headers, signal });
837
+ if (res2.ok) {
838
+ const inbox2 = mergeInbox(takePending(env, throttleKey), await res2.json());
839
+ const mode = wakeMode(env, origin);
840
+ 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.` }) };
843
+ }
844
+ addPending(env, throttleKey, inbox2);
845
+ }
846
+ }
487
847
  if (event === "stop" || event === "session-end") {
488
848
  await fetchFn(`${hub}/api/presence`, {
489
849
  method: "POST",
@@ -498,28 +858,81 @@ async function run(opts, event, deps) {
498
858
  const { membership } = await res.json().catch(() => ({}));
499
859
  const args = [`remote="${origin}"`, ...upstream ? [`upstream="${upstream}"`] : [], ...agentName ? [`agent_name="${agentName}"`] : []].join(", ");
500
860
  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`);
861
+ const waitingFile = (0, import_node_path8.join)(cacheDir(env), `waiting-${throttleKey}.json`);
502
862
  const wasWaiting = readFlag(waitingFile);
503
863
  if (membership === "pending") {
504
864
  if (!wasWaiting) writeFlag(waitingFile);
505
865
  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
866
  }
507
867
  if (wasWaiting) {
508
- (0, import_node_fs6.rmSync)(waitingFile, { force: true });
868
+ (0, import_node_fs7.rmSync)(waitingFile, { force: true });
509
869
  if (membership === "active") return out(`Agent Hub: a project admin approved you. Call the agenthub join_project tool with ${args} to join.${keep}`);
510
870
  return out("Agent Hub: a project admin turned down your request to join this project.");
511
871
  }
512
872
  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}`);
873
+ const branch = (deps.branch ?? gitBranch)(cwd);
874
+ 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.`;
875
+ 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
876
  }
515
877
  if (!res.ok) return "";
516
- return out(formatInbox(await res.json()));
878
+ const inbox = await res.json();
879
+ const card = await keepCardCurrent(inbox, event, {
880
+ // Without a name the hub picks the user's latest agent here, which may be another session's.
881
+ branch: agentName ? (deps.branch ?? gitBranch)(cwd) : void 0,
882
+ stateFile: (0, import_node_path8.join)(cacheDir(env), `card-${throttleKey}.json`),
883
+ now: now(),
884
+ post: (fields) => fetchFn(`${hub}/api/card`, { method: "POST", headers, body: JSON.stringify({ ...who, ...fields }), signal })
885
+ }).catch(() => "");
886
+ return out([formatInbox(mergeInbox(takePending(env, throttleKey), inbox)), card].filter(Boolean).join("\n"));
887
+ }
888
+ async function keepCardCurrent(inbox, event, ctx) {
889
+ const card = inbox.card;
890
+ if (!card || card.kind === "bot") return "";
891
+ const lines = [];
892
+ const state = readState(ctx.stateFile);
893
+ const had = typeof card.branch === "string" ? card.branch : void 0;
894
+ if (ctx.branch && ctx.branch !== had) {
895
+ const res = await ctx.post({ branch: ctx.branch });
896
+ 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.`);
897
+ }
898
+ const missing = (inbox.card_missing ?? []).filter((k) => k === "description" || k === "task");
899
+ if (missing.length && (event === "session-start" || ctx.now - (state.nudged ?? 0) >= CARD_NUDGE_MS)) {
900
+ const list = missing.length > 1 ? missing.join(" or ") : missing[0];
901
+ 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.`);
902
+ state.nudged = ctx.now;
903
+ } else if (!missing.length && event === "prompt" && typeof card.task === "string" && ctx.now - (state.taskChecked ?? 0) >= TASK_CHECK_MS) {
904
+ 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.`);
905
+ state.taskChecked = ctx.now;
906
+ }
907
+ if (lines.length) writeState(ctx.stateFile, state);
908
+ return lines.join("\n");
909
+ }
910
+ function readState(path) {
911
+ try {
912
+ return JSON.parse((0, import_node_fs7.readFileSync)(path, "utf8"));
913
+ } catch {
914
+ return {};
915
+ }
916
+ }
917
+ 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));
920
+ }
921
+ function gitBranch(cwd) {
922
+ 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;
924
+ } catch {
925
+ return void 0;
926
+ }
927
+ }
928
+ function sessionKey(hub, origin, agentName) {
929
+ return (0, import_node_crypto3.createHash)("sha256").update(`${hub}|${origin}|${agentName ?? ""}`).digest("hex").slice(0, 24);
517
930
  }
518
931
  function hookAgentName(payload, env) {
519
932
  if (env.AGENTHUB_AGENT_NAME) return env.AGENTHUB_AGENT_NAME;
520
933
  const session = payload?.session_id ?? payload?.sessionId;
521
934
  if (typeof session !== "string" || !session) return void 0;
522
- return `s-${(0, import_node_crypto2.createHash)("sha256").update(session).digest("hex").slice(0, 8)}`;
935
+ return `s-${(0, import_node_crypto3.createHash)("sha256").update(session).digest("hex").slice(0, 8)}`;
523
936
  }
524
937
  function formatInbox(inbox) {
525
938
  const fresh = inbox.messages.filter((m) => m.new);
@@ -557,31 +970,31 @@ function render(format, event, text) {
557
970
  }
558
971
  function readFlag(path) {
559
972
  try {
560
- (0, import_node_fs6.readFileSync)(path);
973
+ (0, import_node_fs7.readFileSync)(path);
561
974
  return true;
562
975
  } catch {
563
976
  return false;
564
977
  }
565
978
  }
566
979
  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, "{}");
980
+ (0, import_node_fs7.mkdirSync)((0, import_node_path8.join)(path, ".."), { recursive: true, mode: 448 });
981
+ (0, import_node_fs7.writeFileSync)(path, "{}");
569
982
  }
570
983
  function throttle(path, waitMs, now) {
571
984
  try {
572
- const last = JSON.parse((0, import_node_fs6.readFileSync)(path, "utf8")).at;
985
+ const last = JSON.parse((0, import_node_fs7.readFileSync)(path, "utf8")).at;
573
986
  if (now - last < waitMs) return false;
574
987
  } catch {
575
988
  }
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 }));
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 }));
578
991
  return true;
579
992
  }
580
993
  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`);
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`);
582
995
  let providers;
583
996
  try {
584
- const cached2 = JSON.parse((0, import_node_fs6.readFileSync)(path, "utf8"));
997
+ const cached2 = JSON.parse((0, import_node_fs7.readFileSync)(path, "utf8"));
585
998
  if (now() - cached2.at < PROVIDERS_TTL_MS) providers = cached2.providers;
586
999
  } catch {
587
1000
  }
@@ -589,8 +1002,8 @@ async function onHub(hub, remotes, fetchFn, env, now) {
589
1002
  const res = await fetchFn(`${hub}/api/providers`, { signal: AbortSignal.timeout(3e3) });
590
1003
  if (!res.ok) return false;
591
1004
  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 }));
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 }));
594
1007
  }
595
1008
  const hosts = /* @__PURE__ */ new Set();
596
1009
  for (const p of providers) {
@@ -618,19 +1031,20 @@ function gitRemotes(cwd) {
618
1031
  const upstream = origin ? get("upstream") : void 0;
619
1032
  return { ...origin ? { origin } : {}, ...upstream ? { upstream } : {} };
620
1033
  }
621
- var import_node_child_process4, import_node_crypto2, import_node_fs6, import_node_path7, HOOK_EVENTS, EVENT_NAMES, THROTTLE_MS, PROVIDERS_TTL_MS;
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;
622
1035
  var init_notify = __esm({
623
1036
  "src/cli/notify.ts"() {
624
1037
  "use strict";
625
1038
  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");
1039
+ import_node_crypto3 = require("node:crypto");
1040
+ import_node_fs7 = require("node:fs");
1041
+ import_node_path8 = require("node:path");
629
1042
  init_registry();
630
1043
  init_paths();
631
1044
  init_session();
632
1045
  init_settings();
633
1046
  init_update();
1047
+ init_wait();
634
1048
  HOOK_EVENTS = {
635
1049
  SessionStart: "session-start",
636
1050
  UserPromptSubmit: "prompt",
@@ -647,150 +1061,8 @@ var init_notify = __esm({
647
1061
  };
648
1062
  THROTTLE_MS = { "post-tool": 3e4, prompt: 1e4 };
649
1063
  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;
1064
+ CARD_NUDGE_MS = 10 * 6e4;
1065
+ TASK_CHECK_MS = 30 * 6e4;
794
1066
  }
795
1067
  });
796
1068
 
@@ -3664,16 +3936,16 @@ var require_connect = __commonJS({
3664
3936
  }
3665
3937
  });
3666
3938
  }
3667
- get(sessionKey) {
3668
- const ref = this._sessionCache.get(sessionKey);
3939
+ get(sessionKey2) {
3940
+ const ref = this._sessionCache.get(sessionKey2);
3669
3941
  return ref ? ref.deref() : null;
3670
3942
  }
3671
- set(sessionKey, session) {
3943
+ set(sessionKey2, session) {
3672
3944
  if (this._maxCachedSessions === 0) {
3673
3945
  return;
3674
3946
  }
3675
- if (this._sessionCache.has(sessionKey)) {
3676
- this._sessionCache.delete(sessionKey);
3947
+ if (this._sessionCache.has(sessionKey2)) {
3948
+ this._sessionCache.delete(sessionKey2);
3677
3949
  } else if (this._sessionCache.size >= this._maxCachedSessions) {
3678
3950
  let evicted = false;
3679
3951
  for (const [key, ref] of this._sessionCache) {
@@ -3690,8 +3962,8 @@ var require_connect = __commonJS({
3690
3962
  }
3691
3963
  }
3692
3964
  }
3693
- this._sessionCache.set(sessionKey, new WeakRef(session));
3694
- this._sessionRegistry.register(session, sessionKey);
3965
+ this._sessionCache.set(sessionKey2, new WeakRef(session));
3966
+ this._sessionRegistry.register(session, sessionKey2);
3695
3967
  }
3696
3968
  };
3697
3969
  function buildConnector({ allowH2, useH2c, maxCachedSessions, socketPath, timeout, session: customSession, ...opts }) {
@@ -3709,9 +3981,9 @@ var require_connect = __commonJS({
3709
3981
  tls = require("node:tls");
3710
3982
  }
3711
3983
  servername = servername || options.servername || util.getServerName(host) || null;
3712
- const sessionKey = servername || hostname3;
3713
- assert2(sessionKey);
3714
- const session = customSession || sessionCache.get(sessionKey) || null;
3984
+ const sessionKey2 = servername || hostname3;
3985
+ assert2(sessionKey2);
3986
+ const session = customSession || sessionCache.get(sessionKey2) || null;
3715
3987
  port = port || 443;
3716
3988
  socket = tls.connect({
3717
3989
  highWaterMark: 16384,
@@ -3727,7 +3999,7 @@ var require_connect = __commonJS({
3727
3999
  host: hostname3
3728
4000
  });
3729
4001
  socket.on("session", function(session2) {
3730
- sessionCache.set(sessionKey, session2);
4002
+ sessionCache.set(sessionKey2, session2);
3731
4003
  });
3732
4004
  } else {
3733
4005
  assert2(!httpSocket, "httpSocket can only be sent on TLS update");
@@ -10903,7 +11175,7 @@ var require_socks5_client = __commonJS({
10903
11175
  USERNAME_PASSWORD: 2,
10904
11176
  NO_ACCEPTABLE: 255
10905
11177
  };
10906
- var COMMANDS = {
11178
+ var COMMANDS2 = {
10907
11179
  CONNECT: 1,
10908
11180
  BIND: 2,
10909
11181
  UDP_ASSOCIATE: 3
@@ -11105,7 +11377,7 @@ var require_socks5_client = __commonJS({
11105
11377
  }
11106
11378
  debug("connecting to", address, port);
11107
11379
  this.state = STATES.CONNECTING;
11108
- const request = this.buildConnectRequest(COMMANDS.CONNECT, address, port);
11380
+ const request = this.buildConnectRequest(COMMANDS2.CONNECT, address, port);
11109
11381
  this.socket.write(request);
11110
11382
  }
11111
11383
  /**
@@ -11210,7 +11482,7 @@ var require_socks5_client = __commonJS({
11210
11482
  module2.exports = {
11211
11483
  Socks5Client,
11212
11484
  AUTH_METHODS,
11213
- COMMANDS,
11485
+ COMMANDS: COMMANDS2,
11214
11486
  ADDRESS_TYPES,
11215
11487
  REPLY_CODES,
11216
11488
  STATES
@@ -27432,7 +27704,7 @@ function dispatcherFor(instance) {
27432
27704
  if (!instance.caBundlePath) return void 0;
27433
27705
  let d = dispatchers.get(instance.id);
27434
27706
  if (!d) {
27435
- const ca = (0, import_node_fs7.readFileSync)(instance.caBundlePath, "utf8");
27707
+ const ca = (0, import_node_fs8.readFileSync)(instance.caBundlePath, "utf8");
27436
27708
  d = new import_undici.Agent({ connect: { ca: [...import_node_tls.rootCertificates, ca] } });
27437
27709
  dispatchers.set(instance.id, d);
27438
27710
  }
@@ -27449,11 +27721,11 @@ function fetchFor(instance, override) {
27449
27721
  signal: AbortSignal.timeout(1e4)
27450
27722
  });
27451
27723
  }
27452
- var import_node_fs7, import_node_tls, import_undici, dispatchers;
27724
+ var import_node_fs8, import_node_tls, import_undici, dispatchers;
27453
27725
  var init_http = __esm({
27454
27726
  "src/providers/http.ts"() {
27455
27727
  "use strict";
27456
- import_node_fs7 = require("node:fs");
27728
+ import_node_fs8 = require("node:fs");
27457
27729
  import_node_tls = require("node:tls");
27458
27730
  import_undici = __toESM(require_undici(), 1);
27459
27731
  dispatchers = /* @__PURE__ */ new Map();
@@ -55332,7 +55604,7 @@ var init_types = __esm({
55332
55604
 
55333
55605
  // src/providers/config.ts
55334
55606
  function loadConfigFile(path, opts = {}) {
55335
- const read = opts.readFile ?? ((p) => (0, import_node_fs8.readFileSync)(p, "utf8"));
55607
+ const read = opts.readFile ?? ((p) => (0, import_node_fs9.readFileSync)(p, "utf8"));
55336
55608
  let text;
55337
55609
  try {
55338
55610
  text = read(path);
@@ -55343,7 +55615,7 @@ function loadConfigFile(path, opts = {}) {
55343
55615
  }
55344
55616
  function buildConfig(raw, opts = {}) {
55345
55617
  const env = opts.env ?? process.env;
55346
- const read = opts.readFile ?? ((p) => (0, import_node_fs8.readFileSync)(p, "utf8"));
55618
+ const read = opts.readFile ?? ((p) => (0, import_node_fs9.readFileSync)(p, "utf8"));
55347
55619
  const parsed = configSchema.safeParse(raw);
55348
55620
  if (!parsed.success) {
55349
55621
  throw new ConfigError(
@@ -55524,11 +55796,11 @@ function checkNoHostCollisions(instances) {
55524
55796
  for (const a of inst.remoteAliases) claim2(a.port ? `${a.host}:${a.port}` : a.host, inst.id);
55525
55797
  }
55526
55798
  }
55527
- var import_node_fs8, import_yaml, secretRef, oauthSchema, providerSchema, configSchema, ConfigError;
55799
+ var import_node_fs9, import_yaml, secretRef, oauthSchema, providerSchema, configSchema, ConfigError;
55528
55800
  var init_config = __esm({
55529
55801
  "src/providers/config.ts"() {
55530
55802
  "use strict";
55531
- import_node_fs8 = require("node:fs");
55803
+ import_node_fs9 = require("node:fs");
55532
55804
  import_yaml = __toESM(require_dist(), 1);
55533
55805
  init_zod();
55534
55806
  init_defaults();
@@ -55629,7 +55901,7 @@ var init_admin = __esm({
55629
55901
  });
55630
55902
 
55631
55903
  // src/cli/index.ts
55632
- var import_node_fs9 = require("node:fs");
55904
+ var import_node_fs10 = require("node:fs");
55633
55905
 
55634
55906
  // src/cli/credentials.ts
55635
55907
  var import_node_child_process = require("node:child_process");
@@ -55788,15 +56060,52 @@ function notify(event, sessionID, cwd) {
55788
56060
  });
55789
56061
  }
55790
56062
 
56063
+ /** Runs \`agenthub wait\`; resolves to the text to wake the session with, or "" when it shouldn't. */
56064
+ function waitForMessage(sessionID, cwd, children) {
56065
+ return new Promise((resolve) => {
56066
+ let out = "";
56067
+ let child;
56068
+ try {
56069
+ child = spawn(COMMAND, ["wait"], { cwd, stdio: ["pipe", "pipe", "ignore"], windowsHide: true, shell: process.platform === "win32" });
56070
+ } catch {
56071
+ return resolve("");
56072
+ }
56073
+ children.get(sessionID)?.kill();
56074
+ children.set(sessionID, child);
56075
+ child.stdout.on("data", (chunk) => (out += chunk));
56076
+ child.on("error", () => resolve(""));
56077
+ child.on("close", (code) => {
56078
+ if (children.get(sessionID) === child) children.delete(sessionID);
56079
+ // An agenthub too old to know \`wait\` exits 2 too, but prints nothing on stdout.
56080
+ resolve(code === 2 ? out.trim() : "");
56081
+ });
56082
+ child.stdin.on("error", () => {});
56083
+ child.stdin.end(JSON.stringify({ session_id: sessionID, cwd }));
56084
+ });
56085
+ }
56086
+
55791
56087
  function partId() {
55792
56088
  return "prt_" + Date.now().toString(16) + Math.random().toString(36).slice(2, 12);
55793
56089
  }
55794
56090
 
55795
- export const AgentHub = async ({ directory }) => {
56091
+ export const AgentHub = async ({ directory, client }) => {
55796
56092
  const started = new Set();
56093
+ const waiting = new Map();
55797
56094
  let presence = Promise.resolve();
56095
+ const wake = async (sessionID) => {
56096
+ const send = client?.session?.promptAsync ?? client?.session?.prompt;
56097
+ if (!send) return;
56098
+ const text = await waitForMessage(sessionID, directory, waiting);
56099
+ if (!text) return;
56100
+ try {
56101
+ await send.call(client.session, { path: { id: sessionID }, body: { parts: [{ type: "text", text }] } });
56102
+ } catch {
56103
+ /* the session is gone, or busy again */
56104
+ }
56105
+ };
55798
56106
  return {
55799
56107
  "chat.message": async (input, output) => {
56108
+ waiting.get(input.sessionID)?.kill();
55800
56109
  const first = !started.has(input.sessionID);
55801
56110
  started.add(input.sessionID);
55802
56111
  const text = await notify(first ? "session-start" : "prompt", input.sessionID, directory);
@@ -55811,8 +56120,15 @@ export const AgentHub = async ({ directory }) => {
55811
56120
  // Presence updates don't block OpenCode, but run one at a time so "offline" can't be
55812
56121
  // overtaken by an earlier "idle".
55813
56122
  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));
56123
+ if (event.type === "session.idle") {
56124
+ const sessionID = event.properties.sessionID;
56125
+ presence = presence.then(() => notify("stop", sessionID, directory));
56126
+ // Not awaited: it waits as long as the session is idle.
56127
+ presence.then(() => wake(sessionID)).catch(() => {});
56128
+ } else if (event.type === "session.deleted") {
56129
+ waiting.get(event.properties.info.id)?.kill();
56130
+ presence = presence.then(() => notify("session-end", event.properties.info.id, directory));
56131
+ }
55816
56132
  },
55817
56133
  };
55818
56134
  };
@@ -55869,6 +56185,8 @@ var COPILOT_EVENTS = [
55869
56185
  ["agentStop", "stop"],
55870
56186
  ["sessionEnd", "session-end"]
55871
56187
  ];
56188
+ var WAIT_MAX_S = 12 * 3600;
56189
+ var WAIT_TIMEOUT_S = WAIT_MAX_S + 60;
55872
56190
  var TIMEOUT_S = { "claude-code": {}, codex: { SessionEnd: 3 } };
55873
56191
  function hookFile(harness, scope, cwd, home = (0, import_node_os2.homedir)(), env = process.env) {
55874
56192
  const user = scope === "user";
@@ -55883,7 +56201,7 @@ function hookFile(harness, scope, cwd, home = (0, import_node_os2.homedir)(), en
55883
56201
  return user ? (0, import_node_path3.join)(env.XDG_CONFIG_HOME || (0, import_node_path3.join)(home, ".config"), "opencode", "plugins", "agenthub.js") : (0, import_node_path3.join)(cwd, ".opencode", "plugins", "agenthub.js");
55884
56202
  }
55885
56203
  }
55886
- var OURS = [/^(?:\S*[\\/])?agenthub(?:\.js|\.exe)?\s+notify\b/i, /^"(?:[^"]*[\\/])?agenthub(?:\.js|\.exe)?"\s+notify\b/i];
56204
+ var OURS = [/^(?:\S*[\\/])?agenthub(?:\.js|\.exe)?\s+(?:notify|wait)\b/i, /^"(?:[^"]*[\\/])?agenthub(?:\.js|\.exe)?"\s+(?:notify|wait)\b/i];
55887
56205
  var isOurs = (h) => typeof h.command === "string" && OURS.some((r) => r.test(h.command));
55888
56206
  function installHooks(harness, file2, command = "agenthub") {
55889
56207
  (0, import_node_fs2.mkdirSync)((0, import_node_path3.dirname)(file2), { recursive: true });
@@ -55908,6 +56226,9 @@ function installHooks(harness, file2, command = "agenthub") {
55908
56226
  const groups = (hooks[event] ?? []).map((g) => ({ ...g, hooks: g.hooks.filter((h) => !isOurs(h)) })).filter((g) => g.hooks.length);
55909
56227
  const entry = { hooks: [{ type: "command", command: `${command} notify --format ${harness}`, timeout: TIMEOUT_S[harness][event] ?? 10 }] };
55910
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
+ }
55911
56232
  hooks[event] = [...groups, entry];
55912
56233
  }
55913
56234
  writeJson(file2, config2);
@@ -56007,34 +56328,182 @@ function openBrowser(url2) {
56007
56328
 
56008
56329
  // src/cli/index.ts
56009
56330
  init_notify();
56331
+
56332
+ // src/cli/help.ts
56333
+ var HARNESS_LIST = "claude-code, codex, copilot or opencode";
56334
+ var COMMANDS = {
56335
+ login: {
56336
+ short: "sign in to a hub",
56337
+ usage: ["agenthub login [--hub <url>]"],
56338
+ summary: "Sign in to a hub with the device flow.",
56339
+ details: [
56340
+ "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.",
56341
+ "The hub is remembered, so later commands and the hooks don't need --hub. Hooks refresh the token themselves."
56342
+ ],
56343
+ examples: [
56344
+ ["agenthub login --hub https://hub.theagenthub.app", "sign in to the public hub"],
56345
+ ["agenthub login --hub https://hub.example.com", "sign in to a self-hosted hub"],
56346
+ ["agenthub login", "sign in again to the hub you used last"]
56347
+ ]
56348
+ },
56349
+ logout: {
56350
+ short: "forget this machine's token",
56351
+ usage: ["agenthub logout"],
56352
+ summary: "Forget this machine's token for the current hub.",
56353
+ examples: [["agenthub logout", ""]]
56354
+ },
56355
+ status: {
56356
+ short: "show the hub, sign-in and version",
56357
+ usage: ["agenthub status"],
56358
+ summary: "Show the hub, whether you're signed in, and the CLI version (and whether a newer one is out).",
56359
+ examples: [["agenthub status", ""]]
56360
+ },
56361
+ token: {
56362
+ short: "print a fresh hub access token",
56363
+ usage: ["agenthub token [--json]"],
56364
+ summary: "Print a fresh hub access token, for plugins that call the hub's MCP server (the pi extension uses it).",
56365
+ examples: [
56366
+ ["agenthub token", "just the token"],
56367
+ ["agenthub token --json", '{"hub", "token", "expires_at"}']
56368
+ ]
56369
+ },
56370
+ update: {
56371
+ short: "update this CLI",
56372
+ usage: ["agenthub update [--check] [--version <v>]"],
56373
+ summary: "Replace this CLI with the latest release, after checking it against SHA256SUMS and that it runs.",
56374
+ details: ["A CLI installed with npm can't replace itself; update it the way you installed it."],
56375
+ examples: [
56376
+ ["agenthub update", "install the latest release"],
56377
+ ["agenthub update --check", "only say whether there's a newer one"],
56378
+ ["agenthub update --version v0.3.0", "install a specific release"]
56379
+ ]
56380
+ },
56381
+ hooks: {
56382
+ short: "add or remove the hooks for a harness",
56383
+ usage: [`agenthub hooks install <harness> [--scope user|project]`, `agenthub hooks uninstall <harness> [--scope user|project]`],
56384
+ summary: `Add the hooks that bring hub messages into a harness's sessions, for ${HARNESS_LIST}. Not needed with a harness plugin.`,
56385
+ details: [
56386
+ "--scope user (the default) writes your own settings; --scope project writes the repo's local settings.",
56387
+ "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/.",
56389
+ "Re-running install updates the hooks; uninstall removes only ours."
56390
+ ],
56391
+ examples: [
56392
+ ["agenthub hooks install claude-code", "for all your Claude Code sessions"],
56393
+ ["agenthub hooks install codex", "then open /hooks in Codex to trust them"],
56394
+ ["agenthub hooks install opencode --scope project", "only in this repo"],
56395
+ ["agenthub hooks uninstall claude-code", ""]
56396
+ ]
56397
+ },
56398
+ watch: {
56399
+ short: "print new messages as they arrive",
56400
+ usage: ["agenthub watch [--json] [--agent <name>]"],
56401
+ 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.",
56402
+ details: ["Waits for the agent to join, reconnects by itself, and stops when the agent is replaced or loses access."],
56403
+ examples: [
56404
+ ["agenthub watch", "your most recently active agent in this repo"],
56405
+ ["agenthub watch --agent reviewer", "a named agent"],
56406
+ ["agenthub watch --json | jq .messages", "JSON lines, for a script"]
56407
+ ]
56408
+ },
56409
+ 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.",
56413
+ details: [
56414
+ "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."
56417
+ ],
56418
+ examples: [
56419
+ ["agenthub wake", "what applies in this repo"],
56420
+ ["agenthub wake off", "never wake idle agents"],
56421
+ ["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"]
56424
+ ]
56425
+ },
56426
+ notify: {
56427
+ 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>]"],
56429
+ 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."],
56431
+ examples: [[`echo '{"hook_event_name":"UserPromptSubmit","session_id":"abc"}' | agenthub notify --format text`, "see what a hook would print"]]
56432
+ },
56433
+ wait: {
56434
+ short: "wait for a message that should wake an idle agent",
56435
+ usage: ["agenthub wait [--max <seconds>]"],
56436
+ 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."],
56438
+ examples: [[`echo '{"session_id":"abc"}' | agenthub wait --max 60; echo $?`, "wait up to a minute; 2 means it would wake the agent"]]
56439
+ },
56440
+ admin: {
56441
+ short: "check a hub's code host config",
56442
+ usage: ["agenthub admin providers check [--config <file>]", "agenthub admin providers resolve <remote> [--config <file>]"],
56443
+ summary: "For hub operators: test the code hosts in a providers.yaml, or show the project ID a git remote maps to.",
56444
+ details: ["--config defaults to $AGENTHUB_CONFIG, else the built-in public hosts."],
56445
+ examples: [
56446
+ ["agenthub admin providers check --config providers.yaml", ""],
56447
+ ["agenthub admin providers resolve git@github.com:acme/shop.git", ""]
56448
+ ]
56449
+ }
56450
+ };
56451
+ var GROUPS = [
56452
+ ["Set up", ["login", "hooks", "status", "logout"]],
56453
+ ["Day to day", ["watch", "wake", "update"]],
56454
+ ["Used by hooks and plugins", ["notify", "wait", "token"]],
56455
+ ["Hub operators", ["admin"]]
56456
+ ];
56457
+ var ENVIRONMENT = `Environment:
56458
+ AGENTHUB_URL the hub (else the one from agenthub login)
56459
+ AGENTHUB_AGENT_NAME a fixed agent name for hooks (default: one per harness session)
56460
+ AGENTHUB_WAKE=0|on|all whether idle agents are woken; overrides agenthub wake
56461
+ AGENTHUB_CREDENTIAL_STORE=file|keychain|auto
56462
+ AGENTHUB_HOME config directory (default ~/.config/agenthub)
56463
+ AGENTHUB_DOWNLOAD_URL where releases come from
56464
+ AGENTHUB_NO_UPDATE_CHECK=1 no daily "newer version" notice`;
56465
+ function overview() {
56466
+ const width = Math.max(...Object.keys(COMMANDS).map((c) => c.length)) + 2;
56467
+ const lines = ["agenthub: connects your coding agents to Agent Hub, so they can trade reviews, questions and handoffs with teammates' agents.", ""];
56468
+ for (const [title, names] of GROUPS) {
56469
+ lines.push(`${title}:`);
56470
+ for (const n of names) lines.push(` ${n.padEnd(width)}${COMMANDS[n].short}`);
56471
+ lines.push("");
56472
+ }
56473
+ lines.push(
56474
+ "Get started:",
56475
+ " agenthub login --hub https://hub.theagenthub.app",
56476
+ " agenthub hooks install claude-code (or use the Claude Code, Codex, OpenCode or pi plugin instead)",
56477
+ " agenthub status",
56478
+ "",
56479
+ ENVIRONMENT,
56480
+ "",
56481
+ "agenthub help <command> (or agenthub <command> --help) shows its options and examples. agenthub --version prints the version."
56482
+ );
56483
+ return lines.join("\n");
56484
+ }
56485
+ function commandHelp(name) {
56486
+ const c = COMMANDS[name];
56487
+ if (!c) return void 0;
56488
+ const lines = ["usage:", ...c.usage.map((u) => ` ${u}`), "", c.summary];
56489
+ if (c.details?.length) lines.push("", ...c.details);
56490
+ lines.push("", "examples:");
56491
+ for (const [cmd, note] of c.examples) lines.push(` ${cmd}${note ? `
56492
+ ${note}` : ""}`);
56493
+ return lines.join("\n");
56494
+ }
56495
+
56496
+ // src/cli/index.ts
56010
56497
  init_settings();
56011
56498
  var import_meta = {};
56012
56499
  function version2() {
56013
- if (true) return "0.3.0";
56500
+ if (true) return "0.3.1";
56014
56501
  try {
56015
- return JSON.parse((0, import_node_fs9.readFileSync)(new URL("../../package.json", import_meta.url), "utf8")).version;
56502
+ return JSON.parse((0, import_node_fs10.readFileSync)(new URL("../../package.json", import_meta.url), "utf8")).version;
56016
56503
  } catch {
56017
56504
  return "unknown";
56018
56505
  }
56019
56506
  }
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
56507
  function flag(args, name) {
56039
56508
  const i = args.indexOf(name);
56040
56509
  if (i < 0) return void 0;
@@ -56072,6 +56541,24 @@ async function readStdin(timeoutMs) {
56072
56541
  async function main(argv) {
56073
56542
  const args = [...argv];
56074
56543
  const cmd = args.shift();
56544
+ if (cmd === void 0 || cmd === "help" || cmd === "--help" || cmd === "-h") {
56545
+ const topic = cmd === "help" ? args[0] : void 0;
56546
+ if (!topic) {
56547
+ console.log(overview());
56548
+ return 0;
56549
+ }
56550
+ const text = commandHelp(topic);
56551
+ if (!text) {
56552
+ console.error(`agenthub: no command "${topic}". Run agenthub help for the list.`);
56553
+ return 2;
56554
+ }
56555
+ console.log(text);
56556
+ return 0;
56557
+ }
56558
+ if (commandHelp(cmd) && (args.includes("--help") || args.includes("-h"))) {
56559
+ console.log(commandHelp(cmd));
56560
+ return 0;
56561
+ }
56075
56562
  if (cmd === "--version" || cmd === "version") {
56076
56563
  console.log(version2());
56077
56564
  return 0;
@@ -56096,6 +56583,61 @@ async function main(argv) {
56096
56583
  process.once("SIGTERM", () => ac.abort());
56097
56584
  return watch2({ json: json2, agentName, signal: ac.signal }, { store: credentialStore(), write: (l) => console.log(l) });
56098
56585
  }
56586
+ if (cmd === "wake") {
56587
+ const { wakeSetting: wakeSetting2 } = await Promise.resolve().then(() => (init_wait(), wait_exports));
56588
+ const { gitRemotes: gitRemotes2 } = await Promise.resolve().then(() => (init_notify(), notify_exports));
56589
+ const perProject = args.includes("--project");
56590
+ const value = args.filter((a) => a !== "--project").shift();
56591
+ const origin = gitRemotes2(process.cwd()).origin;
56592
+ const key = origin ? projectKey(origin) : void 0;
56593
+ 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";
56594
+ 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);
56598
+ return 0;
56599
+ }
56600
+ if (!["on", "off", "all", ...perProject ? ["default"] : []].includes(value)) {
56601
+ console.error(commandHelp("wake"));
56602
+ return 2;
56603
+ }
56604
+ const settings = readSettings();
56605
+ if (perProject) {
56606
+ if (!key) {
56607
+ console.error("Not in a git repo with an origin remote; run it in the repo, or leave out --project.");
56608
+ return 1;
56609
+ }
56610
+ const projects = { ...settings.wake_projects };
56611
+ if (value === "default") delete projects[key];
56612
+ else projects[key] = value;
56613
+ writeSettings({ ...settings, wake_projects: projects });
56614
+ } else {
56615
+ writeSettings({ ...settings, wake: value });
56616
+ }
56617
+ 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)}.`);
56619
+ 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.`);
56622
+ return 0;
56623
+ }
56624
+ if (cmd === "wait") {
56625
+ try {
56626
+ const max = Number(flag(args, "--max"));
56627
+ const payload = await readStdin(500);
56628
+ const { wait: wait2 } = await Promise.resolve().then(() => (init_wait(), wait_exports));
56629
+ const ac = new AbortController();
56630
+ process.once("SIGINT", () => ac.abort());
56631
+ process.once("SIGTERM", () => ac.abort());
56632
+ const result = await wait2({ payload, signal: ac.signal, ...max > 0 ? { maxMs: max * 1e3 } : {} }, { store: credentialStore() });
56633
+ if (result.code === 2 && result.text) {
56634
+ process.stdout.write(result.text + "\n");
56635
+ return 2;
56636
+ }
56637
+ } catch {
56638
+ }
56639
+ return 0;
56640
+ }
56099
56641
  if (cmd === "update") {
56100
56642
  const { selfUpdate: selfUpdate2 } = await Promise.resolve().then(() => (init_update(), update_exports));
56101
56643
  const v = flag(args, "--version");
@@ -56161,7 +56703,7 @@ version: ${version2()}${newer}`);
56161
56703
  const harness = args.shift();
56162
56704
  const scope = flag(args, "--scope") ?? "user";
56163
56705
  if (action !== "install" && action !== "uninstall" || !HARNESSES.includes(harness) || scope !== "user" && scope !== "project") {
56164
- console.error(USAGE);
56706
+ console.error(commandHelp("hooks"));
56165
56707
  return 2;
56166
56708
  }
56167
56709
  const h = harness;
@@ -56179,9 +56721,9 @@ version: ${version2()}${newer}`);
56179
56721
  }
56180
56722
  if (cmd === "admin") {
56181
56723
  const { admin: admin2 } = await Promise.resolve().then(() => (init_admin(), admin_exports));
56182
- return admin2(args, USAGE);
56724
+ return admin2(args, commandHelp("admin"));
56183
56725
  }
56184
- console.error(USAGE);
56726
+ console.error(`agenthub: no command "${cmd}". Run agenthub help for the list.`);
56185
56727
  return 2;
56186
56728
  }
56187
56729
  main(process.argv.slice(2)).then(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theagenthub/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "The Agent Hub CLI: sign in to a hub and bring teammates' agents' messages into Claude Code, Codex, Copilot CLI and OpenCode.",
5
5
  "bin": {
6
6
  "agenthub": "bin/agenthub.cjs"