@join-pantheon/cli 0.25.1 → 0.25.3

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/measure.js CHANGED
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { resolve, dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { claudeGranted, cursorGranted, claudeAllow, cursorAllow } from "./grants.js";
5
+ import { claudeGrantedEffective, cursorGrantedEffective, claudeAllow, cursorAllow, isServerWide } from "./grants.js";
6
6
  import { codexApprovalState } from "./install.js";
7
7
  import { PANTHEON_TOOL_NAMES, WRITE_TOOL_NAMES } from "./tools.js";
8
8
  const ROOT = process.env.PANTHEON_MCP_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..");
@@ -71,29 +71,55 @@ export function measureMcp(opts = {}) {
71
71
  });
72
72
  });
73
73
  }
74
- function agentLine(name, seen, wanted, strip) {
74
+ function agentLine(name, seen, wanted, strip, approval) {
75
75
  if (seen.state === "absent")
76
76
  return { text: `${name} not set up here`, drift: false };
77
77
  if (seen.state === "invalid")
78
78
  return { text: `${name} settings unreadable`, drift: true };
79
- const have = seen.tools.map(strip);
79
+ const wide = seen.tools.filter(isServerWide);
80
+ const named = seen.tools.filter((t) => !isServerWide(t)).map(strip);
81
+ const known = named.filter((t) => PANTHEON_TOOL_NAMES.includes(t));
82
+ const unknown = named.length - known.length;
80
83
  const want = wanted.map(strip);
81
- const missing = want.filter((t) => !have.includes(t));
82
- const extra = have.filter((t) => !want.includes(t));
83
- if (!missing.length && !extra.length)
84
- return { text: `${name} allows ${have.length}`, drift: false };
84
+ const missing = wide.length ? [] : want.filter((t) => !known.includes(t));
85
+ const extra = known.filter((t) => !want.includes(t));
85
86
  const parts = [];
86
- if (missing.length)
87
+ let drift = false;
88
+ if (wide.length && approval === "ask-before-sending") {
89
+ parts.push(`a server-wide rule (${wide.join(", ")}) allows every tool, against your choice`);
90
+ drift = true;
91
+ }
92
+ if (missing.length) {
87
93
  parts.push(`${missing.length} would prompt on every call (${missing.map((t) => t.replace(/^pantheon_/, "")).join(", ")})`);
88
- if (extra.length)
94
+ drift = true;
95
+ }
96
+ if (extra.length) {
89
97
  parts.push(`${extra.length} allowed against your choice (${extra.map((t) => t.replace(/^pantheon_/, "")).join(", ")})`);
90
- return { text: `${name} allows ${have.length} — ${parts.join("; ")}`, drift: true };
98
+ drift = true;
99
+ }
100
+ const deniedKnown = (seen.denied ?? []).filter((t) => !isServerWide(t)).map(strip).filter((t) => PANTHEON_TOOL_NAMES.includes(t));
101
+ const deniedWide = (seen.denied ?? []).filter(isServerWide);
102
+ if (deniedWide.length) {
103
+ parts.push(`a deny rule (${deniedWide.join(", ")}) refuses every pantheon tool`);
104
+ drift = true;
105
+ }
106
+ else if (deniedKnown.length) {
107
+ parts.push(`${deniedKnown.length} refused by a deny rule (${deniedKnown.map((t) => t.replace(/^pantheon_/, "")).join(", ")})`);
108
+ drift = true;
109
+ }
110
+ const count = wide.length && approval !== "ask-before-sending" ? `allows all ${want.length} via a server-wide rule` : `allows ${known.length}`;
111
+ const fromProject = seen.fromProject?.length ? ` (${seen.fromProject.length} of them from this repo's own settings)` : "";
112
+ const rest = unknown ? `, plus ${unknown} entr${unknown === 1 ? "y" : "ies"} that ${unknown === 1 ? "is" : "are"} not a pantheon tool (left as ${unknown === 1 ? "it is" : "they are"})` : "";
113
+ return { text: `${name} ${count}${fromProject}${rest}${parts.length ? ` — ${parts.join("; ")}` : ""}`, drift };
91
114
  }
92
- export function toolsReport(registered, seen, approval) {
115
+ export function toolsReport(registered, seen, approval, opts = {}) {
93
116
  const problems = [];
94
117
  const expected = [...PANTHEON_TOOL_NAMES].sort();
95
118
  let head;
96
- if (registered === null) {
119
+ if (opts.unbound) {
120
+ head = `${expected.length} tools in this build; the server offers pantheon_status alone in a folder bound to no team, so the list was not compared`;
121
+ }
122
+ else if (registered === null) {
97
123
  head = `${expected.length} tools`;
98
124
  }
99
125
  else if (JSON.stringify(registered) !== JSON.stringify(expected)) {
@@ -105,8 +131,8 @@ export function toolsReport(registered, seen, approval) {
105
131
  }
106
132
  const stripClaude = (t) => t.replace(/^mcp__pantheon__/, "");
107
133
  const stripCursor = (t) => t.replace(/^Mcp\(pantheon:(.*)\)$/, "$1");
108
- const claude = agentLine("Claude Code", seen.claude, claudeAllow(approval), stripClaude);
109
- const cursor = agentLine("Cursor", seen.cursor, cursorAllow(approval), stripCursor);
134
+ const claude = agentLine("Claude Code", seen.claude, claudeAllow(approval), stripClaude, approval);
135
+ const cursor = agentLine("Cursor", seen.cursor, cursorAllow(approval), stripCursor, approval);
110
136
  if (claude.drift)
111
137
  problems.push("Claude Code's allow-list does not match");
112
138
  if (cursor.drift)
@@ -134,9 +160,9 @@ export function toolsReport(registered, seen, approval) {
134
160
  const detail = `${head}; ${claude.text}, ${cursor.text}, ${codex}`;
135
161
  if (!problems.length)
136
162
  return { level: "pass", detail };
137
- return { level: "warn", detail, fix: `${problems.join("; ")} — pantheon install --user rewrites every grant to match this build and your approval choice` };
163
+ return { level: "warn", detail, fix: `${problems.join("; ")} — pantheon install --user rewrites every grant in your own settings to match this build and your approval choice; a rule in this repo's .claude/settings*.json or .cursor/cli.json is yours to edit by hand` };
138
164
  }
139
- export function grantsSeen() {
140
- return { claude: claudeGranted(), cursor: cursorGranted(), codex: codexApprovalState() };
165
+ export function grantsSeen(projectDir = null) {
166
+ return { claude: claudeGrantedEffective(projectDir), cursor: cursorGrantedEffective(projectDir), codex: codexApprovalState() };
141
167
  }
142
168
  //# sourceMappingURL=measure.js.map
package/dist/notice.js CHANGED
@@ -95,7 +95,7 @@ export function composeDecision(o, act, goal, peer, ctx) {
95
95
  body: fit(body, BUDGET.body),
96
96
  ttl: 0,
97
97
  sound: true,
98
- primary: primaryFor(ctx.tools, ctx.preferred, decisionPrompt(o.id)),
98
+ primary: primaryFor(ctx.tools, ctx.preferred, decisionPrompt(o.id, decisionTitle(o.body))),
99
99
  details: { decision: o.id, ...(act ? { act: act.id } : {}) },
100
100
  };
101
101
  }
package/dist/relay.js CHANGED
@@ -405,10 +405,11 @@ export class Relay {
405
405
  if (!authed)
406
406
  return this.json(res, 401, { error: "admin token required" });
407
407
  const rows = this.fleetHealth();
408
+ const relayAgeMs = Date.now() - this.startedAt;
408
409
  if (url.searchParams.get("format") === "json")
409
- return this.json(res, 200, { relay: VERSION, generated: Date.now(), spaces: rows });
410
+ return this.json(res, 200, { relay: VERSION, relay_age_ms: relayAgeMs, generated: Date.now(), spaces: rows });
410
411
  res.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
411
- return void res.end(fleetReport(rows, VERSION) + "\n");
412
+ return void res.end(fleetReport(rows, VERSION, Date.now(), relayAgeMs) + "\n");
412
413
  }
413
414
  if (url.pathname === "/admin/export") {
414
415
  if (!authed)
@@ -762,9 +763,11 @@ button:hover{background:#1d4ed8}.err{color:var(--bad);margin-top:12px}
762
763
  const safeOf = (name) => name.replace(/[^a-z0-9._-]/gi, "_");
763
764
  for (const sp of this.spaces.values()) {
764
765
  seen.add(safeOf(sp.name));
766
+ const sessions = [...sp.members.values()].flatMap((m) => [...m.sessions.values()]);
765
767
  out.push(spaceHealth({
766
768
  name: sp.name, records: sp.ledger.all(), members: sp.allow.all(),
767
- online: [...sp.members.values()].filter((m) => m.sessions.size > 0).length, relayPkg: VERSION, now,
769
+ online: sessions.length, interactive: sessions.filter((x) => x.role === "interactive").length,
770
+ relayPkg: VERSION, now, relayAgeMs: now - this.startedAt,
768
771
  }));
769
772
  }
770
773
  let files = [];
@@ -783,7 +786,7 @@ button:hover{background:#1d4ed8}.err{color:var(--bad);margin-top:12px}
783
786
  members = new Allowlist(this.dataDir, safe).all();
784
787
  }
785
788
  catch { }
786
- out.push(spaceHealth({ name: safe, records: this.sleepingRecords(safe), members, online: 0, relayPkg: VERSION, now }));
789
+ out.push(spaceHealth({ name: safe, records: this.sleepingRecords(safe), members, online: 0, interactive: 0, relayPkg: VERSION, now, relayAgeMs: now - this.startedAt }));
787
790
  }
788
791
  return out;
789
792
  }
@@ -268,8 +268,17 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
268
268
  }
269
269
  const wiring = deps.applyAgentWiring ?? ((ap) => installForUser(ap).map((t) => ({ tool: t.tool, status: t.status })));
270
270
  try {
271
- const codex = wiring(a.approval).find((t) => t.tool === "codex");
272
- applied.push(`approval: ${approvalSummary(a.approval)}${codex ? ` — codex: ${codex.status}` : ""}`);
271
+ const rows = wiring(a.approval);
272
+ const failed = rows.filter((t) => /^failed/.test(t.status));
273
+ const codex = rows.find((t) => t.tool === "codex");
274
+ if (failed.length) {
275
+ for (const f of failed)
276
+ warnings.push(`${f.tool}: ${f.status} — that agent keeps its previous permissions until this is fixed; then run pantheon install --user`);
277
+ applied.push(`approval: "${a.approval}" is saved but NOT applied everywhere — see the warnings; nothing about it should be relied on yet`);
278
+ }
279
+ else {
280
+ applied.push(`approval: ${approvalSummary(a.approval)}${codex ? ` — codex: ${codex.status}` : ""}`);
281
+ }
273
282
  }
274
283
  catch (e) {
275
284
  warnings.push(`agent wiring not re-applied (${e.message}) — run pantheon install --user`);
package/dist/tray.js CHANGED
@@ -86,6 +86,7 @@ export async function trayStatus(cfg = load(), f = loadAll()) {
86
86
  return {
87
87
  ...first,
88
88
  space: ok.map((p) => p.space).join(" · "),
89
+ spaces: ok.map((p) => p.space),
89
90
  relay_ok: ok.every((p) => p.relay_ok),
90
91
  decisions_ok: ok.every((p) => p.decisions_ok),
91
92
  unavailable_spaces: ok.flatMap((p) => p.unavailable_spaces ?? []),
@@ -117,11 +118,11 @@ async function trayStatusOne(cfg) {
117
118
  const mineHuman = all.filter((o) => o.owner === `human:${cfg.member}`);
118
119
  const decisionsOk = ok;
119
120
  const decisions = mineHuman
120
- .map((o) => ({ id: o.id, body: decisionTitle(o.body), detail: o.body, prompt: decisionPrompt(o.id) }));
121
+ .map((o) => ({ id: o.id, body: decisionTitle(o.body), detail: o.body, prompt: decisionPrompt(o.id, decisionTitle(o.body)) }));
121
122
  const senderOf = (o) => stripCeremony(o.body).from;
122
123
  const row = (o, kind, who, prompt) => ({
123
124
  id: o.id, kind, title: decisionTitle(o.body), headline: decisionTitle(o.body, 200), detail: o.body, prompt, who,
124
- when: o.created_at ?? 0,
125
+ when: o.created_at ?? 0, space: cfg.space,
125
126
  ...(o.goal ? { goal: o.goal } : {}),
126
127
  ...(o.ref ? { ref: o.ref } : {}),
127
128
  });
@@ -130,7 +131,7 @@ async function trayStatusOne(cfg) {
130
131
  const items = [
131
132
  ...mineHuman.map((o) => {
132
133
  const from = senderOf(o);
133
- return row(o, "decision", !from || from === cfg.member ? "your agent" : from, decisionPrompt(o.id));
134
+ return row(o, "decision", !from || from === cfg.member ? "your agent" : from, decisionPrompt(o.id, decisionTitle(o.body)));
134
135
  }),
135
136
  ...owed.map((o) => row(o, "owed", "you", owedPrompt(decisionTitle(o.body), o.goal))),
136
137
  ...waiting.map((o) => {
@@ -149,6 +150,7 @@ async function trayStatusOne(cfg) {
149
150
  configured: true,
150
151
  version: VERSION,
151
152
  space: cfg.space,
153
+ spaces: [cfg.space],
152
154
  member: cfg.member,
153
155
  relay: cfg.relay,
154
156
  relay_ok: !!space,