@join-pantheon/cli 0.22.0 → 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/README.md CHANGED
@@ -57,6 +57,11 @@ pantheon start
57
57
  pantheon join pantheons://relay.joinpantheon.network/acme#C1qhJfdHBaPjuG3jF48vcqrP
58
58
  ```
59
59
 
60
+ On the hosted relay, `pantheon start` and `pantheon invite` also print the same invite as a link,
61
+ `https://www.joinpantheon.network/join/acme#C1qhJfdHBaPjuG3jF48vcqrP`. Pasted into a chat it
62
+ unfurls with the team's name and hands the recipient the line for their agent. The token stays in the
63
+ URL fragment, which browsers never send to any server. `pantheon join` accepts either form.
64
+
60
65
  Signed in, `pantheon start` puts your space on the hosted relay, so that link works from any
61
66
  network — no tunnel, no VPN. The space is named after the folder you run it from (your repo,
62
67
  usually); if another team already took that name, start picks `<name>-<you>` and says so. Choose
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import { randomBytes } from "node:crypto";
10
10
  import { readFileSync } from "node:fs";
11
11
  import { basename, dirname, join, resolve } from "node:path";
12
12
  import { PantheonClient } from "./client.js";
13
- import { fromInvite, load, save, toInvite, resolveFor, loadAll, repoInfo, bindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY, resolveForHook } from "./config.js";
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";
14
14
  import { fmtLimit } from "./plans.js";
15
15
  import { validEmail, validUsername } from "./accounts.js";
16
16
  import { install, launcher } from "./install.js";
@@ -18,6 +18,7 @@ import { Relay } from "./relay.js";
18
18
  import { runMcp } from "./mcp.js";
19
19
  import { Worker } from "./worker.js";
20
20
  import { DEFAULT_REMINDER_MS, ensureNativeNotifier, HumanNotifier, sendDesktopNotification } from "./notifier.js";
21
+ import { desktopUnavailable, hostEnv } from "./host-env.js";
21
22
  import { AGENT_SURFACES, applyProfileInit, cleanList, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile, wakeDefault } from "./profile.js";
22
23
  import { runGuide, terminalIO, parseSetupFlags, questionnaire } from "./setup-guide.js";
23
24
  import { selectActs, formatAct, resolveRef } from "./history-view.js";
@@ -375,8 +376,9 @@ ${B("first time — verify your email once")}
375
376
 
376
377
  ${B("getting going — two commands, from your project's repo if you have one")}
377
378
  pantheon start you: sets up this whole machine + one line to send your teammate
378
- pantheon join <line> them: that one line sets up their whole machine
379
- pantheon invite another single-use line, for a third person or a second machine
379
+ pantheon join <invite> them: the link (or the line) sets up their whole machine
380
+ pantheon invite another single-use invite, for a third person or a second machine
381
+ (--list shows the open ones; --revoke <token|all> cancels)
380
382
 
381
383
  Both take the setup answers as flags (--name, --title, --wake, --commands, … — pantheon setup
382
384
  --questions lists them), so an agent sets a person up in one command. No repo? Messages, questions
@@ -510,12 +512,21 @@ async function main() {
510
512
  process.exit(1);
511
513
  }
512
514
  if (r.invite) {
513
- console.error(`\n${B("Send your teammate this one line — it sets up their whole machine:")}\n`);
514
- console.error(` ${C(`pantheon join ${r.invite}`)}\n`);
515
+ const link = toJoinLink(fromInvite(r.invite));
516
+ if (link) {
517
+ console.error(`\n${B("Send your teammate this link — it unfurls in chat, and it sets up their whole machine:")}\n`);
518
+ console.error(` ${C(link)}\n`);
519
+ console.error(D(` The same invite as a command, for pasting straight into an agent:`));
520
+ console.error(` ${C(`pantheon join ${r.invite}`)}\n`);
521
+ }
522
+ else {
523
+ console.error(`\n${B("Send your teammate this one line — it sets up their whole machine:")}\n`);
524
+ console.error(` ${C(`pantheon join ${r.invite}`)}\n`);
525
+ }
515
526
  console.error(D(` From their project's repo if they have one — a repo is only needed to hand over code or to be woken. Single use, expires in ${Math.round(Number(flag("ttl", "1440")) / 60)}h.`));
516
527
  console.error(D(` Redeeming it admits them — no separate approval step. Need another? \`pantheon invite\``));
517
528
  console.error(D(`\n Not technical? Forward them this instead, and their agent does the rest:`));
518
- console.error(` ${D("Tell your agent: run")} ${C("curl -fsSL https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${r.invite}`)}`);
529
+ console.error(` ${D("Tell your agent: run")} ${C("curl -fsSL https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${link ?? r.invite}`)}`);
519
530
  }
520
531
  if (!r.reach.shareable && r.reach.hint) {
521
532
  console.error(`\n\x1b[33m That address only works on this machine.\x1b[0m`);
@@ -535,13 +546,43 @@ async function main() {
535
546
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
536
547
  await client.connect().catch((e) => die(client.pending ?? e.message));
537
548
  await new Promise((r) => setTimeout(r, 250));
549
+ const revokeAt = argv.indexOf("--revoke");
550
+ const revokeArg = revokeAt >= 0 ? (argv[revokeAt + 1] && !argv[revokeAt + 1].startsWith("--") ? argv[revokeAt + 1] : "all") : undefined;
551
+ if (argv.includes("--list") || revokeArg !== undefined) {
552
+ const open = await client.listInvites().catch((e) => die(e.message));
553
+ if (revokeArg !== undefined) {
554
+ const targets = revokeArg === "all" ? open.filter((i) => i.mine) : open.filter((i) => i.token === revokeArg || i.token.endsWith(revokeArg.replace(/^…/, "")));
555
+ if (!targets.length) {
556
+ client.close();
557
+ die(revokeArg === "all" ? "no open invites of yours to revoke" : `no open invite matches "${revokeArg}" — pantheon invite --list shows them`);
558
+ }
559
+ for (const i of targets) {
560
+ await client.revokeInvite(i.token).catch((e) => { client.close(); return die(e.message); });
561
+ console.log(`revoked …${i.token.slice(-6)} (minted by ${i.created_by}, would have expired ${new Date(i.expires_at).toLocaleString()})`);
562
+ }
563
+ client.close();
564
+ return;
565
+ }
566
+ if (!open.length)
567
+ console.log(D("no open invites"));
568
+ for (const i of open)
569
+ console.log(`${i.mine ? C(i.token) : D(i.token)} by ${i.created_by} expires ${new Date(i.expires_at).toLocaleString()}${i.may_wake ? " may wake" : ""}${i.mine ? "" : D(" (not yours — masked)")}`);
570
+ if (open.length)
571
+ console.log(D(`\nrevoke one: pantheon invite --revoke <token or its last 6 characters> · all of yours: pantheon invite --revoke all`));
572
+ client.close();
573
+ return;
574
+ }
538
575
  const ttl = Number(flag("ttl", "1440"));
539
576
  const mayWake = argv.includes("--may-wake") ? true : argv.includes("--no-wake") ? false : wakeDefault(loadProfile(cfg));
540
577
  const inv = await client.makeInvite(mayWake, ttl).catch((e) => die(e.message));
541
- console.log(`pantheon join ${toInvite({ ...cfg, token: inv.url })}`);
578
+ const raw = toInvite({ ...cfg, token: inv.url });
579
+ const link = toJoinLink({ relay: cfg.relay, space: cfg.space, token: inv.url });
580
+ console.log(`pantheon join ${raw}`);
581
+ if (link)
582
+ console.error(`\n${D("Or send this — it unfurls in chat with the team's name, and pantheon join accepts it too:")}\n ${C(link)}`);
542
583
  console.error(D(`\nSingle use, expires ${new Date(inv.expires_at).toLocaleString()}.`));
543
584
  console.error(D(`For a teammate who is not technical, forward this and their agent does the rest:`));
544
- console.error(` ${D("Tell your agent: run")} ${C("curl -fsSL https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${toInvite({ ...cfg, token: inv.url })}`)}`);
585
+ console.error(` ${D("Tell your agent: run")} ${C("curl -fsSL https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${link ?? raw}`)}`);
545
586
  client.close();
546
587
  return;
547
588
  }
@@ -616,7 +657,7 @@ async function main() {
616
657
  return;
617
658
  }
618
659
  case "join": {
619
- const invite = argv[1] ?? die("usage: pantheon join pantheon://host/space#token [setup answers as flags — pantheon setup --questions]");
660
+ const invite = argv[1] ?? die("usage: pantheon join <invite> (pantheons://host/space#token, or https://www.joinpantheon.network/join/<team>#<token>) [setup answers as flags — pantheon setup --questions]");
620
661
  const setupFlags = parseSetupFlags(argv);
621
662
  if (setupFlags.errors.length)
622
663
  die(setupFlags.errors.join("\n") + "\n pantheon setup --questions lists every answer");
@@ -674,8 +715,8 @@ async function main() {
674
715
  console.error(`${B("setting this machine up")} ${D("(user scope)")}\n`);
675
716
  const wired = wireUp({
676
717
  hook: !argv.includes("--no-hook"),
677
- notifications: !argv.includes("--no-notifications") && process.platform === "darwin",
678
- menubar: !argv.includes("--no-menubar") && process.platform === "darwin",
718
+ notifications: !argv.includes("--no-notifications"),
719
+ menubar: !argv.includes("--no-menubar"),
679
720
  unattended: wp.project,
680
721
  workerHow: wp.how,
681
722
  });
@@ -1088,7 +1129,8 @@ async function main() {
1088
1129
  console.log(` menu bar ${C("running")} ${menubar.managedBy === "launchd" ? `service ${menubar.label}` : "started by hand"} pid ${menubar.pid}`);
1089
1130
  }
1090
1131
  else {
1091
- console.log(` menu bar ${D("not running")} ${D("pantheon service install menubar — status and pause/resume in the menu bar")}`);
1132
+ const why = desktopUnavailable(hostEnv(), "the menu bar");
1133
+ console.log(` menu bar ${D("not running")} ${D(why ?? "pantheon service install menubar — status and pause/resume in the menu bar")}`);
1092
1134
  }
1093
1135
  if (notifier.running) {
1094
1136
  console.log(` notifier ${C("running")} ${notifier.managedBy === "launchd" ? `service ${notifier.label}` : "started by hand"} pid ${notifier.pid}`);
package/dist/client.js CHANGED
@@ -41,6 +41,7 @@ export class PantheonClient {
41
41
  plan = null;
42
42
  enroll = null;
43
43
  inviteUrl = null;
44
+ invitesList = null;
44
45
  constructor(cfg, onEvent, opts) {
45
46
  this.cfg = cfg;
46
47
  this.onEvent = onEvent;
@@ -138,6 +139,9 @@ export class PantheonClient {
138
139
  break;
139
140
  case "claimed":
140
141
  break;
142
+ case "invites":
143
+ this.invitesList = msg.invites;
144
+ break;
141
145
  case "invite":
142
146
  this.inviteUrl = { url: msg.url, expires_at: msg.expires_at };
143
147
  break;
@@ -417,6 +421,36 @@ export class PantheonClient {
417
421
  }
418
422
  });
419
423
  }
424
+ listInvites() {
425
+ if (!this.ws || this.ws.readyState !== 1)
426
+ return Promise.reject(new Error(`not connected to relay (${this.pending ?? this.lastError ?? "offline"})`));
427
+ return new Promise((res, rej) => {
428
+ this.invitesList = null;
429
+ const cid = `c${++this.cid}`;
430
+ const finish = () => { clearInterval(poll); clearTimeout(t); this.acks.delete(cid); };
431
+ const t = setTimeout(() => { finish(); rej(new Error("relay did not list the invites in 10s")); }, 10_000);
432
+ const poll = setInterval(() => { if (this.invitesList) {
433
+ finish();
434
+ const v = this.invitesList;
435
+ this.invitesList = null;
436
+ res(v);
437
+ } }, 40);
438
+ this.acks.set(cid, (v) => { if ("error" in v) {
439
+ finish();
440
+ rej(new Error(v.error));
441
+ } });
442
+ try {
443
+ this.ws?.send(JSON.stringify({ t: "invites", cid }));
444
+ }
445
+ catch (e) {
446
+ finish();
447
+ rej(e);
448
+ }
449
+ });
450
+ }
451
+ revokeInvite(token) {
452
+ return this.rpc({ t: "revoke_invite", token: token.replace(/^…/, "") });
453
+ }
420
454
  myObligations() {
421
455
  return this.obligations.filter((o) => o.owner === this.cfg.member || o.owner === `human:${this.cfg.member}`);
422
456
  }
package/dist/config.js CHANGED
@@ -275,17 +275,29 @@ export function toInvite(cfg) {
275
275
  const scheme = u.protocol === "wss:" ? "pantheons" : "pantheon";
276
276
  return `${scheme}://${u.host}/${encodeURIComponent(cfg.space)}#${cfg.token}`;
277
277
  }
278
+ export const JOIN_LINK_BASE = "https://www.joinpantheon.network/join";
279
+ const HOSTED_RELAY = "wss://relay.joinpantheon.network";
280
+ const JOIN_SLUG = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
281
+ export function toJoinLink(cfg) {
282
+ if (cfg.relay.replace(/\/+$/, "") !== HOSTED_RELAY || !JOIN_SLUG.test(cfg.space))
283
+ return null;
284
+ return `${JOIN_LINK_BASE}/${cfg.space}#${cfg.token}`;
285
+ }
278
286
  export function fromInvite(invite) {
279
287
  const clean = invite
280
288
  .replace(/\u001b\[[0-9;]*m/g, "")
281
289
  .replace(/[\u0000-\u001f\u007f]/g, "")
282
290
  .trim()
283
291
  .replace(/^['"`]|['"`]$/g, "")
284
- .replace(/^pantheon\s+join\s+/i, "")
285
292
  .trim();
286
- const m = /^(pantheons?):\/\/([^/]+)\/([^#]+)#(.+)$/.exec(clean);
293
+ const web = /https?:\/\/(?:www\.)?joinpantheon\.network\/join\/([^\s#?/"'`]+)(?:\?[^\s#"'`]*)?#([^\s"'`]+)/i.exec(clean);
294
+ if (web) {
295
+ const [, slug, token] = web;
296
+ return { relay: HOSTED_RELAY, space: decodeURIComponent(slug), token };
297
+ }
298
+ const m = /(pantheons?):\/\/([^/\s"'`]+)\/([^#\s"'`]+)#([^\s"'`]+)/.exec(clean);
287
299
  if (!m)
288
- throw new Error("not a pantheon invite — expected pantheon://host/space#token (or pantheons:// for TLS)");
300
+ throw new Error("not a pantheon invite — expected pantheons://host/space#token (pantheon:// without TLS), or https://www.joinpantheon.network/join/<team>#<token>");
289
301
  const [, scheme, host, space, token] = m;
290
302
  return {
291
303
  relay: `${scheme === "pantheons" ? "wss" : "ws"}://${host}`,
package/dist/doctor.js CHANGED
@@ -11,6 +11,7 @@ import { loadPersonas } from "./persona.js";
11
11
  import { workerProc } from "./machine.js";
12
12
  import { VERSION } from "./version.js";
13
13
  import { serviceInstalled } from "./service.js";
14
+ import { hostEnv } from "./host-env.js";
14
15
  const sh = (cmd, cwd) => {
15
16
  try {
16
17
  return execFileSync("sh", ["-lc", cmd], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
@@ -46,6 +47,13 @@ export async function doctor(projectDir) {
46
47
  major >= 22
47
48
  ? add("pass", "node", `v${process.versions.node}`)
48
49
  : add("fail", "node", `v${process.versions.node} is too old`, "pantheon needs Node 22 or newer (the relay uses node:sqlite) — install it and re-run");
50
+ const host = hostEnv();
51
+ if (host.ephemeral) {
52
+ add("warn", "machine", `${host.platform} in ${host.kind === "container" ? "a container" : "a cloud workspace"} — ${host.evidence.join("; ")}`, `this is not the computer you work on, which is why it reports ${host.platform} to someone who may be sitting at a Mac. The team and your account are fine here, but the menu bar, desktop alerts and the worker belong on your own machine — run the setup from a terminal there.`);
53
+ }
54
+ else {
55
+ add("pass", "machine", `${host.platform}${host.desktop ? " — menu bar and desktop alerts can run here" : " — no menu bar or desktop alerts (macOS only); use phone or chat delivery"}`);
56
+ }
49
57
  let cfg = null;
50
58
  try {
51
59
  cfg = load();
@@ -193,6 +201,11 @@ export async function doctor(projectDir) {
193
201
  add("warn", "wake rights", `${who}, but no worker is running here, so nothing can be woken`, "start one with `pantheon service install worker --project <a git repo>`; until then a peer's act waits for you to type");
194
202
  }
195
203
  }
204
+ if (host.desktop) {
205
+ serviceInstalled("menubar")
206
+ ? add("pass", "menu bar", "installed — status, pause/resume, and a badge when a decision needs you")
207
+ : add("warn", "menu bar", "not installed on a Mac that can run it", "install it with: pantheon service install menubar (needs the Xcode Command Line Tools — xcode-select --install — and a setup that ran on this computer, not in a cloud agent). --no-menubar at setup also leaves it off; that is fine if it was your choice");
208
+ }
196
209
  if (!hookStates.length && !hook.installed) {
197
210
  add("warn", "wake-up hook", "not installed — your agent will not notice a peer's message until you ask it to", "pantheon hook install --user (auto-detects claude / codex / cursor on PATH)");
198
211
  }
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,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/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
  });