@join-pantheon/cli 0.29.1 → 0.30.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.
Files changed (46) hide show
  1. package/README.md +45 -22
  2. package/bin/pantheon.js +12 -0
  3. package/dist/accounts.js +151 -26
  4. package/dist/agents.js +197 -14
  5. package/dist/claude-confine.js +283 -0
  6. package/dist/cli.js +725 -120
  7. package/dist/client.js +37 -2
  8. package/dist/command-state.js +79 -0
  9. package/dist/config.js +211 -10
  10. package/dist/doctor.js +103 -13
  11. package/dist/grants.js +1 -1
  12. package/dist/history-view.js +2 -1
  13. package/dist/home-guard.js +210 -0
  14. package/dist/hooks-install.js +2 -1
  15. package/dist/identity.js +24 -0
  16. package/dist/install.js +76 -26
  17. package/dist/kiro-files.js +2 -2
  18. package/dist/ledger.js +111 -28
  19. package/dist/machine.js +494 -32
  20. package/dist/mcp-supervisor.js +25 -6
  21. package/dist/mcp-windows.js +47 -25
  22. package/dist/mcp.js +25 -10
  23. package/dist/network.js +39 -8
  24. package/dist/notify.js +17 -6
  25. package/dist/pending.js +8 -14
  26. package/dist/persona.js +21 -0
  27. package/dist/profile.js +4 -1
  28. package/dist/quickstart.js +82 -14
  29. package/dist/reads.js +30 -0
  30. package/dist/refusal.js +14 -1
  31. package/dist/relay.js +1143 -180
  32. package/dist/roster-file.js +169 -23
  33. package/dist/run-failure.js +83 -3
  34. package/dist/secret-guard.js +141 -0
  35. package/dist/service.js +84 -34
  36. package/dist/setup-guide.js +28 -9
  37. package/dist/spend.js +6 -2
  38. package/dist/store.js +94 -10
  39. package/dist/text-file.js +74 -0
  40. package/dist/tray.js +11 -2
  41. package/dist/update-check.js +2 -0
  42. package/dist/which.js +69 -12
  43. package/dist/worker-policy.js +137 -3
  44. package/dist/worker.js +702 -81
  45. package/helper/macos/menubar.swift +15 -1
  46. package/package.json +2 -2
@@ -2,11 +2,11 @@ import { execFileSync } from "node:child_process";
2
2
  import { randomBytes } from "node:crypto";
3
3
  import { createServer } from "node:net";
4
4
  import { basename, join as pathJoin } from "node:path";
5
- import { homedir, hostname, platform } from "node:os";
5
+ import { homedir, hostname, platform, userInfo } from "node:os";
6
6
  import { lookup } from "node:dns/promises";
7
7
  import { describeNetError } from "./network.js";
8
8
  import { PantheonClient } from "./client.js";
9
- import { load, identity, save, toInvite, resolveFor, repoInfo, bindRepo, DEFAULT_RELAY } from "./config.js";
9
+ import { load, loadAll, identity, save, toInvite, resolveFor, repoInfo, bindRepo, DEFAULT_RELAY } from "./config.js";
10
10
  import { loadProfile, wakeDefault } from "./profile.js";
11
11
  import { newKeypair } from "./identity.js";
12
12
  import { installForUser } from "./install.js";
@@ -29,6 +29,9 @@ const sh = (cmd) => {
29
29
  return null;
30
30
  }
31
31
  };
32
+ const notLoaded = (r) => r && typeof r === "object" && r.loaded === false
33
+ ? String(r.reason ?? "the service manager did not load it")
34
+ : null;
32
35
  export function wireUp(opts, deps = {}) {
33
36
  const installSvc = deps.installService ?? installService;
34
37
  const ensureNotifier = deps.ensureNotifier ?? ensureNativeNotifier;
@@ -63,9 +66,14 @@ export function wireUp(opts, deps = {}) {
63
66
  else if (opts.notifications) {
64
67
  try {
65
68
  ensureNotifier();
66
- installSvc("notifier", homedir(), []);
69
+ const why = notLoaded(installSvc("notifier", homedir(), []));
67
70
  writeTrayCommands();
68
- tools.push({ tool: "notifications", status: "on — native alerts for decisions that require you" });
71
+ if (why) {
72
+ tools.push({ tool: "notifications", status: `installed, not running: ${why}` });
73
+ warnings.push(`human notifications are installed but not running (${why}). Try \`pantheon service restart notifier\``);
74
+ }
75
+ else
76
+ tools.push({ tool: "notifications", status: "on — native alerts for decisions that require you" });
69
77
  }
70
78
  catch (e) {
71
79
  tools.push({ tool: "notifications", status: `could not install: ${e.message}` });
@@ -82,8 +90,13 @@ export function wireUp(opts, deps = {}) {
82
90
  try {
83
91
  ensureTray();
84
92
  writeTrayCommands();
85
- installSvc("menubar", homedir(), []);
86
- tools.push({ tool: "menu bar", status: "on — status, pause/resume, and decision badge in your menu bar" });
93
+ const why = notLoaded(installSvc("menubar", homedir(), []));
94
+ if (why) {
95
+ tools.push({ tool: "menu bar", status: `installed, not running: ${why}` });
96
+ warnings.push(`the menu bar item is installed but not running (${why}). Try \`pantheon service restart menubar\``);
97
+ }
98
+ else
99
+ tools.push({ tool: "menu bar", status: "on — status, pause/resume, and decision badge in your menu bar" });
87
100
  }
88
101
  catch (e) {
89
102
  tools.push({ tool: "menu bar", status: `could not install: ${e.message}` });
@@ -117,9 +130,14 @@ export function wireUp(opts, deps = {}) {
117
130
  }
118
131
  else {
119
132
  try {
120
- installSvc("worker", project, []);
133
+ const why = notLoaded(installSvc("worker", project, []));
134
+ if (why) {
135
+ tools.push({ tool: "worker", status: `installed, not running: ${why}` });
136
+ warnings.push(`the worker is installed but not running (${why}) — this machine cannot answer the team while you are away. Try \`pantheon service restart worker\``);
137
+ return { tools, granted, warnings };
138
+ }
121
139
  tools.push({ tool: "worker", status: `on — answers peers while you are away (${project})` });
122
- granted.push(`A peer you have granted may_wake can start an agent run on this machine, in an isolated worktree under ${project}.`);
140
+ granted.push(`A teammate you approve from this machine (pantheon admit <name> --may-wake) — or anyone on the team, if you choose wake "anyone" — can start an agent run here, in an isolated worktree under ${project}. Nobody can until then.`);
123
141
  if (!existsSync(policyPath())) {
124
142
  const pol = loadPolicy();
125
143
  grantCap(pol, "self_upgrade", "always", "setup-default");
@@ -151,6 +169,47 @@ export function slugName(raw, fallback) {
151
169
  .replace(/^[-._]+|[-._]+$/g, "");
152
170
  return /[a-z0-9]/.test(s) ? s : fallback;
153
171
  }
172
+ export function osUsername(env = process.env, info = () => userInfo().username) {
173
+ const tries = [info, () => env.USERNAME, () => env.USER];
174
+ for (const t of tries) {
175
+ try {
176
+ const v = t()?.trim();
177
+ if (v && v.toLowerCase() !== "root")
178
+ return v;
179
+ }
180
+ catch { }
181
+ }
182
+ return null;
183
+ }
184
+ export function defaultMemberName(env = process.env, info, host = hostname) {
185
+ const user = osUsername(env, info);
186
+ const random = `member-${randomBytes(3).toString("hex")}`;
187
+ if (user) {
188
+ const s = slugName(user, "");
189
+ if (s)
190
+ return s;
191
+ }
192
+ let h = "";
193
+ try {
194
+ h = host().split(".")[0] ?? "";
195
+ }
196
+ catch { }
197
+ const s = slugName(h, "");
198
+ return s && s !== "localhost" ? `${s}` : random;
199
+ }
200
+ export function deviceLabel(plat = platform(), env = process.env, info, host = hostname) {
201
+ const os = plat === "darwin" ? "Mac" : plat === "win32" ? "Windows" : plat === "linux" ? "Linux" : plat;
202
+ let who = osUsername(env, info);
203
+ if (!who) {
204
+ try {
205
+ who = host().split(".")[0] || null;
206
+ }
207
+ catch {
208
+ who = null;
209
+ }
210
+ }
211
+ return `${who ?? "a"}'s ${os}`;
212
+ }
154
213
  export function teamNameFor(dir, repo = repoInfo(dir)) {
155
214
  const fromOrigin = repo?.origin ? (repo.origin.replace(/\.git$/i, "").split(/[/:]/).filter(Boolean).pop() ?? "") : "";
156
215
  return slugName(fromOrigin || basename(repo?.root ?? dir), "team");
@@ -302,7 +361,7 @@ export async function start(opts) {
302
361
  ` Use a tailnet address (tailscale ip -4) or a tunnel, then re-run pantheon start.` }
303
362
  : { url: chosen, how: place === "this-machine" ? "this machine's address" : place === "tunnel" ? "a tunnel to this machine" : opts.relay ? "as given" : "your signed-in relay", shareable: true })
304
363
  : findReach(opts.port);
305
- const nothing = (why) => ({ cfg: priorCfg ?? { relay: reach.url }, invite: null, claimed: false, reach, tools: [], granted: [], relay: null, warnings: [why] });
364
+ const nothing = (why) => ({ cfg: priorCfg ?? { relay: reach.url }, invite: null, inviteMayWake: false, claimed: false, reach, tools: [], granted: [], relay: null, warnings: [why] });
306
365
  let relay = null;
307
366
  if (chosen && !hostHere) {
308
367
  const probe = await probeRelay(reach.url, opts.remoteTimeoutMs ?? 10_000, opts.fetchImpl);
@@ -345,7 +404,7 @@ export async function start(opts) {
345
404
  relay: reach.url,
346
405
  space: opts.space ?? teamNameFor(process.cwd(), repo),
347
406
  token: randomBytes(16).toString("hex"),
348
- member: opts.me ?? (keepIdentity ? existing.member : slugName(process.env.USER ?? "me", "me")),
407
+ member: opts.me ?? (keepIdentity ? existing.member : defaultMemberName()),
349
408
  agent: detectAgent(),
350
409
  ...(keepIdentity ? { publicKey: existing.publicKey, privateKey: existing.privateKey } : newKeypair()),
351
410
  ...(keepIdentity && existing.account ? { account: existing.account } : {}),
@@ -359,6 +418,10 @@ export async function start(opts) {
359
418
  warnings.push(`you are already "${cfg.member}" here; --me ${opts.me} ignored (use --reset to start over)`);
360
419
  }
361
420
  }
421
+ else if (keepIdentity && existing.space) {
422
+ opts.log(` you ${cfg.member}, as in "${existing.space}" — that team stays; this adds one`);
423
+ opts.log(` default "${cfg.space}" from now on for folders bound to no team — pantheon use ${existing.space} switches back`);
424
+ }
362
425
  else if (keepIdentity && existing.account) {
363
426
  opts.log(` you ${cfg.member} — signed in; creating your first team`);
364
427
  }
@@ -366,6 +429,7 @@ export async function start(opts) {
366
429
  warnings.push(`replacing your identity in "${existing.space}" — that space is no longer reachable from this machine`);
367
430
  }
368
431
  let invite = null;
432
+ let inviteMayWake = false;
369
433
  let claimed = false;
370
434
  const claim = async (c) => {
371
435
  const client = new PantheonClient(c);
@@ -379,13 +443,17 @@ export async function start(opts) {
379
443
  }
380
444
  if (repo && c.spaceId) {
381
445
  const { replaced } = bindRepo(repo, c.spaceId);
382
- if (replaced)
383
- warnings.push(`this repo was bound to another team (${replaced.space}); it now belongs to "${c.space}"`);
446
+ const was = replaced ?? (here.binding && here.binding.space !== c.spaceId ? here.binding : null);
447
+ if (was) {
448
+ const name = Object.values(loadAll()?.spaces ?? {}).find((m) => m.id === was.space)?.name ?? was.space;
449
+ warnings.push(`this repo was bound to "${name}"; it now belongs to "${c.space}" — agents opened here speak in "${c.space}" from now on (pantheon use ${name} --here moves it back)`);
450
+ }
384
451
  }
385
452
  opts.log(` space ${c.space} ${reuse ? "— reconnected" : "— created, you own it"}${repo ? ` ${repo.origin ?? "local repo"} → bound to it` : ""}`);
386
453
  const mayWake = opts.wake ? opts.wake === "anyone" : wakeDefault(loadProfile(c));
387
454
  const inv = await client.makeInvite(mayWake, opts.ttlMinutes);
388
455
  invite = toInvite({ ...c, token: inv.url });
456
+ inviteMayWake = mayWake;
389
457
  claimed = true;
390
458
  }
391
459
  finally {
@@ -426,7 +494,7 @@ export async function start(opts) {
426
494
  ...(opts.workerHow ? { workerHow: opts.workerHow } : {}),
427
495
  });
428
496
  warnings.push(...wired.warnings);
429
- return { cfg, invite, claimed, reach, tools: wired.tools, granted: wired.granted, relay, warnings };
497
+ return { cfg, invite, inviteMayWake, claimed, reach, tools: wired.tools, granted: wired.granted, relay, warnings };
430
498
  }
431
499
  export async function join(opts) {
432
500
  const existing = opts.reset ? null : (load() ?? identity());
@@ -441,7 +509,7 @@ export async function join(opts) {
441
509
  relay: opts.invite.relay,
442
510
  space: opts.invite.space,
443
511
  token: "",
444
- member: opts.me ?? existing?.member ?? slugName(process.env.USER ?? "me", "me"),
512
+ member: opts.me ?? existing?.member ?? defaultMemberName(),
445
513
  agent: detectAgent(),
446
514
  ...keys,
447
515
  ...(existing?.account ? { account: existing.account } : {}),
package/dist/reads.js ADDED
@@ -0,0 +1,30 @@
1
+ import { signRead } from "./identity.js";
2
+ let clockSkewMs = 0;
3
+ export const relayClockSkew = () => clockSkewMs;
4
+ export function learnRelayClock(res) {
5
+ const there = Date.parse(res.headers.get("date") ?? "");
6
+ if (!Number.isFinite(there) || Math.abs(there - Date.now() - clockSkewMs) <= 60_000)
7
+ return false;
8
+ clockSkewMs = there - Date.now();
9
+ return true;
10
+ }
11
+ export const httpBase = (relay) => relay.replace(/^ws/, "http").replace(/\/+$/, "");
12
+ export function signedReadQuery(cfg, path, params, at) {
13
+ const full = { space: cfg.space, token: cfg.token };
14
+ for (const [k, v] of Object.entries(params))
15
+ if (v !== undefined)
16
+ full[k] = String(v);
17
+ full.reader = cfg.member;
18
+ full.ts = String(at);
19
+ const sig = signRead(cfg.privateKey, "GET", path, full);
20
+ return new URLSearchParams({ ...full, sig }).toString();
21
+ }
22
+ export async function signedRead(cfg, path, params = {}, opts = {}) {
23
+ const signal = opts.signal ?? (opts.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined);
24
+ const go = () => fetch(`${httpBase(cfg.relay)}${path}?${signedReadQuery(cfg, path, params, Date.now() + clockSkewMs)}`, signal ? { signal } : {});
25
+ const res = await go();
26
+ if (res.status === 403 && learnRelayClock(res))
27
+ return go();
28
+ return res;
29
+ }
30
+ //# sourceMappingURL=reads.js.map
package/dist/refusal.js CHANGED
@@ -21,9 +21,22 @@ export function connectFailure(e, relay) {
21
21
  const r = refusalOf(e);
22
22
  if (r) {
23
23
  const fix = r.fix && !/pantheon \w/.test(r.message) ? `\n ${r.fix}` : "";
24
- return `the relay refused this machine (${r.code}): ${r.message}${fix}`;
24
+ return `the relay refused this machine (${r.code}): ${r.message}${fix}${signInHint(r.code, relay)}`;
25
25
  }
26
26
  const text = typeof e === "string" ? e : String(e?.message ?? e ?? "");
27
27
  return `cannot reach the relay at ${relay} — ${text}`;
28
28
  }
29
+ export function signInHint(code, relay, hosted = process.env.PANTHEON_DEFAULT_RELAY ?? "wss://relay.joinpantheon.network") {
30
+ if (code !== "NOT_SIGNED_IN" || !relay)
31
+ return "";
32
+ try {
33
+ if (new URL(relay).host === new URL(hosted).host)
34
+ return "";
35
+ }
36
+ catch {
37
+ return "";
38
+ }
39
+ return `\n This team is on ${relay}, so sign in there: pantheon login --email you@work.com --relay ${relay}` +
40
+ `\n (new here: pantheon signup --email you@work.com --username you --relay ${relay})`;
41
+ }
29
42
  //# sourceMappingURL=refusal.js.map