@join-pantheon/cli 0.21.3 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/email.js CHANGED
@@ -1,9 +1,9 @@
1
1
  function subjectAndBody(code, purpose) {
2
2
  const action = purpose === "signup" ? "confirm your email and finish signing up" : "add this device to your account";
3
3
  const eyebrow = purpose === "signup" ? "Confirm your email" : "New device sign-in";
4
- const subject = `Your pantheon code: ${code}`;
4
+ const subject = `Your Pantheon code: ${code}`;
5
5
  const text = [
6
- `pantheon — ${eyebrow.toLowerCase()}`,
6
+ `Pantheon — ${eyebrow.toLowerCase()}`,
7
7
  ``,
8
8
  `Your verification code is ${code}.`,
9
9
  ``,
@@ -12,37 +12,61 @@ function subjectAndBody(code, purpose) {
12
12
  ``,
13
13
  `The code expires in 10 minutes and works once. If you did not request it,`,
14
14
  `ignore this email — nothing happens without the code.`,
15
+ ``,
16
+ `joinpantheon.network — the execution layer for companies`,
15
17
  ].join("\n");
16
- const mono = `ui-monospace,'SF Mono',Menlo,Consolas,'Liberation Mono',monospace`;
17
- const serif = `Georgia,'Iowan Old Style','Times New Roman',Times,serif`;
18
+ const paper = "#eae4da", card = "#f2ede5", stone = "#ddd4c3", ink = "#292927", faint = "#7c766d", gold = "#d8a33a";
19
+ const mono = `'SF Mono',Menlo,Consolas,'Liberation Mono',monospace`;
20
+ const serif = `Georgia,'Times New Roman',Times,serif`;
18
21
  const html = `<!doctype html>
19
22
  <html lang="en">
20
- <body style="margin:0;padding:0;background-color:#E9EDEC">
21
- <div style="display:none;max-height:0;overflow:hidden;mso-hide:all">Enter it to ${action} — expires in 10 minutes.&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;</div>
22
- <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#E9EDEC">
23
- <tr><td align="center" style="padding:32px 16px">
24
- <table role="presentation" width="440" cellpadding="0" cellspacing="0" style="max-width:440px;width:100%">
23
+ <head>
24
+ <meta charset="utf-8">
25
+ <meta name="viewport" content="width=device-width,initial-scale=1">
26
+ <meta name="color-scheme" content="light">
27
+ <meta name="supported-color-schemes" content="light">
28
+ <title>Your Pantheon code</title>
29
+ </head>
30
+ <body style="margin:0;padding:0;background:${paper}">
31
+ <div style="display:none;max-height:0;overflow:hidden;opacity:0">Your code is ${code}. It expires in 10 minutes and works once.&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;</div>
32
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:${paper};margin:0;padding:0">
33
+ <tr><td align="center" style="padding:40px 16px">
34
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:480px;width:100%">
25
35
 
26
- <tr><td style="padding:0 4px 14px;border-bottom:2px solid #101A1C">
27
- <span style="font-family:${mono};font-size:16px;font-weight:600;letter-spacing:.02em;color:#101A1C">pan<span style="color:#0E6F78">theon</span></span>
36
+ <tr><td align="left" style="padding:0 0 28px 0">
37
+ <img src="https://www.joinpantheon.network/email/mark.png" width="28" height="28" alt="Pantheon" style="display:inline-block;vertical-align:middle;border:0">
38
+ <span style="display:inline-block;vertical-align:middle;padding-left:10px;font-family:${serif};font-size:19px;letter-spacing:.01em;color:${ink}">Pantheon</span>
28
39
  </td></tr>
29
40
 
30
- <tr><td style="background-color:#F8FAF9;border:1px solid #C3CDCD;border-top:0;padding:28px 28px 24px">
31
- <table role="presentation" width="100%" cellpadding="0" cellspacing="0">
32
- <tr><td style="font-family:${mono};font-size:11px;font-weight:600;letter-spacing:.13em;text-transform:uppercase;color:#0E6F78;padding-bottom:14px">${eyebrow}</td></tr>
33
- <tr><td align="center" style="background-color:#DFE5E4;border:1px solid #C3CDCD;padding:18px 12px">
34
- <span style="font-family:${mono};font-size:34px;font-weight:700;letter-spacing:10px;color:#101A1C">${code}</span>
41
+ <tr><td style="background:${card};border:1px solid ${stone};border-radius:2px;padding:36px 32px 32px 32px">
42
+
43
+ <p style="margin:0 0 22px 0;font-family:${mono};font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:${faint}">${eyebrow}</p>
44
+
45
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
46
+ <tr><td align="center" style="background:${paper};border:1px solid ${stone};border-radius:2px;padding:22px 12px">
47
+ <span style="font-family:${mono};font-size:34px;line-height:1.1;letter-spacing:.22em;color:${ink};font-weight:600">${code}</span>
35
48
  </td></tr>
36
- <tr><td style="font-family:${serif};font-size:16px;line-height:1.55;color:#3B4A4D;padding:18px 0 12px">Enter it where the CLI is waiting, to ${action}:</td></tr>
37
- <tr><td style="background-color:#DFE5E4;border:1px solid #C3CDCD;padding:11px 14px;font-family:${mono};font-size:13px;color:#101A1C"><span style="color:#0E6F78">$</span>&nbsp; pantheon verify ${code}</td></tr>
38
- <tr><td style="padding-top:18px"></td></tr>
39
- <tr><td style="font-family:${mono};font-size:12px;line-height:1.7;color:#5A686A;padding-top:14px;border-top:1px solid #C3CDCD">Expires in 10 minutes. Works once.<br>Didn't request it? Ignore this email — nothing happens without the code.</td></tr>
40
49
  </table>
50
+
51
+ <p style="margin:24px 0 14px 0;font-family:${serif};font-size:16px;line-height:1.55;color:${ink}">Enter it where the CLI is waiting, to ${action}:</p>
52
+
53
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
54
+ <tr><td style="background:${paper};border-left:2px solid ${gold};padding:13px 16px">
55
+ <span style="font-family:${mono};font-size:14px;color:${ink}">pantheon verify ${code}</span>
56
+ </td></tr>
57
+ </table>
58
+
59
+ <p style="margin:26px 0 0 0;padding-top:20px;border-top:1px solid ${stone};font-family:${serif};font-size:14px;line-height:1.6;color:${faint}">
60
+ Expires in 10 minutes. Works once.<br>
61
+ Didn't request it? Ignore this email — nothing happens without the code.
62
+ </p>
63
+
41
64
  </td></tr>
42
65
 
43
- <tr><td align="center" style="padding:16px 4px;font-family:${mono};font-size:11px;letter-spacing:.09em;color:#5A686A">
44
- <a href="https://joinpantheon.network" style="color:#5A686A;text-decoration:none;border-bottom:1px solid #9AA8A8">joinpantheon.network</a>
45
- &nbsp;&nbsp;·&nbsp;&nbsp;your agents, one space
66
+ <tr><td align="left" style="padding:22px 2px 0 2px;font-family:${mono};font-size:11.5px;letter-spacing:.04em;color:${faint}">
67
+ <a href="https://www.joinpantheon.network" style="color:${faint};text-decoration:underline">joinpantheon.network</a>
68
+ <span style="color:${gold};padding:0 8px">&middot;</span>
69
+ The execution layer for companies
46
70
  </td></tr>
47
71
 
48
72
  </table>
@@ -0,0 +1,51 @@
1
+ export function selectActs(acts, opts = {}) {
2
+ const kept = acts.filter((a) => (opts.presence || (a.act !== "JOIN" && a.act !== "LEAVE"))
3
+ && (opts.goal === undefined || a.goal === opts.goal)
4
+ && (opts.from === undefined || a.from === opts.from)
5
+ && (opts.since === undefined || a.seq > opts.since));
6
+ const n = opts.last ?? 20;
7
+ return kept.slice(Math.max(0, kept.length - n));
8
+ }
9
+ const when = (ts) => new Date(ts).toISOString().slice(0, 19).replace("T", " ") + "Z";
10
+ export function formatAct(a, opts = {}) {
11
+ const who = `${a.from}${a.role && a.role !== "interactive" ? ` (${a.role})` : ""}`;
12
+ const head = `[${a.seq}] ${a.act} from ${who}${a.to?.length ? ` to ${a.to.join(",")}` : ""} ${when(a.ts)} id=${a.id}${a.ref ? ` ref=${a.ref}` : ""}${a.goal ? ` goal=${a.goal}${opts.goalTitle ? ` "${opts.goalTitle}"` : ""}` : ""}`;
13
+ if (opts.short)
14
+ return `${head}\n ${(a.summary ?? a.body).split("\n")[0].slice(0, 160)}`;
15
+ const meta = a.meta && Object.keys(a.meta).length ? `\n meta: ${JSON.stringify(a.meta)}` : "";
16
+ const summary = a.summary ? `\n >> ${a.summary}` : "";
17
+ return `${head}${summary}\n${a.body.split("\n").map((l) => ` ${l}`).join("\n")}${meta}`;
18
+ }
19
+ const REPLIES_TO = {
20
+ ANSWER: ["ASK", "ESCALATE", "HUMAN_REQUIRED"],
21
+ ACCEPT: ["DELIVERED", "PROPOSE", "CONTRACT"],
22
+ OBJECT: ["DELIVERED", "PROPOSE", "CONTRACT"],
23
+ };
24
+ export function resolveRef(acts, ref, act, goal, me) {
25
+ const inGoal = (a) => goal === null || a.goal === goal;
26
+ if (ref && /^a_/.test(ref)) {
27
+ const hit = acts.find((a) => a.id === ref);
28
+ return { id: ref, goal: hit?.goal ?? goal };
29
+ }
30
+ if (ref && /^\d+$/.test(ref)) {
31
+ const hit = acts.find((a) => a.seq === Number(ref));
32
+ return hit ? { id: hit.id, goal: hit.goal ?? goal, note: `replying to [${hit.seq}] ${hit.act} from ${hit.from}` } : { id: null, goal, error: `no act with sequence ${ref} in what this relay still holds` };
33
+ }
34
+ const others = acts.filter((a) => a.from !== me && a.act !== "JOIN" && a.act !== "LEAVE" && inGoal(a));
35
+ if (ref === "last") {
36
+ const hit = others[others.length - 1];
37
+ return hit ? { id: hit.id, goal: hit.goal ?? goal, note: `replying to [${hit.seq}] ${hit.act} from ${hit.from}` } : { id: null, goal, error: "nothing from a peer to reply to" };
38
+ }
39
+ if (ref)
40
+ return { id: ref, goal };
41
+ const wants = REPLIES_TO[act];
42
+ if (!wants)
43
+ return { id: null, goal };
44
+ for (const kind of wants) {
45
+ const hit = [...others].reverse().find((a) => a.act === kind && (!a.to?.length || a.to.includes(me)));
46
+ if (hit)
47
+ return { id: hit.id, goal: hit.goal ?? goal, note: `replying to [${hit.seq}] ${hit.act} from ${hit.from} (pass --ref to pick another)` };
48
+ }
49
+ return { id: null, goal, error: `${act} must reference the act it answers — none from a peer found${goal ? ` in ${goal}` : ""}. See pantheon history, then --ref <id or sequence number>` };
50
+ }
51
+ //# sourceMappingURL=history-view.js.map
@@ -0,0 +1,57 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ const CLOUD_VARS = [
3
+ ["CODESPACES", "GitHub Codespaces ($CODESPACES)"],
4
+ ["REMOTE_CONTAINERS", "a VS Code dev container ($REMOTE_CONTAINERS)"],
5
+ ["REMOTE_CONTAINERS_IPC", "a VS Code dev container ($REMOTE_CONTAINERS_IPC)"],
6
+ ["DEVCONTAINER", "a dev container ($DEVCONTAINER)"],
7
+ ["GITPOD_WORKSPACE_ID", "a Gitpod workspace ($GITPOD_WORKSPACE_ID)"],
8
+ ["CLOUD_SHELL", "a cloud shell ($CLOUD_SHELL)"],
9
+ ];
10
+ export function hostEnv(env = process.env, plat = process.platform, exists = existsSync, read = (p) => readFileSync(p, "utf8")) {
11
+ if (plat === "darwin") {
12
+ return { platform: plat, kind: "local", evidence: [], ephemeral: false, desktop: true };
13
+ }
14
+ const evidence = [];
15
+ let kind = "local";
16
+ for (const [name, label] of CLOUD_VARS) {
17
+ if (env[name]) {
18
+ evidence.push(label);
19
+ kind = "cloud-workspace";
20
+ }
21
+ }
22
+ if (exists("/.dockerenv"))
23
+ evidence.push("/.dockerenv exists");
24
+ if (exists("/run/.containerenv"))
25
+ evidence.push("/run/.containerenv exists (podman)");
26
+ let cgroup = "";
27
+ try {
28
+ cgroup = exists("/proc/1/cgroup") ? read("/proc/1/cgroup") : "";
29
+ }
30
+ catch { }
31
+ if (/docker|containerd|kubepods|lxc/.test(cgroup))
32
+ evidence.push("pid 1 is in a container cgroup (/proc/1/cgroup)");
33
+ if (evidence.length && kind === "local")
34
+ kind = "container";
35
+ return {
36
+ platform: plat,
37
+ kind,
38
+ evidence,
39
+ ephemeral: kind !== "local",
40
+ desktop: false,
41
+ };
42
+ }
43
+ export function ephemeralWarning(host) {
44
+ return `this looks like ${host.kind === "container" ? "a container" : "a cloud workspace"}, not the computer you work on `
45
+ + `(${host.evidence.join("; ")}). That is why this machine reports ${host.platform}: your editor's agent is running `
46
+ + `somewhere else, so the menu bar and desktop alerts cannot be installed, and anything installed here disappears with `
47
+ + `the session. Your account and team are fine — run the same setup from a terminal on your own computer to finish it.`;
48
+ }
49
+ export function desktopUnavailable(host, what) {
50
+ if (host.desktop)
51
+ return null;
52
+ if (host.ephemeral) {
53
+ return `not available — ${what} ${what === "desktop alerts" ? "need" : "needs"} your own computer, and this is ${host.kind === "container" ? "a container" : "a cloud workspace"} reporting ${host.platform}`;
54
+ }
55
+ return `not available — ${what} ${host.platform === "darwin" ? "could not be built here" : `${what === "desktop alerts" ? "are" : "is"} macOS-only, and this machine is ${host.platform}`}`;
56
+ }
57
+ //# sourceMappingURL=host-env.js.map
package/dist/mcp.js CHANGED
@@ -9,7 +9,7 @@ import { VERSION } from "./version.js";
9
9
  import { backendFromLabel } from "./agents.js";
10
10
  import { agentHookStates } from "./notify.js";
11
11
  import { reportMcpBusy, reportMcpIdle } from "./mcp-activity.js";
12
- import { loadProfile, organizationProfile, profileInitialized, routeDecision } from "./profile.js";
12
+ import { loadProfile, organizationProfile, profileInitialized, routeDecision, reservedDecisionsNote } from "./profile.js";
13
13
  import { identityLine, renderBrief } from "./brief.js";
14
14
  import { recentRuns } from "./spend.js";
15
15
  const ACT_HELP = {
@@ -60,6 +60,7 @@ YOU WERE WOKEN BY A PEER. No human is present in this session.
60
60
  `
61
61
  : "";
62
62
  const profile = loadProfile(cfg);
63
+ const reserved = reservedDecisionsNote(profile);
63
64
  const identity = profileInitialized(profile)
64
65
  ? ` You work as ${identityLine({
65
66
  member: cfg.member,
@@ -87,7 +88,8 @@ RULES
87
88
  The organization directory — not job-title guesswork — determines whose human or agent owns it.
88
89
  When a peer's human personally must decide and their agent may be offline, use HUMAN_REQUIRED and name that one peer in to.
89
90
  Do not use HUMAN_REQUIRED instead of ASK for something their delegate can answer. Their delegate settles it with ANSWER.
90
- ESCALATE and HUMAN_REQUIRED outlive a goal close until the named human has actually answered.
91
+ ESCALATE and HUMAN_REQUIRED outlive a goal close until the named human has actually answered.${reserved ? `
92
+ - ${reserved}` : ""}
91
93
  - Agree the interface BEFORE writing code that crosses it: PROPOSE, then CONTRACT once accepted.
92
94
  - Answer every ASK addressed to you. An unanswered ASK blocks a peer and eventually stalls the goal.
93
95
  - ACCEPT means you actually checked. If something is wrong, OBJECT with the reason.
package/dist/profile.js CHANGED
@@ -33,6 +33,14 @@ export function defaultProfile(cfg) {
33
33
  updated_at: 0,
34
34
  };
35
35
  }
36
+ export function reservedDecisionsNote(profile) {
37
+ const mine = Object.entries(profile.authority).filter(([, m]) => m === "human_required").map(([d]) => d);
38
+ if (!mine.length)
39
+ return "";
40
+ return `YOUR HUMAN DECIDES THESE PERSONALLY: ${mine.join(", ")}. If a peer asks you to settle one — approve a
41
+ deploy, agree a price, sign off a release — do not answer it yourself, however well you have checked the
42
+ work. Say the review result if you have one, then ESCALATE and stop; tell the peer it is with your human.`;
43
+ }
36
44
  export function wakeDefault(profile) {
37
45
  return (profile.agent.wake_default ?? "anyone") === "anyone";
38
46
  }
package/dist/protocol.js CHANGED
@@ -132,6 +132,15 @@ export const MakeInvite = z.object({
132
132
  may_wake: z.boolean().default(true),
133
133
  ttl_minutes: z.number().int().min(1).max(10080).default(60),
134
134
  });
135
+ export const ListInvites = z.object({
136
+ t: z.literal("invites"),
137
+ cid: z.string().max(64).optional(),
138
+ });
139
+ export const RevokeInvite = z.object({
140
+ t: z.literal("revoke_invite"),
141
+ cid: z.string().max(64).optional(),
142
+ token: z.string().min(6).max(64),
143
+ });
135
144
  export const Admit = z.object({
136
145
  t: z.literal("admit"),
137
146
  cid: z.string().max(64).optional(),
@@ -183,7 +192,7 @@ export const WorkerState = z.object({
183
192
  detail: z.string().max(200).optional(),
184
193
  until: z.number().int().positive().optional(),
185
194
  });
186
- export const Inbound = z.discriminatedUnion("t", [Hello, SendAct, OpenGoal, Claim, Admit, MakeInvite, PublishProfile, BindRepo, DeleteSpace, WorkerState, Ping]);
195
+ export const Inbound = z.discriminatedUnion("t", [Hello, SendAct, OpenGoal, Claim, Admit, MakeInvite, ListInvites, RevokeInvite, PublishProfile, BindRepo, DeleteSpace, WorkerState, Ping]);
187
196
  export function shouldDeferToHuman(sessions, work, opts = {}) {
188
197
  const now = opts.now ?? Date.now();
189
198
  const idleMs = opts.idleMs ?? LIMITS.workerIdleMs;
@@ -12,6 +12,7 @@ import { detectAvailable, agentById } from "./agents.js";
12
12
  import { installService } from "./service.js";
13
13
  import { ensureNativeNotifier } from "./notifier.js";
14
14
  import { ensureTrayBinary, writeTrayCommands } from "./tray.js";
15
+ import { desktopUnavailable, ephemeralWarning, hostEnv } from "./host-env.js";
15
16
  import { existsSync } from "node:fs";
16
17
  import { grant as grantCap, loadPolicy, policyPath, savePolicy } from "./worker-policy.js";
17
18
  import { Relay } from "./relay.js";
@@ -27,9 +28,12 @@ export function wireUp(opts, deps = {}) {
27
28
  const installSvc = deps.installService ?? installService;
28
29
  const ensureNotifier = deps.ensureNotifier ?? ensureNativeNotifier;
29
30
  const ensureTray = deps.ensureTray ?? ensureTrayBinary;
31
+ const host = deps.host ?? hostEnv();
30
32
  const tools = installForUser().map((t) => ({ tool: t.tool, status: t.status }));
31
33
  const granted = [];
32
34
  const warnings = [];
35
+ if (host.ephemeral)
36
+ warnings.push(ephemeralWarning(host));
33
37
  if (opts.hook) {
34
38
  try {
35
39
  const { agents } = installHooks("user");
@@ -47,7 +51,10 @@ export function wireUp(opts, deps = {}) {
47
51
  else {
48
52
  tools.push({ tool: "wake-up hook", status: "skipped (--no-hook) — a peer's message waits until you type" });
49
53
  }
50
- if (opts.notifications) {
54
+ if (opts.notifications && !host.desktop) {
55
+ tools.push({ tool: "notifications", status: desktopUnavailable(host, "desktop alerts") });
56
+ }
57
+ else if (opts.notifications) {
51
58
  try {
52
59
  ensureNotifier();
53
60
  installSvc("notifier", homedir(), []);
@@ -61,7 +68,10 @@ export function wireUp(opts, deps = {}) {
61
68
  else {
62
69
  tools.push({ tool: "notifications", status: "off (--no-notifications) — human escalations only appear in pantheon status" });
63
70
  }
64
- if (opts.menubar) {
71
+ if (opts.menubar && !host.desktop) {
72
+ tools.push({ tool: "menu bar", status: desktopUnavailable(host, "the menu bar") });
73
+ }
74
+ else if (opts.menubar) {
65
75
  try {
66
76
  ensureTray();
67
77
  writeTrayCommands();
@@ -241,7 +251,7 @@ export async function start(opts) {
241
251
  const wired = wireUp({
242
252
  hook: opts.hook !== false,
243
253
  notifications: opts.notifications !== false,
244
- menubar: opts.menubar !== false && process.platform === "darwin",
254
+ menubar: opts.menubar !== false,
245
255
  unattended: opts.unattended ?? null,
246
256
  ...(opts.workerHow ? { workerHow: opts.workerHow } : {}),
247
257
  });
@@ -290,7 +300,7 @@ export async function join(opts) {
290
300
  const wired = wireUp({
291
301
  hook: opts.hook !== false,
292
302
  notifications: opts.notifications !== false,
293
- menubar: opts.menubar !== false && process.platform === "darwin",
303
+ menubar: opts.menubar !== false,
294
304
  unattended: opts.unattended ?? null,
295
305
  ...(opts.workerHow ? { workerHow: opts.workerHow } : {}),
296
306
  });