@join-pantheon/cli 0.21.3 → 0.23.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 +25 -9
- package/dist/cli.js +163 -29
- package/dist/client.js +34 -0
- package/dist/config.js +15 -3
- package/dist/doctor.js +13 -0
- package/dist/email.js +47 -23
- package/dist/history-view.js +51 -0
- package/dist/host-env.js +57 -0
- package/dist/mcp.js +4 -2
- package/dist/profile.js +8 -0
- package/dist/protocol.js +10 -1
- package/dist/quickstart.js +14 -4
- package/dist/relay.js +180 -74
- package/dist/roster-file.js +19 -0
- package/dist/service.js +25 -2
- package/dist/setup-guide.js +64 -24
- package/dist/tray.js +8 -2
- package/dist/worker.js +7 -2
- package/helper/macos/menubar.swift +110 -43
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -45,21 +45,32 @@ out with `pantheon sessions`; every identity and membership event is in `pantheo
|
|
|
45
45
|
audit`. (Self-hosting or on a LAN/tailnet? Accounts are optional — skip straight
|
|
46
46
|
to `pantheon start`.)
|
|
47
47
|
|
|
48
|
-
**Then, from your project's repo:**
|
|
48
|
+
**Then, from your project's repo — or anywhere, if there is no repo yet (see below):**
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
51
|
pantheon start
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
**Your teammate, from their clone of the project:**
|
|
54
|
+
**Your teammate, from their clone of the project if they have one:**
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
57
|
pantheon join pantheons://relay.joinpantheon.network/acme#C1qhJfdHBaPjuG3jF48vcqrP
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
On the hosted relay, `pantheon start` and `pantheon invite` also print the same invite as a link,
|
|
61
|
+
`https://www.joinpantheon.network/join/acme#C1qhJfdHBaPjuG3jF48vcqrP`. Pasted into a chat it
|
|
62
|
+
unfurls with the team's name and hands the recipient the line for their agent. The token stays in the
|
|
63
|
+
URL fragment, which browsers never send to any server. `pantheon join` accepts either form.
|
|
64
|
+
|
|
60
65
|
Signed in, `pantheon start` puts your space on the hosted relay, so that link works from any
|
|
61
|
-
network — no tunnel, no VPN. The space is named after
|
|
62
|
-
that name, start picks `<
|
|
66
|
+
network — no tunnel, no VPN. The space is named after the folder you run it from (your repo,
|
|
67
|
+
usually); if another team already took that name, start picks `<name>-<you>` and says so. Choose
|
|
68
|
+
your own with `--space` — always do that when there is no repo to name it after.
|
|
69
|
+
|
|
70
|
+
**You do not need a git repository.** Messages, questions, decisions and escalations work from any
|
|
71
|
+
folder. A repo adds exactly two things: handing over code (a `DELIVERED` names a pushed branch and
|
|
72
|
+
commit) and letting a teammate wake this machine while you are away; `pantheon repo add` binds one
|
|
73
|
+
whenever you have it.
|
|
63
74
|
|
|
64
75
|
That's it — one command each sets up the **whole machine**: a relay and space (you) or membership
|
|
65
76
|
(them), pantheon wired into whichever agents each of you already has (**user scope** — every project,
|
|
@@ -67,13 +78,17 @@ every Conductor window, nothing to commit), the wake-up hook so an agent finishe
|
|
|
67
78
|
waiting for, the **worker** so the machine answers the team even while its human is away (on by
|
|
68
79
|
default when run inside a git repo; `--no-worker` opts out — and a peer still needs an explicit
|
|
69
80
|
`--may-wake` grant before they can use it), native decision alerts, and the menu bar. Each command
|
|
70
|
-
ends with a "You're set" summary, then a short guided setup on a terminal:
|
|
71
|
-
your
|
|
72
|
-
|
|
73
|
-
|
|
81
|
+
ends with a "You're set" summary, then a short guided setup on a terminal: three questions — your
|
|
82
|
+
name, your role, and (inside a repo) whether a run a teammate wakes may execute commands — and one
|
|
83
|
+
screen of defaults to keep or change: who may wake your machine while you are away, which alerts you
|
|
84
|
+
want, which decisions are yours alone (so agents stop and ask you), the worker's playbook and its base
|
|
85
|
+
branch. `pantheon setup` re-runs it any time. An agent setting you up runs `pantheon setup --questions`,
|
|
86
|
+
asks you in chat, and passes your answers back as flags — to `pantheon setup --yes`, or straight to
|
|
87
|
+
`pantheon start` / `pantheon join`, which apply them the moment the team exists — so nobody has to
|
|
88
|
+
open a terminal. Restart your agent once and ask it *"check pantheon_status"*.
|
|
74
89
|
|
|
75
90
|
Already a member — new laptop, or just pulled a newer build? The same full setup is one command:
|
|
76
|
-
`pantheon install --user` (from your project's repo).
|
|
91
|
+
`pantheon install --user` (from your project's repo if you have one).
|
|
77
92
|
|
|
78
93
|
**Consent happens once, here.** Setup pre-approves exactly the six pantheon *message* tools in
|
|
79
94
|
each agent's own permission system (Claude Code `permissions.allow`, Cursor `Mcp()` tokens,
|
|
@@ -451,6 +466,7 @@ pantheon doctor preflight: node, keys, MCP entry, relay, memb
|
|
|
451
466
|
pantheon worker / service stay answerable while you are away (install as a service)
|
|
452
467
|
pantheon persona list / init / show — what a woken worker may do
|
|
453
468
|
pantheon spaces / use / repo several teams on one machine — what is bound where; bind this repo; a team's repos
|
|
469
|
+
pantheon history / show read the thread, or one act, in full with ids — from a terminal, no agent needed
|
|
454
470
|
pantheon sessions · logout your devices and sessions; sign this machine out
|
|
455
471
|
pantheon account delete remove your account and everything the relay holds on you
|
|
456
472
|
pantheon upgrade pull the latest release and re-wire your agents
|
package/dist/cli.js
CHANGED
|
@@ -8,9 +8,9 @@ process.emitWarning = ((warning, ...rest) => {
|
|
|
8
8
|
});
|
|
9
9
|
import { randomBytes } from "node:crypto";
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
11
|
-
import { dirname, join, resolve } from "node:path";
|
|
11
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
12
12
|
import { PantheonClient } from "./client.js";
|
|
13
|
-
import { fromInvite, load, save, toInvite, resolveFor, loadAll, repoInfo, bindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY, resolveForHook } from "./config.js";
|
|
13
|
+
import { fromInvite, load, save, toInvite, toJoinLink, resolveFor, loadAll, repoInfo, bindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY, resolveForHook } from "./config.js";
|
|
14
14
|
import { fmtLimit } from "./plans.js";
|
|
15
15
|
import { validEmail, validUsername } from "./accounts.js";
|
|
16
16
|
import { install, launcher } from "./install.js";
|
|
@@ -18,8 +18,10 @@ import { Relay } from "./relay.js";
|
|
|
18
18
|
import { runMcp } from "./mcp.js";
|
|
19
19
|
import { Worker } from "./worker.js";
|
|
20
20
|
import { DEFAULT_REMINDER_MS, ensureNativeNotifier, HumanNotifier, sendDesktopNotification } from "./notifier.js";
|
|
21
|
+
import { desktopUnavailable, hostEnv } from "./host-env.js";
|
|
21
22
|
import { AGENT_SURFACES, applyProfileInit, cleanList, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile, wakeDefault } from "./profile.js";
|
|
22
23
|
import { runGuide, terminalIO, parseSetupFlags, questionnaire } from "./setup-guide.js";
|
|
24
|
+
import { selectActs, formatAct, resolveRef } from "./history-view.js";
|
|
23
25
|
import { doctor, render } from "./doctor.js";
|
|
24
26
|
import { menubarProc, notifierProc, relayProc, workerProc, wiring, logFileFor, dataSummary, stopProc, statFile } from "./machine.js";
|
|
25
27
|
import { assess } from "./untrusted.js";
|
|
@@ -321,6 +323,13 @@ async function guidedSetup(cfg, project, opts = {}) {
|
|
|
321
323
|
rl?.close();
|
|
322
324
|
}
|
|
323
325
|
}
|
|
326
|
+
async function fetchHistory(cfg, since = 0) {
|
|
327
|
+
const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
|
|
328
|
+
const res = await fetch(`${base}/history?space=${encodeURIComponent(cfg.space)}&token=${encodeURIComponent(cfg.token)}&since=${since}`, { signal: AbortSignal.timeout(8000) });
|
|
329
|
+
if (!res.ok)
|
|
330
|
+
throw new Error(`relay answered ${res.status} for the ledger`);
|
|
331
|
+
return (await res.json()).acts ?? [];
|
|
332
|
+
}
|
|
324
333
|
function workerMidRun() {
|
|
325
334
|
try {
|
|
326
335
|
const s = JSON.parse(readFileSync(join(CONFIG_DIR, "worker-state.json"), "utf8"));
|
|
@@ -365,10 +374,16 @@ ${B("first time — verify your email once")}
|
|
|
365
374
|
pantheon login --email you@work.com a second machine, same account
|
|
366
375
|
pantheon whoami · pantheon sessions who you are; your devices & sessions
|
|
367
376
|
|
|
368
|
-
${B("getting going —
|
|
377
|
+
${B("getting going — two commands, from your project's repo if you have one")}
|
|
369
378
|
pantheon start you: sets up this whole machine + one line to send your teammate
|
|
370
|
-
pantheon join <
|
|
371
|
-
pantheon invite another single-use
|
|
379
|
+
pantheon join <invite> them: the link (or the line) sets up their whole machine
|
|
380
|
+
pantheon invite another single-use invite, for a third person or a second machine
|
|
381
|
+
(--list shows the open ones; --revoke <token|all> cancels)
|
|
382
|
+
|
|
383
|
+
Both take the setup answers as flags (--name, --title, --wake, --commands, … — pantheon setup
|
|
384
|
+
--questions lists them), so an agent sets a person up in one command. No repo? Messages, questions
|
|
385
|
+
and decisions work anywhere; a repo only adds handing over code and being woken. Name the team
|
|
386
|
+
with --space when there is no repo to name it after.
|
|
372
387
|
|
|
373
388
|
One command does everything: agents wired and pre-approved, wake-up hook (your
|
|
374
389
|
agent finishes what a peer is waiting for), worker (this machine answers the
|
|
@@ -416,9 +431,11 @@ ${B("control")}
|
|
|
416
431
|
pantheon leave remove what pantheon put on this machine
|
|
417
432
|
pantheon panic cut off all unattended work on this machine, now
|
|
418
433
|
pantheon doctor check this machine end to end
|
|
419
|
-
pantheon setup
|
|
420
|
-
|
|
421
|
-
|
|
434
|
+
pantheon setup three questions — your name, your role, may a woken run execute commands —
|
|
435
|
+
then one screen of defaults to keep or change: who may wake this machine,
|
|
436
|
+
alerts, what you personally decide, playbook, base branch. Re-run any time.
|
|
437
|
+
(--questions prints them for an agent to ask you; answers go back as flags,
|
|
438
|
+
to setup --yes or straight to start / join)
|
|
422
439
|
pantheon upgrade pull the latest pantheon, rebuild, and re-wire your agents
|
|
423
440
|
|
|
424
441
|
${D("more: pantheon help --all")}
|
|
@@ -432,6 +449,9 @@ ${B("less common")}
|
|
|
432
449
|
pantheon whoami your name and fingerprint
|
|
433
450
|
pantheon pending what a live agent window has not yet seen
|
|
434
451
|
pantheon say <ACT> <text> send one act by hand (NOTE, ASK, ANSWER, PROPOSE, ...)
|
|
452
|
+
replies pick the act they answer; --ref <id | seq | last> to choose
|
|
453
|
+
pantheon history the thread in full, with ids (--goal <id>, --from <who>, --last N, --short)
|
|
454
|
+
pantheon show <id | seq> one act in full, and what it replied to
|
|
435
455
|
pantheon invite mint another single-use invite link to send
|
|
436
456
|
pantheon install re-wire this machine's agents (after a Node/agent change)
|
|
437
457
|
pantheon mcp the server your agent runs; not for you
|
|
@@ -457,9 +477,14 @@ async function main() {
|
|
|
457
477
|
switch (cmd) {
|
|
458
478
|
case "start": {
|
|
459
479
|
const port = Number(flag("port", "8787"));
|
|
480
|
+
const setupFlags = parseSetupFlags(argv);
|
|
481
|
+
if (setupFlags.errors.length)
|
|
482
|
+
die(setupFlags.errors.join("\n") + "\n pantheon setup --questions lists every answer");
|
|
460
483
|
const wp = unattendedProject();
|
|
461
484
|
requireSignInOnHostedRelay(flag("relay"));
|
|
462
485
|
console.error(`${B("pantheon start")}\n`);
|
|
486
|
+
if (!flag("space") && !repoInfo(process.cwd()))
|
|
487
|
+
console.error(D(` No git repo here, so the team will be named after this folder: "${basename(process.cwd()).toLowerCase().replace(/[^a-z0-9._-]/g, "-") || "team"}". Pass --space <name> to choose.\n`));
|
|
463
488
|
const r = await qsStart({
|
|
464
489
|
port,
|
|
465
490
|
dataDir: resolveDataDir(flag("data")).dir,
|
|
@@ -487,12 +512,21 @@ async function main() {
|
|
|
487
512
|
process.exit(1);
|
|
488
513
|
}
|
|
489
514
|
if (r.invite) {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
515
|
+
const link = toJoinLink(fromInvite(r.invite));
|
|
516
|
+
if (link) {
|
|
517
|
+
console.error(`\n${B("Send your teammate this link — it unfurls in chat, and it sets up their whole machine:")}\n`);
|
|
518
|
+
console.error(` ${C(link)}\n`);
|
|
519
|
+
console.error(D(` The same invite as a command, for pasting straight into an agent:`));
|
|
520
|
+
console.error(` ${C(`pantheon join ${r.invite}`)}\n`);
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
console.error(`\n${B("Send your teammate this one line — it sets up their whole machine:")}\n`);
|
|
524
|
+
console.error(` ${C(`pantheon join ${r.invite}`)}\n`);
|
|
525
|
+
}
|
|
526
|
+
console.error(D(` From their project's repo if they have one — a repo is only needed to hand over code or to be woken. Single use, expires in ${Math.round(Number(flag("ttl", "1440")) / 60)}h.`));
|
|
493
527
|
console.error(D(` Redeeming it admits them — no separate approval step. Need another? \`pantheon invite\``));
|
|
494
528
|
console.error(D(`\n Not technical? Forward them this instead, and their agent does the rest:`));
|
|
495
|
-
console.error(` ${D("Tell your agent: run")} ${C("curl -fsSL https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${r.invite}`)}`);
|
|
529
|
+
console.error(` ${D("Tell your agent: run")} ${C("curl -fsSL https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${link ?? r.invite}`)}`);
|
|
496
530
|
}
|
|
497
531
|
if (!r.reach.shareable && r.reach.hint) {
|
|
498
532
|
console.error(`\n\x1b[33m That address only works on this machine.\x1b[0m`);
|
|
@@ -500,7 +534,7 @@ async function main() {
|
|
|
500
534
|
}
|
|
501
535
|
printReady(r.tools);
|
|
502
536
|
if (r.claimed)
|
|
503
|
-
await guidedSetup(load() ?? r.cfg, repoInfo(process.cwd())?.root ?? null);
|
|
537
|
+
await guidedSetup(load() ?? r.cfg, repoInfo(process.cwd())?.root ?? null, { preset: setupFlags.preset, force: Object.keys(setupFlags.preset).length > 0 || argv.includes("--yes") });
|
|
504
538
|
if (r.relay) {
|
|
505
539
|
console.error(D(`\nKeeping the relay running here. Ctrl-C stops it (your teammate goes offline too).`));
|
|
506
540
|
await new Promise(() => { });
|
|
@@ -512,13 +546,43 @@ async function main() {
|
|
|
512
546
|
const client = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
513
547
|
await client.connect().catch((e) => die(client.pending ?? e.message));
|
|
514
548
|
await new Promise((r) => setTimeout(r, 250));
|
|
549
|
+
const revokeAt = argv.indexOf("--revoke");
|
|
550
|
+
const revokeArg = revokeAt >= 0 ? (argv[revokeAt + 1] && !argv[revokeAt + 1].startsWith("--") ? argv[revokeAt + 1] : "all") : undefined;
|
|
551
|
+
if (argv.includes("--list") || revokeArg !== undefined) {
|
|
552
|
+
const open = await client.listInvites().catch((e) => die(e.message));
|
|
553
|
+
if (revokeArg !== undefined) {
|
|
554
|
+
const targets = revokeArg === "all" ? open.filter((i) => i.mine) : open.filter((i) => i.token === revokeArg || i.token.endsWith(revokeArg.replace(/^…/, "")));
|
|
555
|
+
if (!targets.length) {
|
|
556
|
+
client.close();
|
|
557
|
+
die(revokeArg === "all" ? "no open invites of yours to revoke" : `no open invite matches "${revokeArg}" — pantheon invite --list shows them`);
|
|
558
|
+
}
|
|
559
|
+
for (const i of targets) {
|
|
560
|
+
await client.revokeInvite(i.token).catch((e) => { client.close(); return die(e.message); });
|
|
561
|
+
console.log(`revoked …${i.token.slice(-6)} (minted by ${i.created_by}, would have expired ${new Date(i.expires_at).toLocaleString()})`);
|
|
562
|
+
}
|
|
563
|
+
client.close();
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (!open.length)
|
|
567
|
+
console.log(D("no open invites"));
|
|
568
|
+
for (const i of open)
|
|
569
|
+
console.log(`${i.mine ? C(i.token) : D(i.token)} by ${i.created_by} expires ${new Date(i.expires_at).toLocaleString()}${i.may_wake ? " may wake" : ""}${i.mine ? "" : D(" (not yours — masked)")}`);
|
|
570
|
+
if (open.length)
|
|
571
|
+
console.log(D(`\nrevoke one: pantheon invite --revoke <token or its last 6 characters> · all of yours: pantheon invite --revoke all`));
|
|
572
|
+
client.close();
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
515
575
|
const ttl = Number(flag("ttl", "1440"));
|
|
516
576
|
const mayWake = argv.includes("--may-wake") ? true : argv.includes("--no-wake") ? false : wakeDefault(loadProfile(cfg));
|
|
517
577
|
const inv = await client.makeInvite(mayWake, ttl).catch((e) => die(e.message));
|
|
518
|
-
|
|
578
|
+
const raw = toInvite({ ...cfg, token: inv.url });
|
|
579
|
+
const link = toJoinLink({ relay: cfg.relay, space: cfg.space, token: inv.url });
|
|
580
|
+
console.log(`pantheon join ${raw}`);
|
|
581
|
+
if (link)
|
|
582
|
+
console.error(`\n${D("Or send this — it unfurls in chat with the team's name, and pantheon join accepts it too:")}\n ${C(link)}`);
|
|
519
583
|
console.error(D(`\nSingle use, expires ${new Date(inv.expires_at).toLocaleString()}.`));
|
|
520
584
|
console.error(D(`For a teammate who is not technical, forward this and their agent does the rest:`));
|
|
521
|
-
console.error(` ${D("Tell your agent: run")} ${C("curl -fsSL https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${
|
|
585
|
+
console.error(` ${D("Tell your agent: run")} ${C("curl -fsSL https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${link ?? raw}`)}`);
|
|
522
586
|
client.close();
|
|
523
587
|
return;
|
|
524
588
|
}
|
|
@@ -593,7 +657,10 @@ async function main() {
|
|
|
593
657
|
return;
|
|
594
658
|
}
|
|
595
659
|
case "join": {
|
|
596
|
-
const invite = argv[1] ?? die("usage: pantheon join
|
|
660
|
+
const invite = argv[1] ?? die("usage: pantheon join <invite> (pantheons://host/space#token, or https://www.joinpantheon.network/join/<team>#<token>) [setup answers as flags — pantheon setup --questions]");
|
|
661
|
+
const setupFlags = parseSetupFlags(argv);
|
|
662
|
+
if (setupFlags.errors.length)
|
|
663
|
+
die(setupFlags.errors.join("\n") + "\n pantheon setup --questions lists every answer");
|
|
597
664
|
let parsed;
|
|
598
665
|
try {
|
|
599
666
|
parsed = fromInvite(invite);
|
|
@@ -637,7 +704,7 @@ async function main() {
|
|
|
637
704
|
{
|
|
638
705
|
const joined = load();
|
|
639
706
|
if (joined)
|
|
640
|
-
await guidedSetup(joined, repoInfo(process.cwd())?.root ?? null);
|
|
707
|
+
await guidedSetup(joined, repoInfo(process.cwd())?.root ?? null, { preset: setupFlags.preset, force: Object.keys(setupFlags.preset).length > 0 || argv.includes("--yes") });
|
|
641
708
|
}
|
|
642
709
|
console.error(`\n${D('Then ask your agent: "check pantheon_status and say hello to the team".')}`);
|
|
643
710
|
return;
|
|
@@ -648,8 +715,8 @@ async function main() {
|
|
|
648
715
|
console.error(`${B("setting this machine up")} ${D("(user scope)")}\n`);
|
|
649
716
|
const wired = wireUp({
|
|
650
717
|
hook: !argv.includes("--no-hook"),
|
|
651
|
-
notifications: !argv.includes("--no-notifications")
|
|
652
|
-
menubar: !argv.includes("--no-menubar")
|
|
718
|
+
notifications: !argv.includes("--no-notifications"),
|
|
719
|
+
menubar: !argv.includes("--no-menubar"),
|
|
653
720
|
unattended: wp.project,
|
|
654
721
|
workerHow: wp.how,
|
|
655
722
|
});
|
|
@@ -822,7 +889,7 @@ async function main() {
|
|
|
822
889
|
for (const o of human) {
|
|
823
890
|
const who = o.owner.replace(/^human:/, "");
|
|
824
891
|
const stale = Date.now() - o.created_at > 3_600_000;
|
|
825
|
-
console.log(` ${stale ? "\x1b[33m" : C("")}${who.padEnd(14)}${ageOf(o.created_at).padEnd(6)}\x1b[0m ${o.body}`);
|
|
892
|
+
console.log(` ${stale ? "\x1b[33m" : C("")}${who.padEnd(14)}${ageOf(o.created_at).padEnd(6)}\x1b[0m ${o.body.split("\n")[0].slice(0, 110)} ${D(o.ref)}`);
|
|
826
893
|
if (who === cfg.member)
|
|
827
894
|
console.log(` ${D("your peer was told this decision was coming — answer it or say it is dropped")}`);
|
|
828
895
|
}
|
|
@@ -830,7 +897,10 @@ async function main() {
|
|
|
830
897
|
if (!between.length)
|
|
831
898
|
console.log(` ${D("(nothing owed)")}`);
|
|
832
899
|
for (const o of between) {
|
|
833
|
-
console.log(` ${o.kind.padEnd(7)} ${o.owner.padEnd(14)}${ageOf(o.created_at).padEnd(6)} ${o.body}`);
|
|
900
|
+
console.log(` ${o.kind.padEnd(7)} ${o.owner.padEnd(14)}${ageOf(o.created_at).padEnd(6)} ${o.body.split("\n")[0].slice(0, 110)} ${D(o.ref)}`);
|
|
901
|
+
}
|
|
902
|
+
if (human.length || between.length || goals.some((g) => g.state !== "CLOSED")) {
|
|
903
|
+
console.log(D(`\n read in full: pantheon show <id> · a thread: pantheon history --goal <goal> · reply: pantheon say ANSWER "…" --ref <id>`));
|
|
834
904
|
}
|
|
835
905
|
client.close();
|
|
836
906
|
};
|
|
@@ -931,13 +1001,58 @@ async function main() {
|
|
|
931
1001
|
console.log(argv.includes("--json") ? JSON.stringify(report) : renderPending(report));
|
|
932
1002
|
return;
|
|
933
1003
|
}
|
|
1004
|
+
case "history": {
|
|
1005
|
+
const cfg = need();
|
|
1006
|
+
const acts = await fetchHistory(cfg, numFlag("since", 0)).catch((e) => die(`cannot read the ledger — ${e.message}`));
|
|
1007
|
+
const picked = selectActs(acts, {
|
|
1008
|
+
...(flag("goal") ? { goal: flag("goal") } : {}), ...(flag("from") ? { from: flag("from") } : {}),
|
|
1009
|
+
last: numFlag("last", 20), presence: argv.includes("--presence"),
|
|
1010
|
+
});
|
|
1011
|
+
if (argv.includes("--json")) {
|
|
1012
|
+
console.log(JSON.stringify(picked, null, 2));
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (!picked.length) {
|
|
1016
|
+
console.log(D("no acts match"));
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
const client = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
1020
|
+
const titles = new Map();
|
|
1021
|
+
try {
|
|
1022
|
+
await client.connect();
|
|
1023
|
+
for (const g of client.goals.values())
|
|
1024
|
+
titles.set(g.id, g.title);
|
|
1025
|
+
}
|
|
1026
|
+
catch { }
|
|
1027
|
+
finally {
|
|
1028
|
+
client.close();
|
|
1029
|
+
}
|
|
1030
|
+
for (const a of picked)
|
|
1031
|
+
console.log(formatAct(a, { short: argv.includes("--short"), ...(a.goal && titles.get(a.goal) ? { goalTitle: titles.get(a.goal) } : {}) }) + "\n");
|
|
1032
|
+
console.log(D(`${picked.length} of ${acts.filter((a) => a.act !== "JOIN" && a.act !== "LEAVE").length} acts · --last N · --goal <id> · --from <member> · --short · --json · reply: pantheon say ANSWER "…" --ref <id or seq>`));
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
case "show": {
|
|
1036
|
+
const cfg = need();
|
|
1037
|
+
const key = argv[1] ?? die("usage: pantheon show <act id | sequence number>");
|
|
1038
|
+
const acts = await fetchHistory(cfg).catch((e) => die(`cannot read the ledger — ${e.message}`));
|
|
1039
|
+
const a = acts.find((x) => x.id === key || String(x.seq) === key) ?? die(`no act ${key} in what the relay still holds (pantheon history lists them)`);
|
|
1040
|
+
console.log(formatAct(a));
|
|
1041
|
+
if (a.ref) {
|
|
1042
|
+
const r = acts.find((x) => x.id === a.ref);
|
|
1043
|
+
if (r)
|
|
1044
|
+
console.log(D(`\nin reply to:\n`) + formatAct(r, { short: true }));
|
|
1045
|
+
}
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
934
1048
|
case "say": {
|
|
935
1049
|
const cfg = need();
|
|
936
|
-
const act = argv[1]?.toUpperCase() ?? die("usage: pantheon say <ACT> <body> [--goal id] [--to a,b] [--domains technical,gtm]");
|
|
1050
|
+
const act = argv[1]?.toUpperCase() ?? die("usage: pantheon say <ACT> <body> [--goal id] [--to a,b] [--ref <id | seq | last>] [--summary 'one line'] [--domains technical,gtm]");
|
|
937
1051
|
const stop = argv.findIndex((a, i) => i >= 2 && a.startsWith("--"));
|
|
938
1052
|
const body = argv.slice(2, stop === -1 ? undefined : stop).join(" ");
|
|
939
1053
|
if (!body)
|
|
940
1054
|
die("nothing to say — pantheon say NOTE 'your message here'");
|
|
1055
|
+
const summary = flag("summary");
|
|
941
1056
|
const client = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
942
1057
|
await client.connect().catch((e) => die(`cannot reach relay — ${e.message}`));
|
|
943
1058
|
const to = flag("to");
|
|
@@ -950,13 +1065,28 @@ async function main() {
|
|
|
950
1065
|
const domains = flag("domains");
|
|
951
1066
|
if (domains)
|
|
952
1067
|
meta.decision_domains = cleanList(domains.split(","));
|
|
1068
|
+
let goalArg = flag("goal") ?? null;
|
|
1069
|
+
let ref = flag("ref") ?? null;
|
|
1070
|
+
if (ref !== null || ["ANSWER", "ACCEPT", "OBJECT"].includes(act)) {
|
|
1071
|
+
const acts = await fetchHistory(cfg).catch(() => []);
|
|
1072
|
+
const rr = resolveRef(acts, ref, act, goalArg, cfg.member);
|
|
1073
|
+
if (rr.error) {
|
|
1074
|
+
client.close();
|
|
1075
|
+
die(rr.error);
|
|
1076
|
+
}
|
|
1077
|
+
if (rr.note)
|
|
1078
|
+
console.error(D(` ${rr.note}`));
|
|
1079
|
+
ref = rr.id;
|
|
1080
|
+
goalArg = goalArg ?? rr.goal;
|
|
1081
|
+
}
|
|
953
1082
|
const r = await client.send({
|
|
954
1083
|
act: act, body,
|
|
955
|
-
goal:
|
|
1084
|
+
goal: goalArg,
|
|
956
1085
|
to: to ? to.split(",").map((s) => s.trim()) : null,
|
|
957
|
-
ref
|
|
1086
|
+
ref,
|
|
1087
|
+
...(summary ? { summary } : {}),
|
|
958
1088
|
...(Object.keys(meta).length ? { meta } : {}),
|
|
959
|
-
}).catch((e) => die(e.message));
|
|
1089
|
+
}).catch((e) => die(/SUMMARY_REQUIRED/.test(e.message) ? `${e.message}\n Add one: --summary 'the point they must not miss'` : e.message));
|
|
960
1090
|
console.log(`sent ${act} id=${r.id} seq=${r.seq}`);
|
|
961
1091
|
client.close();
|
|
962
1092
|
return;
|
|
@@ -999,7 +1129,8 @@ async function main() {
|
|
|
999
1129
|
console.log(` menu bar ${C("running")} ${menubar.managedBy === "launchd" ? `service ${menubar.label}` : "started by hand"} pid ${menubar.pid}`);
|
|
1000
1130
|
}
|
|
1001
1131
|
else {
|
|
1002
|
-
|
|
1132
|
+
const why = desktopUnavailable(hostEnv(), "the menu bar");
|
|
1133
|
+
console.log(` menu bar ${D("not running")} ${D(why ?? "pantheon service install menubar — status and pause/resume in the menu bar")}`);
|
|
1003
1134
|
}
|
|
1004
1135
|
if (notifier.running) {
|
|
1005
1136
|
console.log(` notifier ${C("running")} ${notifier.managedBy === "launchd" ? `service ${notifier.label}` : "started by hand"} pid ${notifier.pid}`);
|
|
@@ -2013,10 +2144,13 @@ async function main() {
|
|
|
2013
2144
|
const find = (want) => all.find((x) => x.o.id === want);
|
|
2014
2145
|
if (sub === "answer") {
|
|
2015
2146
|
if (!id)
|
|
2016
|
-
die("usage: pantheon approvals answer <obligation-id> <decision>");
|
|
2147
|
+
die("usage: pantheon approvals answer <obligation-id> <decision> [--summary 'one line']");
|
|
2017
2148
|
const hit = find(id) ?? die(`no decision ${id} is waiting on you${conns.length > 1 ? ` in any of your ${conns.length} teams` : ""}`);
|
|
2018
|
-
const
|
|
2019
|
-
const
|
|
2149
|
+
const stopAt = argv.findIndex((a, i) => i >= 3 && a.startsWith("--"));
|
|
2150
|
+
const answer = argv.slice(3, stopAt === -1 ? undefined : stopAt).join(" ").trim() || die("state the decision; it will be written to the audit ledger");
|
|
2151
|
+
const summary = flag("summary");
|
|
2152
|
+
const sent = await hit.client.send({ act: "ANSWER", body: answer, goal: null, ref: hit.o.ref, after_seq: hit.client.readSeq, ...(summary ? { summary } : {}) })
|
|
2153
|
+
.catch((e) => { closeAll(); return die(/SUMMARY_REQUIRED/.test(e.message) ? `${e.message}\n Add one: --summary 'the point they must not miss'` : e.message); });
|
|
2020
2154
|
console.log(`${C("answered")} ${id}${conns.length > 1 ? ` in ${B(hit.cfg.space)}` : ""} — ledger act ${sent.id}`);
|
|
2021
2155
|
closeAll();
|
|
2022
2156
|
return;
|
package/dist/client.js
CHANGED
|
@@ -41,6 +41,7 @@ export class PantheonClient {
|
|
|
41
41
|
plan = null;
|
|
42
42
|
enroll = null;
|
|
43
43
|
inviteUrl = null;
|
|
44
|
+
invitesList = null;
|
|
44
45
|
constructor(cfg, onEvent, opts) {
|
|
45
46
|
this.cfg = cfg;
|
|
46
47
|
this.onEvent = onEvent;
|
|
@@ -138,6 +139,9 @@ export class PantheonClient {
|
|
|
138
139
|
break;
|
|
139
140
|
case "claimed":
|
|
140
141
|
break;
|
|
142
|
+
case "invites":
|
|
143
|
+
this.invitesList = msg.invites;
|
|
144
|
+
break;
|
|
141
145
|
case "invite":
|
|
142
146
|
this.inviteUrl = { url: msg.url, expires_at: msg.expires_at };
|
|
143
147
|
break;
|
|
@@ -417,6 +421,36 @@ export class PantheonClient {
|
|
|
417
421
|
}
|
|
418
422
|
});
|
|
419
423
|
}
|
|
424
|
+
listInvites() {
|
|
425
|
+
if (!this.ws || this.ws.readyState !== 1)
|
|
426
|
+
return Promise.reject(new Error(`not connected to relay (${this.pending ?? this.lastError ?? "offline"})`));
|
|
427
|
+
return new Promise((res, rej) => {
|
|
428
|
+
this.invitesList = null;
|
|
429
|
+
const cid = `c${++this.cid}`;
|
|
430
|
+
const finish = () => { clearInterval(poll); clearTimeout(t); this.acks.delete(cid); };
|
|
431
|
+
const t = setTimeout(() => { finish(); rej(new Error("relay did not list the invites in 10s")); }, 10_000);
|
|
432
|
+
const poll = setInterval(() => { if (this.invitesList) {
|
|
433
|
+
finish();
|
|
434
|
+
const v = this.invitesList;
|
|
435
|
+
this.invitesList = null;
|
|
436
|
+
res(v);
|
|
437
|
+
} }, 40);
|
|
438
|
+
this.acks.set(cid, (v) => { if ("error" in v) {
|
|
439
|
+
finish();
|
|
440
|
+
rej(new Error(v.error));
|
|
441
|
+
} });
|
|
442
|
+
try {
|
|
443
|
+
this.ws?.send(JSON.stringify({ t: "invites", cid }));
|
|
444
|
+
}
|
|
445
|
+
catch (e) {
|
|
446
|
+
finish();
|
|
447
|
+
rej(e);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
revokeInvite(token) {
|
|
452
|
+
return this.rpc({ t: "revoke_invite", token: token.replace(/^…/, "") });
|
|
453
|
+
}
|
|
420
454
|
myObligations() {
|
|
421
455
|
return this.obligations.filter((o) => o.owner === this.cfg.member || o.owner === `human:${this.cfg.member}`);
|
|
422
456
|
}
|
package/dist/config.js
CHANGED
|
@@ -275,17 +275,29 @@ export function toInvite(cfg) {
|
|
|
275
275
|
const scheme = u.protocol === "wss:" ? "pantheons" : "pantheon";
|
|
276
276
|
return `${scheme}://${u.host}/${encodeURIComponent(cfg.space)}#${cfg.token}`;
|
|
277
277
|
}
|
|
278
|
+
export const JOIN_LINK_BASE = "https://www.joinpantheon.network/join";
|
|
279
|
+
const HOSTED_RELAY = "wss://relay.joinpantheon.network";
|
|
280
|
+
const JOIN_SLUG = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
|
281
|
+
export function toJoinLink(cfg) {
|
|
282
|
+
if (cfg.relay.replace(/\/+$/, "") !== HOSTED_RELAY || !JOIN_SLUG.test(cfg.space))
|
|
283
|
+
return null;
|
|
284
|
+
return `${JOIN_LINK_BASE}/${cfg.space}#${cfg.token}`;
|
|
285
|
+
}
|
|
278
286
|
export function fromInvite(invite) {
|
|
279
287
|
const clean = invite
|
|
280
288
|
.replace(/\u001b\[[0-9;]*m/g, "")
|
|
281
289
|
.replace(/[\u0000-\u001f\u007f]/g, "")
|
|
282
290
|
.trim()
|
|
283
291
|
.replace(/^['"`]|['"`]$/g, "")
|
|
284
|
-
.replace(/^pantheon\s+join\s+/i, "")
|
|
285
292
|
.trim();
|
|
286
|
-
const
|
|
293
|
+
const web = /https?:\/\/(?:www\.)?joinpantheon\.network\/join\/([^\s#?/"'`]+)(?:\?[^\s#"'`]*)?#([^\s"'`]+)/i.exec(clean);
|
|
294
|
+
if (web) {
|
|
295
|
+
const [, slug, token] = web;
|
|
296
|
+
return { relay: HOSTED_RELAY, space: decodeURIComponent(slug), token };
|
|
297
|
+
}
|
|
298
|
+
const m = /(pantheons?):\/\/([^/\s"'`]+)\/([^#\s"'`]+)#([^\s"'`]+)/.exec(clean);
|
|
287
299
|
if (!m)
|
|
288
|
-
throw new Error("not a pantheon invite — expected
|
|
300
|
+
throw new Error("not a pantheon invite — expected pantheons://host/space#token (pantheon:// without TLS), or https://www.joinpantheon.network/join/<team>#<token>");
|
|
289
301
|
const [, scheme, host, space, token] = m;
|
|
290
302
|
return {
|
|
291
303
|
relay: `${scheme === "pantheons" ? "wss" : "ws"}://${host}`,
|
package/dist/doctor.js
CHANGED
|
@@ -11,6 +11,7 @@ import { loadPersonas } from "./persona.js";
|
|
|
11
11
|
import { workerProc } from "./machine.js";
|
|
12
12
|
import { VERSION } from "./version.js";
|
|
13
13
|
import { serviceInstalled } from "./service.js";
|
|
14
|
+
import { hostEnv } from "./host-env.js";
|
|
14
15
|
const sh = (cmd, cwd) => {
|
|
15
16
|
try {
|
|
16
17
|
return execFileSync("sh", ["-lc", cmd], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
@@ -46,6 +47,13 @@ export async function doctor(projectDir) {
|
|
|
46
47
|
major >= 22
|
|
47
48
|
? add("pass", "node", `v${process.versions.node}`)
|
|
48
49
|
: add("fail", "node", `v${process.versions.node} is too old`, "pantheon needs Node 22 or newer (the relay uses node:sqlite) — install it and re-run");
|
|
50
|
+
const host = hostEnv();
|
|
51
|
+
if (host.ephemeral) {
|
|
52
|
+
add("warn", "machine", `${host.platform} in ${host.kind === "container" ? "a container" : "a cloud workspace"} — ${host.evidence.join("; ")}`, `this is not the computer you work on, which is why it reports ${host.platform} to someone who may be sitting at a Mac. The team and your account are fine here, but the menu bar, desktop alerts and the worker belong on your own machine — run the setup from a terminal there.`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
add("pass", "machine", `${host.platform}${host.desktop ? " — menu bar and desktop alerts can run here" : " — no menu bar or desktop alerts (macOS only); use phone or chat delivery"}`);
|
|
56
|
+
}
|
|
49
57
|
let cfg = null;
|
|
50
58
|
try {
|
|
51
59
|
cfg = load();
|
|
@@ -193,6 +201,11 @@ export async function doctor(projectDir) {
|
|
|
193
201
|
add("warn", "wake rights", `${who}, but no worker is running here, so nothing can be woken`, "start one with `pantheon service install worker --project <a git repo>`; until then a peer's act waits for you to type");
|
|
194
202
|
}
|
|
195
203
|
}
|
|
204
|
+
if (host.desktop) {
|
|
205
|
+
serviceInstalled("menubar")
|
|
206
|
+
? add("pass", "menu bar", "installed — status, pause/resume, and a badge when a decision needs you")
|
|
207
|
+
: add("warn", "menu bar", "not installed on a Mac that can run it", "install it with: pantheon service install menubar (needs the Xcode Command Line Tools — xcode-select --install — and a setup that ran on this computer, not in a cloud agent). --no-menubar at setup also leaves it off; that is fine if it was your choice");
|
|
208
|
+
}
|
|
196
209
|
if (!hookStates.length && !hook.installed) {
|
|
197
210
|
add("warn", "wake-up hook", "not installed — your agent will not notice a peer's message until you ask it to", "pantheon hook install --user (auto-detects claude / codex / cursor on PATH)");
|
|
198
211
|
}
|