@join-pantheon/cli 0.23.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -521,6 +521,8 @@ pantheon upgrade # installs the latest release and finishes the job
521
521
  Every command prints its version and nudges you when a newer release exists. `pantheon upgrade`
522
522
  installs the new release, re-wires your agents, restarts pantheon's own background services (the
523
523
  worker, alerts, menu bar) so they load the new code, and asks any setup question the new version
524
- has that you have never answered. The one thing it cannot do is restart your agent windows: quit
525
- and reopen them so they load the new server. A self-hosted relay is shared with your teammates, so
526
- it prints the restart for that and leaves the timing to you.
524
+ has that you have never answered. Open agent windows come along too: the server each window runs
525
+ notices the new version and switches to it within a minute, with no disconnect and nothing to
526
+ reopen (windows opened on a pantheon older than 0.24 need one `/mcp` reconnect; `pantheon doctor`
527
+ names them). A self-hosted relay is shared with your teammates, so it prints the restart for that
528
+ and leaves the timing to you.
package/dist/agents.js CHANGED
@@ -17,6 +17,11 @@ const sh = (cmd) => {
17
17
  return null;
18
18
  }
19
19
  };
20
+ export function cliOnPath(name) {
21
+ if (!binaryCache.has(`path:${name}`))
22
+ binaryCache.set(`path:${name}`, sh(`command -v ${name}`));
23
+ return binaryCache.get(`path:${name}`) ?? null;
24
+ }
20
25
  export function agentById(id) {
21
26
  return AGENTS.find((a) => a.id === id || a.binaries.includes(id));
22
27
  }
@@ -57,8 +62,15 @@ export function detectOnPath() {
57
62
  const binaryCache = new Map();
58
63
  export function resolveBinary(backend) {
59
64
  for (const b of backend.binaries) {
60
- if (!binaryCache.has(b))
61
- binaryCache.set(b, sh(`command -v ${b}`) ? b : null);
65
+ if (!binaryCache.has(b)) {
66
+ let found = sh(`command -v ${b}`) ? b : null;
67
+ if (found && b === "agent") {
68
+ const v = `${sh("agent --version 2>/dev/null | head -1") ?? ""} ${sh("agent --help 2>/dev/null | head -3") ?? ""}`;
69
+ if (!/cursor/i.test(v))
70
+ found = null;
71
+ }
72
+ binaryCache.set(b, found);
73
+ }
62
74
  if (binaryCache.get(b))
63
75
  return b;
64
76
  }
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ process.emitWarning = ((warning, ...rest) => {
7
7
  return _emitWarning(warning, ...rest);
8
8
  });
9
9
  import { randomBytes } from "node:crypto";
10
- import { readFileSync } from "node:fs";
10
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
11
  import { basename, dirname, join, resolve } from "node:path";
12
12
  import { PantheonClient } from "./client.js";
13
13
  import { fromInvite, load, save, toInvite, toJoinLink, resolveFor, loadAll, repoInfo, bindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY, resolveForHook } from "./config.js";
@@ -38,6 +38,7 @@ import { LIMITS } from "./protocol.js";
38
38
  import { loadPersonas, personaByName, describePersona, initPersona, personaDir } from "./persona.js";
39
39
  import { fingerprint, newKeypair, signAccount } from "./identity.js";
40
40
  import { VERSION } from "./version.js";
41
+ import * as mcpWindows from "./mcp-windows.js";
41
42
  import { updateBanner } from "./update-check.js";
42
43
  const argv = process.argv.slice(2);
43
44
  const cmd = argv[0] ?? "help";
@@ -339,6 +340,39 @@ function workerMidRun() {
339
340
  return false;
340
341
  }
341
342
  }
343
+ function staleWindowMessage(cwd) {
344
+ const { scanMcpWindows, RECONNECT_HINT } = mcpWindows;
345
+ const legacy = scanMcpWindows().filter((w) => w.kind === "legacy" && w.cwd === cwd);
346
+ if (!legacy.length)
347
+ return null;
348
+ const marker = join(CONFIG_DIR, "mcp", "nudged.json");
349
+ let seen = {};
350
+ try {
351
+ seen = JSON.parse(readFileSync(marker, "utf8"));
352
+ }
353
+ catch { }
354
+ const last = seen[cwd] ?? 0;
355
+ if (Date.now() - last < 60 * 60_000)
356
+ return null;
357
+ seen[cwd] = Date.now();
358
+ try {
359
+ mkdirSync(dirname(marker), { recursive: true });
360
+ writeFileSync(marker, JSON.stringify(seen));
361
+ }
362
+ catch { }
363
+ return `pantheon: this window's pantheon server is from before the installed version (${VERSION}) and cannot reach your team any more — ${RECONNECT_HINT}. Windows opened from now on update themselves.`;
364
+ }
365
+ function reportWindows(installed) {
366
+ const { scanMcpWindows, describeWindows, killOrphanWindows } = mcpWindows;
367
+ const windows = scanMcpWindows();
368
+ const ended = killOrphanWindows(windows);
369
+ const w = describeWindows(windows, installed);
370
+ console.error(`\n${B("agent windows:")} ${w.detail}`);
371
+ if (w.fix)
372
+ console.error(` ${D("->")} ${w.fix}`);
373
+ if (ended.length)
374
+ console.error(D(` ended ${ended.length} pantheon server${ended.length > 1 ? "s" : ""} left behind by windows that no longer exist`));
375
+ }
342
376
  function printReady(tools) {
343
377
  const workerOn = tools.some((t) => t.tool === "worker" && t.status.startsWith("on"));
344
378
  console.error(`\n${B("You're set.")} Restart your agent once (quit and reopen) and it is connected.`);
@@ -781,6 +815,7 @@ async function main() {
781
815
  const cur = load();
782
816
  if (cur && loadProfile(cur).agent.wake_default === undefined)
783
817
  await guidedSetup(cur, repoInfo(process.cwd())?.root ?? null);
818
+ reportWindows(r.from);
784
819
  return;
785
820
  }
786
821
  console.error(`\n${B(`upgraded ${r.from} → ${r.to}`)}`);
@@ -820,7 +855,7 @@ async function main() {
820
855
  else
821
856
  console.error(D(`\n Your setup answers still apply — pantheon setup to change them.`));
822
857
  }
823
- console.error(`\n${B("One thing only you can do:")} quit and reopen your agent windows so they load the new pantheon.`);
858
+ reportWindows(r.to);
824
859
  return;
825
860
  }
826
861
  case "setup": {
@@ -1260,6 +1295,17 @@ async function main() {
1260
1295
  }
1261
1296
  }
1262
1297
  catch { }
1298
+ try {
1299
+ let cwd = process.cwd();
1300
+ try {
1301
+ cwd = JSON.parse(raw).cwd ?? cwd;
1302
+ }
1303
+ catch { }
1304
+ const msg = staleWindowMessage(cwd);
1305
+ if (msg)
1306
+ process.stdout.write(JSON.stringify({ systemMessage: msg }) + "\n");
1307
+ }
1308
+ catch { }
1263
1309
  process.exit(0);
1264
1310
  }
1265
1311
  if (sub === "end") {
@@ -2122,6 +2168,13 @@ async function main() {
2122
2168
  if (hud?.action === "details") {
2123
2169
  const acts = await fetchHistory(cfg).catch(() => []);
2124
2170
  const a = hud.act ? acts.find((x) => x.id === hud.act) : undefined;
2171
+ let o;
2172
+ if (hud.decision) {
2173
+ const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
2174
+ await c2.connect().catch((e) => die(e.message));
2175
+ o = c2.obligations.find((x) => x.id === hud.decision);
2176
+ c2.close();
2177
+ }
2125
2178
  if (a) {
2126
2179
  console.log(formatAct(a));
2127
2180
  if (a.ref) {
@@ -2130,17 +2183,80 @@ async function main() {
2130
2183
  console.log(D("\nin reply to:\n") + formatAct(r, { short: true }));
2131
2184
  }
2132
2185
  }
2133
- else if (!hud.decision)
2186
+ else if (!o)
2134
2187
  console.log(D("that act is no longer in what the relay holds"));
2135
- if (hud.decision) {
2136
- const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
2137
- await c2.connect().catch((e) => die(e.message));
2138
- const o = c2.obligations.find((x) => x.id === hud.decision);
2139
- c2.close();
2140
- if (o)
2141
- console.log(`\n${B("Decision required")} ${o.id}\n ${o.body}\n\nAnswer: ${C(`pantheon approvals answer ${o.id} 'your decision and rationale'`)}\n${D(`or have your agent explain it: pantheon explain ${o.id}`)}`);
2142
- else
2143
- console.log(D("\nthat decision is no longer waiting on you"));
2188
+ if (o)
2189
+ console.log(`\n${B("Decision required")} ${o.id}\n ${o.body}`);
2190
+ else if (hud.decision)
2191
+ console.log(D("\nthat decision is no longer waiting on you"));
2192
+ if (!process.stdin.isTTY || (!a && !o))
2193
+ return;
2194
+ const p0 = loadProfile(cfg);
2195
+ const agent = pickExplainAgent(hud.tool ?? p0.agent.primary_surface, detectAvailable());
2196
+ const goalId = a?.goal ?? o?.goal ?? null;
2197
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
2198
+ const ask = (q) => new Promise((res) => rl.question(q, (v) => res(v.trim())));
2199
+ try {
2200
+ for (;;) {
2201
+ console.error(`\n${B("Next")}${agent ? ` ${C("1")} open in ${agent.id} with this loaded` : ""} ${C("2")} reply from here${goalId ? ` ${C("3")} show the whole thread` : ""} ${D("Enter) close")}`);
2202
+ const choice = await ask(" choose: ");
2203
+ if (!choice)
2204
+ break;
2205
+ if (choice === "1" && agent) {
2206
+ rl.close();
2207
+ const prompt = o ? dp(o.id) : `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.`;
2208
+ console.error(D(`opening ${agent.id}…`));
2209
+ const { spawnSync } = await import("node:child_process");
2210
+ spawnSync(agent.binary, [prompt], { stdio: "inherit" });
2211
+ return;
2212
+ }
2213
+ if (choice === "2") {
2214
+ let kind = "NOTE";
2215
+ let ref = null;
2216
+ let to = null;
2217
+ if (o) {
2218
+ kind = "ANSWER";
2219
+ ref = o.ref;
2220
+ }
2221
+ else if (a) {
2222
+ ref = a.id;
2223
+ to = a.from === cfg.member ? null : [a.from];
2224
+ if (["ASK", "ESCALATE", "HUMAN_REQUIRED"].includes(a.act))
2225
+ kind = "ANSWER";
2226
+ else if (["DELIVERED", "PROPOSE", "CONTRACT"].includes(a.act)) {
2227
+ const v = (await ask(" accept or object? (a/o): ")).toLowerCase();
2228
+ kind = v.startsWith("o") ? "OBJECT" : "ACCEPT";
2229
+ }
2230
+ }
2231
+ const text = await ask(` your ${kind} (one line): `);
2232
+ if (!text) {
2233
+ console.error(D(" nothing sent"));
2234
+ continue;
2235
+ }
2236
+ const c3 = new PantheonClient(cfg, undefined, { role: "cli" });
2237
+ try {
2238
+ await c3.connect();
2239
+ const sent = await c3.send({ act: kind, body: text, goal: o ? null : goalId, ref, to });
2240
+ console.error(` ${C("sent")} ${kind} id=${sent.id}`);
2241
+ }
2242
+ catch (e) {
2243
+ console.error(` ${e.message}`);
2244
+ }
2245
+ finally {
2246
+ c3.close();
2247
+ }
2248
+ continue;
2249
+ }
2250
+ if (choice === "3" && goalId) {
2251
+ for (const t of selectActs(acts, { goal: goalId, last: 40 }))
2252
+ console.log(formatAct(t, { short: true }));
2253
+ continue;
2254
+ }
2255
+ console.error(D(" 1, 2, 3 or Enter"));
2256
+ }
2257
+ }
2258
+ finally {
2259
+ rl.close();
2144
2260
  }
2145
2261
  return;
2146
2262
  }
@@ -2150,6 +2266,14 @@ async function main() {
2150
2266
  saveProfile({ ...p, agent: { ...p.agent, primary_surface: hud.tool }, updated_at: Date.now() });
2151
2267
  console.error(D(`${hud.tool} is now where your decisions open`));
2152
2268
  }
2269
+ if (hud.prompt && hud.tool === "grok") {
2270
+ const { cliOnPath } = await import("./agents.js");
2271
+ const bin = cliOnPath("grok") ?? die("grok is not on PATH here");
2272
+ console.error(D("opening grok…"));
2273
+ const { spawnSync } = await import("node:child_process");
2274
+ spawnSync(bin, [hud.prompt], { stdio: "inherit" });
2275
+ return;
2276
+ }
2153
2277
  if (hud.prompt) {
2154
2278
  const agent = pickExplainAgent(hud.tool ?? p.agent.primary_surface, detectAvailable());
2155
2279
  if (!agent)
@@ -2537,7 +2661,12 @@ async function main() {
2537
2661
  return;
2538
2662
  }
2539
2663
  case "mcp":
2540
- await runMcp();
2664
+ if (argv.includes("--child") || process.env.PANTHEON_MCP_SUPERVISED)
2665
+ await runMcp();
2666
+ else {
2667
+ const { superviseMcp } = await import("./mcp-supervisor.js");
2668
+ await superviseMcp();
2669
+ }
2541
2670
  return;
2542
2671
  case "limits":
2543
2672
  console.log(JSON.stringify(LIMITS, null, 2));
package/dist/doctor.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { describeWindows, scanMcpWindows } from "./mcp-windows.js";
2
3
  import { loadProfile, profileInitialized } from "./profile.js";
3
4
  import { existsSync, readFileSync } from "node:fs";
4
5
  import { join } from "node:path";
@@ -221,6 +222,13 @@ export async function doctor(projectDir) {
221
222
  if (hookStates.some((s) => s.agent === "codex")) {
222
223
  add("warn", "codex hooks", "installed — but codex skips new or changed hooks until they are approved", "run /hooks inside codex once to approve them (needs codex ≥ 0.115; sessions still report presence over MCP either way)");
223
224
  }
225
+ const windows = scanMcpWindows();
226
+ const w = describeWindows(windows, VERSION);
227
+ add(w.level, "agent windows", w.detail, w.fix);
228
+ const orphans = windows.filter((x) => x.kind === "orphan");
229
+ if (orphans.length) {
230
+ add("warn", "orphans", `${orphans.length} pantheon server${orphans.length > 1 ? "s" : ""} left behind by windows that no longer exist (pid ${orphans.map((x) => x.pid).join(", ")})`, `harmless but pointless — pantheon upgrade ends them, or: kill ${orphans.map((x) => x.pid).join(" ")}`);
231
+ }
224
232
  const personas = loadPersonas(projectDir);
225
233
  if (personas.length) {
226
234
  const broken = personas.filter((p) => p.warnings.length);