@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.
- package/README.md +2 -2
- package/bin/agenthub.cjs +757 -215
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
#
|
|
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
|
|
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 = (
|
|
480
|
-
|
|
481
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
568
|
-
(0,
|
|
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,
|
|
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,
|
|
577
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
593
|
-
(0,
|
|
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,
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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(
|
|
3668
|
-
const ref = this._sessionCache.get(
|
|
3939
|
+
get(sessionKey2) {
|
|
3940
|
+
const ref = this._sessionCache.get(sessionKey2);
|
|
3669
3941
|
return ref ? ref.deref() : null;
|
|
3670
3942
|
}
|
|
3671
|
-
set(
|
|
3943
|
+
set(sessionKey2, session) {
|
|
3672
3944
|
if (this._maxCachedSessions === 0) {
|
|
3673
3945
|
return;
|
|
3674
3946
|
}
|
|
3675
|
-
if (this._sessionCache.has(
|
|
3676
|
-
this._sessionCache.delete(
|
|
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(
|
|
3694
|
-
this._sessionRegistry.register(session,
|
|
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
|
|
3713
|
-
assert2(
|
|
3714
|
-
const session = customSession || sessionCache.get(
|
|
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(
|
|
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
|
|
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(
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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")
|
|
55815
|
-
|
|
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.
|
|
56500
|
+
if (true) return "0.3.1";
|
|
56014
56501
|
try {
|
|
56015
|
-
return JSON.parse((0,
|
|
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(
|
|
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,
|
|
56724
|
+
return admin2(args, commandHelp("admin"));
|
|
56183
56725
|
}
|
|
56184
|
-
console.error(
|
|
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