@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.
- package/README.md +45 -22
- package/bin/pantheon.js +12 -0
- package/dist/accounts.js +151 -26
- package/dist/agents.js +197 -14
- package/dist/claude-confine.js +283 -0
- package/dist/cli.js +725 -120
- package/dist/client.js +37 -2
- package/dist/command-state.js +79 -0
- package/dist/config.js +211 -10
- package/dist/doctor.js +103 -13
- package/dist/grants.js +1 -1
- package/dist/history-view.js +2 -1
- package/dist/home-guard.js +210 -0
- package/dist/hooks-install.js +2 -1
- package/dist/identity.js +24 -0
- package/dist/install.js +76 -26
- package/dist/kiro-files.js +2 -2
- package/dist/ledger.js +111 -28
- package/dist/machine.js +494 -32
- package/dist/mcp-supervisor.js +25 -6
- package/dist/mcp-windows.js +47 -25
- package/dist/mcp.js +25 -10
- package/dist/network.js +39 -8
- package/dist/notify.js +17 -6
- package/dist/pending.js +8 -14
- package/dist/persona.js +21 -0
- package/dist/profile.js +4 -1
- package/dist/quickstart.js +82 -14
- package/dist/reads.js +30 -0
- package/dist/refusal.js +14 -1
- package/dist/relay.js +1143 -180
- package/dist/roster-file.js +169 -23
- package/dist/run-failure.js +83 -3
- package/dist/secret-guard.js +141 -0
- package/dist/service.js +84 -34
- package/dist/setup-guide.js +28 -9
- package/dist/spend.js +6 -2
- package/dist/store.js +94 -10
- package/dist/text-file.js +74 -0
- package/dist/tray.js +11 -2
- package/dist/update-check.js +2 -0
- package/dist/which.js +69 -12
- package/dist/worker-policy.js +137 -3
- package/dist/worker.js +702 -81
- package/helper/macos/menubar.swift +15 -1
- package/package.json +2 -2
package/dist/quickstart.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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 :
|
|
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
|
-
|
|
383
|
-
|
|
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 ??
|
|
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
|