@join-pantheon/cli 0.29.0 → 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 -119
- 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 +507 -38
- 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 +88 -15
- package/dist/reads.js +30 -0
- package/dist/refusal.js +14 -1
- package/dist/relay.js +1193 -185
- 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 +105 -36
- 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}` });
|
|
@@ -102,7 +115,12 @@ export function wireUp(opts, deps = {}) {
|
|
|
102
115
|
}
|
|
103
116
|
else if (opts.unattended) {
|
|
104
117
|
const project = opts.unattended;
|
|
105
|
-
const isRepo =
|
|
118
|
+
const isRepo = (() => { try {
|
|
119
|
+
return execFileSync("git", ["-C", project, "rev-parse", "--is-inside-work-tree"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 10_000 }).trim() === "true";
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
} })();
|
|
106
124
|
if (!isRepo && how === "explicit") {
|
|
107
125
|
tools.push({ tool: "worker", status: `not installed — ${project} is not a git repo` });
|
|
108
126
|
warnings.push(`--unattended needs a git repo to work in; pass --project <a clone>. Nothing was installed.`);
|
|
@@ -112,9 +130,14 @@ export function wireUp(opts, deps = {}) {
|
|
|
112
130
|
}
|
|
113
131
|
else {
|
|
114
132
|
try {
|
|
115
|
-
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
|
+
}
|
|
116
139
|
tools.push({ tool: "worker", status: `on — answers peers while you are away (${project})` });
|
|
117
|
-
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.`);
|
|
118
141
|
if (!existsSync(policyPath())) {
|
|
119
142
|
const pol = loadPolicy();
|
|
120
143
|
grantCap(pol, "self_upgrade", "always", "setup-default");
|
|
@@ -146,6 +169,47 @@ export function slugName(raw, fallback) {
|
|
|
146
169
|
.replace(/^[-._]+|[-._]+$/g, "");
|
|
147
170
|
return /[a-z0-9]/.test(s) ? s : fallback;
|
|
148
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
|
+
}
|
|
149
213
|
export function teamNameFor(dir, repo = repoInfo(dir)) {
|
|
150
214
|
const fromOrigin = repo?.origin ? (repo.origin.replace(/\.git$/i, "").split(/[/:]/).filter(Boolean).pop() ?? "") : "";
|
|
151
215
|
return slugName(fromOrigin || basename(repo?.root ?? dir), "team");
|
|
@@ -297,7 +361,7 @@ export async function start(opts) {
|
|
|
297
361
|
` Use a tailnet address (tailscale ip -4) or a tunnel, then re-run pantheon start.` }
|
|
298
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 })
|
|
299
363
|
: findReach(opts.port);
|
|
300
|
-
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] });
|
|
301
365
|
let relay = null;
|
|
302
366
|
if (chosen && !hostHere) {
|
|
303
367
|
const probe = await probeRelay(reach.url, opts.remoteTimeoutMs ?? 10_000, opts.fetchImpl);
|
|
@@ -340,7 +404,7 @@ export async function start(opts) {
|
|
|
340
404
|
relay: reach.url,
|
|
341
405
|
space: opts.space ?? teamNameFor(process.cwd(), repo),
|
|
342
406
|
token: randomBytes(16).toString("hex"),
|
|
343
|
-
member: opts.me ?? (keepIdentity ? existing.member :
|
|
407
|
+
member: opts.me ?? (keepIdentity ? existing.member : defaultMemberName()),
|
|
344
408
|
agent: detectAgent(),
|
|
345
409
|
...(keepIdentity ? { publicKey: existing.publicKey, privateKey: existing.privateKey } : newKeypair()),
|
|
346
410
|
...(keepIdentity && existing.account ? { account: existing.account } : {}),
|
|
@@ -354,6 +418,10 @@ export async function start(opts) {
|
|
|
354
418
|
warnings.push(`you are already "${cfg.member}" here; --me ${opts.me} ignored (use --reset to start over)`);
|
|
355
419
|
}
|
|
356
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
|
+
}
|
|
357
425
|
else if (keepIdentity && existing.account) {
|
|
358
426
|
opts.log(` you ${cfg.member} — signed in; creating your first team`);
|
|
359
427
|
}
|
|
@@ -361,6 +429,7 @@ export async function start(opts) {
|
|
|
361
429
|
warnings.push(`replacing your identity in "${existing.space}" — that space is no longer reachable from this machine`);
|
|
362
430
|
}
|
|
363
431
|
let invite = null;
|
|
432
|
+
let inviteMayWake = false;
|
|
364
433
|
let claimed = false;
|
|
365
434
|
const claim = async (c) => {
|
|
366
435
|
const client = new PantheonClient(c);
|
|
@@ -374,13 +443,17 @@ export async function start(opts) {
|
|
|
374
443
|
}
|
|
375
444
|
if (repo && c.spaceId) {
|
|
376
445
|
const { replaced } = bindRepo(repo, c.spaceId);
|
|
377
|
-
|
|
378
|
-
|
|
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
|
+
}
|
|
379
451
|
}
|
|
380
452
|
opts.log(` space ${c.space} ${reuse ? "— reconnected" : "— created, you own it"}${repo ? ` ${repo.origin ?? "local repo"} → bound to it` : ""}`);
|
|
381
453
|
const mayWake = opts.wake ? opts.wake === "anyone" : wakeDefault(loadProfile(c));
|
|
382
454
|
const inv = await client.makeInvite(mayWake, opts.ttlMinutes);
|
|
383
455
|
invite = toInvite({ ...c, token: inv.url });
|
|
456
|
+
inviteMayWake = mayWake;
|
|
384
457
|
claimed = true;
|
|
385
458
|
}
|
|
386
459
|
finally {
|
|
@@ -421,7 +494,7 @@ export async function start(opts) {
|
|
|
421
494
|
...(opts.workerHow ? { workerHow: opts.workerHow } : {}),
|
|
422
495
|
});
|
|
423
496
|
warnings.push(...wired.warnings);
|
|
424
|
-
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 };
|
|
425
498
|
}
|
|
426
499
|
export async function join(opts) {
|
|
427
500
|
const existing = opts.reset ? null : (load() ?? identity());
|
|
@@ -436,7 +509,7 @@ export async function join(opts) {
|
|
|
436
509
|
relay: opts.invite.relay,
|
|
437
510
|
space: opts.invite.space,
|
|
438
511
|
token: "",
|
|
439
|
-
member: opts.me ?? existing?.member ??
|
|
512
|
+
member: opts.me ?? existing?.member ?? defaultMemberName(),
|
|
440
513
|
agent: detectAgent(),
|
|
441
514
|
...keys,
|
|
442
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
|