@join-pantheon/cli 0.19.1 → 0.20.0
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 +24 -0
- package/dist/accounts.js +61 -0
- package/dist/admin-stats.js +116 -0
- package/dist/agents.js +2 -0
- package/dist/cli.js +337 -89
- package/dist/client.js +31 -1
- package/dist/config.js +229 -23
- package/dist/doctor.js +13 -1
- package/dist/mcp.js +24 -15
- package/dist/notifier.js +1 -1
- package/dist/persona.js +4 -1
- package/dist/profile.js +28 -4
- package/dist/protocol.js +10 -1
- package/dist/quickstart.js +35 -6
- package/dist/relay.js +524 -39
- package/dist/roster-file.js +26 -0
- package/dist/store.js +5 -0
- package/dist/tray.js +22 -1
- package/dist/worker-fleet.js +84 -0
- package/dist/worker.js +41 -9
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ process.emitWarning = ((warning, ...rest) => {
|
|
|
9
9
|
import { randomBytes } from "node:crypto";
|
|
10
10
|
import { dirname, join, resolve } from "node:path";
|
|
11
11
|
import { PantheonClient } from "./client.js";
|
|
12
|
-
import { fromInvite, load, save, toInvite, CONFIG_PATH, DEFAULT_RELAY } from "./config.js";
|
|
12
|
+
import { fromInvite, load, save, toInvite, resolveFor, loadAll, repoInfo, bindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY } from "./config.js";
|
|
13
13
|
import { validEmail, validUsername } from "./accounts.js";
|
|
14
14
|
import { install, launcher } from "./install.js";
|
|
15
15
|
import { Relay } from "./relay.js";
|
|
@@ -219,7 +219,37 @@ function sameHost(a, b) {
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
function need() {
|
|
222
|
-
|
|
222
|
+
const r = resolveFor(process.cwd());
|
|
223
|
+
if (r.cfg && r.cfg.space)
|
|
224
|
+
return r.cfg;
|
|
225
|
+
if (r.reason === "unbound") {
|
|
226
|
+
const names = Object.values(loadAll()?.spaces ?? {}).map((m) => m.name).join(", ");
|
|
227
|
+
die(`this folder is not bound to one of your ${r.memberships} teams (${names}).\n From the team's repo: pantheon use <team> --here\n Or pick a default: pantheon use <team>`);
|
|
228
|
+
}
|
|
229
|
+
if (r.reason === "stale")
|
|
230
|
+
die(`this repo is bound to a team this machine is no longer in (${r.binding?.space}).\n Rebind it: pantheon use <team> --here · or rejoin: pantheon join <invite>`);
|
|
231
|
+
if (r.cfg && !r.cfg.space)
|
|
232
|
+
die("signed in, but not on a team yet. Run `pantheon start` (create one) or `pantheon join <invite>` (join one).");
|
|
233
|
+
return die("not configured. Run `pantheon signup` then `pantheon start` (create a team), or `pantheon join <invite>` (join one).");
|
|
234
|
+
}
|
|
235
|
+
function targets() {
|
|
236
|
+
const f = loadAll();
|
|
237
|
+
if (flag("space")) {
|
|
238
|
+
const m = f && findMembership(f, flag("space"));
|
|
239
|
+
if (!f || !m)
|
|
240
|
+
die(`you are not in a team called "${flag("space")}"`);
|
|
241
|
+
return [configFor(f, m, null)];
|
|
242
|
+
}
|
|
243
|
+
const r = resolveFor(process.cwd());
|
|
244
|
+
if (r.cfg && r.cfg.space && r.reason !== "default")
|
|
245
|
+
return [r.cfg];
|
|
246
|
+
if (r.reason === "default" && f)
|
|
247
|
+
return Object.values(f.spaces ?? {}).map((m) => configFor(f, m, null));
|
|
248
|
+
return [need()];
|
|
249
|
+
}
|
|
250
|
+
function needStrict() {
|
|
251
|
+
const r = resolveFor(process.cwd(), { strict: true });
|
|
252
|
+
return r.cfg && r.cfg.space ? r.cfg : null;
|
|
223
253
|
}
|
|
224
254
|
function printGranted(granted) {
|
|
225
255
|
if (!granted.length)
|
|
@@ -283,6 +313,13 @@ ${B("getting going — then two commands, run from your project's repo")}
|
|
|
283
313
|
--no-worker, --no-hook, --no-notifications, --no-menubar.
|
|
284
314
|
Already a member, new machine or fresh pull? pantheon install --user does the same.
|
|
285
315
|
|
|
316
|
+
${B("several teams on one machine")}
|
|
317
|
+
pantheon spaces your teams, which repo is bound to which, and where this folder points
|
|
318
|
+
pantheon use <team> --here bind this repo to a team (agents opened here speak in it)
|
|
319
|
+
pantheon repo add | list the repos a team works from; a delivery must come from one of them
|
|
320
|
+
pantheon leave <team> drop one team from this machine (its token and repo bindings)
|
|
321
|
+
pantheon profile set --space <team> … authority that applies in one team only (e.g. --human-required pricing)
|
|
322
|
+
|
|
286
323
|
${B("day to day")}
|
|
287
324
|
pantheon status who is here, what is being worked on, what needs you
|
|
288
325
|
pantheon brief what needs you, and what your agent already handled
|
|
@@ -586,86 +623,94 @@ async function main() {
|
|
|
586
623
|
return;
|
|
587
624
|
}
|
|
588
625
|
case "status": {
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
626
|
+
const showStatus = async (cfg) => {
|
|
627
|
+
const client = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
628
|
+
await client.connect().catch((e) => {
|
|
629
|
+
if (client.pending) {
|
|
630
|
+
console.error(`${B("not admitted yet")} (${C(cfg.member)}, fingerprint ${C(fingerprint(cfg.publicKey))})\n`);
|
|
631
|
+
console.error(client.pending);
|
|
632
|
+
process.exit(2);
|
|
633
|
+
}
|
|
634
|
+
die(`cannot reach relay at ${cfg.relay} — ${e.message}`);
|
|
635
|
+
});
|
|
636
|
+
await new Promise((r) => setTimeout(r, 350));
|
|
637
|
+
const peers = client.roster.filter((m) => m.id !== cfg.member);
|
|
638
|
+
console.log(`${B(cfg.space)} you are ${C(cfg.member)} ${D(cfg.relay)} ${D(`${client.head} acts`)}\n`);
|
|
639
|
+
console.log(B("members"));
|
|
640
|
+
if (!peers.length)
|
|
641
|
+
console.log(` ${D("(nobody else yet — send them an invite from `pantheon invite`)")}`);
|
|
642
|
+
for (const p of peers) {
|
|
643
|
+
const behind = p.seen_seq === undefined ? null : client.head - p.seen_seq;
|
|
644
|
+
const reach = behind === null
|
|
645
|
+
? D(p.delivered_seq !== undefined && p.delivered_seq >= client.head
|
|
646
|
+
? "delivered, but no read-receipt — no wake-up hook on their machine"
|
|
647
|
+
: "no wake-up hook on their machine — nothing surfaces until their human types")
|
|
648
|
+
: behind > 0
|
|
649
|
+
? `\x1b[33mtheir agent has not seen your last ${behind} act${behind === 1 ? "" : "s"}\x1b[0m`
|
|
650
|
+
: D("their agent is caught up");
|
|
651
|
+
console.log(` ${p.online ? C("●") : D("○")} ${p.id.padEnd(14)} ${D(p.agent.padEnd(12))} ${reach}`);
|
|
596
652
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
console.log(
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
653
|
+
const goals = [...client.goals.values()];
|
|
654
|
+
console.log(`\n${B("goals")}`);
|
|
655
|
+
if (!goals.length)
|
|
656
|
+
console.log(` ${D("(none)")}`);
|
|
657
|
+
for (const g of goals) {
|
|
658
|
+
console.log(` ${g.state.padEnd(8)} ${String(g.acts).padStart(3)}/${g.budget} ${g.title} ${D(g.id)}`);
|
|
659
|
+
}
|
|
660
|
+
const owed = client.obligations;
|
|
661
|
+
const ageOf = (t) => {
|
|
662
|
+
const m = Math.round((Date.now() - t) / 60000);
|
|
663
|
+
return m < 60 ? `${m}m` : `${Math.round(m / 60)}h`;
|
|
664
|
+
};
|
|
665
|
+
const human = owed.filter((o) => o.kind === "HUMAN");
|
|
666
|
+
const between = owed.filter((o) => o.kind !== "HUMAN");
|
|
667
|
+
console.log(`\n${B("needs a human")}`);
|
|
668
|
+
if (!human.length)
|
|
669
|
+
console.log(` ${D("(nothing waiting on you)")}`);
|
|
670
|
+
for (const o of human) {
|
|
671
|
+
const who = o.owner.replace(/^human:/, "");
|
|
672
|
+
const stale = Date.now() - o.created_at > 3_600_000;
|
|
673
|
+
console.log(` ${stale ? "\x1b[33m" : C("")}${who.padEnd(14)}${ageOf(o.created_at).padEnd(6)}\x1b[0m ${o.body}`);
|
|
674
|
+
if (who === cfg.member)
|
|
675
|
+
console.log(` ${D("your peer was told this decision was coming — answer it or say it is dropped")}`);
|
|
676
|
+
}
|
|
677
|
+
console.log(`\n${B("between delegates")}`);
|
|
678
|
+
if (!between.length)
|
|
679
|
+
console.log(` ${D("(nothing owed)")}`);
|
|
680
|
+
for (const o of between) {
|
|
681
|
+
console.log(` ${o.kind.padEnd(7)} ${o.owner.padEnd(14)}${ageOf(o.created_at).padEnd(6)} ${o.body}`);
|
|
682
|
+
}
|
|
683
|
+
client.close();
|
|
627
684
|
};
|
|
628
|
-
const
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
for (const o of human) {
|
|
634
|
-
const who = o.owner.replace(/^human:/, "");
|
|
635
|
-
const stale = Date.now() - o.created_at > 3_600_000;
|
|
636
|
-
console.log(` ${stale ? "\x1b[33m" : C("")}${who.padEnd(14)}${ageOf(o.created_at).padEnd(6)}\x1b[0m ${o.body}`);
|
|
637
|
-
if (who === cfg.member)
|
|
638
|
-
console.log(` ${D("your peer was told this decision was coming — answer it or say it is dropped")}`);
|
|
639
|
-
}
|
|
640
|
-
console.log(`\n${B("between delegates")}`);
|
|
641
|
-
if (!between.length)
|
|
642
|
-
console.log(` ${D("(nothing owed)")}`);
|
|
643
|
-
for (const o of between) {
|
|
644
|
-
console.log(` ${o.kind.padEnd(7)} ${o.owner.padEnd(14)}${ageOf(o.created_at).padEnd(6)} ${o.body}`);
|
|
685
|
+
const ts = targets();
|
|
686
|
+
for (const [i, c] of ts.entries()) {
|
|
687
|
+
if (ts.length > 1)
|
|
688
|
+
console.log(`${i ? "\n" : ""}${D("━━━ team")} ${B(c.space)} ${D(`(${i + 1} of ${ts.length}) ━━━`)}`);
|
|
689
|
+
await showStatus(c);
|
|
645
690
|
}
|
|
646
|
-
client.close();
|
|
647
691
|
return;
|
|
648
692
|
}
|
|
649
693
|
case "brief": {
|
|
650
|
-
const cfg
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
694
|
+
for (const cfg of targets()) {
|
|
695
|
+
const profile = loadProfile(cfg);
|
|
696
|
+
const client = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
697
|
+
await client.connect().catch((e) => die(client.pending ?? e.message));
|
|
698
|
+
await new Promise((r) => setTimeout(r, 350));
|
|
699
|
+
process.stdout.write(renderBrief({
|
|
700
|
+
space: cfg.space,
|
|
701
|
+
person: {
|
|
702
|
+
member: cfg.member,
|
|
703
|
+
display_name: profile.human.display_name,
|
|
704
|
+
title: profile.human.title,
|
|
705
|
+
responsibilities: profile.human.responsibilities,
|
|
706
|
+
},
|
|
707
|
+
owed: client.myObligations().map((o) => ({
|
|
708
|
+
id: o.id, kind: o.kind, body: o.body, created_at: o.created_at,
|
|
709
|
+
})),
|
|
710
|
+
recentRuns: recentRuns(86_400_000),
|
|
711
|
+
}));
|
|
712
|
+
client.close();
|
|
713
|
+
}
|
|
669
714
|
return;
|
|
670
715
|
}
|
|
671
716
|
case "team": {
|
|
@@ -899,11 +944,14 @@ async function main() {
|
|
|
899
944
|
}
|
|
900
945
|
case "hook": {
|
|
901
946
|
const sub = argv[1] ?? "status";
|
|
947
|
+
const hookCfg = needStrict();
|
|
902
948
|
const reportIdle = async (session, seen) => {
|
|
949
|
+
if (!hookCfg)
|
|
950
|
+
return true;
|
|
903
951
|
const { reportActivity } = await import("./pending.js");
|
|
904
952
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
905
953
|
const ok = await reportActivity({
|
|
906
|
-
session: `hook:${session}`, state: "idle", ...(seen !== undefined ? { seen } : {}),
|
|
954
|
+
cfg: hookCfg, session: `hook:${session}`, state: "idle", ...(seen !== undefined ? { seen } : {}),
|
|
907
955
|
}).catch(() => false);
|
|
908
956
|
if (ok)
|
|
909
957
|
return true;
|
|
@@ -923,8 +971,10 @@ async function main() {
|
|
|
923
971
|
}
|
|
924
972
|
catch { }
|
|
925
973
|
try {
|
|
926
|
-
|
|
927
|
-
|
|
974
|
+
if (hookCfg) {
|
|
975
|
+
const { reportActivity } = await import("./pending.js");
|
|
976
|
+
await reportActivity({ cfg: hookCfg, session: `hook:${session}`, state: "busy" });
|
|
977
|
+
}
|
|
928
978
|
}
|
|
929
979
|
catch { }
|
|
930
980
|
process.exit(0);
|
|
@@ -1139,6 +1189,15 @@ async function main() {
|
|
|
1139
1189
|
return;
|
|
1140
1190
|
}
|
|
1141
1191
|
case "leave": {
|
|
1192
|
+
if (argv[1] && !argv[1].startsWith("--")) {
|
|
1193
|
+
const all = loadAll() ?? die("not configured");
|
|
1194
|
+
const m = findMembership(all, argv[1]) ?? die(`you are not in a team called "${argv[1]}". Yours: ${Object.values(all.spaces ?? {}).map((x) => x.name).join(", ") || "none"}`);
|
|
1195
|
+
const bound = Object.values(all.bindings ?? {}).filter((b) => b.space === m.id).map((b) => b.origin ?? b.path);
|
|
1196
|
+
removeMembership(m.id);
|
|
1197
|
+
console.error(`${B("left")} "${m.name}" — its token is gone from this machine${bound.length ? `, and ${bound.length} repo binding${bound.length === 1 ? "" : "s"} with it (${bound.join(", ")})` : ""}.`);
|
|
1198
|
+
console.error(D(`Your teammates still list you in "${m.name}". Ask one of them to run: pantheon admit ${m.name === all.member ? "you" : all.member} --revoke if you are not coming back.`));
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1142
1201
|
const cfg = load();
|
|
1143
1202
|
const project = resolve(flag("project", "."));
|
|
1144
1203
|
console.error(`${B("leaving pantheon")}\n`);
|
|
@@ -1503,7 +1562,7 @@ async function main() {
|
|
|
1503
1562
|
const client = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
1504
1563
|
try {
|
|
1505
1564
|
await client.connect();
|
|
1506
|
-
await client.publishProfile(organizationProfile(profile));
|
|
1565
|
+
await client.publishProfile(organizationProfile(profile, cfg.spaceId, cfg.space));
|
|
1507
1566
|
}
|
|
1508
1567
|
finally {
|
|
1509
1568
|
client.close();
|
|
@@ -1544,6 +1603,39 @@ async function main() {
|
|
|
1544
1603
|
if (!surface && !publicChange) {
|
|
1545
1604
|
die("usage: pantheon profile set [--surface codex|claude-code|cursor] [--name name] [--title title] [--responsibilities a,b] [--human-required a,b] [--agent-can-decide a,b] [--keywords 'domain=word|word;domain=word']");
|
|
1546
1605
|
}
|
|
1606
|
+
if (flag("space")) {
|
|
1607
|
+
const pf = loadAll() ?? die("not configured");
|
|
1608
|
+
const m = findMembership(pf, flag("space")) ?? die(`you are not in a team called "${flag("space")}"`);
|
|
1609
|
+
const key = m.id.startsWith("legacy:") ? m.name : m.id;
|
|
1610
|
+
const prev = profile.spaces?.[key] ?? profile.spaces?.[m.name] ?? {};
|
|
1611
|
+
const auth = { ...(prev.authority ?? {}) };
|
|
1612
|
+
for (const domain of humanRequired ?? [])
|
|
1613
|
+
auth[domain] = "human_required";
|
|
1614
|
+
for (const domain of agentCanDecide ?? [])
|
|
1615
|
+
auth[domain] = "agent_can_decide";
|
|
1616
|
+
profile = { ...profile, spaces: { ...(profile.spaces ?? {}), [key]: {
|
|
1617
|
+
...prev,
|
|
1618
|
+
...(responsibilities !== undefined ? { responsibilities } : {}),
|
|
1619
|
+
...(humanRequired !== undefined || agentCanDecide !== undefined ? { authority: auth } : {}),
|
|
1620
|
+
...(keywords !== undefined ? { decision_keywords: keywordFlag(keywords) } : {}),
|
|
1621
|
+
} }, updated_at: Date.now() };
|
|
1622
|
+
saveProfile(profile);
|
|
1623
|
+
console.error(`${C("saved")} — authority for team "${m.name}" only`);
|
|
1624
|
+
const tc = configFor(pf, m, null);
|
|
1625
|
+
const tcl = new PantheonClient(tc, undefined, { role: "cli" });
|
|
1626
|
+
try {
|
|
1627
|
+
await tcl.connect();
|
|
1628
|
+
await tcl.publishProfile(organizationProfile(profile, m.id, m.name));
|
|
1629
|
+
console.error(`${C("published")} into "${m.name}"`);
|
|
1630
|
+
}
|
|
1631
|
+
catch (e) {
|
|
1632
|
+
console.error(`\x1b[33mnot published\x1b[0m — ${e.message}`);
|
|
1633
|
+
}
|
|
1634
|
+
finally {
|
|
1635
|
+
tcl.close();
|
|
1636
|
+
}
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1547
1639
|
const authority = { ...profile.authority };
|
|
1548
1640
|
for (const domain of humanRequired ?? [])
|
|
1549
1641
|
authority[domain] = "human_required";
|
|
@@ -1604,7 +1696,7 @@ async function main() {
|
|
|
1604
1696
|
}
|
|
1605
1697
|
profile = { ...profile, delegations: [...profile.delegations, { to, domains, starts_at: startsAt, expires_at: expiresAt }], updated_at: Date.now() };
|
|
1606
1698
|
saveProfile(profile);
|
|
1607
|
-
await client.publishProfile(organizationProfile(profile)).catch((e) => die(e.message));
|
|
1699
|
+
await client.publishProfile(organizationProfile(profile, cfg.spaceId, cfg.space)).catch((e) => die(e.message));
|
|
1608
1700
|
client.close();
|
|
1609
1701
|
console.error(`${C("delegated")} — ${domains.join(", ")} routes to ${to} until ${new Date(expiresAt).toISOString()}`);
|
|
1610
1702
|
}
|
|
@@ -1735,6 +1827,50 @@ async function main() {
|
|
|
1735
1827
|
return;
|
|
1736
1828
|
}
|
|
1737
1829
|
case "approvals": {
|
|
1830
|
+
const sub = argv[1] ?? "list";
|
|
1831
|
+
const id = argv[2];
|
|
1832
|
+
const conns = [];
|
|
1833
|
+
for (const c of targets()) {
|
|
1834
|
+
const cl = new PantheonClient(c, undefined, { role: "cli" });
|
|
1835
|
+
await cl.connect().catch((e) => die(`${c.space}: ${e.message}`));
|
|
1836
|
+
conns.push({ cfg: c, client: cl });
|
|
1837
|
+
}
|
|
1838
|
+
const closeAll = () => { for (const c of conns)
|
|
1839
|
+
c.client.close(); };
|
|
1840
|
+
const all = conns.flatMap(({ cfg, client }) => client.obligations.filter((o) => o.kind === "HUMAN" && o.owner === `human:${cfg.member}`).map((o) => ({ o, cfg, client })));
|
|
1841
|
+
const find = (want) => all.find((x) => x.o.id === want);
|
|
1842
|
+
if (sub === "answer") {
|
|
1843
|
+
if (!id)
|
|
1844
|
+
die("usage: pantheon approvals answer <obligation-id> <decision>");
|
|
1845
|
+
const hit = find(id) ?? die(`no decision ${id} is waiting on you${conns.length > 1 ? ` in any of your ${conns.length} teams` : ""}`);
|
|
1846
|
+
const answer = argv.slice(3).join(" ").trim() || die("state the decision; it will be written to the audit ledger");
|
|
1847
|
+
const sent = await hit.client.send({ act: "ANSWER", body: answer, goal: null, ref: hit.o.ref, after_seq: hit.client.readSeq });
|
|
1848
|
+
console.log(`${C("answered")} ${id}${conns.length > 1 ? ` in ${B(hit.cfg.space)}` : ""} — ledger act ${sent.id}`);
|
|
1849
|
+
closeAll();
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
if (sub === "show") {
|
|
1853
|
+
if (!id)
|
|
1854
|
+
die("usage: pantheon approvals show <obligation-id>");
|
|
1855
|
+
const hit = find(id) ?? die(`no decision ${id} is waiting on you`);
|
|
1856
|
+
console.log(`${B("Decision required")} ${hit.o.id}${conns.length > 1 ? ` ${D("in " + hit.cfg.space)}` : ""}`);
|
|
1857
|
+
console.log(` ${hit.o.body}`);
|
|
1858
|
+
if (hit.o.goal)
|
|
1859
|
+
console.log(` goal ${hit.client.goals.get(hit.o.goal)?.title ?? hit.o.goal}`);
|
|
1860
|
+
console.log(`\nAnswer: ${C(`pantheon approvals answer ${hit.o.id} 'your decision and rationale'`)}`);
|
|
1861
|
+
closeAll();
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
if (sub !== "list")
|
|
1865
|
+
die("usage: pantheon approvals [list | show <id> | answer <id> <decision>]");
|
|
1866
|
+
if (!all.length)
|
|
1867
|
+
console.log(D(conns.length > 1 ? `no decisions waiting on you in any of your ${conns.length} teams` : "no decisions waiting on you"));
|
|
1868
|
+
for (const { o, cfg, client } of all)
|
|
1869
|
+
console.log(`${C(o.id)} ${conns.length > 1 ? D(`[${cfg.space}] `) : ""}${client.goals.get(o.goal ?? "")?.title ?? "No goal"}\n ${o.body}`);
|
|
1870
|
+
closeAll();
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
case "approvals-legacy-unreachable": {
|
|
1738
1874
|
const cfg = need();
|
|
1739
1875
|
const sub = argv[1] ?? "list";
|
|
1740
1876
|
const client = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
@@ -1773,6 +1909,41 @@ async function main() {
|
|
|
1773
1909
|
return;
|
|
1774
1910
|
}
|
|
1775
1911
|
case "worker": {
|
|
1912
|
+
const allCfg = loadAll();
|
|
1913
|
+
const bindings = Object.values(allCfg?.bindings ?? {});
|
|
1914
|
+
if (!flag("project") && bindings.length) {
|
|
1915
|
+
const { WorkerFleet } = await import("./worker-fleet.js");
|
|
1916
|
+
const fleet = new WorkerFleet({
|
|
1917
|
+
...(flag("agent-cmd") ? { agentCmd: flag("agent-cmd") } : { agent: flag("agent", "auto") }),
|
|
1918
|
+
...(flag("persona") ? { persona: flag("persona") } : {}),
|
|
1919
|
+
continuations: !argv.includes("--no-continue"),
|
|
1920
|
+
allowBash: argv.includes("--allow-bash"),
|
|
1921
|
+
idleMs: numFlag("idle", LIMITS.workerIdleMs / 1000) * 1000,
|
|
1922
|
+
maxRunsPerHour: numFlag("max-runs", 12),
|
|
1923
|
+
maxRunsPerDay: numFlag("max-runs-day", 40),
|
|
1924
|
+
graceMs: numFlag("grace", LIMITS.workerGraceMs / 1000) * 1000,
|
|
1925
|
+
once: argv.includes("--once"),
|
|
1926
|
+
...(argv.includes("--no-notify") ? {} : {
|
|
1927
|
+
notify: (n) => {
|
|
1928
|
+
try {
|
|
1929
|
+
const c = load();
|
|
1930
|
+
if (c)
|
|
1931
|
+
sendDesktopNotification(n.title, n.subtitle, n.body, loadProfile(c), "", n.eyebrow);
|
|
1932
|
+
}
|
|
1933
|
+
catch { }
|
|
1934
|
+
},
|
|
1935
|
+
}),
|
|
1936
|
+
log: (l) => console.error(l),
|
|
1937
|
+
});
|
|
1938
|
+
console.error(`${B("pantheon worker")} ${D(`(${bindings.length} bound repo${bindings.length === 1 ? "" : "s"} · ctrl-c to stop)`)}`);
|
|
1939
|
+
if (argv.includes("--allow-bash")) {
|
|
1940
|
+
console.error(`\x1b[33m --allow-bash: a woken run may execute commands. Only do this if you trust every\x1b[0m`);
|
|
1941
|
+
console.error(`\x1b[33m peer you granted --may-wake, since their request is what starts the run.\x1b[0m`);
|
|
1942
|
+
}
|
|
1943
|
+
process.on("SIGINT", () => { fleet.stop(); process.exit(0); });
|
|
1944
|
+
await fleet.run();
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1776
1947
|
const cfg = need();
|
|
1777
1948
|
const project = resolve(flag("project", "."));
|
|
1778
1949
|
const worker = new Worker(cfg, {
|
|
@@ -1833,15 +2004,20 @@ async function main() {
|
|
|
1833
2004
|
return;
|
|
1834
2005
|
}
|
|
1835
2006
|
case "notifier": {
|
|
1836
|
-
const
|
|
1837
|
-
const
|
|
2007
|
+
const nf = loadAll();
|
|
2008
|
+
const teams = nf ? Object.values(nf.spaces ?? {}).map((m) => configFor(nf, m, null)) : [];
|
|
2009
|
+
const cfgs = teams.length ? teams : [need()];
|
|
2010
|
+
const notifiers = cfgs.map((c) => new HumanNotifier(c, {
|
|
1838
2011
|
reminderMs: Math.max(1, numFlag("remind", DEFAULT_REMINDER_MS / 60_000)) * 60_000,
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
2012
|
+
...(cfgs.length > 1 ? { label: c.space, stateFile: join(CONFIG_DIR, `notifier-state-${encodeURIComponent(c.spaceId ?? c.space)}.json`) } : {}),
|
|
2013
|
+
log: (line) => console.error(cfgs.length > 1 ? `[${c.space}] ${line}` : line),
|
|
2014
|
+
}));
|
|
2015
|
+
console.error(`${B("pantheon notifier")} ${D(`(${cfgs.length === 1 ? "" : cfgs.length + " teams · "}ctrl-c to stop)`)}`);
|
|
2016
|
+
const stopAll = () => { for (const n of notifiers)
|
|
2017
|
+
n.stop(); process.exit(0); };
|
|
2018
|
+
process.on("SIGINT", stopAll);
|
|
2019
|
+
process.on("SIGTERM", stopAll);
|
|
2020
|
+
await Promise.all(notifiers.map((n) => n.run()));
|
|
1845
2021
|
return;
|
|
1846
2022
|
}
|
|
1847
2023
|
case "doctor": {
|
|
@@ -1927,6 +2103,78 @@ async function main() {
|
|
|
1927
2103
|
console.error(`\n${D("It only acts on requests from peers you granted --may-wake, one run at a time.")}`);
|
|
1928
2104
|
return;
|
|
1929
2105
|
}
|
|
2106
|
+
case "spaces": {
|
|
2107
|
+
const all = loadAll() ?? die("not configured. Run `pantheon signup` first.");
|
|
2108
|
+
const spaces = Object.values(all.spaces ?? {});
|
|
2109
|
+
if (!spaces.length) {
|
|
2110
|
+
console.error(D("no teams yet — pantheon start (create one) or pantheon join <invite>"));
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
const here = resolveFor(process.cwd());
|
|
2114
|
+
console.error(`${B("your teams")} ${D(`(${spaces.length})`)}\n`);
|
|
2115
|
+
for (const m of spaces) {
|
|
2116
|
+
const bound = Object.values(all.bindings ?? {}).filter((b) => b.space === m.id);
|
|
2117
|
+
const tags = [m.id === all.default_space ? C("default") : "", here.cfg?.spaceId === m.id || (here.cfg?.space === m.name && here.reason !== "none") ? C("← here") : ""].filter(Boolean).join(" ");
|
|
2118
|
+
console.error(` ${B(m.name.padEnd(18))} ${D(new URL(m.relay).host)} ${tags}`);
|
|
2119
|
+
for (const b of bound)
|
|
2120
|
+
console.error(` ${D("repo")} ${b.origin ?? b.path}${b.origin ? D(` (${b.path})`) : ""}`);
|
|
2121
|
+
if (!bound.length)
|
|
2122
|
+
console.error(` ${D("repo none bound — run `pantheon use " + m.name + " --here` from its checkout")}`);
|
|
2123
|
+
}
|
|
2124
|
+
const r = repoInfo(process.cwd());
|
|
2125
|
+
console.error(`\n ${D("this folder:")} ${r ? (r.origin ?? r.root) : D("not a git repo")} ${D("→")} ${here.reason === "binding" ? `"${here.cfg.space}"` : here.reason === "single" ? `"${here.cfg.space}" (your only team)` : here.reason === "default" ? `unbound — humans get the default "${here.cfg.space}"; agents refuse` : here.reason === "stale" ? "bound to a team you have left" : "unbound"}`);
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
case "use": {
|
|
2129
|
+
const all = loadAll() ?? die("not configured. Run `pantheon signup` first.");
|
|
2130
|
+
const ref = argv[1] ?? die("usage: pantheon use <team> [--here] # --here binds this repo to the team; without it, sets your default");
|
|
2131
|
+
const m = findMembership(all, ref) ?? die(`you are not in a team called "${ref}". Yours: ${Object.values(all.spaces ?? {}).map((x) => x.name).join(", ") || "none"}`);
|
|
2132
|
+
if (argv.includes("--here")) {
|
|
2133
|
+
const r = repoInfo(process.cwd()) ?? die("this folder is not inside a git repo. Run it from the team's checkout.");
|
|
2134
|
+
const { replaced } = bindRepo(r, m.id);
|
|
2135
|
+
console.error(`${B("bound")} ${r.origin ?? r.root} → "${m.name}"${replaced ? D(` (was: ${findMembership(all, replaced.space)?.name ?? replaced.space})`) : ""}`);
|
|
2136
|
+
if (!r.shared)
|
|
2137
|
+
console.error(D(" this repo has no origin remote — deliveries from it cannot be fetched by teammates"));
|
|
2138
|
+
const c = resolveFor(process.cwd()).cfg;
|
|
2139
|
+
if (c && c.space === m.name) {
|
|
2140
|
+
const { repo: _r, repoOrigin: _o, ...plain } = c;
|
|
2141
|
+
const client = new PantheonClient(plain, undefined, { role: "cli" });
|
|
2142
|
+
await client.connect().then(() => client.bindRepo(r.fingerprint, r.origin)).catch((e) => console.error(D(` (could not register the repo with the relay: ${e.message} — run \`pantheon repo add\` when it is reachable)`)));
|
|
2143
|
+
client.close();
|
|
2144
|
+
}
|
|
2145
|
+
console.error(D(` Agents opened in this repo now speak as you in "${m.name}". Restart any that are open.`));
|
|
2146
|
+
}
|
|
2147
|
+
else {
|
|
2148
|
+
setDefaultSpace(m.id);
|
|
2149
|
+
console.error(`${B("default")} team is now "${m.name}" — used by your own commands when you run them outside a bound repo.`);
|
|
2150
|
+
console.error(D(" Agents never use the default; bind each repo with: pantheon use <team> --here"));
|
|
2151
|
+
}
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
case "repo": {
|
|
2155
|
+
const cfg = need();
|
|
2156
|
+
const sub = argv[1] ?? "list";
|
|
2157
|
+
const { repo: _r, repoOrigin: _o, ...plain } = cfg;
|
|
2158
|
+
const client = new PantheonClient(plain, undefined, { role: "cli" });
|
|
2159
|
+
await client.connect().catch((e) => die(client.pending ?? e.message));
|
|
2160
|
+
if (sub === "add") {
|
|
2161
|
+
const r = repoInfo(flag("path") ?? process.cwd()) ?? die("not inside a git repo");
|
|
2162
|
+
await client.bindRepo(r.fingerprint, r.origin).catch((e) => die(e.message));
|
|
2163
|
+
if (cfg.spaceId)
|
|
2164
|
+
bindRepo(r, cfg.spaceId);
|
|
2165
|
+
console.error(`${B("registered")} ${r.origin ?? r.root} as a repo "${cfg.space}" works from${r.shared ? "" : D(" (no origin remote — deliveries from it cannot be fetched)")}`);
|
|
2166
|
+
}
|
|
2167
|
+
else {
|
|
2168
|
+
await new Promise((res) => setTimeout(res, 150));
|
|
2169
|
+
console.error(`${B(`repos "${cfg.space}" works from`)} ${D(`(${client.repos.length})`)}`);
|
|
2170
|
+
for (const r of client.repos)
|
|
2171
|
+
console.error(` ${(r.origin ?? r.fingerprint).padEnd(48)} ${D(`added by ${r.added_by}`)}`);
|
|
2172
|
+
if (!client.repos.length)
|
|
2173
|
+
console.error(D(" none yet — the first member to connect from a checkout registers it"));
|
|
2174
|
+
}
|
|
2175
|
+
client.close();
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
1930
2178
|
case "mcp":
|
|
1931
2179
|
await runMcp();
|
|
1932
2180
|
return;
|
package/dist/client.js
CHANGED
|
@@ -36,6 +36,8 @@ export class PantheonClient {
|
|
|
36
36
|
session;
|
|
37
37
|
role;
|
|
38
38
|
enrolledToken = null;
|
|
39
|
+
spaceId = null;
|
|
40
|
+
repos = [];
|
|
39
41
|
enroll = null;
|
|
40
42
|
inviteUrl = null;
|
|
41
43
|
constructor(cfg, onEvent, opts) {
|
|
@@ -115,6 +117,7 @@ export class PantheonClient {
|
|
|
115
117
|
agent: this.cfg.agent ?? "unknown",
|
|
116
118
|
pkg: VERSION,
|
|
117
119
|
from_seq: this.seq,
|
|
120
|
+
...(this.cfg.repo ? { repo: this.cfg.repo, ...(this.cfg.repoOrigin ? { repo_origin: this.cfg.repoOrigin } : {}) } : {}),
|
|
118
121
|
}));
|
|
119
122
|
break;
|
|
120
123
|
case "pending":
|
|
@@ -149,6 +152,12 @@ export class PantheonClient {
|
|
|
149
152
|
this.enrolledToken = msg.token;
|
|
150
153
|
this.cfg.token = msg.token;
|
|
151
154
|
}
|
|
155
|
+
if (msg.space_id) {
|
|
156
|
+
this.spaceId = msg.space_id;
|
|
157
|
+
this.cfg.spaceId = msg.space_id;
|
|
158
|
+
}
|
|
159
|
+
this.repos = msg.repos ?? [];
|
|
160
|
+
this.adoptKnownRepo();
|
|
152
161
|
this.backoff = 500;
|
|
153
162
|
this.roster = msg.roster;
|
|
154
163
|
this.obligations = msg.obligations;
|
|
@@ -161,6 +170,10 @@ export class PantheonClient {
|
|
|
161
170
|
resolve();
|
|
162
171
|
}
|
|
163
172
|
break;
|
|
173
|
+
case "repos":
|
|
174
|
+
this.repos = msg.repos;
|
|
175
|
+
this.adoptKnownRepo();
|
|
176
|
+
break;
|
|
164
177
|
case "act":
|
|
165
178
|
if (msg.act.seq > this.seq)
|
|
166
179
|
this.seq = msg.act.seq;
|
|
@@ -308,7 +321,9 @@ export class PantheonClient {
|
|
|
308
321
|
});
|
|
309
322
|
}
|
|
310
323
|
async send(a) {
|
|
311
|
-
const
|
|
324
|
+
const repo = this.cfg.repo;
|
|
325
|
+
const meta = a.act === "DELIVERED" && repo && !(a.meta && "repo" in a.meta) ? { ...(a.meta ?? {}), repo } : a.meta;
|
|
326
|
+
const r = await this.rpc({ t: "act", ...a, ...(meta ? { meta } : {}), ...(repo ? { repo } : {}) });
|
|
312
327
|
if (a.ref) {
|
|
313
328
|
for (const [id, o] of this.claimedByMe)
|
|
314
329
|
if (o.ref === a.ref)
|
|
@@ -350,6 +365,21 @@ export class PantheonClient {
|
|
|
350
365
|
publishProfile(profile) {
|
|
351
366
|
return this.rpc({ t: "publish_profile", profile });
|
|
352
367
|
}
|
|
368
|
+
adoptKnownRepo() {
|
|
369
|
+
const c = this.cfg;
|
|
370
|
+
if (c.repo || !c.cwdRepo)
|
|
371
|
+
return;
|
|
372
|
+
if (!this.repos.some((r) => r.fingerprint === c.cwdRepo))
|
|
373
|
+
return;
|
|
374
|
+
c.repo = c.cwdRepo;
|
|
375
|
+
if (c.cwdRepoOrigin)
|
|
376
|
+
c.repoOrigin = c.cwdRepoOrigin;
|
|
377
|
+
}
|
|
378
|
+
bindRepo(fingerprint = this.cfg.repo, origin = this.cfg.repoOrigin ?? null) {
|
|
379
|
+
if (!fingerprint)
|
|
380
|
+
return Promise.reject(new Error("not inside a git repo — run this from the team's checkout"));
|
|
381
|
+
return this.rpc({ t: "bind_repo", repo: fingerprint, origin });
|
|
382
|
+
}
|
|
353
383
|
makeInvite(mayWake = true, ttlMinutes = 60) {
|
|
354
384
|
return new Promise((res, rej) => {
|
|
355
385
|
const t = setTimeout(() => rej(new Error("relay did not return an invite in 10s")), 10_000);
|