@join-pantheon/cli 0.27.0 → 0.29.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/dist/cli.js CHANGED
@@ -7,21 +7,21 @@ process.emitWarning = ((warning, ...rest) => {
7
7
  return _emitWarning(warning, ...rest);
8
8
  });
9
9
  import { randomBytes } from "node:crypto";
10
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
- import { basename, dirname, join, resolve } from "node:path";
10
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
11
+ import { dirname, join, resolve } from "node:path";
12
12
  import { PantheonClient } from "./client.js";
13
- import { agentSpawn } from "./which.js";
14
- import { fromInvite, looksLikeInviteCode, resolveInviteCode, load, save, toInvite, toJoinLink, resolveFor, loadAll, repoInfo, bindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY, resolveForHook } from "./config.js";
13
+ import { agentSpawn, CmdLineUnsafe } from "./which.js";
14
+ import { fromInvite, looksLikeInviteCode, resolveInviteCode, load, save, toInvite, toJoinLink, resolveFor, loadAll, identity, repoInfo, bindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY, resolveForHook } from "./config.js";
15
15
  import { fmtLimit } from "./plans.js";
16
16
  import { validEmail, validUsername } from "./accounts.js";
17
- import { install, launcher, installForUser, removeCodexUserScope } from "./install.js";
17
+ import { install, launcher, installForUser } from "./install.js";
18
18
  import { Relay } from "./relay.js";
19
19
  import { runMcp } from "./mcp.js";
20
20
  import { Worker } from "./worker.js";
21
21
  import { DEFAULT_REMINDER_MS, ensureNativeNotifier, HumanNotifier, sendDesktopNotification } from "./notifier.js";
22
22
  import { desktopUnavailable, hostEnv } from "./host-env.js";
23
23
  import { AGENT_SURFACES, applyProfileInit, cleanList, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile, wakeDefault } from "./profile.js";
24
- import { runGuide, terminalIO, parseSetupFlags, questionnaire } from "./setup-guide.js";
24
+ import { runGuide, terminalIO, parseSetupFlags, questionnaire, optOutsFrom } from "./setup-guide.js";
25
25
  import { selectActs, formatAct, resolveRef } from "./history-view.js";
26
26
  import { doctor, render, shareBlock } from "./doctor.js";
27
27
  import { FEEDBACK_ADDRESS } from "./feedback.js";
@@ -29,17 +29,19 @@ import { menubarProc, notifierProc, relayProc, workerProc, wiring, logFileFor, d
29
29
  import { assess } from "./untrusted.js";
30
30
  import { report as spendReport, recentRuns } from "./spend.js";
31
31
  import { renderBrief, renderTeam } from "./brief.js";
32
- import { resolveDataDir } from "./config.js";
32
+ import { resolveDataDir, saveAll } from "./config.js";
33
+ import { currentPlan, installNetwork, redactProxy, redactText, normalizeProxy, proxyToRemember, describeNetError } from "./network.js";
33
34
  import { createInterface } from "node:readline";
34
35
  import { homedir } from "node:os";
35
36
  import { removeHook, decideStop, writeIntent, clearIntent, readIntent, installHooks, agentHookStates } from "./notify.js";
36
37
  import { pending, render as renderPending } from "./pending.js";
37
- import { start as qsStart, join as qsJoin, wireUp } from "./quickstart.js";
38
+ import { start as qsStart, join as qsJoin, wireUp, teamNameFor } from "./quickstart.js";
38
39
  import { installService, uninstallService } from "./service.js";
39
40
  import { LIMITS } from "./protocol.js";
40
41
  import { loadPersonas, personaByName, describePersona, initPersona, personaDir } from "./persona.js";
41
42
  import { fingerprint, newKeypair, signAccount } from "./identity.js";
42
43
  import { VERSION } from "./version.js";
44
+ import { connectFailure } from "./refusal.js";
43
45
  import * as mcpWindows from "./mcp-windows.js";
44
46
  import { updateBanner } from "./update-check.js";
45
47
  const argv = process.argv.slice(2);
@@ -61,7 +63,7 @@ async function signedFetch(cfg, method, path, params = {}) {
61
63
  return res;
62
64
  }
63
65
  function requireSignInOnHostedRelay(relayFlag) {
64
- const cfg = load();
66
+ const cfg = identity();
65
67
  if (cfg?.account)
66
68
  return;
67
69
  const relay = relayFlag ?? cfg?.relay ?? process.env.PANTHEON_RELAY ?? DEFAULT_RELAY;
@@ -71,7 +73,7 @@ function requireSignInOnHostedRelay(relayFlag) {
71
73
  die(`sign in first — this relay verifies email.\n New here: pantheon signup --email you@work.com --username you\n Have an account: pantheon login --email you@work.com\n Then run pantheon start again.`);
72
74
  }
73
75
  async function interactiveOnboard(relayArg) {
74
- const prior = load();
76
+ const prior = identity();
75
77
  const relay = relayArg ?? prior?.relay ?? process.env.PANTHEON_RELAY ?? DEFAULT_RELAY;
76
78
  const base = relay.replace(/^ws/, "http").replace(/\/+$/, "");
77
79
  if (!process.stdin.isTTY) {
@@ -151,7 +153,7 @@ async function interactiveOnboard(relayArg) {
151
153
  console.error(D(` could not resend: ${rs?.j.error ?? "the relay did not send an email"} — try again in a minute.`));
152
154
  continue;
153
155
  }
154
- const v = await postJson("/auth/verify", { email, code });
156
+ const v = await postJson("/auth/verify", { email, code, pubkey: kp.publicKey });
155
157
  if (v.status === 200) {
156
158
  const acct = v.j.account;
157
159
  const cfg = { ...(prior ?? {}), relay, member: acct.username, space: prior?.space ?? "", token: prior?.token ?? "",
@@ -251,10 +253,44 @@ function targets() {
251
253
  const r = resolveFor(process.cwd());
252
254
  if (r.cfg && r.cfg.space && r.reason !== "default")
253
255
  return [r.cfg];
254
- if (r.reason === "default" && f)
256
+ if ((r.reason === "default" || r.reason === "unbound") && f && Object.keys(f.spaces ?? {}).length)
255
257
  return Object.values(f.spaces ?? {}).map((m) => configFor(f, m, null));
256
258
  return [need()];
257
259
  }
260
+ async function launchAgent(binary, args, prompt, cwd) {
261
+ const { spawnSync } = await import("node:child_process");
262
+ let sp;
263
+ let handoff = null;
264
+ try {
265
+ sp = agentSpawn(binary, args(prompt));
266
+ }
267
+ catch (e) {
268
+ if (!(e instanceof CmdLineUnsafe))
269
+ throw e;
270
+ const where = cwd ?? process.cwd();
271
+ const rel = join(".pantheon", "handoff", `${Date.now()}-${randomBytes(3).toString("hex")}.md`);
272
+ handoff = join(where, rel);
273
+ mkdirSync(dirname(handoff), { recursive: true });
274
+ writeFileSync(handoff, prompt + "\n");
275
+ try {
276
+ const { excludeRunFiles } = await import("./worker.js");
277
+ excludeRunFiles(where);
278
+ }
279
+ catch { }
280
+ console.error(D(` the question is in ${rel} — the agent is asked to read it`));
281
+ sp = agentSpawn(binary, args(`Read the file ${rel.split(/[\\/]/).join("/")} in this folder and do what it asks.`));
282
+ }
283
+ try {
284
+ spawnSync(sp.command, sp.args, { stdio: "inherit", ...(cwd ? { cwd } : {}), ...sp.options });
285
+ }
286
+ finally {
287
+ if (handoff)
288
+ try {
289
+ rmSync(handoff, { force: true });
290
+ }
291
+ catch { }
292
+ }
293
+ }
258
294
  function needStrict() {
259
295
  const r = resolveFor(process.cwd(), { strict: true });
260
296
  return r.cfg && r.cfg.space ? r.cfg : null;
@@ -285,7 +321,7 @@ async function guidedSetup(cfg, project, opts = {}) {
285
321
  const io = rl ? terminalIO(rl, (l) => console.error(l)) : null;
286
322
  console.error(`\n${B("A few choices, then you're done.")}${io ? "" : Object.keys(preset).length ? D(" (applying the answers you passed)") : D(" (no terminal — taking the defaults)")}`);
287
323
  try {
288
- const r = await runGuide(io, { cfg, project }, {
324
+ const r = await runGuide(io, { cfg, project, optOut: optOutsFrom(argv) }, {
289
325
  publish: async (p) => {
290
326
  const c = new PantheonClient(cfg, undefined, { role: "cli" });
291
327
  try {
@@ -335,23 +371,27 @@ async function workdirFor(cfg, hud) {
335
371
  const bindings = all && m ? Object.values(all.bindings ?? {}).filter((b) => b.space === m.id) : [];
336
372
  let hints = [];
337
373
  if (bindings.length > 1 && hud.goal) {
338
- const acts = await fetchHistory(cfg).catch(() => []);
374
+ const acts = await fetchHistory(cfg, { goal: hud.goal }).catch(() => []);
339
375
  hints = acts.filter((x) => x.goal === hud.goal).flatMap((x) => [x.body, JSON.stringify(x.meta ?? {})]);
340
376
  }
341
377
  return pickWorkdir(bindings, hints, team);
342
378
  }
343
- async function fetchHistory(cfg, since = 0) {
379
+ async function fetchHistory(cfg, opts = {}) {
344
380
  const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
345
- const res = await fetch(`${base}/history?space=${encodeURIComponent(cfg.space)}&token=${encodeURIComponent(cfg.token)}&since=${since}`, { signal: AbortSignal.timeout(8000) });
381
+ const q = `space=${encodeURIComponent(cfg.space)}&token=${encodeURIComponent(cfg.token)}`
382
+ + (opts.goal ? `&goal=${encodeURIComponent(opts.goal)}` : opts.since !== undefined ? `&since=${opts.since}` : "");
383
+ const res = await fetch(`${base}/history?${q}`, { signal: AbortSignal.timeout(8000) });
346
384
  if (!res.ok)
347
385
  throw new Error(`relay answered ${res.status} for the ledger`);
348
386
  return (await res.json()).acts ?? [];
349
387
  }
350
- async function fetchAct(cfg, id) {
388
+ async function fetchAct(cfg, id, budgetMs = Number.POSITIVE_INFINITY) {
351
389
  const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
352
390
  const q = `space=${encodeURIComponent(cfg.space)}&token=${encodeURIComponent(cfg.token)}`;
391
+ const deadline = Date.now() + budgetMs;
392
+ const left = () => Math.max(1, Math.min(8000, deadline - Date.now()));
353
393
  try {
354
- const res = await fetch(`${base}/act?${q}&id=${encodeURIComponent(id)}`, { signal: AbortSignal.timeout(8000) });
394
+ const res = await fetch(`${base}/act?${q}&id=${encodeURIComponent(id)}`, { signal: AbortSignal.timeout(left()) });
355
395
  if (res.ok) {
356
396
  const body = await res.json();
357
397
  if (body.act)
@@ -363,7 +403,9 @@ async function fetchAct(cfg, id) {
363
403
  catch { }
364
404
  let head = 0;
365
405
  try {
366
- const h = await (await fetch(`${base}/health?space=${encodeURIComponent(cfg.space)}`, { signal: AbortSignal.timeout(8000) })).json();
406
+ if (Date.now() >= deadline)
407
+ return null;
408
+ const h = await (await fetch(`${base}/health?space=${encodeURIComponent(cfg.space)}`, { signal: AbortSignal.timeout(left()) })).json();
367
409
  head = h.spaces?.[0]?.seq ?? 0;
368
410
  }
369
411
  catch { }
@@ -371,8 +413,10 @@ async function fetchAct(cfg, id) {
371
413
  if (windows[windows.length - 1] !== 0)
372
414
  windows.push(0);
373
415
  for (const since of windows) {
416
+ if (Date.now() >= deadline)
417
+ return null;
374
418
  try {
375
- const acts = await fetchHistory(cfg, since);
419
+ const acts = await fetchHistory(cfg, { since });
376
420
  const act = acts.find((a) => a.id === id || String(a.seq) === id);
377
421
  if (act)
378
422
  return { act };
@@ -521,6 +565,7 @@ ${B("control")}
521
565
  pantheon leave remove what pantheon put on this machine
522
566
  pantheon panic cut off all unattended work on this machine, now
523
567
  pantheon doctor [--share] check this machine end to end; --share prints it without names or paths, to paste to us
568
+ pantheon network [--proxy <url> | --direct | --clear] how this machine reaches the relay; save a proxy for every Pantheon process here
524
569
  pantheon setup three questions — your name, your role, may a woken run execute commands —
525
570
  then one screen of defaults to keep or change: who may wake this machine,
526
571
  alerts, what you personally decide, playbook, base branch. Re-run any time.
@@ -549,7 +594,39 @@ ${B("less common")}
549
594
  pantheon version the pantheon version this machine is running
550
595
  `;
551
596
  const QUIET = new Set(["mcp", "hook", "pending", "version", "tray-status"]);
597
+ function usageFor(command, help) {
598
+ const plain = help.replace(/\x1b\[[0-9;]*m/g, "").split("\n");
599
+ const out = [];
600
+ const names = new RegExp(`(^\\s+|·\\s+)pantheon ${command.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`);
601
+ for (let i = 0; i < plain.length; i++) {
602
+ if (!names.test(plain[i]))
603
+ continue;
604
+ out.push(plain[i]);
605
+ while (i + 1 < plain.length && /^\s{20,}\S/.test(plain[i + 1]) && !/^\s+pantheon /.test(plain[i + 1]))
606
+ out.push(plain[++i]);
607
+ }
608
+ return out.length ? out.join("\n") : null;
609
+ }
610
+ const HELP_OPAQUE = new Set(["mcp", "hook", "pending", "tray-status", "help", "version", "-v", "--help", "-h"]);
611
+ function rememberWorkingProxy() {
612
+ try {
613
+ const all = loadAll();
614
+ if (!all)
615
+ return;
616
+ const latest = Object.values(all.spaces ?? {}).sort((a, b) => (b.joined_at ?? 0) - (a.joined_at ?? 0))[0];
617
+ const keep = proxyToRemember(latest?.relay ?? all.relay, currentPlan(), all.network);
618
+ if (!keep)
619
+ return;
620
+ saveAll({ ...all, network: { ...keep, saved_at: Date.now() } });
621
+ console.error(D(` saved the proxy ${redactProxy(keep.proxy)} for this machine's other Pantheon processes (worker, editors) — change it with pantheon network`));
622
+ }
623
+ catch { }
624
+ }
552
625
  async function main() {
626
+ try {
627
+ await installNetwork();
628
+ }
629
+ catch { }
553
630
  if (!QUIET.has(cmd)) {
554
631
  try {
555
632
  console.error(D(updateBanner(VERSION)));
@@ -557,14 +634,51 @@ async function main() {
557
634
  catch { }
558
635
  }
559
636
  if (argv.length === 0) {
560
- const cfg = load();
637
+ const cfg = identity();
561
638
  if (cfg?.account) {
562
639
  console.error(`${B("pantheon")} — signed in as ${C(cfg.account.username)}. ${D(`Your plate: pantheon brief · all commands: pantheon help`)}`);
563
640
  return;
564
641
  }
565
642
  return interactiveOnboard();
566
643
  }
644
+ if (!HELP_OPAQUE.has(cmd)) {
645
+ const helpAt = cmd === "say" ? [argv[1]] : argv.slice(1);
646
+ if (helpAt.some((a) => a === "--help" || a === "-h")) {
647
+ const u = usageFor(cmd, HELP_ALL);
648
+ console.error(u ? `${u}\n\n${D("nothing was run · all commands: pantheon help --all")}` : HELP);
649
+ return;
650
+ }
651
+ }
567
652
  switch (cmd) {
653
+ case "network": {
654
+ const all = loadAll() ?? die("not configured — sign up or join a team first");
655
+ const set = flag("proxy");
656
+ if (set !== undefined) {
657
+ const p = normalizeProxy(set) ?? die(`"${redactText(set)}" is not a proxy address — use http://host:port (add user:password@ if it needs a login)`);
658
+ saveAll({ ...all, network: { proxy: p, ...(flag("no-proxy") ? { no_proxy: flag("no-proxy") } : {}), source: "pantheon network", saved_at: Date.now() } });
659
+ console.error(`${B("saved")} every Pantheon process on this machine now reaches the relay through ${redactProxy(p)}`);
660
+ return;
661
+ }
662
+ if (argv.includes("--direct")) {
663
+ saveAll({ ...all, network: { proxy: "", source: "pantheon network --direct", saved_at: Date.now() } });
664
+ console.error(`${B("saved")} direct — set PANTHEON_PROXY=none in a shell to do the same for one command`);
665
+ return;
666
+ }
667
+ if (argv.includes("--clear")) {
668
+ const { network: _drop, ...rest } = all;
669
+ saveAll(rest);
670
+ console.error(`${B("cleared")} — Pantheon follows HTTPS_PROXY / NO_PROXY and the system settings again`);
671
+ return;
672
+ }
673
+ const plan = currentPlan();
674
+ const via = plan.https ?? plan.http;
675
+ console.log(`${B("network")} ${via ? `through ${redactProxy(via)}` : "direct"}${plan.source !== "none" ? D(` from ${plan.source}${plan.fallbackOnly ? ", used only if a direct connection fails" : plan.retryDirect ? " (saved by start/join; direct is tried if it cannot be reached)" : ""}`) : all.network?.proxy === "" ? D(" saved by pantheon network --direct") : ""}`);
676
+ if (via)
677
+ console.log(D(` not for: ${plan.noProxy.join(", ")}`));
678
+ if (plan.pac)
679
+ console.log(D(` this machine uses an auto-config script (${plan.pac}), which Pantheon cannot read — if the relay is unreachable, save the proxy it points to: pantheon network --proxy http://host:port`));
680
+ return;
681
+ }
568
682
  case "start": {
569
683
  const port = Number(flag("port", "8787"));
570
684
  const setupFlags = parseSetupFlags(argv);
@@ -574,7 +688,7 @@ async function main() {
574
688
  requireSignInOnHostedRelay(flag("relay"));
575
689
  console.error(`${B("pantheon start")}\n`);
576
690
  if (!flag("space") && !repoInfo(process.cwd()))
577
- 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`));
691
+ console.error(D(` No git repo here, so the team will be named after this folder: "${teamNameFor(process.cwd())}". Pass --space <name> to choose.\n`));
578
692
  const r = await qsStart({
579
693
  port,
580
694
  dataDir: resolveDataDir(flag("data")).dir,
@@ -583,6 +697,7 @@ async function main() {
583
697
  ...(flag("space") ? { space: flag("space") } : {}),
584
698
  ...(flag("me") ? { me: flag("me") } : {}),
585
699
  ...(flag("relay") ? { relay: flag("relay") } : {}),
700
+ ...(setupFlags.preset.wake ? { wake: setupFlags.preset.wake } : {}),
586
701
  hook: !argv.includes("--no-hook"),
587
702
  notifications: !argv.includes("--no-notifications"),
588
703
  menubar: !argv.includes("--no-menubar"),
@@ -595,6 +710,7 @@ async function main() {
595
710
  for (const w of r.warnings)
596
711
  console.error(`\x1b[33m ! ${w}\x1b[0m`);
597
712
  printGranted(r.granted);
713
+ rememberWorkingProxy();
598
714
  if (!r.claimed) {
599
715
  console.error(`\n\x1b[31m No team was created.\x1b[0m ${D("Fix the warning above, then run `pantheon start` again.")}`);
600
716
  if (r.relay)
@@ -634,7 +750,7 @@ async function main() {
634
750
  case "invite": {
635
751
  const cfg = need();
636
752
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
637
- await client.connect().catch((e) => die(client.pending ?? e.message));
753
+ await client.connect().catch((e) => die(client.pending ?? connectFailure(e, cfg.relay)));
638
754
  await new Promise((r) => setTimeout(r, 250));
639
755
  const revokeAt = argv.indexOf("--revoke");
640
756
  const revokeArg = revokeAt >= 0 ? (argv[revokeAt + 1] && !argv[revokeAt + 1].startsWith("--") ? argv[revokeAt + 1] : "all") : undefined;
@@ -677,6 +793,7 @@ async function main() {
677
793
  return;
678
794
  }
679
795
  case "relay": {
796
+ process.on("unhandledRejection", (e) => { console.error(`! unhandled rejection: ${e?.stack ?? String(e)}`); });
680
797
  const relay = new Relay({
681
798
  port: Number(flag("port", process.env.PORT ?? "8787")),
682
799
  host: flag("host", "0.0.0.0"),
@@ -706,16 +823,18 @@ async function main() {
706
823
  return;
707
824
  }
708
825
  case "init": {
709
- const kp = newKeypair();
826
+ const had = argv.includes("--reset") ? null : identity();
827
+ const kp = had?.publicKey && had.privateKey ? { publicKey: had.publicKey, privateKey: had.privateKey } : newKeypair();
710
828
  const cfg = {
711
829
  relay: flag("relay", "ws://localhost:8787"),
712
830
  space: flag("space", "team"),
713
831
  token: flag("token", randomBytes(16).toString("hex")),
714
- member: flag("me", process.env.USER ?? "me"),
832
+ member: flag("me") ?? had?.member ?? process.env.USER ?? "me",
715
833
  publicKey: kp.publicKey,
716
834
  privateKey: kp.privateKey,
717
835
  display: flag("display"),
718
836
  agent: flag("agent", "unknown"),
837
+ ...(had?.account ? { account: had.account } : {}),
719
838
  };
720
839
  save(cfg);
721
840
  console.error(`${B("space claimed.")} config: ${D(CONFIG_PATH)}\n`);
@@ -762,7 +881,7 @@ async function main() {
762
881
  parsed = await resolveInviteCode(invite, candidates).catch((err) => die(err.message.replace(/^INVITE_CODE_[A-Z]+:\s*/, "")));
763
882
  console.error(D(`That was just the code — it opens "${parsed.space}" on ${new URL(parsed.relay).host}. Joining that team.\n`));
764
883
  }
765
- if (!load()?.account && sameHost(parsed.relay, DEFAULT_RELAY)) {
884
+ if (!identity()?.account && sameHost(parsed.relay, DEFAULT_RELAY)) {
766
885
  die(`sign in first — this relay verifies email.\n New here: pantheon signup --email you@work.com --username you\n Have an account: pantheon login --email you@work.com\n Then run this same \`pantheon join\` command again — the invite is still good.`);
767
886
  }
768
887
  const wp = unattendedProject();
@@ -794,6 +913,7 @@ async function main() {
794
913
  for (const w of r.warnings)
795
914
  console.error(`\x1b[33m ! ${w}\x1b[0m`);
796
915
  printGranted(r.granted);
916
+ rememberWorkingProxy();
797
917
  printReady(r.tools);
798
918
  {
799
919
  const joined = load();
@@ -836,6 +956,22 @@ async function main() {
836
956
  }
837
957
  case "rewire": {
838
958
  const rows = installForUser().map((t) => ({ tool: t.tool, status: t.status }));
959
+ try {
960
+ const { refreshServiceDefinition, serviceInstalled: installed, restartService: restart } = await import("./service.js");
961
+ for (const kind of ["worker", "notifier", "menubar"]) {
962
+ if (!installed(kind))
963
+ continue;
964
+ const fresh = refreshServiceDefinition(kind);
965
+ if (fresh.line)
966
+ console.error(fresh.line);
967
+ if (fresh.changed && kind !== "worker")
968
+ for (const l of restart(kind, undefined, { reload: true }))
969
+ console.error(l);
970
+ else if (fresh.changed)
971
+ console.error(D(" worker: the new definition applies at its next restart"));
972
+ }
973
+ }
974
+ catch { }
839
975
  if (argv.includes("--json")) {
840
976
  console.log(JSON.stringify(rows));
841
977
  return;
@@ -906,6 +1042,7 @@ async function main() {
906
1042
  }
907
1043
  }
908
1044
  }
1045
+ const { refreshServiceDefinition } = await import("./service.js");
909
1046
  for (const kind of ["worker", "notifier", "menubar"]) {
910
1047
  if (!serviceInstalled(kind))
911
1048
  continue;
@@ -913,7 +1050,10 @@ async function main() {
913
1050
  console.error(` ${D("worker: a run is in progress — left alone. When it is done:")} pantheon service restart worker`);
914
1051
  continue;
915
1052
  }
916
- for (const l of restartService(kind))
1053
+ const fresh = refreshServiceDefinition(kind);
1054
+ if (fresh.line)
1055
+ console.error(fresh.line);
1056
+ for (const l of restartService(kind, undefined, { reload: fresh.changed }))
917
1057
  console.error(l);
918
1058
  }
919
1059
  if (r.restart.includes("relay"))
@@ -953,7 +1093,7 @@ async function main() {
953
1093
  console.error(client.pending);
954
1094
  process.exit(2);
955
1095
  }
956
- die(`cannot reach relay at ${cfg.relay} — ${e.message}`);
1096
+ die(connectFailure(e, cfg.relay));
957
1097
  });
958
1098
  await new Promise((r) => setTimeout(r, 350));
959
1099
  const peers = client.roster.filter((m) => m.id !== cfg.member);
@@ -1021,7 +1161,7 @@ async function main() {
1021
1161
  for (const cfg of targets()) {
1022
1162
  const profile = loadProfile(cfg);
1023
1163
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
1024
- await client.connect().catch((e) => die(client.pending ?? e.message));
1164
+ await client.connect().catch((e) => die(client.pending ?? connectFailure(e, cfg.relay)));
1025
1165
  await new Promise((r) => setTimeout(r, 350));
1026
1166
  process.stdout.write(renderBrief({
1027
1167
  space: cfg.space,
@@ -1051,7 +1191,7 @@ async function main() {
1051
1191
  }
1052
1192
  const cfg = need();
1053
1193
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
1054
- await client.connect().catch((e) => die(client.pending ?? e.message));
1194
+ await client.connect().catch((e) => die(client.pending ?? connectFailure(e, cfg.relay)));
1055
1195
  await new Promise((r) => setTimeout(r, 350));
1056
1196
  process.stdout.write(renderTeam({
1057
1197
  space: cfg.space,
@@ -1079,7 +1219,7 @@ async function main() {
1079
1219
  };
1080
1220
  const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
1081
1221
  const q = `space=${encodeURIComponent(cfg.space)}&token=${encodeURIComponent(cfg.token)}`;
1082
- const first = await fetch(`${base}/history?${q}`).catch((e) => die(`cannot reach relay — ${e.message}`));
1222
+ const first = await fetch(`${base}/history?${q}`).catch((e) => die(describeNetError(e, cfg.relay)));
1083
1223
  if (!first.ok)
1084
1224
  die(`relay answered ${first.status} — check the space name and token`);
1085
1225
  let since = ((await first.json()).acts ?? []).at(-1)?.seq ?? 0;
@@ -1108,7 +1248,8 @@ async function main() {
1108
1248
  }
1109
1249
  case "history": {
1110
1250
  const cfg = need();
1111
- const acts = await fetchHistory(cfg, numFlag("since", 0)).catch((e) => die(`cannot read the ledger — ${e.message}`));
1251
+ const acts = await fetchHistory(cfg, flag("goal") ? { goal: flag("goal") } : flag("since") ? { since: numFlag("since", 0) } : {})
1252
+ .catch((e) => die(`cannot read the ledger — ${e.message}`));
1112
1253
  const picked = selectActs(acts, {
1113
1254
  ...(flag("goal") ? { goal: flag("goal") } : {}), ...(flag("from") ? { from: flag("from") } : {}),
1114
1255
  last: numFlag("last", 20), presence: argv.includes("--presence"),
@@ -1140,11 +1281,11 @@ async function main() {
1140
1281
  case "show": {
1141
1282
  const cfg = need();
1142
1283
  const key = argv[1] ?? die("usage: pantheon show <act id | sequence number>");
1143
- const acts = await fetchHistory(cfg).catch((e) => die(`cannot read the ledger — ${e.message}`));
1144
- 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)`);
1145
- console.log(formatAct(a));
1284
+ const found = await fetchAct(cfg, key);
1285
+ const a = found?.act ?? die(`no act ${key} in what the relay still holds (pantheon history lists them)`);
1286
+ console.log(formatAct(a, found?.goalTitle ? { goalTitle: found.goalTitle } : {}));
1146
1287
  if (a.ref) {
1147
- const r = acts.find((x) => x.id === a.ref);
1288
+ const r = (await fetchAct(cfg, a.ref))?.act;
1148
1289
  if (r)
1149
1290
  console.log(D(`\nin reply to:\n`) + formatAct(r, { short: true }));
1150
1291
  }
@@ -1159,7 +1300,7 @@ async function main() {
1159
1300
  die("nothing to say — pantheon say NOTE 'your message here'");
1160
1301
  const summary = flag("summary");
1161
1302
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
1162
- await client.connect().catch((e) => die(`cannot reach relay — ${e.message}`));
1303
+ await client.connect().catch((e) => die(connectFailure(e, cfg.relay)));
1163
1304
  const to = flag("to");
1164
1305
  const meta = {};
1165
1306
  for (const k of ["branch", "commit", "remote"]) {
@@ -1173,7 +1314,15 @@ async function main() {
1173
1314
  let goalArg = flag("goal") ?? null;
1174
1315
  let ref = flag("ref") ?? null;
1175
1316
  if (ref !== null || ["ANSWER", "ACCEPT", "OBJECT"].includes(act)) {
1176
- const acts = await fetchHistory(cfg).catch(() => []);
1317
+ let acts;
1318
+ if (ref && ref !== "last") {
1319
+ const hit = (await fetchAct(cfg, ref))?.act;
1320
+ acts = hit ? [hit] : [];
1321
+ if (!hit && /^a_/.test(ref))
1322
+ console.error(D(` ${ref} is not in what this relay holds — sending it as given`));
1323
+ }
1324
+ else
1325
+ acts = await fetchHistory(cfg, goalArg ? { goal: goalArg } : {}).catch(() => []);
1177
1326
  const rr = resolveRef(acts, ref, act, goalArg, cfg.member);
1178
1327
  if (rr.error) {
1179
1328
  client.close();
@@ -1299,23 +1448,17 @@ async function main() {
1299
1448
  return;
1300
1449
  }
1301
1450
  case "stop": {
1302
- const what = (argv[1] ?? "all").toLowerCase();
1451
+ const what = (argv[1] && !argv[1].startsWith("--") ? argv[1] : "all").toLowerCase();
1452
+ if (!["all", "relay", "worker", "notifier", "menubar"].includes(what))
1453
+ die(`usage: pantheon stop [relay|worker|notifier|menubar|all]`);
1454
+ const { stopMachine } = await import("./machine.js");
1303
1455
  console.log(B("stopping"));
1304
- if (what === "relay" || what === "all")
1305
- for (const l of stopProc("relay"))
1306
- console.log(` ${l}`);
1307
- if (what === "worker" || what === "all")
1308
- for (const l of stopProc("worker"))
1309
- console.log(` ${l}`);
1310
- if (what === "notifier" || what === "all")
1311
- for (const l of stopProc("notifier"))
1312
- console.log(` ${l}`);
1313
- if (what === "menubar" || what === "all")
1314
- for (const l of stopProc("menubar"))
1315
- console.log(` ${l}`);
1316
- if (what === "relay" || what === "all") {
1317
- console.log(D(`\n Your teammates are offline until a relay is running again.`));
1318
- }
1456
+ const everyTeam = Object.values(loadAll()?.spaces ?? {}).map((m) => ({ name: m.name, relay: m.relay }));
1457
+ const res = stopMachine(what, { port: Number(flag("port", "8787")), teams: everyTeam.length ? everyTeam : (load()?.relay ? [{ name: load().space, relay: load().relay }] : []) });
1458
+ for (const l of res.lines)
1459
+ console.log(` ${l}`);
1460
+ if (res.note)
1461
+ console.log(D(`\n ${res.note}`));
1319
1462
  return;
1320
1463
  }
1321
1464
  case "logs": {
@@ -1323,7 +1466,7 @@ async function main() {
1323
1466
  const proc = kind === "relay" ? relayProc(Number(flag("port", "8787"))) : kind === "worker" ? workerProc() : notifierProc();
1324
1467
  const f = logFileFor(kind, proc);
1325
1468
  if (!f)
1326
- die(`no ${kind} log found. If it runs as a service, the plist names the path; if you started it by hand, its output went to that terminal.`);
1469
+ die(`no ${kind} log found. ${process.platform === "linux" ? `If it runs as a service: journalctl --user -u pantheon-${kind}` : process.platform === "darwin" ? "If it runs as a service, the plist names the path" : "Services are not used on Windows"}; if you started it by hand, its output went to that terminal.`);
1327
1470
  console.error(D(`${f} (${statFile(f)})\n`));
1328
1471
  const follow = argv.includes("-f") || argv.includes("--follow");
1329
1472
  const { spawn } = await import("node:child_process");
@@ -1612,7 +1755,7 @@ async function main() {
1612
1755
  if (!argv.includes("--yes"))
1613
1756
  die(`this deletes "${m.name}" for everyone in it — its history, goals and invites — and cannot be undone.\n If you mean it: pantheon space delete ${m.name} --yes`);
1614
1757
  const client = new PantheonClient(configFor(all, m, null), undefined, { role: "cli" });
1615
- await client.connect().catch((e) => die(client.pending ?? e.message));
1758
+ await client.connect().catch((e) => die(client.pending ?? connectFailure(e, m.relay)));
1616
1759
  await client.deleteSpace(m.name).catch((e) => { client.close(); die(e.message); });
1617
1760
  client.close();
1618
1761
  const bound = Object.values(all.bindings ?? {}).filter((b) => b.space === m.id).length;
@@ -1630,53 +1773,103 @@ async function main() {
1630
1773
  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.`));
1631
1774
  return;
1632
1775
  }
1633
- const cfg = load();
1776
+ const all = loadAll();
1777
+ const me = identity(all);
1778
+ const { leavePlan } = await import("./machine.js");
1779
+ const plan = leavePlan(all);
1634
1780
  const project = resolve(flag("project", "."));
1635
- console.error(`${B("leaving pantheon")}\n`);
1636
- for (const l of stopProc("worker"))
1637
- console.error(` ${l}`);
1638
- for (const l of stopProc("notifier"))
1639
- console.error(` ${l}`);
1640
- for (const l of stopProc("menubar"))
1781
+ const keepIdentity = argv.includes("--keep-identity");
1782
+ if (plan.teams.length > 1 && !keepIdentity && !argv.includes("--yes")) {
1783
+ const list = plan.teams.map((t) => `"${t.name}"`).join(", ");
1784
+ if (!process.stdin.isTTY)
1785
+ die(`this machine is in ${plan.teams.length} teams — ${list}. Leaving removes its key, so it leaves ALL of them.\n If you mean it: pantheon leave --yes\n Just one team: pantheon leave <team>`);
1786
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
1787
+ const answer = await new Promise((res) => rl.question(`This machine is in ${plan.teams.length} teams: ${list}. Leaving removes its key, so it leaves ALL of them. Type "yes" to continue: `, (x) => res(x.trim())));
1788
+ rl.close();
1789
+ if (answer.toLowerCase() !== "yes")
1790
+ die(`nothing was changed. Leave one team with: pantheon leave <team>`);
1791
+ }
1792
+ console.error(`${B("leaving pantheon")}${plan.teams.length ? D(` — ${plan.teams.map((t) => t.name).join(", ")}`) : ""}\n`);
1793
+ const { uninstallMachine, realUninstallIo } = await import("./machine.js");
1794
+ const { removeUserMcpEntries } = await import("./install.js");
1795
+ const done = uninstallMachine({ project, stopRelay: argv.includes("--stop-relay") }, realUninstallIo({
1796
+ port: Number(flag("port", "8787")),
1797
+ removeMcpEntries: () => removeUserMcpEntries({ project }),
1798
+ hasIntent: () => readIntent() !== null,
1799
+ clearIntent: () => clearIntent(),
1800
+ }));
1801
+ for (const l of done.lines)
1641
1802
  console.error(` ${l}`);
1642
- if (argv.includes("--stop-relay"))
1643
- for (const l of stopProc("relay"))
1644
- console.error(` ${l}`);
1645
- else if (relayProc(Number(flag("port", "8787"))).running) {
1646
- console.error(` relay: left running — others may be using it. Stop it with: pantheon stop relay`);
1647
- }
1803
+ const removed = [...done.removed];
1804
+ const left = [...done.left];
1648
1805
  for (const l of removeHook(project))
1649
1806
  console.error(` hook: ${l.trim()}`);
1650
1807
  const { revokeToolPermissions } = await import("./grants.js");
1651
- for (const l of revokeToolPermissions())
1808
+ for (const l of revokeToolPermissions()) {
1652
1809
  console.error(` perms:${l}`);
1653
- {
1654
- const l = removeCodexUserScope();
1655
- if (l)
1656
- console.error(` perms:${l}`);
1810
+ removed.push(l.trim().replace(/^removed /, ""));
1657
1811
  }
1658
- const { unlinkSync, existsSync: exists, rmSync } = await import("node:fs");
1659
- if (cfg && exists(CONFIG_PATH)) {
1660
- if (argv.includes("--keep-identity")) {
1661
- console.error(` config: kept at ${CONFIG_PATH} (--keep-identity)`);
1812
+ const { unlinkSync, existsSync: exists, rmSync, readdirSync } = await import("node:fs");
1813
+ if (all && exists(CONFIG_PATH)) {
1814
+ if (keepIdentity) {
1815
+ left.push(`your identity and ${plan.teams.length} team membership${plan.teams.length === 1 ? "" : "s"}, ${CONFIG_PATH} (--keep-identity)`);
1662
1816
  }
1663
1817
  else {
1664
1818
  unlinkSync(CONFIG_PATH);
1819
+ removed.push(`${CONFIG_PATH} — your key and ${plan.teams.length ? `the token${plan.teams.length === 1 ? "" : "s"} for ${plan.teams.map((t) => `"${t.name}"`).join(", ")}` : "account"}; rejoining means a new invite`);
1820
+ const runHomes = join(CONFIG_PATH, "..", "run-homes");
1821
+ const { restorePendingQuarantines, pendingQuarantines } = await import("./agents.js");
1822
+ const put = restorePendingQuarantines(runHomes);
1823
+ if (put.length)
1824
+ removed.push(`nothing of your repos' — what interrupted runs had held aside: ${put.join("; ")}`);
1825
+ const still = pendingQuarantines(runHomes);
1826
+ const keptAside = (dir) => { try {
1827
+ return readdirSync(join(dir, "quarantine-kept")).length > 0;
1828
+ }
1829
+ catch {
1830
+ return false;
1831
+ } };
1832
+ let homes = [];
1665
1833
  try {
1666
- rmSync(join(CONFIG_PATH, "..", "run-homes"), { recursive: true, force: true });
1834
+ homes = readdirSync(runHomes);
1667
1835
  }
1668
1836
  catch { }
1669
- console.error(` config: removed ${CONFIG_PATH} — your keypair is gone, so rejoining means a new invite`);
1670
- }
1671
- }
1672
- else {
1673
- console.error(` config: none to remove`);
1674
- }
1675
- console.error(`\n ${D("Not touched: the MCP entries in Claude Code, Cursor, Copilot, VS Code and Kiro (pantheon install wrote them; remove them there),")}`);
1676
- console.error(` ${D("the relay's ledger on disk, and anything you pushed to git. Codex's entry and approval line were removed above.")}`);
1677
- if (cfg) {
1678
- console.error(`\n ${D(`Your teammates still list you in "${cfg.space}". Ask one of them to run:`)}`);
1679
- console.error(` ${D(`pantheon admit ${cfg.member} --revoke`)}`);
1837
+ const keep = new Set([...still.map((x) => x.runHome), ...homes.map((h) => join(runHomes, h)).filter(keptAside)]);
1838
+ for (const h of homes) {
1839
+ const dir = join(runHomes, h);
1840
+ if (keep.has(dir))
1841
+ continue;
1842
+ try {
1843
+ rmSync(dir, { recursive: true, force: true });
1844
+ }
1845
+ catch { }
1846
+ }
1847
+ if (!keep.size) {
1848
+ try {
1849
+ rmSync(runHomes, { recursive: true, force: true });
1850
+ }
1851
+ catch { }
1852
+ }
1853
+ for (const x of still)
1854
+ left.push(`${x.runHome} — it still holds ${x.paths.join(", ")} from ${x.worktree}, which could not be put back safely; compare and move them back by hand, then delete the folder`);
1855
+ for (const dir of keep)
1856
+ if (keptAside(dir) && !still.some((x) => x.runHome === dir))
1857
+ left.push(`${join(dir, "quarantine-kept")} — agent config an interrupted run held, set aside because the repo had moved on; check it, then delete the folder`);
1858
+ }
1859
+ }
1860
+ console.error(`\n${B("Removed")}`);
1861
+ for (const r of removed)
1862
+ console.error(` ${C("✓")} ${r}`);
1863
+ if (!removed.length)
1864
+ console.error(` ${D("nothing — Pantheon had nothing installed here")}`);
1865
+ console.error(`\n${B("Left")}`);
1866
+ for (const l of left)
1867
+ console.error(` • ${l}`);
1868
+ console.error(` • ${D("files committed in your repos (.mcp.json, .cursor/mcp.json, .vscode/mcp.json, the brief in AGENTS.md / CLAUDE.md) — your team's; delete them in a commit if the whole team is leaving")}`);
1869
+ console.error(` • ${D("the relay's ledger on disk, and anything you pushed to git")}`);
1870
+ if (plan.teams.length) {
1871
+ console.error(`\n ${D(`Your teammates still list you in ${plan.teams.map((t) => `"${t.name}"`).join(", ")}. Ask someone in each to run:`)}`);
1872
+ console.error(` ${D(`pantheon admit ${me?.member ?? plan.member ?? "<your name>"} --revoke`)}`);
1680
1873
  }
1681
1874
  console.error(`\n ${D(`If Pantheon let you down, one line on why to ${FEEDBACK_ADDRESS} would help more than you might think.`)}`);
1682
1875
  return;
@@ -1689,35 +1882,42 @@ async function main() {
1689
1882
  return;
1690
1883
  }
1691
1884
  case "panic": {
1692
- const cfg = need();
1693
1885
  console.error(`${B("PANIC — locking down this machine")}\n`);
1694
- for (const l of stopProc("worker"))
1695
- console.error(` ${l}`);
1696
- const client = new PantheonClient(cfg, undefined, { role: "cli" });
1697
- const up = await client.connect().then(() => true).catch(() => false);
1698
- if (!up) {
1699
- console.error(` ${D("cannot reach the relay, so wake grants were not changed")}`);
1700
- console.error(`\n ${C("The worker is stopped, so nothing can start a run here regardless.")}`);
1701
- return;
1702
- }
1703
- await new Promise((r) => setTimeout(r, 300));
1704
- const wakers = client.members.filter((m) => m.admitted && m.may_wake && m.member !== cfg.member);
1705
- for (const w of wakers) {
1706
- await client.admit(w.member, false, false).catch(() => { });
1707
- console.error(` revoked wake rights from ${C(w.member)}`);
1708
- }
1709
- if (!wakers.length)
1710
- console.error(` ${D("no peer had wake rights to revoke")}`);
1711
- client.close();
1712
- console.error(`\n ${C("Nothing can start an agent run on this machine now.")}`);
1886
+ const { panicMachine, waitStopped } = await import("./machine.js");
1887
+ const all = loadAll();
1888
+ const teams = Object.values(all?.spaces ?? {});
1889
+ const r = await panicMachine({
1890
+ stopWorker: () => stopProc("worker"),
1891
+ workerDown: () => waitStopped("worker", 5000),
1892
+ teams,
1893
+ revoke: async (m) => {
1894
+ const cfg = configFor(all, m, null);
1895
+ const client = new PantheonClient(cfg, undefined, { role: "cli" });
1896
+ try {
1897
+ await client.connect();
1898
+ await new Promise((res) => setTimeout(res, 300));
1899
+ const wakers = client.members.filter((x) => x.admitted && x.may_wake && x.member !== cfg.member);
1900
+ for (const w of wakers)
1901
+ await client.admit(w.member, false, false).catch(() => { });
1902
+ return wakers.map((w) => w.member);
1903
+ }
1904
+ finally {
1905
+ client.close();
1906
+ }
1907
+ },
1908
+ });
1909
+ for (const l of r.lines)
1910
+ console.error(l);
1713
1911
  console.error(D(` Your peers can still message you; only unattended runs are off.`));
1714
1912
  console.error(D(` To undo: pantheon service install worker and pantheon admit <who> --may-wake`));
1913
+ if (!r.ok)
1914
+ process.exitCode = 1;
1715
1915
  return;
1716
1916
  }
1717
1917
  case "members": {
1718
1918
  const cfg = need();
1719
1919
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
1720
- await client.connect().catch((e) => die(client.pending ?? e.message));
1920
+ await client.connect().catch((e) => die(client.pending ?? connectFailure(e, cfg.relay)));
1721
1921
  await new Promise((r) => setTimeout(r, 300));
1722
1922
  console.log(`${B(`who is in ${cfg.space}`)}\n`);
1723
1923
  for (const e of client.members) {
@@ -1738,7 +1938,7 @@ async function main() {
1738
1938
  const cfg = need();
1739
1939
  const who = argv[1] ?? die("usage: pantheon admit <member> [--may-wake] [--revoke] [--forget]");
1740
1940
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
1741
- await client.connect().catch((e) => die(client.pending ?? e.message));
1941
+ await client.connect().catch((e) => die(client.pending ?? connectFailure(e, cfg.relay)));
1742
1942
  await new Promise((r) => setTimeout(r, 250));
1743
1943
  const forget = argv.includes("--forget");
1744
1944
  const revoke = argv.includes("--revoke");
@@ -1785,16 +1985,21 @@ async function main() {
1785
1985
  return;
1786
1986
  }
1787
1987
  case "whoami": {
1788
- const cfg = need();
1988
+ const cfg = identity() ?? die("not set up on this machine — pantheon signup, or pantheon join <invite>");
1989
+ const team = resolveFor(process.cwd()).cfg;
1789
1990
  if (cfg.account) {
1790
1991
  console.log(`account ${C(cfg.account.username)} ${D(`<${cfg.account.email}>`)} ${D("verified")}`);
1791
1992
  }
1993
+ else if (cfg.pendingAuth) {
1994
+ console.log(`account ${D(`not signed in yet — a code was sent to ${cfg.pendingAuth.email}: pantheon verify <code>`)}`);
1995
+ }
1792
1996
  else {
1793
1997
  console.log(`account ${D("not signed in")} ${D("— pantheon signup / pantheon login")}`);
1794
1998
  }
1795
1999
  console.log(`member ${C(cfg.member)}`);
1796
- console.log(`space ${cfg.space}`);
1797
- console.log(`relay ${cfg.relay}`);
2000
+ const teams = Object.values(loadAll()?.spaces ?? {}).map((m) => m.name);
2001
+ console.log(`space ${team?.space || (teams.length ? D(`none picked here — you are in ${teams.join(", ")}`) : D("none yet — pantheon start, or pantheon join <invite>"))}`);
2002
+ console.log(`relay ${team?.space ? team.relay : cfg.relay}`);
1798
2003
  console.log(`this device ${C(fingerprint(cfg.publicKey))}`);
1799
2004
  console.log(D(`\nManage your devices and sessions: pantheon sessions`));
1800
2005
  return;
@@ -1806,7 +2011,7 @@ async function main() {
1806
2011
  return interactiveOnboard(flag("relay"));
1807
2012
  const email = flag("email") ?? die(`usage: pantheon ${purpose} --email you@work.com${purpose === "signup" ? " --username <name>" : ""} [--relay wss://...]`);
1808
2013
  const username = purpose === "signup" ? (flag("username") ?? die("usage: pantheon signup --email you@work.com --username <name>")) : undefined;
1809
- const prior = load();
2014
+ const prior = identity();
1810
2015
  const relay = flag("relay") ?? prior?.relay ?? process.env.PANTHEON_RELAY ?? DEFAULT_RELAY;
1811
2016
  const kp = prior?.publicKey && prior.privateKey ? { publicKey: prior.publicKey, privateKey: prior.privateKey } : newKeypair();
1812
2017
  const label = flag("label") ?? `${process.env.USER ?? "a"}'s ${process.platform === "darwin" ? "mac" : process.platform}`;
@@ -1814,7 +2019,7 @@ async function main() {
1814
2019
  const body = purpose === "signup" ? { email, username, pubkey: kp.publicKey, label } : { email, pubkey: kp.publicKey, label };
1815
2020
  const r = await fetch(`${base}/auth/${purpose}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })
1816
2021
  .then(async (x) => ({ status: x.status, j: await x.json() }))
1817
- .catch((e) => die(`could not reach the relay at ${base}: ${e.message}`));
2022
+ .catch((e) => die(describeNetError(e, relay)));
1818
2023
  if (r.status !== 200)
1819
2024
  die(String(r.j.error ?? `signup failed (${r.status})`));
1820
2025
  save({ ...(prior ?? {}), relay, member: username ?? prior?.member ?? "me", space: prior?.space ?? "", token: prior?.token ?? "",
@@ -1831,7 +2036,7 @@ async function main() {
1831
2036
  return;
1832
2037
  }
1833
2038
  case "verify": {
1834
- const cfg = load() ?? die("run `pantheon signup` or `pantheon login` first");
2039
+ const cfg = identity() ?? die("run `pantheon signup` or `pantheon login` first");
1835
2040
  const pending = cfg.pendingAuth ?? die("nothing to verify — run `pantheon signup` or `pantheon login` first");
1836
2041
  let code = (argv[1] ?? flag("code") ?? "").trim();
1837
2042
  if (!code && process.stdin.isTTY) {
@@ -1842,9 +2047,9 @@ async function main() {
1842
2047
  if (!code)
1843
2048
  die("usage: pantheon verify <6-digit-code>");
1844
2049
  const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
1845
- const r = await fetch(`${base}/auth/verify`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: pending.email, code }) })
2050
+ const r = await fetch(`${base}/auth/verify`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: pending.email, code, pubkey: cfg.publicKey }) })
1846
2051
  .then(async (x) => ({ status: x.status, j: await x.json() }))
1847
- .catch((e) => die(`could not reach the relay: ${e.message}`));
2052
+ .catch((e) => die(describeNetError(e, cfg.relay)));
1848
2053
  if (r.status !== 200)
1849
2054
  die(String(r.j.error ?? "verification failed"));
1850
2055
  const acct = r.j.account;
@@ -1858,7 +2063,7 @@ async function main() {
1858
2063
  return;
1859
2064
  }
1860
2065
  case "sessions": {
1861
- const cfg = need();
2066
+ const cfg = identity() ?? die("not set up on this machine — pantheon signup first");
1862
2067
  if (!cfg.account)
1863
2068
  die("not signed in — run `pantheon signup` or `pantheon login` first");
1864
2069
  const sub = argv[1];
@@ -1870,7 +2075,7 @@ async function main() {
1870
2075
  const me = await signedFetch(cfg, "GET", "/account/sessions").then((x) => x.json()).catch(() => ({}));
1871
2076
  thisDevice = me.this_device ?? "";
1872
2077
  }
1873
- const r = await signedFetch(cfg, "POST", "/account/revoke", { kind, id }).then((x) => x.json()).catch((e) => die(e.message));
2078
+ const r = await signedFetch(cfg, "POST", "/account/revoke", { kind, id }).then((x) => x.json()).catch((e) => die(describeNetError(e, cfg.relay)));
1874
2079
  if (kind === "device" && id === thisDevice) {
1875
2080
  const cl = { ...cfg };
1876
2081
  delete cl.account;
@@ -1879,7 +2084,7 @@ async function main() {
1879
2084
  console.error(`${C("revoked")} — ${r.revoked ?? 0} session(s)${r.device_removed ? ", device removed" : ""}${kind === "device" && id === thisDevice ? " (this device — you are now signed out)" : ""}`);
1880
2085
  return;
1881
2086
  }
1882
- const r = await signedFetch(cfg, "GET", "/account/sessions").then((x) => x.json()).catch((e) => die(e.message));
2087
+ const r = await signedFetch(cfg, "GET", "/account/sessions").then((x) => x.json()).catch((e) => die(describeNetError(e, cfg.relay)));
1883
2088
  console.log(`${B(r.account.username)} ${D(`<${r.account.email}>`)}\n`);
1884
2089
  console.log(B("devices"));
1885
2090
  for (const d of r.devices)
@@ -1894,12 +2099,12 @@ async function main() {
1894
2099
  return;
1895
2100
  }
1896
2101
  case "audit": {
1897
- const cfg = need();
2102
+ const cfg = identity() ?? die("not set up on this machine — pantheon signup first");
1898
2103
  if (!cfg.account)
1899
2104
  die("not signed in — run `pantheon signup` first");
1900
2105
  const space = flag("space");
1901
2106
  const since = flag("since") ? String(Date.now() - Number(flag("since")) * 3_600_000) : undefined;
1902
- const r = await signedFetch(cfg, "GET", "/audit", { ...(space ? { space } : {}), ...(since ? { since } : {}) }).then((x) => x.json()).catch((e) => die(e.message));
2107
+ const r = await signedFetch(cfg, "GET", "/audit", { ...(space ? { space } : {}), ...(since ? { since } : {}) }).then((x) => x.json()).catch((e) => die(describeNetError(e, cfg.relay)));
1903
2108
  if (r.error)
1904
2109
  die(r.error);
1905
2110
  if (argv.includes("--json")) {
@@ -1914,22 +2119,34 @@ async function main() {
1914
2119
  return;
1915
2120
  }
1916
2121
  case "logout": {
1917
- const cfg = need();
2122
+ const cfg = identity() ?? die("not set up on this machine — pantheon signup first");
1918
2123
  if (!cfg.account) {
1919
2124
  console.error(D("not signed in — nothing to log out"));
1920
2125
  return;
1921
2126
  }
1922
- const list = await signedFetch(cfg, "GET", "/account/sessions").then((x) => x.json()).catch((e) => die(e.message));
1923
- if (list.this_device)
1924
- await signedFetch(cfg, "POST", "/account/revoke", { kind: "device", id: list.this_device }).catch(() => { });
2127
+ const list = await signedFetch(cfg, "GET", "/account/sessions").then(async (x) => ({ ok: x.ok, status: x.status, j: await x.json().catch(() => ({})) }))
2128
+ .catch((e) => die(describeNetError(e, cfg.relay)));
2129
+ if (!list.ok && list.status !== 403)
2130
+ die(`the relay did not sign this device out (${list.j.error ?? list.status}) — nothing changed. Try again, or remove it from another device: pantheon sessions`);
2131
+ let revoked = list.status === 403;
2132
+ if (list.ok && list.j.this_device) {
2133
+ const r = await signedFetch(cfg, "POST", "/account/revoke", { kind: "device", id: list.j.this_device })
2134
+ .then(async (x) => ({ ok: x.ok, j: await x.json().catch(() => ({})) }))
2135
+ .catch((e) => die(describeNetError(e, cfg.relay)));
2136
+ if (!r.ok)
2137
+ die(`the relay did not sign this device out (${r.j.error ?? "refused"}) — nothing changed. Try again, or remove it from another device: pantheon sessions`);
2138
+ revoked = true;
2139
+ }
2140
+ else if (list.ok)
2141
+ revoked = true;
1925
2142
  const cleared = { ...cfg };
1926
2143
  delete cleared.account;
1927
2144
  save(cleared);
1928
- console.error(`${C("logged out")} — this device is signed out. Sign back in with ${C("pantheon login --email " + cfg.account.email)}.`);
2145
+ console.error(`${C("logged out")} — ${revoked ? "this device is signed out" : "signed out here"}. Sign back in with ${C("pantheon login --email " + cfg.account.email)}.`);
1929
2146
  return;
1930
2147
  }
1931
2148
  case "account": {
1932
- const cfg = need();
2149
+ const cfg = identity() ?? die("not set up on this machine — pantheon signup first");
1933
2150
  if (!cfg.account)
1934
2151
  die("not signed in");
1935
2152
  const sub = argv[1];
@@ -1937,7 +2154,7 @@ async function main() {
1937
2154
  die("usage: pantheon account delete");
1938
2155
  if (!argv.includes("--yes"))
1939
2156
  die(`This permanently deletes your account (${cfg.account.username} <${cfg.account.email}>), all your devices, and all your sessions. Re-run with --yes to confirm.`);
1940
- const r = await signedFetch(cfg, "POST", "/account/delete").then((x) => x.json()).catch((e) => die(e.message));
2157
+ const r = await signedFetch(cfg, "POST", "/account/delete").then((x) => x.json()).catch((e) => die(describeNetError(e, cfg.relay)));
1941
2158
  if (!r.ok)
1942
2159
  die(String(r.error ?? "delete failed"));
1943
2160
  const cleared = { ...cfg };
@@ -2127,7 +2344,7 @@ async function main() {
2127
2344
  if (overlap)
2128
2345
  die("an overlapping delegation already exists for one of these domains; remove it first");
2129
2346
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
2130
- await client.connect().catch((e) => die(e.message));
2347
+ await client.connect().catch((e) => die(connectFailure(e, cfg.relay)));
2131
2348
  if (!client.profiles.has(to)) {
2132
2349
  client.close();
2133
2350
  die(`${to} has no published profile; delegation would have nowhere verifiable to land`);
@@ -2169,7 +2386,7 @@ async function main() {
2169
2386
  case "directory": {
2170
2387
  const cfg = need();
2171
2388
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
2172
- await client.connect().catch((e) => die(e.message));
2389
+ await client.connect().catch((e) => die(connectFailure(e, cfg.relay)));
2173
2390
  if (!client.profiles.size)
2174
2391
  console.log(`${D("no organization profiles published yet")}`);
2175
2392
  for (const p of client.profiles.values()) {
@@ -2226,12 +2443,41 @@ async function main() {
2226
2443
  return;
2227
2444
  }
2228
2445
  case "explain": {
2229
- const cfg = need();
2230
2446
  const { readFileSync, rmSync } = await import("node:fs");
2447
+ const CONFIG_DIR = dirname(CONFIG_PATH);
2448
+ let hud = null;
2449
+ if (argv.includes("--queued")) {
2450
+ const f = join(CONFIG_DIR, "bin", ".hud-action.json");
2451
+ try {
2452
+ hud = JSON.parse(readFileSync(f, "utf8"));
2453
+ rmSync(f, { force: true });
2454
+ }
2455
+ catch {
2456
+ hud = null;
2457
+ }
2458
+ }
2459
+ const cfg = (() => {
2460
+ if (!hud?.space)
2461
+ return need();
2462
+ const f = loadAll();
2463
+ const m = f ? findMembership(f, hud.space) : undefined;
2464
+ if (f && m)
2465
+ return configFor(f, m, null);
2466
+ const here = resolveFor(process.cwd()).cfg;
2467
+ if (here?.space === hud.space)
2468
+ return here;
2469
+ const why = `this machine is no longer in the team "${hud.space}"`;
2470
+ if (hud.action === "reply" || hud.action === "retract") {
2471
+ console.log(`not sent: ${why}`);
2472
+ process.exit(1);
2473
+ }
2474
+ return die(why);
2475
+ })();
2476
+ if (hud && hud.goal === "")
2477
+ delete hud.goal;
2231
2478
  const { pickExplainAgent, decisionPrompt: dp, allDecisionsPrompt: adp, decisionTitle: dt, sessionName } = await import("./decision-ui.js");
2232
2479
  const { loadProfile } = await import("./profile.js");
2233
2480
  const { detectAvailable, launchArgs } = await import("./agents.js");
2234
- const CONFIG_DIR = dirname(CONFIG_PATH);
2235
2481
  let id = argv.find((a, i) => i > 0 && !a.startsWith("--"));
2236
2482
  if (argv.includes("--queued")) {
2237
2483
  const queued = join(CONFIG_DIR, "bin", ".explain-id");
@@ -2243,17 +2489,6 @@ async function main() {
2243
2489
  if (id === "all")
2244
2490
  id = undefined;
2245
2491
  }
2246
- let hud = null;
2247
- if (argv.includes("--queued")) {
2248
- const f = join(CONFIG_DIR, "bin", ".hud-action.json");
2249
- try {
2250
- hud = JSON.parse(readFileSync(f, "utf8"));
2251
- rmSync(f, { force: true });
2252
- }
2253
- catch {
2254
- hud = null;
2255
- }
2256
- }
2257
2492
  if (hud?.action === "fix") {
2258
2493
  const dir = hud.dir && existsSync(hud.dir) ? hud.dir : process.cwd();
2259
2494
  const steps = hud.lines ?? [];
@@ -2301,22 +2536,31 @@ async function main() {
2301
2536
  if (!body)
2302
2537
  notSent("nothing to send");
2303
2538
  const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
2304
- await c2.connect().catch((e) => notSent(e.message));
2539
+ await c2.connect().catch((e) => notSent(connectFailure(e, cfg.relay)));
2305
2540
  const o = hud.decision ? c2.obligations.find((x) => x.id === hud.decision) : undefined;
2306
- const acts = await fetchHistory(cfg).catch(() => []);
2307
- const target = acts.find((x) => x.id === (o?.ref ?? hud.act));
2308
- const goal = o?.goal ?? target?.goal ?? hud.goal ?? null;
2541
+ const targetId = o?.ref ?? hud.act;
2542
+ const target = targetId ? (await fetchAct(cfg, targetId, 8_000))?.act : undefined;
2543
+ let goal = o?.goal ?? target?.goal ?? hud.goal ?? null;
2309
2544
  if (kind === "ANSWER" && !o && !target) {
2310
2545
  c2.close();
2311
2546
  notSent("that decision is no longer waiting — nothing to answer");
2312
2547
  }
2548
+ if (goal && c2.goals.get(goal)?.state === "CLOSED") {
2549
+ if (kind === "ASK") {
2550
+ c2.close();
2551
+ notSent("that goal is closed — open a new one to ask more");
2552
+ }
2553
+ goal = null;
2554
+ }
2313
2555
  const ref = kind === "ANSWER" ? (o?.ref ?? target?.id ?? null) : kind === "ASK" ? (target?.id ?? null) : null;
2314
2556
  const to = kind === "ASK" ? (target && target.from !== cfg.member ? [target.from] : null) : null;
2315
2557
  if (kind === "ASK" && !to) {
2316
2558
  c2.close();
2317
2559
  notSent("cannot tell who raised this — use Show the thread and reply from there");
2318
2560
  }
2319
- const sent = await c2.send({ act: kind, body, goal, ref, to, after_seq: c2.readSeq, meta: { surface: "menubar" } })
2561
+ const summary = body.length > LIMITS.summaryRequiredOver
2562
+ ? (body.split(/\n|(?<=[.!?])\s/)[0] ?? body).trim().slice(0, LIMITS.summaryChars) : undefined;
2563
+ const sent = await c2.send({ act: kind, body, goal, ref, to, after_seq: c2.readSeq, meta: { surface: "menubar" }, ...(summary ? { summary } : {}) })
2320
2564
  .catch((e) => notSent(e.message));
2321
2565
  c2.close();
2322
2566
  console.log(`sent ${kind} id=${sent.id} seq=${sent.seq}`);
@@ -2327,7 +2571,7 @@ async function main() {
2327
2571
  if (!hud.act)
2328
2572
  notSent("nothing to retract");
2329
2573
  const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
2330
- await c2.connect().catch((e) => notSent(e.message));
2574
+ await c2.connect().catch((e) => notSent(connectFailure(e, cfg.relay)));
2331
2575
  const sent = await c2.send({ act: "RETRACT", body: "Retracted from the menu bar.", goal: null, ref: hud.act, to: null, meta: { surface: "menubar" } })
2332
2576
  .catch((e) => notSent(e.message));
2333
2577
  c2.close();
@@ -2336,14 +2580,15 @@ async function main() {
2336
2580
  }
2337
2581
  if (hud?.action === "done") {
2338
2582
  const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
2339
- await c2.connect().catch((e) => die(e.message));
2583
+ await c2.connect().catch((e) => die(connectFailure(e, cfg.relay)));
2340
2584
  const o = hud.decision ? c2.obligations.find((x) => x.id === hud.decision) : undefined;
2341
2585
  if (!o) {
2342
2586
  c2.close();
2343
2587
  console.error(D("that decision is no longer waiting on you"));
2344
2588
  return;
2345
2589
  }
2346
- await c2.send({ act: "ANSWER", ref: o.ref, goal: o.goal, after_seq: c2.readSeq,
2590
+ const open = o.goal && c2.goals.get(o.goal)?.state !== "CLOSED" ? o.goal : null;
2591
+ await c2.send({ act: "ANSWER", ref: o.ref, goal: open, after_seq: c2.readSeq,
2347
2592
  body: "Marked done from the alert — handled outside this thread." }).catch((e) => die(e.message));
2348
2593
  c2.close();
2349
2594
  console.error(D(`done — ${o.id} is no longer waiting on you`));
@@ -2359,19 +2604,18 @@ async function main() {
2359
2604
  }
2360
2605
  if (wd)
2361
2606
  console.error(D(` ${wd.note}`));
2362
- const acts = await fetchHistory(cfg).catch(() => []);
2363
- const a = hud.act ? acts.find((x) => x.id === hud.act) : undefined;
2607
+ const a = hud.act ? (await fetchAct(cfg, hud.act))?.act : undefined;
2364
2608
  let o;
2365
2609
  if (hud.decision) {
2366
2610
  const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
2367
- await c2.connect().catch((e) => die(e.message));
2611
+ await c2.connect().catch((e) => die(connectFailure(e, cfg.relay)));
2368
2612
  o = c2.obligations.find((x) => x.id === hud.decision);
2369
2613
  c2.close();
2370
2614
  }
2371
2615
  if (a) {
2372
2616
  console.log(formatAct(a));
2373
2617
  if (a.ref) {
2374
- const r = acts.find((x) => x.id === a.ref);
2618
+ const r = (await fetchAct(cfg, a.ref))?.act;
2375
2619
  if (r)
2376
2620
  console.log(D("\nin reply to:\n") + formatAct(r, { short: true }));
2377
2621
  }
@@ -2399,11 +2643,7 @@ async function main() {
2399
2643
  rl.close();
2400
2644
  const prompt = o ? dp(o.id, dt(o.body)) : `Read Pantheon act ${a.id}${goalId ? ` and the thread of goal ${goalId}` : ""}. Explain in plain words what happened and whether anything is owed by me, then give me numbered next steps. Do not act until I choose.`;
2401
2645
  console.error(D(`opening ${agent.id}…`));
2402
- const { spawnSync } = await import("node:child_process");
2403
- {
2404
- const sp = agentSpawn(agent.binary, launchArgs(agent.id, prompt, sessionName(prompt)));
2405
- spawnSync(sp.command, sp.args, { stdio: "inherit", ...sp.options });
2406
- }
2646
+ await launchAgent(agent.binary, (p) => launchArgs(agent.id, p, sessionName(prompt)), prompt);
2407
2647
  return;
2408
2648
  }
2409
2649
  if (choice === "2") {
@@ -2444,7 +2684,8 @@ async function main() {
2444
2684
  continue;
2445
2685
  }
2446
2686
  if (choice === "3" && goalId) {
2447
- for (const t of selectActs(acts, { goal: goalId, last: 40 }))
2687
+ const thread = await fetchHistory(cfg, { goal: goalId }).catch(() => []);
2688
+ for (const t of selectActs(thread, { goal: goalId, last: 40 }))
2448
2689
  console.log(formatAct(t, { short: true }));
2449
2690
  continue;
2450
2691
  }
@@ -2471,10 +2712,7 @@ async function main() {
2471
2712
  const bin = cliOnPath("grok") ?? die("grok is not on PATH here");
2472
2713
  console.error(D("opening grok…"));
2473
2714
  const { spawnSync } = await import("node:child_process");
2474
- {
2475
- const sp = agentSpawn(bin, [hud.prompt]);
2476
- spawnSync(sp.command, sp.args, { stdio: "inherit", ...(cwd ? { cwd } : {}), ...sp.options });
2477
- }
2715
+ await launchAgent(bin, (p) => [p], hud.prompt, cwd ?? undefined);
2478
2716
  return;
2479
2717
  }
2480
2718
  if (hud.prompt) {
@@ -2483,10 +2721,7 @@ async function main() {
2483
2721
  die("no agent CLI found on this machine (claude, cursor-agent, codex, copilot, or kiro-cli)");
2484
2722
  console.error(D(`opening ${agent.id}…`));
2485
2723
  const { spawnSync } = await import("node:child_process");
2486
- {
2487
- const sp = agentSpawn(agent.binary, launchArgs(agent.id, hud.prompt, sessionName(hud.prompt)));
2488
- spawnSync(sp.command, sp.args, { stdio: "inherit", ...(cwd ? { cwd } : {}), ...sp.options });
2489
- }
2724
+ await launchAgent(agent.binary, (p) => launchArgs(agent.id, p, sessionName(hud.prompt)), hud.prompt, cwd ?? undefined);
2490
2725
  return;
2491
2726
  }
2492
2727
  if (hud.decision)
@@ -2499,7 +2734,7 @@ async function main() {
2499
2734
  }
2500
2735
  }
2501
2736
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
2502
- await client.connect().catch((e) => die(e.message));
2737
+ await client.connect().catch((e) => die(connectFailure(e, cfg.relay)));
2503
2738
  const waiting = client.obligations.filter((o) => o.kind === "HUMAN" && o.owner === `human:${cfg.member}`);
2504
2739
  client.close();
2505
2740
  const one = id ? waiting.find((o) => o.id === id) : waiting.length === 1 ? waiting[0] : undefined;
@@ -2515,10 +2750,7 @@ async function main() {
2515
2750
  die("no agent CLI found on this machine (claude, cursor-agent, codex, copilot, or kiro-cli)");
2516
2751
  console.error(D(`opening ${agent.id} with the question…`));
2517
2752
  const { spawnSync } = await import("node:child_process");
2518
- {
2519
- const sp = agentSpawn(agent.binary, launchArgs(agent.id, prompt, sessionName(prompt)));
2520
- spawnSync(sp.command, sp.args, { stdio: "inherit", ...sp.options });
2521
- }
2753
+ await launchAgent(agent.binary, (p) => launchArgs(agent.id, p, sessionName(prompt)), prompt);
2522
2754
  return;
2523
2755
  }
2524
2756
  case "approvals": {
@@ -2527,7 +2759,7 @@ async function main() {
2527
2759
  const conns = [];
2528
2760
  for (const c of targets()) {
2529
2761
  const cl = new PantheonClient(c, undefined, { role: "cli" });
2530
- await cl.connect().catch((e) => die(`${c.space}: ${e.message}`));
2762
+ await cl.connect().catch((e) => die(`${c.space}: ${connectFailure(e, c.relay)}`));
2531
2763
  conns.push({ cfg: c, client: cl });
2532
2764
  }
2533
2765
  const closeAll = () => { for (const c of conns)
@@ -2572,7 +2804,7 @@ async function main() {
2572
2804
  const cfg = need();
2573
2805
  const sub = argv[1] ?? "list";
2574
2806
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
2575
- await client.connect().catch((e) => die(e.message));
2807
+ await client.connect().catch((e) => die(connectFailure(e, cfg.relay)));
2576
2808
  const waiting = client.obligations.filter((o) => o.kind === "HUMAN" && o.owner === `human:${cfg.member}`);
2577
2809
  const id = argv[2];
2578
2810
  if (sub === "answer") {
@@ -2885,7 +3117,7 @@ async function main() {
2885
3117
  const sub = argv[1] ?? "list";
2886
3118
  const { repo: _r, repoOrigin: _o, ...plain } = cfg;
2887
3119
  const client = new PantheonClient(plain, undefined, { role: "cli" });
2888
- await client.connect().catch((e) => die(client.pending ?? e.message));
3120
+ await client.connect().catch((e) => die(client.pending ?? connectFailure(e, plain.relay)));
2889
3121
  if (sub === "add") {
2890
3122
  const r = repoInfo(flag("path") ?? process.cwd()) ?? die("not inside a git repo");
2891
3123
  const base = flag("base") ?? null;
@@ -2929,5 +3161,10 @@ async function main() {
2929
3161
  process.exit(cmd === "help" || cmd === "--help" ? 0 : 1);
2930
3162
  }
2931
3163
  }
2932
- main().catch((e) => die(e.message));
3164
+ main().catch((e) => die(describeNetError(e, (() => { try {
3165
+ return load()?.relay;
3166
+ }
3167
+ catch {
3168
+ return undefined;
3169
+ } })())));
2933
3170
  //# sourceMappingURL=cli.js.map