@join-pantheon/cli 0.28.0 → 0.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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";
@@ -30,17 +30,18 @@ import { assess } from "./untrusted.js";
30
30
  import { report as spendReport, recentRuns } from "./spend.js";
31
31
  import { renderBrief, renderTeam } from "./brief.js";
32
32
  import { resolveDataDir, saveAll } from "./config.js";
33
- import { currentPlan, installNetwork, redactProxy, normalizeProxy, usedSystemFallback } from "./network.js";
33
+ import { currentPlan, installNetwork, redactProxy, redactText, normalizeProxy, proxyToRemember, describeNetError } from "./network.js";
34
34
  import { createInterface } from "node:readline";
35
35
  import { homedir } from "node:os";
36
36
  import { removeHook, decideStop, writeIntent, clearIntent, readIntent, installHooks, agentHookStates } from "./notify.js";
37
37
  import { pending, render as renderPending } from "./pending.js";
38
- import { start as qsStart, join as qsJoin, wireUp } from "./quickstart.js";
38
+ import { start as qsStart, join as qsJoin, wireUp, teamNameFor } from "./quickstart.js";
39
39
  import { installService, uninstallService } from "./service.js";
40
40
  import { LIMITS } from "./protocol.js";
41
41
  import { loadPersonas, personaByName, describePersona, initPersona, personaDir } from "./persona.js";
42
42
  import { fingerprint, newKeypair, signAccount } from "./identity.js";
43
43
  import { VERSION } from "./version.js";
44
+ import { connectFailure } from "./refusal.js";
44
45
  import * as mcpWindows from "./mcp-windows.js";
45
46
  import { updateBanner } from "./update-check.js";
46
47
  const argv = process.argv.slice(2);
@@ -62,7 +63,7 @@ async function signedFetch(cfg, method, path, params = {}) {
62
63
  return res;
63
64
  }
64
65
  function requireSignInOnHostedRelay(relayFlag) {
65
- const cfg = load();
66
+ const cfg = identity();
66
67
  if (cfg?.account)
67
68
  return;
68
69
  const relay = relayFlag ?? cfg?.relay ?? process.env.PANTHEON_RELAY ?? DEFAULT_RELAY;
@@ -72,7 +73,7 @@ function requireSignInOnHostedRelay(relayFlag) {
72
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.`);
73
74
  }
74
75
  async function interactiveOnboard(relayArg) {
75
- const prior = load();
76
+ const prior = identity();
76
77
  const relay = relayArg ?? prior?.relay ?? process.env.PANTHEON_RELAY ?? DEFAULT_RELAY;
77
78
  const base = relay.replace(/^ws/, "http").replace(/\/+$/, "");
78
79
  if (!process.stdin.isTTY) {
@@ -152,7 +153,7 @@ async function interactiveOnboard(relayArg) {
152
153
  console.error(D(` could not resend: ${rs?.j.error ?? "the relay did not send an email"} — try again in a minute.`));
153
154
  continue;
154
155
  }
155
- const v = await postJson("/auth/verify", { email, code });
156
+ const v = await postJson("/auth/verify", { email, code, pubkey: kp.publicKey });
156
157
  if (v.status === 200) {
157
158
  const acct = v.j.account;
158
159
  const cfg = { ...(prior ?? {}), relay, member: acct.username, space: prior?.space ?? "", token: prior?.token ?? "",
@@ -252,10 +253,44 @@ function targets() {
252
253
  const r = resolveFor(process.cwd());
253
254
  if (r.cfg && r.cfg.space && r.reason !== "default")
254
255
  return [r.cfg];
255
- if (r.reason === "default" && f)
256
+ if ((r.reason === "default" || r.reason === "unbound") && f && Object.keys(f.spaces ?? {}).length)
256
257
  return Object.values(f.spaces ?? {}).map((m) => configFor(f, m, null));
257
258
  return [need()];
258
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
+ }
259
294
  function needStrict() {
260
295
  const r = resolveFor(process.cwd(), { strict: true });
261
296
  return r.cfg && r.cfg.space ? r.cfg : null;
@@ -286,7 +321,7 @@ async function guidedSetup(cfg, project, opts = {}) {
286
321
  const io = rl ? terminalIO(rl, (l) => console.error(l)) : null;
287
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)")}`);
288
323
  try {
289
- const r = await runGuide(io, { cfg, project }, {
324
+ const r = await runGuide(io, { cfg, project, optOut: optOutsFrom(argv) }, {
290
325
  publish: async (p) => {
291
326
  const c = new PantheonClient(cfg, undefined, { role: "cli" });
292
327
  try {
@@ -336,23 +371,27 @@ async function workdirFor(cfg, hud) {
336
371
  const bindings = all && m ? Object.values(all.bindings ?? {}).filter((b) => b.space === m.id) : [];
337
372
  let hints = [];
338
373
  if (bindings.length > 1 && hud.goal) {
339
- const acts = await fetchHistory(cfg).catch(() => []);
374
+ const acts = await fetchHistory(cfg, { goal: hud.goal }).catch(() => []);
340
375
  hints = acts.filter((x) => x.goal === hud.goal).flatMap((x) => [x.body, JSON.stringify(x.meta ?? {})]);
341
376
  }
342
377
  return pickWorkdir(bindings, hints, team);
343
378
  }
344
- async function fetchHistory(cfg, since = 0) {
379
+ async function fetchHistory(cfg, opts = {}) {
345
380
  const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
346
- 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) });
347
384
  if (!res.ok)
348
385
  throw new Error(`relay answered ${res.status} for the ledger`);
349
386
  return (await res.json()).acts ?? [];
350
387
  }
351
- async function fetchAct(cfg, id) {
388
+ async function fetchAct(cfg, id, budgetMs = Number.POSITIVE_INFINITY) {
352
389
  const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
353
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()));
354
393
  try {
355
- 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()) });
356
395
  if (res.ok) {
357
396
  const body = await res.json();
358
397
  if (body.act)
@@ -364,7 +403,9 @@ async function fetchAct(cfg, id) {
364
403
  catch { }
365
404
  let head = 0;
366
405
  try {
367
- 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();
368
409
  head = h.spaces?.[0]?.seq ?? 0;
369
410
  }
370
411
  catch { }
@@ -372,8 +413,10 @@ async function fetchAct(cfg, id) {
372
413
  if (windows[windows.length - 1] !== 0)
373
414
  windows.push(0);
374
415
  for (const since of windows) {
416
+ if (Date.now() >= deadline)
417
+ return null;
375
418
  try {
376
- const acts = await fetchHistory(cfg, since);
419
+ const acts = await fetchHistory(cfg, { since });
377
420
  const act = acts.find((a) => a.id === id || String(a.seq) === id);
378
421
  if (act)
379
422
  return { act };
@@ -551,18 +594,31 @@ ${B("less common")}
551
594
  pantheon version the pantheon version this machine is running
552
595
  `;
553
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"]);
554
611
  function rememberWorkingProxy() {
555
612
  try {
556
- const plan = currentPlan();
557
- const via = plan.https ?? plan.http;
558
- if (!via || (plan.source !== "env" && !(plan.fallbackOnly && usedSystemFallback())))
559
- return;
560
613
  const all = loadAll();
561
- if (!all || all.network?.source === "pantheon network" || all.network?.proxy === via)
614
+ if (!all)
562
615
  return;
563
- const custom = plan.noProxy.filter((h) => !["localhost", "127.0.0.1", "::1", "[::1]"].includes(h));
564
- saveAll({ ...all, network: { proxy: via, ...(custom.length ? { no_proxy: custom.join(",") } : {}), source: plan.source, saved_at: Date.now() } });
565
- console.error(D(` saved the proxy ${redactProxy(via)} for this machine's other Pantheon processes (worker, editors) — change it with pantheon network`));
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`));
566
622
  }
567
623
  catch { }
568
624
  }
@@ -578,19 +634,27 @@ async function main() {
578
634
  catch { }
579
635
  }
580
636
  if (argv.length === 0) {
581
- const cfg = load();
637
+ const cfg = identity();
582
638
  if (cfg?.account) {
583
639
  console.error(`${B("pantheon")} — signed in as ${C(cfg.account.username)}. ${D(`Your plate: pantheon brief · all commands: pantheon help`)}`);
584
640
  return;
585
641
  }
586
642
  return interactiveOnboard();
587
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
+ }
588
652
  switch (cmd) {
589
653
  case "network": {
590
654
  const all = loadAll() ?? die("not configured — sign up or join a team first");
591
655
  const set = flag("proxy");
592
656
  if (set !== undefined) {
593
- const p = normalizeProxy(set) ?? die(`"${set}" is not a proxy address — use http://host:port (add user:password@ if it needs a login)`);
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)`);
594
658
  saveAll({ ...all, network: { proxy: p, ...(flag("no-proxy") ? { no_proxy: flag("no-proxy") } : {}), source: "pantheon network", saved_at: Date.now() } });
595
659
  console.error(`${B("saved")} every Pantheon process on this machine now reaches the relay through ${redactProxy(p)}`);
596
660
  return;
@@ -608,7 +672,7 @@ async function main() {
608
672
  }
609
673
  const plan = currentPlan();
610
674
  const via = plan.https ?? plan.http;
611
- 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" : ""}`) : ""}`);
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") : ""}`);
612
676
  if (via)
613
677
  console.log(D(` not for: ${plan.noProxy.join(", ")}`));
614
678
  if (plan.pac)
@@ -624,7 +688,7 @@ async function main() {
624
688
  requireSignInOnHostedRelay(flag("relay"));
625
689
  console.error(`${B("pantheon start")}\n`);
626
690
  if (!flag("space") && !repoInfo(process.cwd()))
627
- 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`));
628
692
  const r = await qsStart({
629
693
  port,
630
694
  dataDir: resolveDataDir(flag("data")).dir,
@@ -633,6 +697,7 @@ async function main() {
633
697
  ...(flag("space") ? { space: flag("space") } : {}),
634
698
  ...(flag("me") ? { me: flag("me") } : {}),
635
699
  ...(flag("relay") ? { relay: flag("relay") } : {}),
700
+ ...(setupFlags.preset.wake ? { wake: setupFlags.preset.wake } : {}),
636
701
  hook: !argv.includes("--no-hook"),
637
702
  notifications: !argv.includes("--no-notifications"),
638
703
  menubar: !argv.includes("--no-menubar"),
@@ -685,7 +750,7 @@ async function main() {
685
750
  case "invite": {
686
751
  const cfg = need();
687
752
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
688
- await client.connect().catch((e) => die(client.pending ?? e.message));
753
+ await client.connect().catch((e) => die(client.pending ?? connectFailure(e, cfg.relay)));
689
754
  await new Promise((r) => setTimeout(r, 250));
690
755
  const revokeAt = argv.indexOf("--revoke");
691
756
  const revokeArg = revokeAt >= 0 ? (argv[revokeAt + 1] && !argv[revokeAt + 1].startsWith("--") ? argv[revokeAt + 1] : "all") : undefined;
@@ -728,6 +793,8 @@ async function main() {
728
793
  return;
729
794
  }
730
795
  case "relay": {
796
+ process.on("unhandledRejection", (e) => { console.error(`! unhandled rejection: ${e?.stack ?? String(e)}`); });
797
+ process.on("uncaughtException", (e) => { console.error(`! uncaught exception (kept serving): ${e?.stack ?? String(e)}`); });
731
798
  const relay = new Relay({
732
799
  port: Number(flag("port", process.env.PORT ?? "8787")),
733
800
  host: flag("host", "0.0.0.0"),
@@ -757,16 +824,18 @@ async function main() {
757
824
  return;
758
825
  }
759
826
  case "init": {
760
- const kp = newKeypair();
827
+ const had = argv.includes("--reset") ? null : identity();
828
+ const kp = had?.publicKey && had.privateKey ? { publicKey: had.publicKey, privateKey: had.privateKey } : newKeypair();
761
829
  const cfg = {
762
830
  relay: flag("relay", "ws://localhost:8787"),
763
831
  space: flag("space", "team"),
764
832
  token: flag("token", randomBytes(16).toString("hex")),
765
- member: flag("me", process.env.USER ?? "me"),
833
+ member: flag("me") ?? had?.member ?? process.env.USER ?? "me",
766
834
  publicKey: kp.publicKey,
767
835
  privateKey: kp.privateKey,
768
836
  display: flag("display"),
769
837
  agent: flag("agent", "unknown"),
838
+ ...(had?.account ? { account: had.account } : {}),
770
839
  };
771
840
  save(cfg);
772
841
  console.error(`${B("space claimed.")} config: ${D(CONFIG_PATH)}\n`);
@@ -813,7 +882,7 @@ async function main() {
813
882
  parsed = await resolveInviteCode(invite, candidates).catch((err) => die(err.message.replace(/^INVITE_CODE_[A-Z]+:\s*/, "")));
814
883
  console.error(D(`That was just the code — it opens "${parsed.space}" on ${new URL(parsed.relay).host}. Joining that team.\n`));
815
884
  }
816
- if (!load()?.account && sameHost(parsed.relay, DEFAULT_RELAY)) {
885
+ if (!identity()?.account && sameHost(parsed.relay, DEFAULT_RELAY)) {
817
886
  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.`);
818
887
  }
819
888
  const wp = unattendedProject();
@@ -888,6 +957,22 @@ async function main() {
888
957
  }
889
958
  case "rewire": {
890
959
  const rows = installForUser().map((t) => ({ tool: t.tool, status: t.status }));
960
+ try {
961
+ const { refreshServiceDefinition, serviceInstalled: installed, restartService: restart } = await import("./service.js");
962
+ for (const kind of ["worker", "notifier", "menubar"]) {
963
+ if (!installed(kind))
964
+ continue;
965
+ const fresh = refreshServiceDefinition(kind);
966
+ if (fresh.line)
967
+ console.error(fresh.line);
968
+ if (fresh.changed && kind !== "worker")
969
+ for (const l of restart(kind, undefined, { reload: true }))
970
+ console.error(l);
971
+ else if (fresh.changed)
972
+ console.error(D(" worker: the new definition applies at its next restart"));
973
+ }
974
+ }
975
+ catch { }
891
976
  if (argv.includes("--json")) {
892
977
  console.log(JSON.stringify(rows));
893
978
  return;
@@ -958,6 +1043,7 @@ async function main() {
958
1043
  }
959
1044
  }
960
1045
  }
1046
+ const { refreshServiceDefinition } = await import("./service.js");
961
1047
  for (const kind of ["worker", "notifier", "menubar"]) {
962
1048
  if (!serviceInstalled(kind))
963
1049
  continue;
@@ -965,7 +1051,10 @@ async function main() {
965
1051
  console.error(` ${D("worker: a run is in progress — left alone. When it is done:")} pantheon service restart worker`);
966
1052
  continue;
967
1053
  }
968
- for (const l of restartService(kind))
1054
+ const fresh = refreshServiceDefinition(kind);
1055
+ if (fresh.line)
1056
+ console.error(fresh.line);
1057
+ for (const l of restartService(kind, undefined, { reload: fresh.changed }))
969
1058
  console.error(l);
970
1059
  }
971
1060
  if (r.restart.includes("relay"))
@@ -1005,7 +1094,7 @@ async function main() {
1005
1094
  console.error(client.pending);
1006
1095
  process.exit(2);
1007
1096
  }
1008
- die(`cannot reach relay at ${cfg.relay} — ${e.message}`);
1097
+ die(connectFailure(e, cfg.relay));
1009
1098
  });
1010
1099
  await new Promise((r) => setTimeout(r, 350));
1011
1100
  const peers = client.roster.filter((m) => m.id !== cfg.member);
@@ -1073,7 +1162,7 @@ async function main() {
1073
1162
  for (const cfg of targets()) {
1074
1163
  const profile = loadProfile(cfg);
1075
1164
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
1076
- await client.connect().catch((e) => die(client.pending ?? e.message));
1165
+ await client.connect().catch((e) => die(client.pending ?? connectFailure(e, cfg.relay)));
1077
1166
  await new Promise((r) => setTimeout(r, 350));
1078
1167
  process.stdout.write(renderBrief({
1079
1168
  space: cfg.space,
@@ -1103,7 +1192,7 @@ async function main() {
1103
1192
  }
1104
1193
  const cfg = need();
1105
1194
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
1106
- await client.connect().catch((e) => die(client.pending ?? e.message));
1195
+ await client.connect().catch((e) => die(client.pending ?? connectFailure(e, cfg.relay)));
1107
1196
  await new Promise((r) => setTimeout(r, 350));
1108
1197
  process.stdout.write(renderTeam({
1109
1198
  space: cfg.space,
@@ -1131,7 +1220,7 @@ async function main() {
1131
1220
  };
1132
1221
  const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
1133
1222
  const q = `space=${encodeURIComponent(cfg.space)}&token=${encodeURIComponent(cfg.token)}`;
1134
- const first = await fetch(`${base}/history?${q}`).catch((e) => die(`cannot reach relay — ${e.message}`));
1223
+ const first = await fetch(`${base}/history?${q}`).catch((e) => die(describeNetError(e, cfg.relay)));
1135
1224
  if (!first.ok)
1136
1225
  die(`relay answered ${first.status} — check the space name and token`);
1137
1226
  let since = ((await first.json()).acts ?? []).at(-1)?.seq ?? 0;
@@ -1160,7 +1249,8 @@ async function main() {
1160
1249
  }
1161
1250
  case "history": {
1162
1251
  const cfg = need();
1163
- const acts = await fetchHistory(cfg, numFlag("since", 0)).catch((e) => die(`cannot read the ledger — ${e.message}`));
1252
+ const acts = await fetchHistory(cfg, flag("goal") ? { goal: flag("goal") } : flag("since") ? { since: numFlag("since", 0) } : {})
1253
+ .catch((e) => die(`cannot read the ledger — ${e.message}`));
1164
1254
  const picked = selectActs(acts, {
1165
1255
  ...(flag("goal") ? { goal: flag("goal") } : {}), ...(flag("from") ? { from: flag("from") } : {}),
1166
1256
  last: numFlag("last", 20), presence: argv.includes("--presence"),
@@ -1192,11 +1282,11 @@ async function main() {
1192
1282
  case "show": {
1193
1283
  const cfg = need();
1194
1284
  const key = argv[1] ?? die("usage: pantheon show <act id | sequence number>");
1195
- const acts = await fetchHistory(cfg).catch((e) => die(`cannot read the ledger — ${e.message}`));
1196
- 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)`);
1197
- console.log(formatAct(a));
1285
+ const found = await fetchAct(cfg, key);
1286
+ const a = found?.act ?? die(`no act ${key} in what the relay still holds (pantheon history lists them)`);
1287
+ console.log(formatAct(a, found?.goalTitle ? { goalTitle: found.goalTitle } : {}));
1198
1288
  if (a.ref) {
1199
- const r = acts.find((x) => x.id === a.ref);
1289
+ const r = (await fetchAct(cfg, a.ref))?.act;
1200
1290
  if (r)
1201
1291
  console.log(D(`\nin reply to:\n`) + formatAct(r, { short: true }));
1202
1292
  }
@@ -1211,7 +1301,7 @@ async function main() {
1211
1301
  die("nothing to say — pantheon say NOTE 'your message here'");
1212
1302
  const summary = flag("summary");
1213
1303
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
1214
- await client.connect().catch((e) => die(`cannot reach relay — ${e.message}`));
1304
+ await client.connect().catch((e) => die(connectFailure(e, cfg.relay)));
1215
1305
  const to = flag("to");
1216
1306
  const meta = {};
1217
1307
  for (const k of ["branch", "commit", "remote"]) {
@@ -1225,7 +1315,15 @@ async function main() {
1225
1315
  let goalArg = flag("goal") ?? null;
1226
1316
  let ref = flag("ref") ?? null;
1227
1317
  if (ref !== null || ["ANSWER", "ACCEPT", "OBJECT"].includes(act)) {
1228
- const acts = await fetchHistory(cfg).catch(() => []);
1318
+ let acts;
1319
+ if (ref && ref !== "last") {
1320
+ const hit = (await fetchAct(cfg, ref))?.act;
1321
+ acts = hit ? [hit] : [];
1322
+ if (!hit && /^a_/.test(ref))
1323
+ console.error(D(` ${ref} is not in what this relay holds — sending it as given`));
1324
+ }
1325
+ else
1326
+ acts = await fetchHistory(cfg, goalArg ? { goal: goalArg } : {}).catch(() => []);
1229
1327
  const rr = resolveRef(acts, ref, act, goalArg, cfg.member);
1230
1328
  if (rr.error) {
1231
1329
  client.close();
@@ -1351,23 +1449,17 @@ async function main() {
1351
1449
  return;
1352
1450
  }
1353
1451
  case "stop": {
1354
- const what = (argv[1] ?? "all").toLowerCase();
1452
+ const what = (argv[1] && !argv[1].startsWith("--") ? argv[1] : "all").toLowerCase();
1453
+ if (!["all", "relay", "worker", "notifier", "menubar"].includes(what))
1454
+ die(`usage: pantheon stop [relay|worker|notifier|menubar|all]`);
1455
+ const { stopMachine } = await import("./machine.js");
1355
1456
  console.log(B("stopping"));
1356
- if (what === "relay" || what === "all")
1357
- for (const l of stopProc("relay"))
1358
- console.log(` ${l}`);
1359
- if (what === "worker" || what === "all")
1360
- for (const l of stopProc("worker"))
1361
- console.log(` ${l}`);
1362
- if (what === "notifier" || what === "all")
1363
- for (const l of stopProc("notifier"))
1364
- console.log(` ${l}`);
1365
- if (what === "menubar" || what === "all")
1366
- for (const l of stopProc("menubar"))
1367
- console.log(` ${l}`);
1368
- if (what === "relay" || what === "all") {
1369
- console.log(D(`\n Your teammates are offline until a relay is running again.`));
1370
- }
1457
+ const everyTeam = Object.values(loadAll()?.spaces ?? {}).map((m) => ({ name: m.name, relay: m.relay }));
1458
+ const res = stopMachine(what, { port: Number(flag("port", "8787")), teams: everyTeam.length ? everyTeam : (load()?.relay ? [{ name: load().space, relay: load().relay }] : []) });
1459
+ for (const l of res.lines)
1460
+ console.log(` ${l}`);
1461
+ if (res.note)
1462
+ console.log(D(`\n ${res.note}`));
1371
1463
  return;
1372
1464
  }
1373
1465
  case "logs": {
@@ -1375,7 +1467,7 @@ async function main() {
1375
1467
  const proc = kind === "relay" ? relayProc(Number(flag("port", "8787"))) : kind === "worker" ? workerProc() : notifierProc();
1376
1468
  const f = logFileFor(kind, proc);
1377
1469
  if (!f)
1378
- 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.`);
1470
+ 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.`);
1379
1471
  console.error(D(`${f} (${statFile(f)})\n`));
1380
1472
  const follow = argv.includes("-f") || argv.includes("--follow");
1381
1473
  const { spawn } = await import("node:child_process");
@@ -1664,7 +1756,7 @@ async function main() {
1664
1756
  if (!argv.includes("--yes"))
1665
1757
  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`);
1666
1758
  const client = new PantheonClient(configFor(all, m, null), undefined, { role: "cli" });
1667
- await client.connect().catch((e) => die(client.pending ?? e.message));
1759
+ await client.connect().catch((e) => die(client.pending ?? connectFailure(e, m.relay)));
1668
1760
  await client.deleteSpace(m.name).catch((e) => { client.close(); die(e.message); });
1669
1761
  client.close();
1670
1762
  const bound = Object.values(all.bindings ?? {}).filter((b) => b.space === m.id).length;
@@ -1682,53 +1774,103 @@ async function main() {
1682
1774
  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.`));
1683
1775
  return;
1684
1776
  }
1685
- const cfg = load();
1777
+ const all = loadAll();
1778
+ const me = identity(all);
1779
+ const { leavePlan } = await import("./machine.js");
1780
+ const plan = leavePlan(all);
1686
1781
  const project = resolve(flag("project", "."));
1687
- console.error(`${B("leaving pantheon")}\n`);
1688
- for (const l of stopProc("worker"))
1689
- console.error(` ${l}`);
1690
- for (const l of stopProc("notifier"))
1691
- console.error(` ${l}`);
1692
- for (const l of stopProc("menubar"))
1782
+ const keepIdentity = argv.includes("--keep-identity");
1783
+ if (plan.teams.length > 1 && !keepIdentity && !argv.includes("--yes")) {
1784
+ const list = plan.teams.map((t) => `"${t.name}"`).join(", ");
1785
+ if (!process.stdin.isTTY)
1786
+ 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>`);
1787
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
1788
+ 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())));
1789
+ rl.close();
1790
+ if (answer.toLowerCase() !== "yes")
1791
+ die(`nothing was changed. Leave one team with: pantheon leave <team>`);
1792
+ }
1793
+ console.error(`${B("leaving pantheon")}${plan.teams.length ? D(` — ${plan.teams.map((t) => t.name).join(", ")}`) : ""}\n`);
1794
+ const { uninstallMachine, realUninstallIo } = await import("./machine.js");
1795
+ const { removeUserMcpEntries } = await import("./install.js");
1796
+ const done = uninstallMachine({ project, stopRelay: argv.includes("--stop-relay") }, realUninstallIo({
1797
+ port: Number(flag("port", "8787")),
1798
+ removeMcpEntries: () => removeUserMcpEntries({ project }),
1799
+ hasIntent: () => readIntent() !== null,
1800
+ clearIntent: () => clearIntent(),
1801
+ }));
1802
+ for (const l of done.lines)
1693
1803
  console.error(` ${l}`);
1694
- if (argv.includes("--stop-relay"))
1695
- for (const l of stopProc("relay"))
1696
- console.error(` ${l}`);
1697
- else if (relayProc(Number(flag("port", "8787"))).running) {
1698
- console.error(` relay: left running — others may be using it. Stop it with: pantheon stop relay`);
1699
- }
1804
+ const removed = [...done.removed];
1805
+ const left = [...done.left];
1700
1806
  for (const l of removeHook(project))
1701
1807
  console.error(` hook: ${l.trim()}`);
1702
1808
  const { revokeToolPermissions } = await import("./grants.js");
1703
- for (const l of revokeToolPermissions())
1809
+ for (const l of revokeToolPermissions()) {
1704
1810
  console.error(` perms:${l}`);
1705
- {
1706
- const l = removeCodexUserScope();
1707
- if (l)
1708
- console.error(` perms:${l}`);
1811
+ removed.push(l.trim().replace(/^removed /, ""));
1709
1812
  }
1710
- const { unlinkSync, existsSync: exists, rmSync } = await import("node:fs");
1711
- if (cfg && exists(CONFIG_PATH)) {
1712
- if (argv.includes("--keep-identity")) {
1713
- console.error(` config: kept at ${CONFIG_PATH} (--keep-identity)`);
1813
+ const { unlinkSync, existsSync: exists, rmSync, readdirSync } = await import("node:fs");
1814
+ if (all && exists(CONFIG_PATH)) {
1815
+ if (keepIdentity) {
1816
+ left.push(`your identity and ${plan.teams.length} team membership${plan.teams.length === 1 ? "" : "s"}, ${CONFIG_PATH} (--keep-identity)`);
1714
1817
  }
1715
1818
  else {
1716
1819
  unlinkSync(CONFIG_PATH);
1820
+ 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`);
1821
+ const runHomes = join(CONFIG_PATH, "..", "run-homes");
1822
+ const { restorePendingQuarantines, pendingQuarantines } = await import("./agents.js");
1823
+ const put = restorePendingQuarantines(runHomes);
1824
+ if (put.length)
1825
+ removed.push(`nothing of your repos' — what interrupted runs had held aside: ${put.join("; ")}`);
1826
+ const still = pendingQuarantines(runHomes);
1827
+ const keptAside = (dir) => { try {
1828
+ return readdirSync(join(dir, "quarantine-kept")).length > 0;
1829
+ }
1830
+ catch {
1831
+ return false;
1832
+ } };
1833
+ let homes = [];
1717
1834
  try {
1718
- rmSync(join(CONFIG_PATH, "..", "run-homes"), { recursive: true, force: true });
1835
+ homes = readdirSync(runHomes);
1719
1836
  }
1720
1837
  catch { }
1721
- console.error(` config: removed ${CONFIG_PATH} — your keypair is gone, so rejoining means a new invite`);
1722
- }
1723
- }
1724
- else {
1725
- console.error(` config: none to remove`);
1726
- }
1727
- 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),")}`);
1728
- console.error(` ${D("the relay's ledger on disk, and anything you pushed to git. Codex's entry and approval line were removed above.")}`);
1729
- if (cfg) {
1730
- console.error(`\n ${D(`Your teammates still list you in "${cfg.space}". Ask one of them to run:`)}`);
1731
- console.error(` ${D(`pantheon admit ${cfg.member} --revoke`)}`);
1838
+ const keep = new Set([...still.map((x) => x.runHome), ...homes.map((h) => join(runHomes, h)).filter(keptAside)]);
1839
+ for (const h of homes) {
1840
+ const dir = join(runHomes, h);
1841
+ if (keep.has(dir))
1842
+ continue;
1843
+ try {
1844
+ rmSync(dir, { recursive: true, force: true });
1845
+ }
1846
+ catch { }
1847
+ }
1848
+ if (!keep.size) {
1849
+ try {
1850
+ rmSync(runHomes, { recursive: true, force: true });
1851
+ }
1852
+ catch { }
1853
+ }
1854
+ for (const x of still)
1855
+ 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`);
1856
+ for (const dir of keep)
1857
+ if (keptAside(dir) && !still.some((x) => x.runHome === dir))
1858
+ 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`);
1859
+ }
1860
+ }
1861
+ console.error(`\n${B("Removed")}`);
1862
+ for (const r of removed)
1863
+ console.error(` ${C("✓")} ${r}`);
1864
+ if (!removed.length)
1865
+ console.error(` ${D("nothing — Pantheon had nothing installed here")}`);
1866
+ console.error(`\n${B("Left")}`);
1867
+ for (const l of left)
1868
+ console.error(` • ${l}`);
1869
+ 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")}`);
1870
+ console.error(` • ${D("the relay's ledger on disk, and anything you pushed to git")}`);
1871
+ if (plan.teams.length) {
1872
+ console.error(`\n ${D(`Your teammates still list you in ${plan.teams.map((t) => `"${t.name}"`).join(", ")}. Ask someone in each to run:`)}`);
1873
+ console.error(` ${D(`pantheon admit ${me?.member ?? plan.member ?? "<your name>"} --revoke`)}`);
1732
1874
  }
1733
1875
  console.error(`\n ${D(`If Pantheon let you down, one line on why to ${FEEDBACK_ADDRESS} would help more than you might think.`)}`);
1734
1876
  return;
@@ -1741,35 +1883,42 @@ async function main() {
1741
1883
  return;
1742
1884
  }
1743
1885
  case "panic": {
1744
- const cfg = need();
1745
1886
  console.error(`${B("PANIC — locking down this machine")}\n`);
1746
- for (const l of stopProc("worker"))
1747
- console.error(` ${l}`);
1748
- const client = new PantheonClient(cfg, undefined, { role: "cli" });
1749
- const up = await client.connect().then(() => true).catch(() => false);
1750
- if (!up) {
1751
- console.error(` ${D("cannot reach the relay, so wake grants were not changed")}`);
1752
- console.error(`\n ${C("The worker is stopped, so nothing can start a run here regardless.")}`);
1753
- return;
1754
- }
1755
- await new Promise((r) => setTimeout(r, 300));
1756
- const wakers = client.members.filter((m) => m.admitted && m.may_wake && m.member !== cfg.member);
1757
- for (const w of wakers) {
1758
- await client.admit(w.member, false, false).catch(() => { });
1759
- console.error(` revoked wake rights from ${C(w.member)}`);
1760
- }
1761
- if (!wakers.length)
1762
- console.error(` ${D("no peer had wake rights to revoke")}`);
1763
- client.close();
1764
- console.error(`\n ${C("Nothing can start an agent run on this machine now.")}`);
1887
+ const { panicMachine, waitStopped } = await import("./machine.js");
1888
+ const all = loadAll();
1889
+ const teams = Object.values(all?.spaces ?? {});
1890
+ const r = await panicMachine({
1891
+ stopWorker: () => stopProc("worker"),
1892
+ workerDown: () => waitStopped("worker", 5000),
1893
+ teams,
1894
+ revoke: async (m) => {
1895
+ const cfg = configFor(all, m, null);
1896
+ const client = new PantheonClient(cfg, undefined, { role: "cli" });
1897
+ try {
1898
+ await client.connect();
1899
+ await new Promise((res) => setTimeout(res, 300));
1900
+ const wakers = client.members.filter((x) => x.admitted && x.may_wake && x.member !== cfg.member);
1901
+ for (const w of wakers)
1902
+ await client.admit(w.member, false, false).catch(() => { });
1903
+ return wakers.map((w) => w.member);
1904
+ }
1905
+ finally {
1906
+ client.close();
1907
+ }
1908
+ },
1909
+ });
1910
+ for (const l of r.lines)
1911
+ console.error(l);
1765
1912
  console.error(D(` Your peers can still message you; only unattended runs are off.`));
1766
1913
  console.error(D(` To undo: pantheon service install worker and pantheon admit <who> --may-wake`));
1914
+ if (!r.ok)
1915
+ process.exitCode = 1;
1767
1916
  return;
1768
1917
  }
1769
1918
  case "members": {
1770
1919
  const cfg = need();
1771
1920
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
1772
- await client.connect().catch((e) => die(client.pending ?? e.message));
1921
+ await client.connect().catch((e) => die(client.pending ?? connectFailure(e, cfg.relay)));
1773
1922
  await new Promise((r) => setTimeout(r, 300));
1774
1923
  console.log(`${B(`who is in ${cfg.space}`)}\n`);
1775
1924
  for (const e of client.members) {
@@ -1790,7 +1939,7 @@ async function main() {
1790
1939
  const cfg = need();
1791
1940
  const who = argv[1] ?? die("usage: pantheon admit <member> [--may-wake] [--revoke] [--forget]");
1792
1941
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
1793
- await client.connect().catch((e) => die(client.pending ?? e.message));
1942
+ await client.connect().catch((e) => die(client.pending ?? connectFailure(e, cfg.relay)));
1794
1943
  await new Promise((r) => setTimeout(r, 250));
1795
1944
  const forget = argv.includes("--forget");
1796
1945
  const revoke = argv.includes("--revoke");
@@ -1837,16 +1986,21 @@ async function main() {
1837
1986
  return;
1838
1987
  }
1839
1988
  case "whoami": {
1840
- const cfg = need();
1989
+ const cfg = identity() ?? die("not set up on this machine — pantheon signup, or pantheon join <invite>");
1990
+ const team = resolveFor(process.cwd()).cfg;
1841
1991
  if (cfg.account) {
1842
1992
  console.log(`account ${C(cfg.account.username)} ${D(`<${cfg.account.email}>`)} ${D("verified")}`);
1843
1993
  }
1994
+ else if (cfg.pendingAuth) {
1995
+ console.log(`account ${D(`not signed in yet — a code was sent to ${cfg.pendingAuth.email}: pantheon verify <code>`)}`);
1996
+ }
1844
1997
  else {
1845
1998
  console.log(`account ${D("not signed in")} ${D("— pantheon signup / pantheon login")}`);
1846
1999
  }
1847
2000
  console.log(`member ${C(cfg.member)}`);
1848
- console.log(`space ${cfg.space}`);
1849
- console.log(`relay ${cfg.relay}`);
2001
+ const teams = Object.values(loadAll()?.spaces ?? {}).map((m) => m.name);
2002
+ 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>"))}`);
2003
+ console.log(`relay ${team?.space ? team.relay : cfg.relay}`);
1850
2004
  console.log(`this device ${C(fingerprint(cfg.publicKey))}`);
1851
2005
  console.log(D(`\nManage your devices and sessions: pantheon sessions`));
1852
2006
  return;
@@ -1858,7 +2012,7 @@ async function main() {
1858
2012
  return interactiveOnboard(flag("relay"));
1859
2013
  const email = flag("email") ?? die(`usage: pantheon ${purpose} --email you@work.com${purpose === "signup" ? " --username <name>" : ""} [--relay wss://...]`);
1860
2014
  const username = purpose === "signup" ? (flag("username") ?? die("usage: pantheon signup --email you@work.com --username <name>")) : undefined;
1861
- const prior = load();
2015
+ const prior = identity();
1862
2016
  const relay = flag("relay") ?? prior?.relay ?? process.env.PANTHEON_RELAY ?? DEFAULT_RELAY;
1863
2017
  const kp = prior?.publicKey && prior.privateKey ? { publicKey: prior.publicKey, privateKey: prior.privateKey } : newKeypair();
1864
2018
  const label = flag("label") ?? `${process.env.USER ?? "a"}'s ${process.platform === "darwin" ? "mac" : process.platform}`;
@@ -1866,7 +2020,7 @@ async function main() {
1866
2020
  const body = purpose === "signup" ? { email, username, pubkey: kp.publicKey, label } : { email, pubkey: kp.publicKey, label };
1867
2021
  const r = await fetch(`${base}/auth/${purpose}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })
1868
2022
  .then(async (x) => ({ status: x.status, j: await x.json() }))
1869
- .catch((e) => die(`could not reach the relay at ${base}: ${e.message}`));
2023
+ .catch((e) => die(describeNetError(e, relay)));
1870
2024
  if (r.status !== 200)
1871
2025
  die(String(r.j.error ?? `signup failed (${r.status})`));
1872
2026
  save({ ...(prior ?? {}), relay, member: username ?? prior?.member ?? "me", space: prior?.space ?? "", token: prior?.token ?? "",
@@ -1883,7 +2037,7 @@ async function main() {
1883
2037
  return;
1884
2038
  }
1885
2039
  case "verify": {
1886
- const cfg = load() ?? die("run `pantheon signup` or `pantheon login` first");
2040
+ const cfg = identity() ?? die("run `pantheon signup` or `pantheon login` first");
1887
2041
  const pending = cfg.pendingAuth ?? die("nothing to verify — run `pantheon signup` or `pantheon login` first");
1888
2042
  let code = (argv[1] ?? flag("code") ?? "").trim();
1889
2043
  if (!code && process.stdin.isTTY) {
@@ -1894,9 +2048,9 @@ async function main() {
1894
2048
  if (!code)
1895
2049
  die("usage: pantheon verify <6-digit-code>");
1896
2050
  const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
1897
- const r = await fetch(`${base}/auth/verify`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: pending.email, code }) })
2051
+ const r = await fetch(`${base}/auth/verify`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: pending.email, code, pubkey: cfg.publicKey }) })
1898
2052
  .then(async (x) => ({ status: x.status, j: await x.json() }))
1899
- .catch((e) => die(`could not reach the relay: ${e.message}`));
2053
+ .catch((e) => die(describeNetError(e, cfg.relay)));
1900
2054
  if (r.status !== 200)
1901
2055
  die(String(r.j.error ?? "verification failed"));
1902
2056
  const acct = r.j.account;
@@ -1910,7 +2064,7 @@ async function main() {
1910
2064
  return;
1911
2065
  }
1912
2066
  case "sessions": {
1913
- const cfg = need();
2067
+ const cfg = identity() ?? die("not set up on this machine — pantheon signup first");
1914
2068
  if (!cfg.account)
1915
2069
  die("not signed in — run `pantheon signup` or `pantheon login` first");
1916
2070
  const sub = argv[1];
@@ -1922,7 +2076,7 @@ async function main() {
1922
2076
  const me = await signedFetch(cfg, "GET", "/account/sessions").then((x) => x.json()).catch(() => ({}));
1923
2077
  thisDevice = me.this_device ?? "";
1924
2078
  }
1925
- const r = await signedFetch(cfg, "POST", "/account/revoke", { kind, id }).then((x) => x.json()).catch((e) => die(e.message));
2079
+ const r = await signedFetch(cfg, "POST", "/account/revoke", { kind, id }).then((x) => x.json()).catch((e) => die(describeNetError(e, cfg.relay)));
1926
2080
  if (kind === "device" && id === thisDevice) {
1927
2081
  const cl = { ...cfg };
1928
2082
  delete cl.account;
@@ -1931,7 +2085,7 @@ async function main() {
1931
2085
  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)" : ""}`);
1932
2086
  return;
1933
2087
  }
1934
- const r = await signedFetch(cfg, "GET", "/account/sessions").then((x) => x.json()).catch((e) => die(e.message));
2088
+ const r = await signedFetch(cfg, "GET", "/account/sessions").then((x) => x.json()).catch((e) => die(describeNetError(e, cfg.relay)));
1935
2089
  console.log(`${B(r.account.username)} ${D(`<${r.account.email}>`)}\n`);
1936
2090
  console.log(B("devices"));
1937
2091
  for (const d of r.devices)
@@ -1946,12 +2100,12 @@ async function main() {
1946
2100
  return;
1947
2101
  }
1948
2102
  case "audit": {
1949
- const cfg = need();
2103
+ const cfg = identity() ?? die("not set up on this machine — pantheon signup first");
1950
2104
  if (!cfg.account)
1951
2105
  die("not signed in — run `pantheon signup` first");
1952
2106
  const space = flag("space");
1953
2107
  const since = flag("since") ? String(Date.now() - Number(flag("since")) * 3_600_000) : undefined;
1954
- const r = await signedFetch(cfg, "GET", "/audit", { ...(space ? { space } : {}), ...(since ? { since } : {}) }).then((x) => x.json()).catch((e) => die(e.message));
2108
+ const r = await signedFetch(cfg, "GET", "/audit", { ...(space ? { space } : {}), ...(since ? { since } : {}) }).then((x) => x.json()).catch((e) => die(describeNetError(e, cfg.relay)));
1955
2109
  if (r.error)
1956
2110
  die(r.error);
1957
2111
  if (argv.includes("--json")) {
@@ -1966,22 +2120,34 @@ async function main() {
1966
2120
  return;
1967
2121
  }
1968
2122
  case "logout": {
1969
- const cfg = need();
2123
+ const cfg = identity() ?? die("not set up on this machine — pantheon signup first");
1970
2124
  if (!cfg.account) {
1971
2125
  console.error(D("not signed in — nothing to log out"));
1972
2126
  return;
1973
2127
  }
1974
- const list = await signedFetch(cfg, "GET", "/account/sessions").then((x) => x.json()).catch((e) => die(e.message));
1975
- if (list.this_device)
1976
- await signedFetch(cfg, "POST", "/account/revoke", { kind: "device", id: list.this_device }).catch(() => { });
2128
+ const list = await signedFetch(cfg, "GET", "/account/sessions").then(async (x) => ({ ok: x.ok, status: x.status, j: await x.json().catch(() => ({})) }))
2129
+ .catch((e) => die(describeNetError(e, cfg.relay)));
2130
+ if (!list.ok && list.status !== 403)
2131
+ 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`);
2132
+ let revoked = list.status === 403;
2133
+ if (list.ok && list.j.this_device) {
2134
+ const r = await signedFetch(cfg, "POST", "/account/revoke", { kind: "device", id: list.j.this_device })
2135
+ .then(async (x) => ({ ok: x.ok, j: await x.json().catch(() => ({})) }))
2136
+ .catch((e) => die(describeNetError(e, cfg.relay)));
2137
+ if (!r.ok)
2138
+ 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`);
2139
+ revoked = true;
2140
+ }
2141
+ else if (list.ok)
2142
+ revoked = true;
1977
2143
  const cleared = { ...cfg };
1978
2144
  delete cleared.account;
1979
2145
  save(cleared);
1980
- console.error(`${C("logged out")} — this device is signed out. Sign back in with ${C("pantheon login --email " + cfg.account.email)}.`);
2146
+ 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)}.`);
1981
2147
  return;
1982
2148
  }
1983
2149
  case "account": {
1984
- const cfg = need();
2150
+ const cfg = identity() ?? die("not set up on this machine — pantheon signup first");
1985
2151
  if (!cfg.account)
1986
2152
  die("not signed in");
1987
2153
  const sub = argv[1];
@@ -1989,7 +2155,7 @@ async function main() {
1989
2155
  die("usage: pantheon account delete");
1990
2156
  if (!argv.includes("--yes"))
1991
2157
  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.`);
1992
- const r = await signedFetch(cfg, "POST", "/account/delete").then((x) => x.json()).catch((e) => die(e.message));
2158
+ const r = await signedFetch(cfg, "POST", "/account/delete").then((x) => x.json()).catch((e) => die(describeNetError(e, cfg.relay)));
1993
2159
  if (!r.ok)
1994
2160
  die(String(r.error ?? "delete failed"));
1995
2161
  const cleared = { ...cfg };
@@ -2179,7 +2345,7 @@ async function main() {
2179
2345
  if (overlap)
2180
2346
  die("an overlapping delegation already exists for one of these domains; remove it first");
2181
2347
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
2182
- await client.connect().catch((e) => die(e.message));
2348
+ await client.connect().catch((e) => die(connectFailure(e, cfg.relay)));
2183
2349
  if (!client.profiles.has(to)) {
2184
2350
  client.close();
2185
2351
  die(`${to} has no published profile; delegation would have nowhere verifiable to land`);
@@ -2221,7 +2387,7 @@ async function main() {
2221
2387
  case "directory": {
2222
2388
  const cfg = need();
2223
2389
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
2224
- await client.connect().catch((e) => die(e.message));
2390
+ await client.connect().catch((e) => die(connectFailure(e, cfg.relay)));
2225
2391
  if (!client.profiles.size)
2226
2392
  console.log(`${D("no organization profiles published yet")}`);
2227
2393
  for (const p of client.profiles.values()) {
@@ -2278,12 +2444,41 @@ async function main() {
2278
2444
  return;
2279
2445
  }
2280
2446
  case "explain": {
2281
- const cfg = need();
2282
2447
  const { readFileSync, rmSync } = await import("node:fs");
2448
+ const CONFIG_DIR = dirname(CONFIG_PATH);
2449
+ let hud = null;
2450
+ if (argv.includes("--queued")) {
2451
+ const f = join(CONFIG_DIR, "bin", ".hud-action.json");
2452
+ try {
2453
+ hud = JSON.parse(readFileSync(f, "utf8"));
2454
+ rmSync(f, { force: true });
2455
+ }
2456
+ catch {
2457
+ hud = null;
2458
+ }
2459
+ }
2460
+ const cfg = (() => {
2461
+ if (!hud?.space)
2462
+ return need();
2463
+ const f = loadAll();
2464
+ const m = f ? findMembership(f, hud.space) : undefined;
2465
+ if (f && m)
2466
+ return configFor(f, m, null);
2467
+ const here = resolveFor(process.cwd()).cfg;
2468
+ if (here?.space === hud.space)
2469
+ return here;
2470
+ const why = `this machine is no longer in the team "${hud.space}"`;
2471
+ if (hud.action === "reply" || hud.action === "retract") {
2472
+ console.log(`not sent: ${why}`);
2473
+ process.exit(1);
2474
+ }
2475
+ return die(why);
2476
+ })();
2477
+ if (hud && hud.goal === "")
2478
+ delete hud.goal;
2283
2479
  const { pickExplainAgent, decisionPrompt: dp, allDecisionsPrompt: adp, decisionTitle: dt, sessionName } = await import("./decision-ui.js");
2284
2480
  const { loadProfile } = await import("./profile.js");
2285
2481
  const { detectAvailable, launchArgs } = await import("./agents.js");
2286
- const CONFIG_DIR = dirname(CONFIG_PATH);
2287
2482
  let id = argv.find((a, i) => i > 0 && !a.startsWith("--"));
2288
2483
  if (argv.includes("--queued")) {
2289
2484
  const queued = join(CONFIG_DIR, "bin", ".explain-id");
@@ -2295,17 +2490,6 @@ async function main() {
2295
2490
  if (id === "all")
2296
2491
  id = undefined;
2297
2492
  }
2298
- let hud = null;
2299
- if (argv.includes("--queued")) {
2300
- const f = join(CONFIG_DIR, "bin", ".hud-action.json");
2301
- try {
2302
- hud = JSON.parse(readFileSync(f, "utf8"));
2303
- rmSync(f, { force: true });
2304
- }
2305
- catch {
2306
- hud = null;
2307
- }
2308
- }
2309
2493
  if (hud?.action === "fix") {
2310
2494
  const dir = hud.dir && existsSync(hud.dir) ? hud.dir : process.cwd();
2311
2495
  const steps = hud.lines ?? [];
@@ -2353,22 +2537,31 @@ async function main() {
2353
2537
  if (!body)
2354
2538
  notSent("nothing to send");
2355
2539
  const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
2356
- await c2.connect().catch((e) => notSent(e.message));
2540
+ await c2.connect().catch((e) => notSent(connectFailure(e, cfg.relay)));
2357
2541
  const o = hud.decision ? c2.obligations.find((x) => x.id === hud.decision) : undefined;
2358
- const acts = await fetchHistory(cfg).catch(() => []);
2359
- const target = acts.find((x) => x.id === (o?.ref ?? hud.act));
2360
- const goal = o?.goal ?? target?.goal ?? hud.goal ?? null;
2542
+ const targetId = o?.ref ?? hud.act;
2543
+ const target = targetId ? (await fetchAct(cfg, targetId, 8_000))?.act : undefined;
2544
+ let goal = o?.goal ?? target?.goal ?? hud.goal ?? null;
2361
2545
  if (kind === "ANSWER" && !o && !target) {
2362
2546
  c2.close();
2363
2547
  notSent("that decision is no longer waiting — nothing to answer");
2364
2548
  }
2549
+ if (goal && c2.goals.get(goal)?.state === "CLOSED") {
2550
+ if (kind === "ASK") {
2551
+ c2.close();
2552
+ notSent("that goal is closed — open a new one to ask more");
2553
+ }
2554
+ goal = null;
2555
+ }
2365
2556
  const ref = kind === "ANSWER" ? (o?.ref ?? target?.id ?? null) : kind === "ASK" ? (target?.id ?? null) : null;
2366
2557
  const to = kind === "ASK" ? (target && target.from !== cfg.member ? [target.from] : null) : null;
2367
2558
  if (kind === "ASK" && !to) {
2368
2559
  c2.close();
2369
2560
  notSent("cannot tell who raised this — use Show the thread and reply from there");
2370
2561
  }
2371
- const sent = await c2.send({ act: kind, body, goal, ref, to, after_seq: c2.readSeq, meta: { surface: "menubar" } })
2562
+ const summary = body.length > LIMITS.summaryRequiredOver
2563
+ ? (body.split(/\n|(?<=[.!?])\s/)[0] ?? body).trim().slice(0, LIMITS.summaryChars) : undefined;
2564
+ const sent = await c2.send({ act: kind, body, goal, ref, to, after_seq: c2.readSeq, meta: { surface: "menubar" }, ...(summary ? { summary } : {}) })
2372
2565
  .catch((e) => notSent(e.message));
2373
2566
  c2.close();
2374
2567
  console.log(`sent ${kind} id=${sent.id} seq=${sent.seq}`);
@@ -2379,7 +2572,7 @@ async function main() {
2379
2572
  if (!hud.act)
2380
2573
  notSent("nothing to retract");
2381
2574
  const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
2382
- await c2.connect().catch((e) => notSent(e.message));
2575
+ await c2.connect().catch((e) => notSent(connectFailure(e, cfg.relay)));
2383
2576
  const sent = await c2.send({ act: "RETRACT", body: "Retracted from the menu bar.", goal: null, ref: hud.act, to: null, meta: { surface: "menubar" } })
2384
2577
  .catch((e) => notSent(e.message));
2385
2578
  c2.close();
@@ -2388,14 +2581,15 @@ async function main() {
2388
2581
  }
2389
2582
  if (hud?.action === "done") {
2390
2583
  const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
2391
- await c2.connect().catch((e) => die(e.message));
2584
+ await c2.connect().catch((e) => die(connectFailure(e, cfg.relay)));
2392
2585
  const o = hud.decision ? c2.obligations.find((x) => x.id === hud.decision) : undefined;
2393
2586
  if (!o) {
2394
2587
  c2.close();
2395
2588
  console.error(D("that decision is no longer waiting on you"));
2396
2589
  return;
2397
2590
  }
2398
- await c2.send({ act: "ANSWER", ref: o.ref, goal: o.goal, after_seq: c2.readSeq,
2591
+ const open = o.goal && c2.goals.get(o.goal)?.state !== "CLOSED" ? o.goal : null;
2592
+ await c2.send({ act: "ANSWER", ref: o.ref, goal: open, after_seq: c2.readSeq,
2399
2593
  body: "Marked done from the alert — handled outside this thread." }).catch((e) => die(e.message));
2400
2594
  c2.close();
2401
2595
  console.error(D(`done — ${o.id} is no longer waiting on you`));
@@ -2411,19 +2605,18 @@ async function main() {
2411
2605
  }
2412
2606
  if (wd)
2413
2607
  console.error(D(` ${wd.note}`));
2414
- const acts = await fetchHistory(cfg).catch(() => []);
2415
- const a = hud.act ? acts.find((x) => x.id === hud.act) : undefined;
2608
+ const a = hud.act ? (await fetchAct(cfg, hud.act))?.act : undefined;
2416
2609
  let o;
2417
2610
  if (hud.decision) {
2418
2611
  const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
2419
- await c2.connect().catch((e) => die(e.message));
2612
+ await c2.connect().catch((e) => die(connectFailure(e, cfg.relay)));
2420
2613
  o = c2.obligations.find((x) => x.id === hud.decision);
2421
2614
  c2.close();
2422
2615
  }
2423
2616
  if (a) {
2424
2617
  console.log(formatAct(a));
2425
2618
  if (a.ref) {
2426
- const r = acts.find((x) => x.id === a.ref);
2619
+ const r = (await fetchAct(cfg, a.ref))?.act;
2427
2620
  if (r)
2428
2621
  console.log(D("\nin reply to:\n") + formatAct(r, { short: true }));
2429
2622
  }
@@ -2451,11 +2644,7 @@ async function main() {
2451
2644
  rl.close();
2452
2645
  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.`;
2453
2646
  console.error(D(`opening ${agent.id}…`));
2454
- const { spawnSync } = await import("node:child_process");
2455
- {
2456
- const sp = agentSpawn(agent.binary, launchArgs(agent.id, prompt, sessionName(prompt)));
2457
- spawnSync(sp.command, sp.args, { stdio: "inherit", ...sp.options });
2458
- }
2647
+ await launchAgent(agent.binary, (p) => launchArgs(agent.id, p, sessionName(prompt)), prompt);
2459
2648
  return;
2460
2649
  }
2461
2650
  if (choice === "2") {
@@ -2496,7 +2685,8 @@ async function main() {
2496
2685
  continue;
2497
2686
  }
2498
2687
  if (choice === "3" && goalId) {
2499
- for (const t of selectActs(acts, { goal: goalId, last: 40 }))
2688
+ const thread = await fetchHistory(cfg, { goal: goalId }).catch(() => []);
2689
+ for (const t of selectActs(thread, { goal: goalId, last: 40 }))
2500
2690
  console.log(formatAct(t, { short: true }));
2501
2691
  continue;
2502
2692
  }
@@ -2523,10 +2713,7 @@ async function main() {
2523
2713
  const bin = cliOnPath("grok") ?? die("grok is not on PATH here");
2524
2714
  console.error(D("opening grok…"));
2525
2715
  const { spawnSync } = await import("node:child_process");
2526
- {
2527
- const sp = agentSpawn(bin, [hud.prompt]);
2528
- spawnSync(sp.command, sp.args, { stdio: "inherit", ...(cwd ? { cwd } : {}), ...sp.options });
2529
- }
2716
+ await launchAgent(bin, (p) => [p], hud.prompt, cwd ?? undefined);
2530
2717
  return;
2531
2718
  }
2532
2719
  if (hud.prompt) {
@@ -2535,10 +2722,7 @@ async function main() {
2535
2722
  die("no agent CLI found on this machine (claude, cursor-agent, codex, copilot, or kiro-cli)");
2536
2723
  console.error(D(`opening ${agent.id}…`));
2537
2724
  const { spawnSync } = await import("node:child_process");
2538
- {
2539
- const sp = agentSpawn(agent.binary, launchArgs(agent.id, hud.prompt, sessionName(hud.prompt)));
2540
- spawnSync(sp.command, sp.args, { stdio: "inherit", ...(cwd ? { cwd } : {}), ...sp.options });
2541
- }
2725
+ await launchAgent(agent.binary, (p) => launchArgs(agent.id, p, sessionName(hud.prompt)), hud.prompt, cwd ?? undefined);
2542
2726
  return;
2543
2727
  }
2544
2728
  if (hud.decision)
@@ -2551,7 +2735,7 @@ async function main() {
2551
2735
  }
2552
2736
  }
2553
2737
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
2554
- await client.connect().catch((e) => die(e.message));
2738
+ await client.connect().catch((e) => die(connectFailure(e, cfg.relay)));
2555
2739
  const waiting = client.obligations.filter((o) => o.kind === "HUMAN" && o.owner === `human:${cfg.member}`);
2556
2740
  client.close();
2557
2741
  const one = id ? waiting.find((o) => o.id === id) : waiting.length === 1 ? waiting[0] : undefined;
@@ -2567,10 +2751,7 @@ async function main() {
2567
2751
  die("no agent CLI found on this machine (claude, cursor-agent, codex, copilot, or kiro-cli)");
2568
2752
  console.error(D(`opening ${agent.id} with the question…`));
2569
2753
  const { spawnSync } = await import("node:child_process");
2570
- {
2571
- const sp = agentSpawn(agent.binary, launchArgs(agent.id, prompt, sessionName(prompt)));
2572
- spawnSync(sp.command, sp.args, { stdio: "inherit", ...sp.options });
2573
- }
2754
+ await launchAgent(agent.binary, (p) => launchArgs(agent.id, p, sessionName(prompt)), prompt);
2574
2755
  return;
2575
2756
  }
2576
2757
  case "approvals": {
@@ -2579,7 +2760,7 @@ async function main() {
2579
2760
  const conns = [];
2580
2761
  for (const c of targets()) {
2581
2762
  const cl = new PantheonClient(c, undefined, { role: "cli" });
2582
- await cl.connect().catch((e) => die(`${c.space}: ${e.message}`));
2763
+ await cl.connect().catch((e) => die(`${c.space}: ${connectFailure(e, c.relay)}`));
2583
2764
  conns.push({ cfg: c, client: cl });
2584
2765
  }
2585
2766
  const closeAll = () => { for (const c of conns)
@@ -2624,7 +2805,7 @@ async function main() {
2624
2805
  const cfg = need();
2625
2806
  const sub = argv[1] ?? "list";
2626
2807
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
2627
- await client.connect().catch((e) => die(e.message));
2808
+ await client.connect().catch((e) => die(connectFailure(e, cfg.relay)));
2628
2809
  const waiting = client.obligations.filter((o) => o.kind === "HUMAN" && o.owner === `human:${cfg.member}`);
2629
2810
  const id = argv[2];
2630
2811
  if (sub === "answer") {
@@ -2937,7 +3118,7 @@ async function main() {
2937
3118
  const sub = argv[1] ?? "list";
2938
3119
  const { repo: _r, repoOrigin: _o, ...plain } = cfg;
2939
3120
  const client = new PantheonClient(plain, undefined, { role: "cli" });
2940
- await client.connect().catch((e) => die(client.pending ?? e.message));
3121
+ await client.connect().catch((e) => die(client.pending ?? connectFailure(e, plain.relay)));
2941
3122
  if (sub === "add") {
2942
3123
  const r = repoInfo(flag("path") ?? process.cwd()) ?? die("not inside a git repo");
2943
3124
  const base = flag("base") ?? null;
@@ -2981,5 +3162,10 @@ async function main() {
2981
3162
  process.exit(cmd === "help" || cmd === "--help" ? 0 : 1);
2982
3163
  }
2983
3164
  }
2984
- main().catch((e) => die(e.message));
3165
+ main().catch((e) => die(describeNetError(e, (() => { try {
3166
+ return load()?.relay;
3167
+ }
3168
+ catch {
3169
+ return undefined;
3170
+ } })())));
2985
3171
  //# sourceMappingURL=cli.js.map