@theagenthub/cli 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/agenthub.cjs +685 -160
- package/package.json +1 -1
package/bin/agenthub.cjs
CHANGED
|
@@ -463,6 +463,98 @@ var init_update = __esm({
|
|
|
463
463
|
}
|
|
464
464
|
});
|
|
465
465
|
|
|
466
|
+
// src/cli/kimi.ts
|
|
467
|
+
var kimi_exports = {};
|
|
468
|
+
__export(kimi_exports, {
|
|
469
|
+
KIMI_DEFAULT_PORT: () => KIMI_DEFAULT_PORT,
|
|
470
|
+
deliverToKimi: () => deliverToKimi,
|
|
471
|
+
kimiHome: () => kimiHome,
|
|
472
|
+
kimiServers: () => kimiServers,
|
|
473
|
+
kimiWakeable: () => kimiWakeable,
|
|
474
|
+
startKimiWaiter: () => startKimiWaiter
|
|
475
|
+
});
|
|
476
|
+
function kimiHome(env, home = (0, import_node_os3.homedir)()) {
|
|
477
|
+
return env.KIMI_CODE_HOME || (0, import_node_path7.join)(home, ".kimi-code");
|
|
478
|
+
}
|
|
479
|
+
function kimiWakeable(payload) {
|
|
480
|
+
const client = payload?.client_type;
|
|
481
|
+
return typeof payload?.session_id === "string" && typeof client === "string" && client !== TUI_CLIENT;
|
|
482
|
+
}
|
|
483
|
+
function kimiServers(env, home = (0, import_node_os3.homedir)()) {
|
|
484
|
+
const dir = (0, import_node_path7.join)(kimiHome(env, home), "server", "instances");
|
|
485
|
+
const urls = [];
|
|
486
|
+
let files = [];
|
|
487
|
+
try {
|
|
488
|
+
files = (0, import_node_fs6.readdirSync)(dir);
|
|
489
|
+
} catch {
|
|
490
|
+
}
|
|
491
|
+
for (const f of files) {
|
|
492
|
+
try {
|
|
493
|
+
const entry = JSON.parse((0, import_node_fs6.readFileSync)((0, import_node_path7.join)(dir, f), "utf8"));
|
|
494
|
+
if (typeof entry.url === "string" && /^https?:\/\//.test(entry.url)) urls.push(entry.url.replace(/\/+$/, ""));
|
|
495
|
+
else if (typeof entry.port === "number") {
|
|
496
|
+
const host = typeof entry.host === "string" && entry.host && entry.host !== "0.0.0.0" ? entry.host : "127.0.0.1";
|
|
497
|
+
urls.push(`http://${host}:${entry.port}`);
|
|
498
|
+
}
|
|
499
|
+
} catch {
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
urls.push(`http://127.0.0.1:${KIMI_DEFAULT_PORT}`);
|
|
503
|
+
return [...new Set(urls)];
|
|
504
|
+
}
|
|
505
|
+
async function deliverToKimi(sessionId, text, deps = {}) {
|
|
506
|
+
const env = deps.env ?? process.env;
|
|
507
|
+
const fetchFn = deps.fetch ?? fetch;
|
|
508
|
+
let token = "";
|
|
509
|
+
try {
|
|
510
|
+
token = (0, import_node_fs6.readFileSync)((0, import_node_path7.join)(kimiHome(env, deps.home), "server.token"), "utf8").trim();
|
|
511
|
+
} catch {
|
|
512
|
+
}
|
|
513
|
+
for (const base of kimiServers(env, deps.home)) {
|
|
514
|
+
try {
|
|
515
|
+
const res = await fetchFn(`${base}/api/v1/sessions/${encodeURIComponent(sessionId)}/prompts`, {
|
|
516
|
+
method: "POST",
|
|
517
|
+
headers: { "Content-Type": "application/json", ...token ? { Authorization: `Bearer ${token}` } : {} },
|
|
518
|
+
body: JSON.stringify({ content: [{ type: "text", text }] }),
|
|
519
|
+
signal: AbortSignal.timeout(1e4)
|
|
520
|
+
});
|
|
521
|
+
await res.body?.cancel().catch(() => {
|
|
522
|
+
});
|
|
523
|
+
if (res.ok) return true;
|
|
524
|
+
} catch {
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
function startKimiWaiter(payload, env = process.env) {
|
|
530
|
+
const script = process.argv[1];
|
|
531
|
+
const args = script && /\.[cm]?[jt]s$/.test(script) ? [script, "wait", "--deliver", "kimi"] : ["wait", "--deliver", "kimi"];
|
|
532
|
+
try {
|
|
533
|
+
const child = (0, import_node_child_process4.spawn)(process.execPath, args, {
|
|
534
|
+
detached: true,
|
|
535
|
+
stdio: "ignore",
|
|
536
|
+
windowsHide: true,
|
|
537
|
+
env: { ...env, AGENTHUB_WAIT_PAYLOAD: JSON.stringify(payload) }
|
|
538
|
+
});
|
|
539
|
+
child.on("error", () => {
|
|
540
|
+
});
|
|
541
|
+
child.unref();
|
|
542
|
+
} catch {
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
var import_node_child_process4, import_node_fs6, import_node_os3, import_node_path7, KIMI_DEFAULT_PORT, TUI_CLIENT;
|
|
546
|
+
var init_kimi = __esm({
|
|
547
|
+
"src/cli/kimi.ts"() {
|
|
548
|
+
"use strict";
|
|
549
|
+
import_node_child_process4 = require("node:child_process");
|
|
550
|
+
import_node_fs6 = require("node:fs");
|
|
551
|
+
import_node_os3 = require("node:os");
|
|
552
|
+
import_node_path7 = require("node:path");
|
|
553
|
+
KIMI_DEFAULT_PORT = 58627;
|
|
554
|
+
TUI_CLIENT = "kimi_code_cli";
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
|
|
466
558
|
// src/cli/watch.ts
|
|
467
559
|
var watch_exports = {};
|
|
468
560
|
__export(watch_exports, {
|
|
@@ -611,15 +703,19 @@ var init_watch = __esm({
|
|
|
611
703
|
var wait_exports = {};
|
|
612
704
|
__export(wait_exports, {
|
|
613
705
|
DEFAULT_WAIT_MS: () => DEFAULT_WAIT_MS,
|
|
706
|
+
actMode: () => actMode,
|
|
707
|
+
actSetting: () => actSetting,
|
|
614
708
|
addPending: () => addPending,
|
|
615
709
|
claimFile: () => claimFile,
|
|
710
|
+
continueNotice: () => continueNotice,
|
|
616
711
|
mergeInbox: () => mergeInbox,
|
|
617
712
|
pendingFile: () => pendingFile,
|
|
618
713
|
releaseWaiter: () => releaseWaiter,
|
|
619
714
|
takePending: () => takePending,
|
|
620
715
|
wait: () => wait,
|
|
621
716
|
wakeMode: () => wakeMode,
|
|
622
|
-
wakeSetting: () => wakeSetting
|
|
717
|
+
wakeSetting: () => wakeSetting,
|
|
718
|
+
wokenNotice: () => wokenNotice
|
|
623
719
|
});
|
|
624
720
|
function wakeSetting(env, origin) {
|
|
625
721
|
const settings = readSettings(env);
|
|
@@ -630,23 +726,39 @@ function wakeSetting(env, origin) {
|
|
|
630
726
|
const mode = ["0", "false", "off", "no"].includes(v) ? "off" : v === "all" ? "all" : "requests";
|
|
631
727
|
return { mode, from };
|
|
632
728
|
}
|
|
729
|
+
function actSetting(env, origin) {
|
|
730
|
+
const settings = readSettings(env);
|
|
731
|
+
const key = origin ? projectKey(origin) : void 0;
|
|
732
|
+
const project = key ? settings.wake_act_projects?.[key] : void 0;
|
|
733
|
+
const [raw, from] = env.AGENTHUB_WAKE_ACT !== void 0 ? [env.AGENTHUB_WAKE_ACT, "env"] : project ? [project, "project"] : settings.wake_act ? [settings.wake_act, "user"] : ["", "default"];
|
|
734
|
+
return { mode: raw.trim().toLowerCase() === "ask" ? "ask" : "auto", from };
|
|
735
|
+
}
|
|
736
|
+
function actMode(env, origin) {
|
|
737
|
+
return actSetting(env, origin).mode;
|
|
738
|
+
}
|
|
739
|
+
function wokenNotice(act) {
|
|
740
|
+
return act === "ask" ? `Agent Hub woke this session while it was idle, for new hub messages. ${ASK_FIRST}` : WOKEN;
|
|
741
|
+
}
|
|
742
|
+
function continueNotice(act) {
|
|
743
|
+
return act === "ask" ? `These came in during the turn. ${ASK_FIRST}` : "Handle these as the Agent Hub rules say before you finish.";
|
|
744
|
+
}
|
|
633
745
|
function wakeMode(env, origin) {
|
|
634
746
|
return wakeSetting(env, origin).mode;
|
|
635
747
|
}
|
|
636
748
|
function claimFile(env, key) {
|
|
637
|
-
return (0,
|
|
749
|
+
return (0, import_node_path8.join)(cacheDir(env), `wait-${key}.json`);
|
|
638
750
|
}
|
|
639
751
|
function pendingFile(env, key) {
|
|
640
|
-
return (0,
|
|
752
|
+
return (0, import_node_path8.join)(cacheDir(env), `pending-${key}.json`);
|
|
641
753
|
}
|
|
642
754
|
function releaseWaiter(env, key) {
|
|
643
|
-
(0,
|
|
755
|
+
(0, import_node_fs7.rmSync)(claimFile(env, key), { force: true });
|
|
644
756
|
}
|
|
645
757
|
function takePending(env, key) {
|
|
646
758
|
const file2 = pendingFile(env, key);
|
|
647
759
|
try {
|
|
648
|
-
const pending = JSON.parse((0,
|
|
649
|
-
(0,
|
|
760
|
+
const pending = JSON.parse((0, import_node_fs7.readFileSync)(file2, "utf8"));
|
|
761
|
+
(0, import_node_fs7.rmSync)(file2, { force: true });
|
|
650
762
|
return pending;
|
|
651
763
|
} catch {
|
|
652
764
|
return void 0;
|
|
@@ -657,12 +769,12 @@ function addPending(env, key, inbox) {
|
|
|
657
769
|
const file2 = pendingFile(env, key);
|
|
658
770
|
let before;
|
|
659
771
|
try {
|
|
660
|
-
before = JSON.parse((0,
|
|
772
|
+
before = JSON.parse((0, import_node_fs7.readFileSync)(file2, "utf8"));
|
|
661
773
|
} catch {
|
|
662
774
|
before = void 0;
|
|
663
775
|
}
|
|
664
|
-
(0,
|
|
665
|
-
(0,
|
|
776
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path8.join)(file2, ".."), { recursive: true, mode: 448 });
|
|
777
|
+
(0, import_node_fs7.writeFileSync)(file2, JSON.stringify(mergeInbox(before, inbox)), { mode: 384 });
|
|
666
778
|
}
|
|
667
779
|
function mergeInbox(earlier, later) {
|
|
668
780
|
if (!earlier) return later;
|
|
@@ -677,23 +789,24 @@ async function wait(opts, deps) {
|
|
|
677
789
|
const fetchFn = deps.fetch ?? fetch;
|
|
678
790
|
const now = deps.now ?? Date.now;
|
|
679
791
|
const sleep = deps.sleep ?? defaultSleep2;
|
|
680
|
-
const cwd =
|
|
792
|
+
const cwd = payloadCwd(opts.payload) ?? deps.cwd ?? process.cwd();
|
|
681
793
|
const hub = hubUrl(env);
|
|
682
794
|
if (!hub) return NOTHING;
|
|
683
795
|
const { origin, upstream } = (deps.remotes ?? gitRemotes)(cwd);
|
|
684
796
|
if (!origin) return NOTHING;
|
|
685
797
|
const mode = wakeMode(env, origin);
|
|
686
798
|
if (mode === "off") return NOTHING;
|
|
799
|
+
const act = actMode(env, origin);
|
|
687
800
|
if (!await onHub(hub, [origin, upstream], fetchFn, env, now).catch(() => false)) return NOTHING;
|
|
688
801
|
const agentName = hookAgentName(opts.payload, env);
|
|
689
802
|
const key = sessionKey(hub, origin, agentName);
|
|
690
803
|
const id = (0, import_node_crypto2.randomBytes)(8).toString("hex");
|
|
691
804
|
const claim2 = claimFile(env, key);
|
|
692
|
-
(0,
|
|
693
|
-
(0,
|
|
805
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path8.join)(claim2, ".."), { recursive: true, mode: 448 });
|
|
806
|
+
(0, import_node_fs7.writeFileSync)(claim2, JSON.stringify({ id, pid: process.pid, at: now() }));
|
|
694
807
|
const holds = () => {
|
|
695
808
|
try {
|
|
696
|
-
return JSON.parse((0,
|
|
809
|
+
return JSON.parse((0, import_node_fs7.readFileSync)(claim2, "utf8")).id === id;
|
|
697
810
|
} catch {
|
|
698
811
|
return false;
|
|
699
812
|
}
|
|
@@ -740,7 +853,7 @@ async function wait(opts, deps) {
|
|
|
740
853
|
const data = JSON.parse(ev.data);
|
|
741
854
|
const wakes = data.messages.some((m) => m.new && (mode === "all" || m.type !== "fyi"));
|
|
742
855
|
if (wakes && holds()) return { code: 2, text: `${formatInbox(mergeInbox(takePending(env, key), data))}
|
|
743
|
-
${
|
|
856
|
+
${wokenNotice(act)}` };
|
|
744
857
|
addPending(env, key, data);
|
|
745
858
|
} else if (ev.event === "end") {
|
|
746
859
|
const { reason } = JSON.parse(ev.data);
|
|
@@ -756,7 +869,7 @@ ${WOKEN}` };
|
|
|
756
869
|
clearInterval(check2);
|
|
757
870
|
opts.signal?.removeEventListener("abort", stop);
|
|
758
871
|
ac.abort();
|
|
759
|
-
if (holds()) (0,
|
|
872
|
+
if (holds()) (0, import_node_fs7.rmSync)(claim2, { force: true });
|
|
760
873
|
}
|
|
761
874
|
}
|
|
762
875
|
function defaultSleep2(ms, signal) {
|
|
@@ -768,13 +881,13 @@ function defaultSleep2(ms, signal) {
|
|
|
768
881
|
}, { once: true });
|
|
769
882
|
});
|
|
770
883
|
}
|
|
771
|
-
var import_node_crypto2,
|
|
884
|
+
var import_node_crypto2, import_node_fs7, import_node_path8, DEFAULT_WAIT_MS, MAX_BACKOFF_MS2, NOTHING, ASK_FIRST, WOKEN;
|
|
772
885
|
var init_wait = __esm({
|
|
773
886
|
"src/cli/wait.ts"() {
|
|
774
887
|
"use strict";
|
|
775
888
|
import_node_crypto2 = require("node:crypto");
|
|
776
|
-
|
|
777
|
-
|
|
889
|
+
import_node_fs7 = require("node:fs");
|
|
890
|
+
import_node_path8 = require("node:path");
|
|
778
891
|
init_paths();
|
|
779
892
|
init_notify();
|
|
780
893
|
init_session();
|
|
@@ -783,6 +896,7 @@ var init_wait = __esm({
|
|
|
783
896
|
DEFAULT_WAIT_MS = 12 * 36e5;
|
|
784
897
|
MAX_BACKOFF_MS2 = 6e4;
|
|
785
898
|
NOTHING = { code: 0, text: "" };
|
|
899
|
+
ASK_FIRST = "The user asked to be asked first (agenthub wake ask): don't act on these yet. Tell the user in a few lines who sent what, ask which ones to handle, and stop. Don't claim, answer, complete or decline any of them until the user says so.";
|
|
786
900
|
WOKEN = "Agent Hub woke this session while it was idle, so the user may be away. Handle these as the Agent Hub rules say: answer questions and do reviews read-only, and complete them; leave a handoff, or anything that would run commands, push, merge or touch secrets, for the user and say so.";
|
|
787
901
|
}
|
|
788
902
|
});
|
|
@@ -791,31 +905,48 @@ var init_wait = __esm({
|
|
|
791
905
|
var notify_exports = {};
|
|
792
906
|
__export(notify_exports, {
|
|
793
907
|
CARD_NUDGE_MS: () => CARD_NUDGE_MS,
|
|
908
|
+
NOTIFY_FORMATS: () => NOTIFY_FORMATS,
|
|
794
909
|
TASK_CHECK_MS: () => TASK_CHECK_MS,
|
|
795
910
|
formatInbox: () => formatInbox,
|
|
796
911
|
gitBranch: () => gitBranch,
|
|
797
912
|
gitRemotes: () => gitRemotes,
|
|
798
913
|
hookAgentName: () => hookAgentName,
|
|
799
914
|
notify: () => notify,
|
|
915
|
+
notifyHook: () => notifyHook,
|
|
800
916
|
onHub: () => onHub,
|
|
917
|
+
payloadCwd: () => payloadCwd,
|
|
801
918
|
sessionKey: () => sessionKey
|
|
802
919
|
});
|
|
803
920
|
async function notify(opts, deps) {
|
|
921
|
+
return (await notifyHook(opts, deps)).stdout;
|
|
922
|
+
}
|
|
923
|
+
async function notifyHook(opts, deps) {
|
|
804
924
|
try {
|
|
805
925
|
const event = opts.event ?? HOOK_EVENTS[String(opts.payload?.hook_event_name ?? "")] ?? "post-tool";
|
|
806
926
|
const update = event === "session-start" && deps.version ? updateNotice(deps.version, { env: deps.env ?? process.env, fetch: deps.fetch ?? fetch, ...deps.now ? { now: deps.now } : {} }) : Promise.resolve("");
|
|
807
927
|
const [text, notice] = await Promise.all([run(opts, event, deps).catch(() => ""), update.catch(() => "")]);
|
|
808
|
-
if (typeof text === "object") return text
|
|
809
|
-
return render(opts.format, event, [text, notice].filter(Boolean).join("\n"));
|
|
928
|
+
if (typeof text === "object") return text;
|
|
929
|
+
return { stdout: render(opts.format, event, [text, notice].filter(Boolean).join("\n")) };
|
|
810
930
|
} catch {
|
|
811
|
-
return "";
|
|
931
|
+
return { stdout: "" };
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
function payloadCwd(payload) {
|
|
935
|
+
const first = (v) => Array.isArray(v) ? v[0] : void 0;
|
|
936
|
+
const dir = payload?.cwd ?? payload?.working_dir ?? first(payload?.workspace_roots) ?? first(payload?.workspacePaths);
|
|
937
|
+
if (typeof dir !== "string" || !dir) return void 0;
|
|
938
|
+
if (!dir.startsWith("file:")) return dir;
|
|
939
|
+
try {
|
|
940
|
+
return (0, import_node_url.fileURLToPath)(dir);
|
|
941
|
+
} catch {
|
|
942
|
+
return void 0;
|
|
812
943
|
}
|
|
813
944
|
}
|
|
814
945
|
async function run(opts, event, deps) {
|
|
815
946
|
const env = deps.env ?? process.env;
|
|
816
947
|
const now = deps.now ?? Date.now;
|
|
817
948
|
const fetchFn = deps.fetch ?? fetch;
|
|
818
|
-
const cwd =
|
|
949
|
+
const cwd = payloadCwd(opts.payload) ?? deps.cwd ?? process.cwd();
|
|
819
950
|
const out = (text) => text;
|
|
820
951
|
const hub = hubUrl(env);
|
|
821
952
|
if (!hub) return "";
|
|
@@ -825,24 +956,34 @@ async function run(opts, event, deps) {
|
|
|
825
956
|
const agentName = hookAgentName(opts.payload, env);
|
|
826
957
|
const throttleKey = sessionKey(hub, origin, agentName);
|
|
827
958
|
if (event !== "stop") releaseWaiter(env, throttleKey);
|
|
959
|
+
if (event === FIRST_EVENT_STARTS[opts.format]) {
|
|
960
|
+
const started = (0, import_node_path9.join)(cacheDir(env), `started-${throttleKey}.json`);
|
|
961
|
+
if (!readFlag(started)) {
|
|
962
|
+
writeFlag(started);
|
|
963
|
+
event = "session-start";
|
|
964
|
+
}
|
|
965
|
+
}
|
|
828
966
|
const wait2 = THROTTLE_MS[event];
|
|
829
|
-
if (wait2 && !throttle((0,
|
|
967
|
+
if (wait2 && !throttle((0, import_node_path9.join)(cacheDir(env), `notify-${throttleKey}.json`), wait2, now())) return "";
|
|
830
968
|
const token = await accessToken(hub, { store: deps.store, fetch: fetchFn, env, now });
|
|
831
969
|
if (!token) return event === "session-start" ? out(`Agent Hub: not signed in. Ask the user to run \`agenthub login --hub ${hub}\` to get teammates' messages here.`) : "";
|
|
832
970
|
const who = { project: origin, ...upstream ? { upstream } : {}, ...agentName ? { agent_name: agentName } : {} };
|
|
833
971
|
const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
834
972
|
const signal = AbortSignal.timeout(3e3);
|
|
835
|
-
|
|
973
|
+
const block = STOP_BLOCK[opts.format];
|
|
974
|
+
const stopped = [opts.payload?.status, opts.payload?.terminationReason].some((v) => typeof v === "string" && /abort|cancel|interrupt/i.test(v));
|
|
975
|
+
if (event === "stop" && block && !stopped && wakeMode(env, origin) !== "off") {
|
|
836
976
|
const res2 = await fetchFn(`${hub}/api/inbox?${new URLSearchParams(who)}`, { headers, signal });
|
|
837
977
|
if (res2.ok) {
|
|
838
978
|
const inbox2 = mergeInbox(takePending(env, throttleKey), await res2.json());
|
|
839
979
|
const mode = wakeMode(env, origin);
|
|
840
980
|
if (inbox2.messages.some((m) => m.new && (mode === "all" || m.type !== "fyi"))) {
|
|
841
|
-
return
|
|
842
|
-
|
|
981
|
+
return block(`${formatInbox(inbox2)}
|
|
982
|
+
${continueNotice(actMode(env, origin))}`);
|
|
843
983
|
}
|
|
844
984
|
addPending(env, throttleKey, inbox2);
|
|
845
985
|
}
|
|
986
|
+
if (opts.format === "kimi" && kimiWakeable(opts.payload)) (deps.startKimiWaiter ?? startKimiWaiter)(opts.payload, env);
|
|
846
987
|
}
|
|
847
988
|
if (event === "stop" || event === "session-end") {
|
|
848
989
|
await fetchFn(`${hub}/api/presence`, {
|
|
@@ -858,14 +999,14 @@ Handle these as the Agent Hub rules say before you finish.` }) };
|
|
|
858
999
|
const { membership } = await res.json().catch(() => ({}));
|
|
859
1000
|
const args = [`remote="${origin}"`, ...upstream ? [`upstream="${upstream}"`] : [], ...agentName ? [`agent_name="${agentName}"`] : []].join(", ");
|
|
860
1001
|
const keep = agentName ? " Use exactly that agent_name: it's how hub notices find this session." : "";
|
|
861
|
-
const waitingFile = (0,
|
|
1002
|
+
const waitingFile = (0, import_node_path9.join)(cacheDir(env), `waiting-${throttleKey}.json`);
|
|
862
1003
|
const wasWaiting = readFlag(waitingFile);
|
|
863
1004
|
if (membership === "pending") {
|
|
864
1005
|
if (!wasWaiting) writeFlag(waitingFile);
|
|
865
1006
|
return event === "session-start" || !wasWaiting ? out("Agent Hub: your request to join this project is waiting for a project admin's approval. You'll be told here once it's approved.") : "";
|
|
866
1007
|
}
|
|
867
1008
|
if (wasWaiting) {
|
|
868
|
-
(0,
|
|
1009
|
+
(0, import_node_fs8.rmSync)(waitingFile, { force: true });
|
|
869
1010
|
if (membership === "active") return out(`Agent Hub: a project admin approved you. Call the agenthub join_project tool with ${args} to join.${keep}`);
|
|
870
1011
|
return out("Agent Hub: a project admin turned down your request to join this project.");
|
|
871
1012
|
}
|
|
@@ -879,7 +1020,7 @@ Handle these as the Agent Hub rules say before you finish.` }) };
|
|
|
879
1020
|
const card = await keepCardCurrent(inbox, event, {
|
|
880
1021
|
// Without a name the hub picks the user's latest agent here, which may be another session's.
|
|
881
1022
|
branch: agentName ? (deps.branch ?? gitBranch)(cwd) : void 0,
|
|
882
|
-
stateFile: (0,
|
|
1023
|
+
stateFile: (0, import_node_path9.join)(cacheDir(env), `card-${throttleKey}.json`),
|
|
883
1024
|
now: now(),
|
|
884
1025
|
post: (fields) => fetchFn(`${hub}/api/card`, { method: "POST", headers, body: JSON.stringify({ ...who, ...fields }), signal })
|
|
885
1026
|
}).catch(() => "");
|
|
@@ -909,18 +1050,18 @@ async function keepCardCurrent(inbox, event, ctx) {
|
|
|
909
1050
|
}
|
|
910
1051
|
function readState(path) {
|
|
911
1052
|
try {
|
|
912
|
-
return JSON.parse((0,
|
|
1053
|
+
return JSON.parse((0, import_node_fs8.readFileSync)(path, "utf8"));
|
|
913
1054
|
} catch {
|
|
914
1055
|
return {};
|
|
915
1056
|
}
|
|
916
1057
|
}
|
|
917
1058
|
function writeState(path, state) {
|
|
918
|
-
(0,
|
|
919
|
-
(0,
|
|
1059
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path9.join)(path, ".."), { recursive: true, mode: 448 });
|
|
1060
|
+
(0, import_node_fs8.writeFileSync)(path, JSON.stringify(state));
|
|
920
1061
|
}
|
|
921
1062
|
function gitBranch(cwd) {
|
|
922
1063
|
try {
|
|
923
|
-
return (0,
|
|
1064
|
+
return (0, import_node_child_process5.execFileSync)("git", ["-C", cwd, "branch", "--show-current"], { stdio: ["ignore", "pipe", "ignore"], timeout: 1e3 }).toString().trim() || void 0;
|
|
924
1065
|
} catch {
|
|
925
1066
|
return void 0;
|
|
926
1067
|
}
|
|
@@ -930,7 +1071,7 @@ function sessionKey(hub, origin, agentName) {
|
|
|
930
1071
|
}
|
|
931
1072
|
function hookAgentName(payload, env) {
|
|
932
1073
|
if (env.AGENTHUB_AGENT_NAME) return env.AGENTHUB_AGENT_NAME;
|
|
933
|
-
const session = payload?.session_id ?? payload?.sessionId;
|
|
1074
|
+
const session = payload?.session_id ?? payload?.sessionId ?? payload?.conversation_id ?? payload?.conversationId;
|
|
934
1075
|
if (typeof session !== "string" || !session) return void 0;
|
|
935
1076
|
return `s-${(0, import_node_crypto3.createHash)("sha256").update(session).digest("hex").slice(0, 8)}`;
|
|
936
1077
|
}
|
|
@@ -963,38 +1104,44 @@ function clean(s) {
|
|
|
963
1104
|
}
|
|
964
1105
|
function render(format, event, text) {
|
|
965
1106
|
if (!text) return "";
|
|
966
|
-
if (format === "text") return text;
|
|
1107
|
+
if (format === "text" || format === "kimi" || format === "amp" || format === "roo") return text;
|
|
1108
|
+
if (format === "goose") return "";
|
|
1109
|
+
if (event === "stop" || event === "session-end") return "";
|
|
1110
|
+
if (format === "crush") return JSON.stringify({ context: text });
|
|
1111
|
+
if (format === "cursor") return JSON.stringify({ additional_context: text });
|
|
1112
|
+
if (format === "antigravity") return JSON.stringify({ injectSteps: [{ systemMessage: text }] });
|
|
1113
|
+
if (format === "gemini") return JSON.stringify({ hookSpecificOutput: { hookEventName: GEMINI_EVENT_NAMES[event], additionalContext: text } });
|
|
967
1114
|
if (format === "codex" && event === "session-start") return text;
|
|
968
1115
|
if (format === "copilot") return JSON.stringify({ additionalContext: text });
|
|
969
1116
|
return JSON.stringify({ hookSpecificOutput: { hookEventName: EVENT_NAMES[event], additionalContext: text } });
|
|
970
1117
|
}
|
|
971
1118
|
function readFlag(path) {
|
|
972
1119
|
try {
|
|
973
|
-
(0,
|
|
1120
|
+
(0, import_node_fs8.readFileSync)(path);
|
|
974
1121
|
return true;
|
|
975
1122
|
} catch {
|
|
976
1123
|
return false;
|
|
977
1124
|
}
|
|
978
1125
|
}
|
|
979
1126
|
function writeFlag(path) {
|
|
980
|
-
(0,
|
|
981
|
-
(0,
|
|
1127
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path9.join)(path, ".."), { recursive: true, mode: 448 });
|
|
1128
|
+
(0, import_node_fs8.writeFileSync)(path, "{}");
|
|
982
1129
|
}
|
|
983
1130
|
function throttle(path, waitMs, now) {
|
|
984
1131
|
try {
|
|
985
|
-
const last = JSON.parse((0,
|
|
1132
|
+
const last = JSON.parse((0, import_node_fs8.readFileSync)(path, "utf8")).at;
|
|
986
1133
|
if (now - last < waitMs) return false;
|
|
987
1134
|
} catch {
|
|
988
1135
|
}
|
|
989
|
-
(0,
|
|
990
|
-
(0,
|
|
1136
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path9.join)(path, ".."), { recursive: true, mode: 448 });
|
|
1137
|
+
(0, import_node_fs8.writeFileSync)(path, JSON.stringify({ at: now }));
|
|
991
1138
|
return true;
|
|
992
1139
|
}
|
|
993
1140
|
async function onHub(hub, remotes, fetchFn, env, now) {
|
|
994
|
-
const path = (0,
|
|
1141
|
+
const path = (0, import_node_path9.join)(cacheDir(env), `providers-${(0, import_node_crypto3.createHash)("sha256").update(hub).digest("hex").slice(0, 16)}.json`);
|
|
995
1142
|
let providers;
|
|
996
1143
|
try {
|
|
997
|
-
const cached2 = JSON.parse((0,
|
|
1144
|
+
const cached2 = JSON.parse((0, import_node_fs8.readFileSync)(path, "utf8"));
|
|
998
1145
|
if (now() - cached2.at < PROVIDERS_TTL_MS) providers = cached2.providers;
|
|
999
1146
|
} catch {
|
|
1000
1147
|
}
|
|
@@ -1002,8 +1149,8 @@ async function onHub(hub, remotes, fetchFn, env, now) {
|
|
|
1002
1149
|
const res = await fetchFn(`${hub}/api/providers`, { signal: AbortSignal.timeout(3e3) });
|
|
1003
1150
|
if (!res.ok) return false;
|
|
1004
1151
|
providers = (await res.json()).providers;
|
|
1005
|
-
(0,
|
|
1006
|
-
(0,
|
|
1152
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path9.join)(path, ".."), { recursive: true, mode: 448 });
|
|
1153
|
+
(0, import_node_fs8.writeFileSync)(path, JSON.stringify({ at: now(), providers }));
|
|
1007
1154
|
}
|
|
1008
1155
|
const hosts = /* @__PURE__ */ new Set();
|
|
1009
1156
|
for (const p of providers) {
|
|
@@ -1022,7 +1169,7 @@ async function onHub(hub, remotes, fetchFn, env, now) {
|
|
|
1022
1169
|
function gitRemotes(cwd) {
|
|
1023
1170
|
const get = (name) => {
|
|
1024
1171
|
try {
|
|
1025
|
-
return (0,
|
|
1172
|
+
return (0, import_node_child_process5.execFileSync)("git", ["-C", cwd, "remote", "get-url", name], { stdio: ["ignore", "pipe", "ignore"], timeout: 1e3 }).toString().trim() || void 0;
|
|
1026
1173
|
} catch {
|
|
1027
1174
|
return void 0;
|
|
1028
1175
|
}
|
|
@@ -1031,26 +1178,33 @@ function gitRemotes(cwd) {
|
|
|
1031
1178
|
const upstream = origin ? get("upstream") : void 0;
|
|
1032
1179
|
return { ...origin ? { origin } : {}, ...upstream ? { upstream } : {} };
|
|
1033
1180
|
}
|
|
1034
|
-
var
|
|
1181
|
+
var import_node_child_process5, import_node_crypto3, import_node_fs8, import_node_path9, import_node_url, NOTIFY_FORMATS, HOOK_EVENTS, EVENT_NAMES, GEMINI_EVENT_NAMES, THROTTLE_MS, PROVIDERS_TTL_MS, STOP_BLOCK, FIRST_EVENT_STARTS, CARD_NUDGE_MS, TASK_CHECK_MS;
|
|
1035
1182
|
var init_notify = __esm({
|
|
1036
1183
|
"src/cli/notify.ts"() {
|
|
1037
1184
|
"use strict";
|
|
1038
|
-
|
|
1185
|
+
import_node_child_process5 = require("node:child_process");
|
|
1039
1186
|
import_node_crypto3 = require("node:crypto");
|
|
1040
|
-
|
|
1041
|
-
|
|
1187
|
+
import_node_fs8 = require("node:fs");
|
|
1188
|
+
import_node_path9 = require("node:path");
|
|
1189
|
+
import_node_url = require("node:url");
|
|
1042
1190
|
init_registry();
|
|
1043
1191
|
init_paths();
|
|
1044
1192
|
init_session();
|
|
1045
1193
|
init_settings();
|
|
1046
1194
|
init_update();
|
|
1195
|
+
init_kimi();
|
|
1047
1196
|
init_wait();
|
|
1197
|
+
NOTIFY_FORMATS = ["claude-code", "codex", "copilot", "droid", "gemini", "qwen", "cursor", "antigravity", "kimi", "goose", "amp", "roo", "crush", "text"];
|
|
1048
1198
|
HOOK_EVENTS = {
|
|
1049
1199
|
SessionStart: "session-start",
|
|
1050
1200
|
UserPromptSubmit: "prompt",
|
|
1051
1201
|
PostToolUse: "post-tool",
|
|
1052
1202
|
Stop: "stop",
|
|
1053
|
-
SessionEnd: "session-end"
|
|
1203
|
+
SessionEnd: "session-end",
|
|
1204
|
+
// Gemini CLI
|
|
1205
|
+
BeforeAgent: "prompt",
|
|
1206
|
+
AfterTool: "post-tool",
|
|
1207
|
+
AfterAgent: "stop"
|
|
1054
1208
|
};
|
|
1055
1209
|
EVENT_NAMES = {
|
|
1056
1210
|
"session-start": "SessionStart",
|
|
@@ -1059,8 +1213,29 @@ var init_notify = __esm({
|
|
|
1059
1213
|
stop: "Stop",
|
|
1060
1214
|
"session-end": "SessionEnd"
|
|
1061
1215
|
};
|
|
1216
|
+
GEMINI_EVENT_NAMES = { ...EVENT_NAMES, prompt: "BeforeAgent", "post-tool": "AfterTool", stop: "AfterAgent" };
|
|
1062
1217
|
THROTTLE_MS = { "post-tool": 3e4, prompt: 1e4 };
|
|
1063
1218
|
PROVIDERS_TTL_MS = 24 * 36e5;
|
|
1219
|
+
STOP_BLOCK = {
|
|
1220
|
+
codex: (reason) => ({ stdout: JSON.stringify({ decision: "block", reason }) }),
|
|
1221
|
+
copilot: (reason) => ({ stdout: JSON.stringify({ decision: "block", reason }) }),
|
|
1222
|
+
droid: (reason) => ({ stdout: JSON.stringify({ decision: "block", reason }) }),
|
|
1223
|
+
qwen: (reason) => ({ stdout: JSON.stringify({ decision: "block", reason }) }),
|
|
1224
|
+
goose: (reason) => ({ stdout: JSON.stringify({ decision: "block", reason }) }),
|
|
1225
|
+
// The Amp plugin returns agent.end's `continue` with the reason as the next user message.
|
|
1226
|
+
amp: (reason) => ({ stdout: JSON.stringify({ decision: "block", reason }) }),
|
|
1227
|
+
// The Roo Code extension sends the reason to the task with sendMessage.
|
|
1228
|
+
roo: (reason) => ({ stdout: JSON.stringify({ decision: "block", reason }) }),
|
|
1229
|
+
// AfterAgent: deny sends the reason to the agent as a new prompt.
|
|
1230
|
+
gemini: (reason) => ({ stdout: JSON.stringify({ decision: "deny", reason }) }),
|
|
1231
|
+
// stop: Cursor submits followup_message as the next user message.
|
|
1232
|
+
cursor: (reason) => ({ stdout: JSON.stringify({ followup_message: reason }) }),
|
|
1233
|
+
// Stop: the reason is injected as a system message and the agent goes on.
|
|
1234
|
+
antigravity: (reason) => ({ stdout: JSON.stringify({ decision: "continue", reason }) }),
|
|
1235
|
+
// Stop: exit 2 blocks it, with stderr as the reason.
|
|
1236
|
+
kimi: (reason) => ({ stdout: "", stderr: reason, code: 2 })
|
|
1237
|
+
};
|
|
1238
|
+
FIRST_EVENT_STARTS = { kimi: "prompt", crush: "post-tool" };
|
|
1064
1239
|
CARD_NUDGE_MS = 10 * 6e4;
|
|
1065
1240
|
TASK_CHECK_MS = 30 * 6e4;
|
|
1066
1241
|
}
|
|
@@ -27704,7 +27879,7 @@ function dispatcherFor(instance) {
|
|
|
27704
27879
|
if (!instance.caBundlePath) return void 0;
|
|
27705
27880
|
let d = dispatchers.get(instance.id);
|
|
27706
27881
|
if (!d) {
|
|
27707
|
-
const ca = (0,
|
|
27882
|
+
const ca = (0, import_node_fs9.readFileSync)(instance.caBundlePath, "utf8");
|
|
27708
27883
|
d = new import_undici.Agent({ connect: { ca: [...import_node_tls.rootCertificates, ca] } });
|
|
27709
27884
|
dispatchers.set(instance.id, d);
|
|
27710
27885
|
}
|
|
@@ -27721,11 +27896,11 @@ function fetchFor(instance, override) {
|
|
|
27721
27896
|
signal: AbortSignal.timeout(1e4)
|
|
27722
27897
|
});
|
|
27723
27898
|
}
|
|
27724
|
-
var
|
|
27899
|
+
var import_node_fs9, import_node_tls, import_undici, dispatchers;
|
|
27725
27900
|
var init_http = __esm({
|
|
27726
27901
|
"src/providers/http.ts"() {
|
|
27727
27902
|
"use strict";
|
|
27728
|
-
|
|
27903
|
+
import_node_fs9 = require("node:fs");
|
|
27729
27904
|
import_node_tls = require("node:tls");
|
|
27730
27905
|
import_undici = __toESM(require_undici(), 1);
|
|
27731
27906
|
dispatchers = /* @__PURE__ */ new Map();
|
|
@@ -55604,7 +55779,7 @@ var init_types = __esm({
|
|
|
55604
55779
|
|
|
55605
55780
|
// src/providers/config.ts
|
|
55606
55781
|
function loadConfigFile(path, opts = {}) {
|
|
55607
|
-
const read = opts.readFile ?? ((p) => (0,
|
|
55782
|
+
const read = opts.readFile ?? ((p) => (0, import_node_fs10.readFileSync)(p, "utf8"));
|
|
55608
55783
|
let text;
|
|
55609
55784
|
try {
|
|
55610
55785
|
text = read(path);
|
|
@@ -55615,7 +55790,7 @@ function loadConfigFile(path, opts = {}) {
|
|
|
55615
55790
|
}
|
|
55616
55791
|
function buildConfig(raw, opts = {}) {
|
|
55617
55792
|
const env = opts.env ?? process.env;
|
|
55618
|
-
const read = opts.readFile ?? ((p) => (0,
|
|
55793
|
+
const read = opts.readFile ?? ((p) => (0, import_node_fs10.readFileSync)(p, "utf8"));
|
|
55619
55794
|
const parsed = configSchema.safeParse(raw);
|
|
55620
55795
|
if (!parsed.success) {
|
|
55621
55796
|
throw new ConfigError(
|
|
@@ -55796,11 +55971,11 @@ function checkNoHostCollisions(instances) {
|
|
|
55796
55971
|
for (const a of inst.remoteAliases) claim2(a.port ? `${a.host}:${a.port}` : a.host, inst.id);
|
|
55797
55972
|
}
|
|
55798
55973
|
}
|
|
55799
|
-
var
|
|
55974
|
+
var import_node_fs10, import_yaml, secretRef, oauthSchema, providerSchema, configSchema, ConfigError;
|
|
55800
55975
|
var init_config = __esm({
|
|
55801
55976
|
"src/providers/config.ts"() {
|
|
55802
55977
|
"use strict";
|
|
55803
|
-
|
|
55978
|
+
import_node_fs10 = require("node:fs");
|
|
55804
55979
|
import_yaml = __toESM(require_dist(), 1);
|
|
55805
55980
|
init_zod();
|
|
55806
55981
|
init_defaults();
|
|
@@ -55901,7 +56076,7 @@ var init_admin = __esm({
|
|
|
55901
56076
|
});
|
|
55902
56077
|
|
|
55903
56078
|
// src/cli/index.ts
|
|
55904
|
-
var
|
|
56079
|
+
var import_node_fs11 = require("node:fs");
|
|
55905
56080
|
|
|
55906
56081
|
// src/cli/credentials.ts
|
|
55907
56082
|
var import_node_child_process = require("node:child_process");
|
|
@@ -56007,11 +56182,133 @@ var import_node_fs2 = require("node:fs");
|
|
|
56007
56182
|
var import_node_os2 = require("node:os");
|
|
56008
56183
|
var import_node_path3 = require("node:path");
|
|
56009
56184
|
|
|
56185
|
+
// src/cli/amp-plugin.ts
|
|
56186
|
+
var AMP_MARKER = "// Agent Hub plugin for Amp, written by `agenthub hooks install amp`.";
|
|
56187
|
+
var PACKAGE_HEADER = `// Agent Hub plugin for Amp. Copy it to ~/.config/amp/plugins/ (or a repo's .amp/plugins/).
|
|
56188
|
+
// Needs the agenthub CLI: curl -fsSL https://get.theagenthub.app | sh`;
|
|
56189
|
+
var PACKAGE_COMMAND = `const COMMAND =
|
|
56190
|
+
process.env.AGENTHUB_BIN ||
|
|
56191
|
+
[join(homedir(), ".local", "bin", "agenthub"), join(process.env.LOCALAPPDATA || homedir(), "Programs", "agenthub", "agenthub.exe")].find((f) => existsSync(f)) ||
|
|
56192
|
+
"agenthub";`;
|
|
56193
|
+
function ampPlugin(command, as = "file") {
|
|
56194
|
+
const header = as === "package" ? PACKAGE_HEADER : `${AMP_MARKER}
|
|
56195
|
+
// Re-run that command to update it, or \`agenthub hooks uninstall amp\` to remove it.`;
|
|
56196
|
+
return `${header}
|
|
56197
|
+
import { spawn } from "node:child_process";
|
|
56198
|
+
import { existsSync } from "node:fs";
|
|
56199
|
+
import { homedir } from "node:os";
|
|
56200
|
+
import { join } from "node:path";
|
|
56201
|
+
import { fileURLToPath } from "node:url";
|
|
56202
|
+
|
|
56203
|
+
${as === "package" ? PACKAGE_COMMAND : `const COMMAND = ${JSON.stringify(command)};`}
|
|
56204
|
+
const TIMEOUT_MS = 5000;
|
|
56205
|
+
|
|
56206
|
+
export const description = "Agent Hub: teammates' agents' messages in your threads, and an idle thread wakes for their requests.";
|
|
56207
|
+
|
|
56208
|
+
/** Runs agenthub; resolves to its exit code and stdout, or code -1 on any failure. */
|
|
56209
|
+
function run(args, input, cwd, timeoutMs, onChild) {
|
|
56210
|
+
return new Promise((resolve) => {
|
|
56211
|
+
let out = "";
|
|
56212
|
+
let child;
|
|
56213
|
+
try {
|
|
56214
|
+
child = spawn(COMMAND, args, { cwd, stdio: ["pipe", "pipe", "ignore"], windowsHide: true, shell: process.platform === "win32" });
|
|
56215
|
+
} catch {
|
|
56216
|
+
return resolve({ code: -1, out: "" });
|
|
56217
|
+
}
|
|
56218
|
+
onChild?.(child);
|
|
56219
|
+
const timer = timeoutMs ? setTimeout(() => child.kill(), timeoutMs) : undefined;
|
|
56220
|
+
child.stdout.on("data", (chunk) => (out += chunk));
|
|
56221
|
+
child.on("error", () => {
|
|
56222
|
+
clearTimeout(timer);
|
|
56223
|
+
resolve({ code: -1, out: "" });
|
|
56224
|
+
});
|
|
56225
|
+
child.on("close", (code) => {
|
|
56226
|
+
clearTimeout(timer);
|
|
56227
|
+
resolve({ code: code ?? -1, out: out.trim() });
|
|
56228
|
+
});
|
|
56229
|
+
child.stdin.on("error", () => {});
|
|
56230
|
+
child.stdin.end(input);
|
|
56231
|
+
});
|
|
56232
|
+
}
|
|
56233
|
+
|
|
56234
|
+
export default function agenthub(amp) {
|
|
56235
|
+
let cwd = process.cwd();
|
|
56236
|
+
try {
|
|
56237
|
+
const root = amp.system?.workspaceRoot ? String(amp.system.workspaceRoot) : "";
|
|
56238
|
+
if (root) cwd = root.startsWith("file:") ? fileURLToPath(root) : root;
|
|
56239
|
+
} catch {}
|
|
56240
|
+
const started = new Set();
|
|
56241
|
+
const waiting = new Map();
|
|
56242
|
+
const stopWaiting = (id) => {
|
|
56243
|
+
waiting.get(id)?.kill();
|
|
56244
|
+
waiting.delete(id);
|
|
56245
|
+
};
|
|
56246
|
+
const notify = async (event, id, extra) => {
|
|
56247
|
+
const { out } = await run(["notify", "--format", "amp", "--event", event], JSON.stringify({ session_id: id, cwd, ...extra }), cwd, TIMEOUT_MS);
|
|
56248
|
+
return out;
|
|
56249
|
+
};
|
|
56250
|
+
const say = async (id, content, steer) => {
|
|
56251
|
+
try {
|
|
56252
|
+
await amp.threads.get(id).appendUserMessage({ type: "user-message", content }, steer ? { steer: true } : undefined);
|
|
56253
|
+
} catch (e) {
|
|
56254
|
+
amp.logger?.log?.("agenthub: couldn't add to thread " + id + ": " + e);
|
|
56255
|
+
}
|
|
56256
|
+
};
|
|
56257
|
+
const wake = async (id) => {
|
|
56258
|
+
const { code, out } = await run(["wait"], JSON.stringify({ session_id: id, cwd }), cwd, 0, (child) => {
|
|
56259
|
+
stopWaiting(id);
|
|
56260
|
+
waiting.set(id, child);
|
|
56261
|
+
});
|
|
56262
|
+
// An agenthub too old to know \`wait\` exits 2 too, but prints nothing on stdout.
|
|
56263
|
+
if (code === 2 && out) await say(id, out, false);
|
|
56264
|
+
};
|
|
56265
|
+
|
|
56266
|
+
amp.on("agent.start", async (event) => {
|
|
56267
|
+
const id = event.thread?.id;
|
|
56268
|
+
if (!id) return;
|
|
56269
|
+
stopWaiting(id);
|
|
56270
|
+
const first = !started.has(id);
|
|
56271
|
+
started.add(id);
|
|
56272
|
+
const text = await notify(first ? "session-start" : "prompt", id);
|
|
56273
|
+
return text ? { message: { content: text, display: false } } : undefined;
|
|
56274
|
+
});
|
|
56275
|
+
|
|
56276
|
+
amp.on("tool.result", async (event) => {
|
|
56277
|
+
const id = event.thread?.id;
|
|
56278
|
+
if (!id) return;
|
|
56279
|
+
const text = await notify("post-tool", id);
|
|
56280
|
+
if (!text) return;
|
|
56281
|
+
if (typeof event.output === "string") return { status: event.status, output: event.output + "\\n\\n" + text };
|
|
56282
|
+
await say(id, text, true);
|
|
56283
|
+
});
|
|
56284
|
+
|
|
56285
|
+
amp.on("agent.end", async (event) => {
|
|
56286
|
+
const id = event.thread?.id;
|
|
56287
|
+
if (!id) return;
|
|
56288
|
+
// A cancelled turn isn't kept going, and doesn't start a waiter.
|
|
56289
|
+
const out = await notify("stop", id, { status: event.status });
|
|
56290
|
+
if (out) {
|
|
56291
|
+
try {
|
|
56292
|
+
const r = JSON.parse(out);
|
|
56293
|
+
if (r.decision === "block" && r.reason) return { action: "continue", userMessage: r.reason };
|
|
56294
|
+
} catch {}
|
|
56295
|
+
}
|
|
56296
|
+
if (event.status !== "cancelled") void wake(id);
|
|
56297
|
+
});
|
|
56298
|
+
|
|
56299
|
+
amp.onDispose?.(() => {
|
|
56300
|
+
for (const child of waiting.values()) child.kill();
|
|
56301
|
+
waiting.clear();
|
|
56302
|
+
});
|
|
56303
|
+
}
|
|
56304
|
+
`;
|
|
56305
|
+
}
|
|
56306
|
+
|
|
56010
56307
|
// src/cli/opencode-plugin.ts
|
|
56011
56308
|
var OPENCODE_MARKER = "// Agent Hub plugin for OpenCode, written by `agenthub hooks install opencode`.";
|
|
56012
|
-
var
|
|
56309
|
+
var PACKAGE_HEADER2 = `// Agent Hub plugin for OpenCode, from the @theagenthub/opencode npm package.
|
|
56013
56310
|
// Needs the agenthub CLI: curl -fsSL https://get.theagenthub.app | sh`;
|
|
56014
|
-
var
|
|
56311
|
+
var PACKAGE_COMMAND2 = `import { existsSync } from "node:fs";
|
|
56015
56312
|
import { homedir } from "node:os";
|
|
56016
56313
|
import { join } from "node:path";
|
|
56017
56314
|
|
|
@@ -56020,12 +56317,12 @@ const COMMAND =
|
|
|
56020
56317
|
[join(homedir(), ".local", "bin", "agenthub"), join(process.env.LOCALAPPDATA || homedir(), "Programs", "agenthub", "agenthub.exe")].find((f) => existsSync(f)) ||
|
|
56021
56318
|
"agenthub";`;
|
|
56022
56319
|
function opencodePlugin(command, as = "file") {
|
|
56023
|
-
const header = as === "package" ?
|
|
56320
|
+
const header = as === "package" ? PACKAGE_HEADER2 : `${OPENCODE_MARKER}
|
|
56024
56321
|
// Re-run that command to update it, or \`agenthub hooks uninstall opencode\` to remove it.`;
|
|
56025
56322
|
return `${header}
|
|
56026
56323
|
import { spawn } from "node:child_process";
|
|
56027
56324
|
|
|
56028
|
-
${as === "package" ?
|
|
56325
|
+
${as === "package" ? PACKAGE_COMMAND2 : `const COMMAND = ${JSON.stringify(command)};`}
|
|
56029
56326
|
const TIMEOUT_MS = 5000;
|
|
56030
56327
|
|
|
56031
56328
|
function notify(event, sessionID, cwd) {
|
|
@@ -56140,11 +56437,23 @@ async function setup(ctx) {
|
|
|
56140
56437
|
if (!ctx?.session?.hook || !ctx.tool?.hook || !ctx.event?.subscribe) return;
|
|
56141
56438
|
const directory = ctx.location?.directory ?? process.cwd();
|
|
56142
56439
|
const started = new Set();
|
|
56440
|
+
const waiting = new Map();
|
|
56143
56441
|
let presence = Promise.resolve();
|
|
56442
|
+
const wake = async (sessionID) => {
|
|
56443
|
+
if (typeof ctx.session.synthetic !== "function") return;
|
|
56444
|
+
const text = await waitForMessage(sessionID, directory, waiting);
|
|
56445
|
+
if (!text) return;
|
|
56446
|
+
try {
|
|
56447
|
+
await ctx.session.synthetic({ sessionID, text, description: "Agent Hub: new message", delivery: "queue", resume: true });
|
|
56448
|
+
} catch {
|
|
56449
|
+
/* the session is gone, or busy again */
|
|
56450
|
+
}
|
|
56451
|
+
};
|
|
56144
56452
|
const wrap = (text) => "\\n\\n<agenthub>\\n" + text + "\\n</agenthub>";
|
|
56145
56453
|
|
|
56146
56454
|
// V2 has no hidden message parts, so the notice is appended to the prompt's text.
|
|
56147
56455
|
await ctx.session.hook("prompt", async (event) => {
|
|
56456
|
+
waiting.get(event.sessionID)?.kill();
|
|
56148
56457
|
const first = !started.has(event.sessionID);
|
|
56149
56458
|
started.add(event.sessionID);
|
|
56150
56459
|
const text = await notify(first ? "session-start" : "prompt", event.sessionID, directory);
|
|
@@ -56164,11 +56473,21 @@ async function setup(ctx) {
|
|
|
56164
56473
|
const sessionID = event.data?.sessionID;
|
|
56165
56474
|
if (!sessionID) continue;
|
|
56166
56475
|
const kind = event.type === "session.deleted" ? "session-end" : "stop";
|
|
56167
|
-
if (kind === "session-end")
|
|
56476
|
+
if (kind === "session-end") {
|
|
56477
|
+
started.delete(sessionID);
|
|
56478
|
+
waiting.get(sessionID)?.kill();
|
|
56479
|
+
}
|
|
56168
56480
|
presence = presence.then(() => notify(kind, sessionID, directory));
|
|
56481
|
+
// A finished run (not one the user interrupted) waits for a hub message; not awaited.
|
|
56482
|
+
if (event.type === "session.execution.succeeded" || event.type === "session.execution.failed") {
|
|
56483
|
+
presence.then(() => wake(sessionID)).catch(() => {});
|
|
56484
|
+
}
|
|
56169
56485
|
}
|
|
56170
56486
|
})().catch(() => {});
|
|
56171
|
-
return () =>
|
|
56487
|
+
return () => {
|
|
56488
|
+
controller.abort();
|
|
56489
|
+
for (const child of waiting.values()) child.kill();
|
|
56490
|
+
};
|
|
56172
56491
|
}
|
|
56173
56492
|
|
|
56174
56493
|
export default { id: "agenthub", server: AgentHub, setup };
|
|
@@ -56176,97 +56495,248 @@ export default { id: "agenthub", server: AgentHub, setup };
|
|
|
56176
56495
|
}
|
|
56177
56496
|
|
|
56178
56497
|
// src/cli/hooks.ts
|
|
56179
|
-
var HARNESSES = ["claude-code", "codex", "copilot", "opencode"];
|
|
56498
|
+
var HARNESSES = ["claude-code", "codex", "copilot", "opencode", "droid", "gemini", "qwen", "cursor", "antigravity", "kimi", "goose", "amp", "crush"];
|
|
56499
|
+
var USER_SCOPE_ONLY = /* @__PURE__ */ new Set(["kimi"]);
|
|
56180
56500
|
var EVENTS = ["SessionStart", "UserPromptSubmit", "PostToolUse", "Stop", "SessionEnd"];
|
|
56181
|
-
var
|
|
56182
|
-
["
|
|
56183
|
-
["
|
|
56184
|
-
["
|
|
56185
|
-
["
|
|
56186
|
-
["
|
|
56501
|
+
var CLAUDE_PAIRS = [
|
|
56502
|
+
["SessionStart", "session-start"],
|
|
56503
|
+
["UserPromptSubmit", "prompt"],
|
|
56504
|
+
["PostToolUse", "post-tool"],
|
|
56505
|
+
["Stop", "stop"],
|
|
56506
|
+
["SessionEnd", "session-end"]
|
|
56187
56507
|
];
|
|
56508
|
+
var HARNESS_EVENTS = {
|
|
56509
|
+
copilot: [
|
|
56510
|
+
["sessionStart", "session-start"],
|
|
56511
|
+
["userPromptSubmitted", "prompt"],
|
|
56512
|
+
["postToolUse", "post-tool"],
|
|
56513
|
+
["agentStop", "stop"],
|
|
56514
|
+
["sessionEnd", "session-end"]
|
|
56515
|
+
],
|
|
56516
|
+
droid: CLAUDE_PAIRS,
|
|
56517
|
+
qwen: CLAUDE_PAIRS,
|
|
56518
|
+
gemini: [
|
|
56519
|
+
["SessionStart", "session-start"],
|
|
56520
|
+
["BeforeAgent", "prompt"],
|
|
56521
|
+
["AfterTool", "post-tool"],
|
|
56522
|
+
["AfterAgent", "stop"],
|
|
56523
|
+
["SessionEnd", "session-end"]
|
|
56524
|
+
],
|
|
56525
|
+
// beforeSubmitPrompt can't add context, so prompts are left out.
|
|
56526
|
+
cursor: [
|
|
56527
|
+
["sessionStart", "session-start"],
|
|
56528
|
+
["postToolUse", "post-tool"],
|
|
56529
|
+
["stop", "stop"],
|
|
56530
|
+
["sessionEnd", "session-end"]
|
|
56531
|
+
],
|
|
56532
|
+
// PostToolUse can't add context; PreInvocation runs before each model call, so it stands in
|
|
56533
|
+
// for both prompts and tool calls. There's no session end.
|
|
56534
|
+
antigravity: [
|
|
56535
|
+
["SessionStart", "session-start"],
|
|
56536
|
+
["PreInvocation", "post-tool"],
|
|
56537
|
+
["Stop", "stop"]
|
|
56538
|
+
],
|
|
56539
|
+
// SessionStart and PostToolUse only observe; the first prompt gets the session-start notice.
|
|
56540
|
+
kimi: [
|
|
56541
|
+
["UserPromptSubmit", "prompt"],
|
|
56542
|
+
["Stop", "stop"],
|
|
56543
|
+
["SessionEnd", "session-end"]
|
|
56544
|
+
],
|
|
56545
|
+
// Crush has only PreToolUse; its first one in a session gets the session-start notice.
|
|
56546
|
+
crush: [["PreToolUse", "post-tool"]],
|
|
56547
|
+
// Goose hooks can't add context: Stop keeps a turn going for a request, and presence.
|
|
56548
|
+
goose: [
|
|
56549
|
+
["Stop", "stop"],
|
|
56550
|
+
["SessionEnd", "session-end"]
|
|
56551
|
+
]
|
|
56552
|
+
};
|
|
56188
56553
|
var WAIT_MAX_S = 12 * 3600;
|
|
56189
56554
|
var WAIT_TIMEOUT_S = WAIT_MAX_S + 60;
|
|
56190
56555
|
var TIMEOUT_S = { "claude-code": {}, codex: { SessionEnd: 3 } };
|
|
56556
|
+
var KIMI_BEGIN = "# >>> agenthub hooks: agenthub hooks uninstall kimi removes this block >>>";
|
|
56557
|
+
var KIMI_END = "# <<< agenthub hooks <<<";
|
|
56558
|
+
var GOOSE_PLUGIN = { name: "agenthub", version: "1.0.0", description: "Agent Hub: handle hub requests that arrive during a turn, and presence" };
|
|
56191
56559
|
function hookFile(harness, scope, cwd, home = (0, import_node_os2.homedir)(), env = process.env) {
|
|
56192
56560
|
const user = scope === "user";
|
|
56561
|
+
const base = user ? home : cwd;
|
|
56193
56562
|
switch (harness) {
|
|
56194
56563
|
case "claude-code":
|
|
56195
56564
|
return user ? (0, import_node_path3.join)(home, ".claude", "settings.json") : (0, import_node_path3.join)(cwd, ".claude", "settings.local.json");
|
|
56196
56565
|
case "codex":
|
|
56197
|
-
return
|
|
56566
|
+
return (0, import_node_path3.join)(base, ".codex", "hooks.json");
|
|
56198
56567
|
case "copilot":
|
|
56199
56568
|
return user ? (0, import_node_path3.join)(env.COPILOT_HOME || (0, import_node_path3.join)(home, ".copilot"), "hooks", "agenthub.json") : (0, import_node_path3.join)(cwd, ".github", "hooks", "agenthub.json");
|
|
56200
56569
|
case "opencode":
|
|
56201
56570
|
return user ? (0, import_node_path3.join)(env.XDG_CONFIG_HOME || (0, import_node_path3.join)(home, ".config"), "opencode", "plugins", "agenthub.js") : (0, import_node_path3.join)(cwd, ".opencode", "plugins", "agenthub.js");
|
|
56571
|
+
case "droid":
|
|
56572
|
+
return (0, import_node_path3.join)(base, ".factory", "hooks.json");
|
|
56573
|
+
case "gemini":
|
|
56574
|
+
return (0, import_node_path3.join)(base, ".gemini", "settings.json");
|
|
56575
|
+
case "qwen":
|
|
56576
|
+
return (0, import_node_path3.join)(base, ".qwen", "settings.json");
|
|
56577
|
+
case "cursor":
|
|
56578
|
+
return (0, import_node_path3.join)(base, ".cursor", "hooks.json");
|
|
56579
|
+
case "antigravity":
|
|
56580
|
+
return user ? (0, import_node_path3.join)(home, ".gemini", "config", "hooks.json") : (0, import_node_path3.join)(cwd, ".agents", "hooks.json");
|
|
56581
|
+
case "kimi":
|
|
56582
|
+
return (0, import_node_path3.join)(home, ".kimi-code", "config.toml");
|
|
56583
|
+
case "goose":
|
|
56584
|
+
return (0, import_node_path3.join)(base, ".agents", "plugins", "agenthub", "hooks", "hooks.json");
|
|
56585
|
+
case "crush":
|
|
56586
|
+
return user ? (0, import_node_path3.join)(env.XDG_CONFIG_HOME || (0, import_node_path3.join)(home, ".config"), "crush", "crush.json") : (0, import_node_fs2.existsSync)((0, import_node_path3.join)(cwd, "crush.json")) ? (0, import_node_path3.join)(cwd, "crush.json") : (0, import_node_path3.join)(cwd, ".crush.json");
|
|
56587
|
+
case "amp":
|
|
56588
|
+
return user ? (0, import_node_path3.join)(env.XDG_CONFIG_HOME || (0, import_node_path3.join)(home, ".config"), "amp", "plugins", "agenthub.ts") : (0, import_node_path3.join)(cwd, ".amp", "plugins", "agenthub.ts");
|
|
56202
56589
|
}
|
|
56203
56590
|
}
|
|
56204
56591
|
var OURS = [/^(?:\S*[\\/])?agenthub(?:\.js|\.exe)?\s+(?:notify|wait)\b/i, /^"(?:[^"]*[\\/])?agenthub(?:\.js|\.exe)?"\s+(?:notify|wait)\b/i];
|
|
56205
|
-
var isOurs = (h) => typeof h
|
|
56592
|
+
var isOurs = (h) => typeof h?.command === "string" && OURS.some((r) => r.test(h.command));
|
|
56206
56593
|
function installHooks(harness, file2, command = "agenthub") {
|
|
56207
56594
|
(0, import_node_fs2.mkdirSync)((0, import_node_path3.dirname)(file2), { recursive: true });
|
|
56208
|
-
|
|
56209
|
-
|
|
56210
|
-
|
|
56211
|
-
|
|
56212
|
-
|
|
56213
|
-
|
|
56214
|
-
|
|
56215
|
-
|
|
56216
|
-
|
|
56217
|
-
|
|
56218
|
-
|
|
56219
|
-
|
|
56220
|
-
|
|
56221
|
-
|
|
56222
|
-
|
|
56223
|
-
|
|
56224
|
-
|
|
56225
|
-
|
|
56226
|
-
|
|
56227
|
-
|
|
56228
|
-
|
|
56229
|
-
|
|
56230
|
-
|
|
56231
|
-
|
|
56232
|
-
|
|
56233
|
-
|
|
56234
|
-
|
|
56235
|
-
|
|
56595
|
+
const notify2 = (event) => `${command} notify --format ${harness} --event ${event}`;
|
|
56596
|
+
const pairs = HARNESS_EVENTS[harness] ?? [];
|
|
56597
|
+
switch (harness) {
|
|
56598
|
+
case "opencode":
|
|
56599
|
+
(0, import_node_fs2.writeFileSync)(file2, opencodePlugin(command));
|
|
56600
|
+
return ["chat.message", "tool.execute.after", "session.idle", "session.deleted"];
|
|
56601
|
+
case "amp":
|
|
56602
|
+
(0, import_node_fs2.writeFileSync)(file2, ampPlugin(command));
|
|
56603
|
+
return ["agent.start", "tool.result", "agent.end"];
|
|
56604
|
+
case "copilot":
|
|
56605
|
+
case "cursor":
|
|
56606
|
+
case "crush": {
|
|
56607
|
+
const config2 = readJson(file2);
|
|
56608
|
+
if (harness !== "crush") config2.version = 1;
|
|
56609
|
+
const hooks = config2.hooks ??= {};
|
|
56610
|
+
for (const [event, notifyEvent] of pairs) {
|
|
56611
|
+
const kept = (hooks[event] ?? []).filter((h) => !isOurs(h));
|
|
56612
|
+
const entry = harness === "copilot" ? { type: "command", command: notify2(notifyEvent), timeoutSec: 10 } : { command: notify2(notifyEvent), timeout: 10 };
|
|
56613
|
+
hooks[event] = [...kept, entry];
|
|
56614
|
+
}
|
|
56615
|
+
writeJson(file2, config2);
|
|
56616
|
+
return pairs.map(([e]) => e);
|
|
56617
|
+
}
|
|
56618
|
+
case "antigravity": {
|
|
56619
|
+
const config2 = readJson(file2);
|
|
56620
|
+
config2.agenthub = Object.fromEntries(pairs.map(([event, n]) => [event, [{ hooks: [{ type: "command", command: notify2(n), timeout: 10 }] }]]));
|
|
56621
|
+
writeJson(file2, config2);
|
|
56622
|
+
return pairs.map(([e]) => e);
|
|
56623
|
+
}
|
|
56624
|
+
case "kimi": {
|
|
56625
|
+
const before = (0, import_node_fs2.existsSync)(file2) ? stripKimi((0, import_node_fs2.readFileSync)(file2, "utf8")) : "";
|
|
56626
|
+
const block = [KIMI_BEGIN, ...pairs.flatMap(([event, n]) => ["[[hooks]]", `event = "${event}"`, `command = ${JSON.stringify(notify2(n))}`, "timeout = 10", ""]), KIMI_END, ""].join("\n");
|
|
56627
|
+
(0, import_node_fs2.writeFileSync)(file2, `${before}${before && !before.endsWith("\n") ? "\n" : ""}${before ? "\n" : ""}${block}`);
|
|
56628
|
+
return pairs.map(([e]) => e);
|
|
56629
|
+
}
|
|
56630
|
+
case "goose": {
|
|
56631
|
+
const root = (0, import_node_path3.dirname)((0, import_node_path3.dirname)(file2));
|
|
56632
|
+
writeJson((0, import_node_path3.join)(root, "plugin.json"), GOOSE_PLUGIN);
|
|
56633
|
+
writeJson(file2, { hooks: Object.fromEntries(pairs.map(([event, n]) => [event, [{ hooks: [{ type: "command", command: notify2(n), timeout: 10 }] }]])) });
|
|
56634
|
+
return pairs.map(([e]) => e);
|
|
56635
|
+
}
|
|
56636
|
+
case "droid":
|
|
56637
|
+
case "gemini":
|
|
56638
|
+
case "qwen": {
|
|
56639
|
+
const config2 = readJson(file2);
|
|
56640
|
+
const hooks = harness === "droid" ? config2 : config2.hooks ??= {};
|
|
56641
|
+
const timeout = harness === "droid" ? 10 : 1e4;
|
|
56642
|
+
for (const [event, n] of pairs) {
|
|
56643
|
+
hooks[event] = [...keepOthers(hooks[event]), { hooks: [{ type: "command", command: notify2(n), timeout }] }];
|
|
56644
|
+
}
|
|
56645
|
+
writeJson(file2, config2);
|
|
56646
|
+
return pairs.map(([e]) => e);
|
|
56647
|
+
}
|
|
56648
|
+
case "claude-code":
|
|
56649
|
+
case "codex": {
|
|
56650
|
+
const config2 = readJson(file2);
|
|
56651
|
+
const hooks = config2.hooks ??= {};
|
|
56652
|
+
for (const event of EVENTS) {
|
|
56653
|
+
const entry = { hooks: [{ type: "command", command: `${command} notify --format ${harness}`, timeout: TIMEOUT_S[harness][event] ?? 10 }] };
|
|
56654
|
+
if (event === "PostToolUse" && harness === "claude-code") entry.matcher = "*";
|
|
56655
|
+
if (event === "Stop" && harness === "claude-code") {
|
|
56656
|
+
entry.hooks.push({ type: "command", command: `${command} wait --max ${WAIT_MAX_S}`, asyncRewake: true, timeout: WAIT_TIMEOUT_S });
|
|
56657
|
+
}
|
|
56658
|
+
hooks[event] = [...keepOthers(hooks[event]), entry];
|
|
56659
|
+
}
|
|
56660
|
+
writeJson(file2, config2);
|
|
56661
|
+
return EVENTS;
|
|
56662
|
+
}
|
|
56663
|
+
}
|
|
56236
56664
|
}
|
|
56237
56665
|
function uninstallHooks(harness, file2) {
|
|
56238
56666
|
if (!(0, import_node_fs2.existsSync)(file2)) return false;
|
|
56239
|
-
|
|
56240
|
-
|
|
56241
|
-
|
|
56242
|
-
|
|
56243
|
-
|
|
56244
|
-
|
|
56245
|
-
|
|
56246
|
-
|
|
56247
|
-
|
|
56248
|
-
|
|
56249
|
-
const
|
|
56250
|
-
|
|
56251
|
-
if (kept
|
|
56252
|
-
|
|
56253
|
-
|
|
56254
|
-
|
|
56255
|
-
|
|
56256
|
-
|
|
56257
|
-
|
|
56258
|
-
|
|
56259
|
-
|
|
56260
|
-
|
|
56261
|
-
|
|
56262
|
-
|
|
56263
|
-
|
|
56264
|
-
|
|
56265
|
-
|
|
56266
|
-
|
|
56667
|
+
switch (harness) {
|
|
56668
|
+
case "opencode":
|
|
56669
|
+
if (!(0, import_node_fs2.readFileSync)(file2, "utf8").startsWith(OPENCODE_MARKER)) return false;
|
|
56670
|
+
(0, import_node_fs2.rmSync)(file2);
|
|
56671
|
+
return true;
|
|
56672
|
+
case "amp":
|
|
56673
|
+
if (!(0, import_node_fs2.readFileSync)(file2, "utf8").startsWith(AMP_MARKER)) return false;
|
|
56674
|
+
(0, import_node_fs2.rmSync)(file2);
|
|
56675
|
+
return true;
|
|
56676
|
+
case "kimi": {
|
|
56677
|
+
const text = (0, import_node_fs2.readFileSync)(file2, "utf8");
|
|
56678
|
+
const kept = stripKimi(text);
|
|
56679
|
+
if (kept === text) return false;
|
|
56680
|
+
(0, import_node_fs2.writeFileSync)(file2, kept);
|
|
56681
|
+
return true;
|
|
56682
|
+
}
|
|
56683
|
+
case "goose": {
|
|
56684
|
+
const root = (0, import_node_path3.dirname)((0, import_node_path3.dirname)(file2));
|
|
56685
|
+
if (readJson((0, import_node_path3.join)(root, "plugin.json")).name !== GOOSE_PLUGIN.name) return false;
|
|
56686
|
+
(0, import_node_fs2.rmSync)(root, { recursive: true, force: true });
|
|
56687
|
+
return true;
|
|
56688
|
+
}
|
|
56689
|
+
case "antigravity": {
|
|
56690
|
+
const config2 = readJson(file2);
|
|
56691
|
+
if (!("agenthub" in config2)) return false;
|
|
56692
|
+
delete config2.agenthub;
|
|
56693
|
+
writeJson(file2, config2);
|
|
56694
|
+
return true;
|
|
56695
|
+
}
|
|
56696
|
+
case "copilot":
|
|
56697
|
+
case "cursor":
|
|
56698
|
+
case "crush": {
|
|
56699
|
+
const config2 = readJson(file2);
|
|
56700
|
+
if (!config2.hooks) return false;
|
|
56701
|
+
let changed = false;
|
|
56702
|
+
for (const [event, entries] of Object.entries(config2.hooks)) {
|
|
56703
|
+
const kept = entries.filter((h) => !isOurs(h));
|
|
56704
|
+
if (kept.length !== entries.length) changed = true;
|
|
56705
|
+
if (kept.length) config2.hooks[event] = kept;
|
|
56706
|
+
else delete config2.hooks[event];
|
|
56707
|
+
}
|
|
56708
|
+
if (!changed) return false;
|
|
56709
|
+
const empty = !Object.keys(config2.hooks).length && Object.keys(config2).every((k) => k === "version" || k === "hooks");
|
|
56710
|
+
if (harness === "copilot" && empty) (0, import_node_fs2.rmSync)(file2);
|
|
56711
|
+
else writeJson(file2, config2);
|
|
56712
|
+
return true;
|
|
56713
|
+
}
|
|
56714
|
+
default: {
|
|
56715
|
+
const config2 = readJson(file2);
|
|
56716
|
+
const hooks = harness === "droid" ? config2 : config2.hooks;
|
|
56717
|
+
if (!hooks || typeof hooks !== "object") return false;
|
|
56718
|
+
let changed = false;
|
|
56719
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
56720
|
+
if (!Array.isArray(groups)) continue;
|
|
56721
|
+
const kept = keepOthers(groups);
|
|
56722
|
+
if (kept.length !== groups.length || kept.some((g, i) => g.hooks.length !== groups[i].hooks.length)) changed = true;
|
|
56723
|
+
if (kept.length) hooks[event] = kept;
|
|
56724
|
+
else delete hooks[event];
|
|
56725
|
+
}
|
|
56726
|
+
if (changed) writeJson(file2, config2);
|
|
56727
|
+
return changed;
|
|
56728
|
+
}
|
|
56267
56729
|
}
|
|
56268
|
-
|
|
56269
|
-
|
|
56730
|
+
}
|
|
56731
|
+
function keepOthers(groups) {
|
|
56732
|
+
return (groups ?? []).map((g) => ({ ...g, hooks: (g.hooks ?? []).filter((h) => !isOurs(h)) })).filter((g) => g.hooks.length);
|
|
56733
|
+
}
|
|
56734
|
+
function stripKimi(text) {
|
|
56735
|
+
const start = text.indexOf(KIMI_BEGIN);
|
|
56736
|
+
if (start < 0) return text;
|
|
56737
|
+
const end = text.indexOf(KIMI_END, start);
|
|
56738
|
+
const after = end < 0 ? "" : text.slice(end + KIMI_END.length).replace(/^\n/, "");
|
|
56739
|
+
return (text.slice(0, start).replace(/\n+$/, "\n") + after).replace(/^\n+$/, "");
|
|
56270
56740
|
}
|
|
56271
56741
|
function readJson(file2) {
|
|
56272
56742
|
return (0, import_node_fs2.existsSync)(file2) ? JSON.parse((0, import_node_fs2.readFileSync)(file2, "utf8")) : {};
|
|
@@ -56330,7 +56800,7 @@ function openBrowser(url2) {
|
|
|
56330
56800
|
init_notify();
|
|
56331
56801
|
|
|
56332
56802
|
// src/cli/help.ts
|
|
56333
|
-
var HARNESS_LIST = "claude-code, codex, copilot or
|
|
56803
|
+
var HARNESS_LIST = "claude-code, codex, copilot, opencode, amp, droid, gemini, qwen, cursor, antigravity, kimi, goose or crush";
|
|
56334
56804
|
var COMMANDS = {
|
|
56335
56805
|
login: {
|
|
56336
56806
|
short: "sign in to a hub",
|
|
@@ -56385,13 +56855,17 @@ var COMMANDS = {
|
|
|
56385
56855
|
details: [
|
|
56386
56856
|
"--scope user (the default) writes your own settings; --scope project writes the repo's local settings.",
|
|
56387
56857
|
"Claude Code: ~/.claude/settings.json (or .claude/settings.local.json), including the Stop hook that wakes an idle session.",
|
|
56388
|
-
"Codex: ~/.codex/hooks.json; then trust the hooks in Codex's /hooks. Copilot CLI: ~/.copilot/hooks/agenthub.json. OpenCode: a plugin in ~/.config/opencode/plugins/.",
|
|
56858
|
+
"Codex: ~/.codex/hooks.json; then trust the hooks in Codex's /hooks. Copilot CLI: ~/.copilot/hooks/agenthub.json. OpenCode: a plugin in ~/.config/opencode/plugins/. Amp: a plugin in ~/.config/amp/plugins/.",
|
|
56859
|
+
"Factory Droid: ~/.factory/hooks.json. Gemini CLI: ~/.gemini/settings.json. Qwen Code: ~/.qwen/settings.json. Cursor: ~/.cursor/hooks.json. Antigravity: ~/.gemini/config/hooks.json (.agents/hooks.json in a repo). Kimi Code: a block in ~/.kimi-code/config.toml (user scope only). Goose: a plugin in ~/.agents/plugins/agenthub/. Crush: ~/.config/crush/crush.json (PreToolUse only).",
|
|
56860
|
+
"Where a harness can't be woken, its Stop hook hands the agent any request that came in during the turn before it goes idle.",
|
|
56389
56861
|
"Re-running install updates the hooks; uninstall removes only ours."
|
|
56390
56862
|
],
|
|
56391
56863
|
examples: [
|
|
56392
56864
|
["agenthub hooks install claude-code", "for all your Claude Code sessions"],
|
|
56393
56865
|
["agenthub hooks install codex", "then open /hooks in Codex to trust them"],
|
|
56394
56866
|
["agenthub hooks install opencode --scope project", "only in this repo"],
|
|
56867
|
+
["agenthub hooks install amp", "an Amp plugin that also wakes idle threads"],
|
|
56868
|
+
["agenthub hooks install gemini", "Gemini CLI; also droid, qwen, cursor, antigravity, kimi, goose or crush"],
|
|
56395
56869
|
["agenthub hooks uninstall claude-code", ""]
|
|
56396
56870
|
]
|
|
56397
56871
|
},
|
|
@@ -56407,34 +56881,44 @@ var COMMANDS = {
|
|
|
56407
56881
|
]
|
|
56408
56882
|
},
|
|
56409
56883
|
wake: {
|
|
56410
|
-
short: "turn waking idle agents on or off,
|
|
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
|
|
56884
|
+
short: "turn waking idle agents on or off, and choose whether they ask you first",
|
|
56885
|
+
usage: ["agenthub wake [on|off|all|ask|auto] [--project]", "agenthub wake default --project"],
|
|
56886
|
+
summary: "Choose whether idle agents are started up when a hub message arrives, in Claude Code, OpenCode, Amp, pi, Roo Code (with its extension) and Kimi Code (in kimi web), and what they do once woken.",
|
|
56413
56887
|
details: [
|
|
56414
56888
|
"on (the default): requests and replies wake an idle agent; fyi waits for its next turn. all: fyi wakes it too. off: nothing wakes it.",
|
|
56415
|
-
"
|
|
56416
|
-
"
|
|
56889
|
+
"auto (the default): a woken agent handles the messages by itself, within the Agent Hub rules (answers and reviews read-only; handoffs, pushes and merges still wait for you). ask: it tells you what came in and waits for your go-ahead. The same applies when a Stop hook keeps a turn going for a message that came in during it.",
|
|
56890
|
+
"on|off|all and ask|auto are separate settings: set one of each.",
|
|
56891
|
+
"--project sets it for the repo you're in only; it wins over your setting for every repo. default --project removes both of the repo's own settings.",
|
|
56892
|
+
"Without a value it shows what applies here and where that comes from. AGENTHUB_WAKE=0|on|all and AGENTHUB_WAKE_ACT=ask|auto override them for one shell."
|
|
56417
56893
|
],
|
|
56418
56894
|
examples: [
|
|
56419
56895
|
["agenthub wake", "what applies in this repo"],
|
|
56420
56896
|
["agenthub wake off", "never wake idle agents"],
|
|
56897
|
+
["agenthub wake ask", "wake them, but have them ask you before acting"],
|
|
56898
|
+
["agenthub wake auto", "back to the default: act without asking, within the rules"],
|
|
56421
56899
|
["agenthub wake off --project", "not in this repo, still elsewhere"],
|
|
56422
|
-
["agenthub wake
|
|
56423
|
-
["agenthub wake default --project", "this repo follows your
|
|
56900
|
+
["agenthub wake ask --project", "ask first in this repo only"],
|
|
56901
|
+
["agenthub wake default --project", "this repo follows your settings again"]
|
|
56424
56902
|
]
|
|
56425
56903
|
},
|
|
56426
56904
|
notify: {
|
|
56427
56905
|
short: "print new messages for a hook",
|
|
56428
|
-
usage: ["agenthub notify --format <
|
|
56906
|
+
usage: ["agenthub notify --format <harness|text> [--event <session-start|prompt|post-tool|stop|session-end>]"],
|
|
56429
56907
|
summary: "What the harness hooks run: reads the hook's JSON on stdin and prints new hub messages for the session, in the harness's format. You don't run it yourself.",
|
|
56430
|
-
details: [
|
|
56908
|
+
details: [
|
|
56909
|
+
"--format is the harness: claude-code, codex, copilot, amp, droid, gemini, qwen, cursor, antigravity, kimi, goose, crush or roo; or text for plain text (the OpenCode and pi plugins use it).",
|
|
56910
|
+
"It never fails the harness: off the hub, signed out or on any error it prints nothing. Stop marks the agent idle and session-end takes it offline. Only Kimi Code's Stop hook exits 2, to keep a turn going."
|
|
56911
|
+
],
|
|
56431
56912
|
examples: [[`echo '{"hook_event_name":"UserPromptSubmit","session_id":"abc"}' | agenthub notify --format text`, "see what a hook would print"]]
|
|
56432
56913
|
},
|
|
56433
56914
|
wait: {
|
|
56434
56915
|
short: "wait for a message that should wake an idle agent",
|
|
56435
|
-
usage: ["agenthub wait [--max <seconds>]"],
|
|
56916
|
+
usage: ["agenthub wait [--max <seconds>] [--deliver kimi]"],
|
|
56436
56917
|
summary: "What harnesses run when their agent goes idle: waits for a hub message that should wake it, then exits 2 and prints the summaries (else 0). You don't run it yourself.",
|
|
56437
|
-
details: [
|
|
56918
|
+
details: [
|
|
56919
|
+
"The next prompt, a newer waiter for the same session, or --max (12 hours by default) stops it. `agenthub wake` decides whether it waits at all, and whether its text tells the agent to ask you first.",
|
|
56920
|
+
"--deliver kimi is what Kimi Code's Stop hook starts in the background: instead of printing, it sends the summaries to the session through the local kimi web server."
|
|
56921
|
+
],
|
|
56438
56922
|
examples: [[`echo '{"session_id":"abc"}' | agenthub wait --max 60; echo $?`, "wait up to a minute; 2 means it would wake the agent"]]
|
|
56439
56923
|
},
|
|
56440
56924
|
admin: {
|
|
@@ -56458,6 +56942,7 @@ var ENVIRONMENT = `Environment:
|
|
|
56458
56942
|
AGENTHUB_URL the hub (else the one from agenthub login)
|
|
56459
56943
|
AGENTHUB_AGENT_NAME a fixed agent name for hooks (default: one per harness session)
|
|
56460
56944
|
AGENTHUB_WAKE=0|on|all whether idle agents are woken; overrides agenthub wake
|
|
56945
|
+
AGENTHUB_WAKE_ACT=ask|auto whether a woken agent asks you first; overrides agenthub wake ask|auto
|
|
56461
56946
|
AGENTHUB_CREDENTIAL_STORE=file|keychain|auto
|
|
56462
56947
|
AGENTHUB_HOME config directory (default ~/.config/agenthub)
|
|
56463
56948
|
AGENTHUB_DOWNLOAD_URL where releases come from
|
|
@@ -56497,9 +56982,9 @@ function commandHelp(name) {
|
|
|
56497
56982
|
init_settings();
|
|
56498
56983
|
var import_meta = {};
|
|
56499
56984
|
function version2() {
|
|
56500
|
-
if (true) return "0.3.
|
|
56985
|
+
if (true) return "0.3.2";
|
|
56501
56986
|
try {
|
|
56502
|
-
return JSON.parse((0,
|
|
56987
|
+
return JSON.parse((0, import_node_fs11.readFileSync)(new URL("../../package.json", import_meta.url), "utf8")).version;
|
|
56503
56988
|
} catch {
|
|
56504
56989
|
return "unknown";
|
|
56505
56990
|
}
|
|
@@ -56568,8 +57053,10 @@ async function main(argv) {
|
|
|
56568
57053
|
const format = flag(args, "--format") ?? "text";
|
|
56569
57054
|
const event = flag(args, "--event");
|
|
56570
57055
|
const payload = await readStdin(500);
|
|
56571
|
-
const out = await
|
|
56572
|
-
if (out) process.stdout.write(out + "\n");
|
|
57056
|
+
const out = await notifyHook({ format, event, payload }, { store: credentialStore(), version: version2() });
|
|
57057
|
+
if (out.stdout) process.stdout.write(out.stdout + "\n");
|
|
57058
|
+
if (out.stderr) process.stderr.write(out.stderr + "\n");
|
|
57059
|
+
return out.code ?? 0;
|
|
56573
57060
|
} catch {
|
|
56574
57061
|
}
|
|
56575
57062
|
return 0;
|
|
@@ -56584,20 +57071,25 @@ async function main(argv) {
|
|
|
56584
57071
|
return watch2({ json: json2, agentName, signal: ac.signal }, { store: credentialStore(), write: (l) => console.log(l) });
|
|
56585
57072
|
}
|
|
56586
57073
|
if (cmd === "wake") {
|
|
56587
|
-
const { wakeSetting: wakeSetting2 } = await Promise.resolve().then(() => (init_wait(), wait_exports));
|
|
57074
|
+
const { actSetting: actSetting2, wakeSetting: wakeSetting2 } = await Promise.resolve().then(() => (init_wait(), wait_exports));
|
|
56588
57075
|
const { gitRemotes: gitRemotes2 } = await Promise.resolve().then(() => (init_notify(), notify_exports));
|
|
56589
57076
|
const perProject = args.includes("--project");
|
|
56590
57077
|
const value = args.filter((a) => a !== "--project").shift();
|
|
56591
57078
|
const origin = gitRemotes2(process.cwd()).origin;
|
|
56592
57079
|
const key = origin ? projectKey(origin) : void 0;
|
|
56593
57080
|
const describe3 = (mode) => mode === "off" ? "off: idle agents aren't woken" : mode === "all" ? "all: idle agents are woken for every message, fyi too" : "on: idle agents are woken for requests and replies, not fyi";
|
|
57081
|
+
const describeAct = (mode) => mode === "ask" ? "ask: a woken agent tells you what came in and asks before acting on it" : "auto: a woken agent handles messages by itself, within the Agent Hub rules";
|
|
57082
|
+
const sources = (name) => ({ env: ` (from ${name})`, project: ` (this repo's setting, ${key})`, user: " (your setting for every repo)", default: " (the default)" });
|
|
57083
|
+
const noEnv = { ...process.env, AGENTHUB_WAKE: void 0, AGENTHUB_WAKE_ACT: void 0 };
|
|
56594
57084
|
if (value === void 0) {
|
|
56595
|
-
const
|
|
56596
|
-
const
|
|
56597
|
-
console.log(describe3(mode) +
|
|
57085
|
+
const wake = wakeSetting2(process.env, origin);
|
|
57086
|
+
const act = actSetting2(process.env, origin);
|
|
57087
|
+
console.log(describe3(wake.mode) + sources("AGENTHUB_WAKE")[wake.from]);
|
|
57088
|
+
console.log(describeAct(act.mode) + sources("AGENTHUB_WAKE_ACT")[act.from]);
|
|
56598
57089
|
return 0;
|
|
56599
57090
|
}
|
|
56600
|
-
|
|
57091
|
+
const isAct = value === "ask" || value === "auto";
|
|
57092
|
+
if (!["on", "off", "all", "ask", "auto", ...perProject ? ["default"] : []].includes(value)) {
|
|
56601
57093
|
console.error(commandHelp("wake"));
|
|
56602
57094
|
return 2;
|
|
56603
57095
|
}
|
|
@@ -56608,28 +57100,52 @@ async function main(argv) {
|
|
|
56608
57100
|
return 1;
|
|
56609
57101
|
}
|
|
56610
57102
|
const projects = { ...settings.wake_projects };
|
|
56611
|
-
|
|
57103
|
+
const actProjects = { ...settings.wake_act_projects };
|
|
57104
|
+
if (value === "default") {
|
|
57105
|
+
delete projects[key];
|
|
57106
|
+
delete actProjects[key];
|
|
57107
|
+
} else if (isAct) actProjects[key] = value;
|
|
56612
57108
|
else projects[key] = value;
|
|
56613
|
-
writeSettings({ ...settings, wake_projects: projects });
|
|
57109
|
+
writeSettings({ ...settings, wake_projects: projects, wake_act_projects: actProjects });
|
|
57110
|
+
} else if (isAct) {
|
|
57111
|
+
writeSettings({ ...settings, wake_act: value });
|
|
56614
57112
|
} else {
|
|
56615
57113
|
writeSettings({ ...settings, wake: value });
|
|
56616
57114
|
}
|
|
56617
57115
|
const where = perProject ? `in ${key}` : "in every repo without its own setting";
|
|
56618
|
-
if (value === "default")
|
|
57116
|
+
if (value === "default") {
|
|
57117
|
+
console.log(`${key} now follows your settings for every repo:`);
|
|
57118
|
+
console.log(` ${describe3(wakeSetting2(noEnv, origin).mode)}`);
|
|
57119
|
+
console.log(` ${describeAct(actSetting2(noEnv, origin).mode)}`);
|
|
57120
|
+
} else if (value === "ask") console.log(`A woken agent will tell you what came in and ask before acting on it, ${where}. This also applies when a Stop hook keeps a turn going for a new message.`);
|
|
57121
|
+
else if (value === "auto") console.log(`A woken agent will handle hub messages by itself, within the Agent Hub rules, ${where}. Handoffs and anything that runs commands, pushes or merges still wait for you.`);
|
|
56619
57122
|
else if (value === "off") console.log(`Idle agents won't be woken for hub messages ${where}. They still see them on their next turn.`);
|
|
56620
|
-
else console.log(`Idle agents will be woken for ${value === "all" ? "every hub message, fyi too" : "requests and replies"} ${where}, in Claude Code, OpenCode and
|
|
56621
|
-
|
|
57123
|
+
else console.log(`Idle agents will be woken for ${value === "all" ? "every hub message, fyi too" : "requests and replies"} ${where}, in Claude Code, OpenCode, Amp, pi, Roo Code and Kimi Code (kimi web).`);
|
|
57124
|
+
const envName = isAct ? "AGENTHUB_WAKE_ACT" : "AGENTHUB_WAKE";
|
|
57125
|
+
if (value !== "default" && process.env[envName] !== void 0) console.log(`${envName}=${process.env[envName]} is set in this shell and wins over this setting.`);
|
|
56622
57126
|
return 0;
|
|
56623
57127
|
}
|
|
56624
57128
|
if (cmd === "wait") {
|
|
56625
57129
|
try {
|
|
56626
57130
|
const max = Number(flag(args, "--max"));
|
|
56627
|
-
const
|
|
57131
|
+
const deliver = flag(args, "--deliver");
|
|
57132
|
+
const fromEnv = process.env.AGENTHUB_WAIT_PAYLOAD;
|
|
57133
|
+
let payload;
|
|
57134
|
+
try {
|
|
57135
|
+
payload = fromEnv ? JSON.parse(fromEnv) : await readStdin(500);
|
|
57136
|
+
} catch {
|
|
57137
|
+
payload = void 0;
|
|
57138
|
+
}
|
|
56628
57139
|
const { wait: wait2 } = await Promise.resolve().then(() => (init_wait(), wait_exports));
|
|
56629
57140
|
const ac = new AbortController();
|
|
56630
57141
|
process.once("SIGINT", () => ac.abort());
|
|
56631
57142
|
process.once("SIGTERM", () => ac.abort());
|
|
56632
57143
|
const result = await wait2({ payload, signal: ac.signal, ...max > 0 ? { maxMs: max * 1e3 } : {} }, { store: credentialStore() });
|
|
57144
|
+
if (deliver === "kimi") {
|
|
57145
|
+
const { deliverToKimi: deliverToKimi2 } = await Promise.resolve().then(() => (init_kimi(), kimi_exports));
|
|
57146
|
+
if (result.code === 2 && result.text && typeof payload?.session_id === "string") await deliverToKimi2(payload.session_id, result.text);
|
|
57147
|
+
return 0;
|
|
57148
|
+
}
|
|
56633
57149
|
if (result.code === 2 && result.text) {
|
|
56634
57150
|
process.stdout.write(result.text + "\n");
|
|
56635
57151
|
return 2;
|
|
@@ -56707,13 +57223,22 @@ version: ${version2()}${newer}`);
|
|
|
56707
57223
|
return 2;
|
|
56708
57224
|
}
|
|
56709
57225
|
const h = harness;
|
|
57226
|
+
if (scope === "project" && USER_SCOPE_ONLY.has(h)) {
|
|
57227
|
+
console.error(`${h} reads hooks only from your own config, so it has no --scope project.`);
|
|
57228
|
+
return 2;
|
|
57229
|
+
}
|
|
56710
57230
|
const file2 = hookFile(h, scope, process.cwd());
|
|
56711
57231
|
if (action === "install") {
|
|
56712
57232
|
const events = installHooks(h, file2);
|
|
56713
57233
|
console.log(`Added agenthub notify to ${events.join(", ")} in ${file2}`);
|
|
56714
57234
|
if (h === "codex") console.log("Codex skips new hooks until you trust them: open /hooks in Codex and review them.");
|
|
56715
57235
|
if (h === "opencode") console.log("OpenCode loads the plugin when it next starts.");
|
|
57236
|
+
if (h === "amp") console.log("Amp loads the plugin in new sessions, or now with plugins: reload from the command palette.");
|
|
56716
57237
|
if (h === "copilot" && scope === "project") console.log("This file is in the repo: commit it only if everyone has agenthub, or add it to .git/info/exclude.");
|
|
57238
|
+
if (scope === "project" && h !== "copilot" && h !== "claude-code") console.log("This file is in the repo: commit it only if everyone has agenthub, or add it to .git/info/exclude.");
|
|
57239
|
+
if (h === "qwen" && scope === "project") console.log("Qwen Code runs a repo's hooks only in a trusted folder.");
|
|
57240
|
+
if (h === "goose") console.log("Goose loads the plugin when it next starts. Its hooks can't add context, so it gets a request that arrives during a turn before it stops, and presence; run agenthub watch beside it for the rest.");
|
|
57241
|
+
if (h === "cursor" || h === "antigravity" || h === "kimi") console.log(`${h === "cursor" ? "Cursor" : h === "kimi" ? "Kimi Code" : "Antigravity"} picks up the hooks in new sessions.`);
|
|
56717
57242
|
} else {
|
|
56718
57243
|
console.log(uninstallHooks(h, file2) ? `Removed agenthub hooks from ${file2}` : `No agenthub hooks in ${file2}`);
|
|
56719
57244
|
}
|