@join-pantheon/cli 0.29.1 → 0.31.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 +142 -40
- package/bin/pantheon.js +12 -0
- package/dist/accounts.js +151 -26
- package/dist/agents.js +224 -17
- package/dist/claude-confine.js +299 -0
- package/dist/cli.js +1116 -137
- package/dist/client.js +294 -10
- package/dist/command-state.js +102 -0
- package/dist/config.js +287 -14
- package/dist/continued.js +42 -10
- package/dist/doctor.js +191 -23
- package/dist/file-lock.js +197 -0
- package/dist/grants.js +1 -1
- package/dist/history-view.js +2 -1
- package/dist/home-guard.js +565 -0
- package/dist/hooks-install.js +2 -1
- package/dist/identity.js +101 -1
- 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 +29 -7
- package/dist/mcp-windows.js +47 -25
- package/dist/mcp.js +99 -36
- package/dist/network.js +39 -8
- package/dist/nonces.js +130 -0
- package/dist/notify.js +26 -9
- package/dist/pending.js +121 -22
- package/dist/persona.js +21 -0
- package/dist/pins.js +161 -0
- package/dist/profile.js +22 -1
- package/dist/protocol.js +40 -1
- package/dist/quickstart.js +173 -24
- package/dist/reads.js +37 -0
- package/dist/refusal.js +14 -1
- package/dist/relay.js +1341 -207
- package/dist/repo-find.js +127 -0
- package/dist/repo-route.js +70 -0
- package/dist/roster-file.js +190 -25
- package/dist/run-failure.js +99 -3
- package/dist/secret-guard.js +141 -0
- package/dist/service.js +91 -35
- package/dist/setup-guide.js +91 -31
- package/dist/spend.js +6 -2
- package/dist/store.js +94 -10
- package/dist/text-file.js +74 -0
- package/dist/tray.js +24 -3
- package/dist/untrusted.js +24 -5
- package/dist/update-check.js +2 -0
- package/dist/upgrade.js +9 -5
- package/dist/verify-act.js +102 -0
- package/dist/which.js +69 -12
- package/dist/worker-fleet.js +30 -9
- package/dist/worker-policy.js +231 -7
- package/dist/worker.js +1717 -130
- package/helper/macos/menubar.swift +63 -2
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -7,11 +7,11 @@ process.emitWarning = ((warning, ...rest) => {
|
|
|
7
7
|
return _emitWarning(warning, ...rest);
|
|
8
8
|
});
|
|
9
9
|
import { randomBytes } from "node:crypto";
|
|
10
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
-
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
12
12
|
import { PantheonClient } from "./client.js";
|
|
13
13
|
import { agentSpawn, CmdLineUnsafe } from "./which.js";
|
|
14
|
-
import { fromInvite, looksLikeInviteCode, resolveInviteCode, load, save, toInvite, toJoinLink, resolveFor, loadAll, identity, repoInfo, bindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY, resolveForHook } from "./config.js";
|
|
14
|
+
import { fromInvite, looksLikeInviteCode, resolveInviteCode, load, save, toInvite, toJoinLink, resolveFor, loadAll, identity, repoInfo, bindRepo, bindFolder, unbindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY, resolveForHook } from "./config.js";
|
|
15
15
|
import { fmtLimit } from "./plans.js";
|
|
16
16
|
import { validEmail, validUsername } from "./accounts.js";
|
|
17
17
|
import { install, launcher, installForUser } from "./install.js";
|
|
@@ -20,8 +20,8 @@ import { runMcp } from "./mcp.js";
|
|
|
20
20
|
import { Worker } from "./worker.js";
|
|
21
21
|
import { DEFAULT_REMINDER_MS, ensureNativeNotifier, HumanNotifier, sendDesktopNotification } from "./notifier.js";
|
|
22
22
|
import { desktopUnavailable, hostEnv } from "./host-env.js";
|
|
23
|
-
import { AGENT_SURFACES, applyProfileInit, cleanList, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile, wakeDefault } from "./profile.js";
|
|
24
|
-
import { runGuide, terminalIO, parseSetupFlags, questionnaire, optOutsFrom } from "./setup-guide.js";
|
|
23
|
+
import { AGENT_SURFACES, applyProfileInit, cleanList, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile, wakeDefault, wakeMode } from "./profile.js";
|
|
24
|
+
import { runGuide, terminalIO, questionOrNull, parseSetupFlags, questionnaire, optOutsFrom } from "./setup-guide.js";
|
|
25
25
|
import { selectActs, formatAct, resolveRef } from "./history-view.js";
|
|
26
26
|
import { doctor, render, shareBlock } from "./doctor.js";
|
|
27
27
|
import { FEEDBACK_ADDRESS } from "./feedback.js";
|
|
@@ -35,16 +35,23 @@ import { createInterface } from "node:readline";
|
|
|
35
35
|
import { homedir } from "node:os";
|
|
36
36
|
import { removeHook, decideStop, writeIntent, clearIntent, readIntent, installHooks, agentHookStates } from "./notify.js";
|
|
37
37
|
import { pending, render as renderPending } from "./pending.js";
|
|
38
|
-
import {
|
|
38
|
+
import { signedRead } from "./reads.js";
|
|
39
|
+
import { start as qsStart, join as qsJoin, wireUp, teamNameFor, deviceLabel } from "./quickstart.js";
|
|
39
40
|
import { installService, uninstallService } from "./service.js";
|
|
40
|
-
import { LIMITS } from "./protocol.js";
|
|
41
|
+
import { ACTS, LIMITS } from "./protocol.js";
|
|
41
42
|
import { loadPersonas, personaByName, describePersona, initPersona, personaDir } from "./persona.js";
|
|
42
43
|
import { fingerprint, newKeypair, signAccount } from "./identity.js";
|
|
43
44
|
import { VERSION } from "./version.js";
|
|
44
|
-
import { connectFailure } from "./refusal.js";
|
|
45
|
+
import { connectFailure, signInHint } from "./refusal.js";
|
|
45
46
|
import * as mcpWindows from "./mcp-windows.js";
|
|
46
47
|
import { updateBanner } from "./update-check.js";
|
|
48
|
+
import { inWokenRun } from "./worker-policy.js";
|
|
47
49
|
const argv = process.argv.slice(2);
|
|
50
|
+
for (let i = argv.length - 1; i >= 1; i--) {
|
|
51
|
+
const m = /^(--[a-z][a-z0-9-]*)=(.*)$/i.exec(argv[i]);
|
|
52
|
+
if (m)
|
|
53
|
+
argv.splice(i, 1, m[1], m[2]);
|
|
54
|
+
}
|
|
48
55
|
const cmd = argv[0] ?? "help";
|
|
49
56
|
function signedQuery(cfg, method, path, params, at = Date.now()) {
|
|
50
57
|
const full = { ...params, pubkey: cfg.publicKey, ts: at };
|
|
@@ -72,6 +79,21 @@ function requireSignInOnHostedRelay(relayFlag) {
|
|
|
72
79
|
return;
|
|
73
80
|
die(`sign in first — this relay verifies email.\n New here: pantheon signup --email you@work.com --username you\n Have an account: pantheon login --email you@work.com\n Then run pantheon start again.`);
|
|
74
81
|
}
|
|
82
|
+
async function requireSignInOnAccountsRelay(relayFlag) {
|
|
83
|
+
if (!relayFlag || identity()?.account)
|
|
84
|
+
return;
|
|
85
|
+
let hasAccounts = false;
|
|
86
|
+
try {
|
|
87
|
+
const base = relayFlag.replace(/^ws/, "http").replace(/\/+$/, "");
|
|
88
|
+
const res = await fetch(`${base}/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: "{}", signal: AbortSignal.timeout(6_000) });
|
|
89
|
+
hasAccounts = res.status !== 404;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (hasAccounts)
|
|
95
|
+
die(`sign in first — ${relayFlag} verifies email.${signInHint("NOT_SIGNED_IN", relayFlag)}\n Then run this same pantheon start again.`);
|
|
96
|
+
}
|
|
75
97
|
async function interactiveOnboard(relayArg) {
|
|
76
98
|
const prior = identity();
|
|
77
99
|
const relay = relayArg ?? prior?.relay ?? process.env.PANTHEON_RELAY ?? DEFAULT_RELAY;
|
|
@@ -82,9 +104,16 @@ async function interactiveOnboard(relayArg) {
|
|
|
82
104
|
return;
|
|
83
105
|
}
|
|
84
106
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
85
|
-
const ask =
|
|
107
|
+
const ask = async (q) => {
|
|
108
|
+
const a = await questionOrNull(rl, q);
|
|
109
|
+
if (a === null) {
|
|
110
|
+
console.error(D("\n cancelled. Pick up where you left off: pantheon signup --email you@work.com --username you"));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
return a;
|
|
114
|
+
};
|
|
86
115
|
const kp = prior?.publicKey && prior.privateKey ? { publicKey: prior.publicKey, privateKey: prior.privateKey } : newKeypair();
|
|
87
|
-
const label =
|
|
116
|
+
const label = deviceLabel();
|
|
88
117
|
const postJson = async (path, body) => fetch(`${base}${path}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })
|
|
89
118
|
.then(async (x) => ({ status: x.status, j: await x.json() }));
|
|
90
119
|
try {
|
|
@@ -166,7 +195,8 @@ async function interactiveOnboard(relayArg) {
|
|
|
166
195
|
console.error(` ${C("pantheon join <invite-link>")} ${D("(join a teammate's team)")}`);
|
|
167
196
|
return;
|
|
168
197
|
}
|
|
169
|
-
|
|
198
|
+
const spent = v.j.reason === "expired" || v.j.reason === "too_many_attempts" || v.j.tries_left === 0;
|
|
199
|
+
console.error(D(` ${v.j.error ?? "that code is not right"}${spent ? " — type 'resend' for a new code" : " — try again"}`));
|
|
170
200
|
}
|
|
171
201
|
console.error(D("\n Too many tries. Run `pantheon` again to restart, or `pantheon verify <code>` once you have it."));
|
|
172
202
|
}
|
|
@@ -228,10 +258,199 @@ function sameHost(a, b) {
|
|
|
228
258
|
return false;
|
|
229
259
|
}
|
|
230
260
|
}
|
|
261
|
+
async function findAndBindRepos(all, m, teamRepos, opts = {}) {
|
|
262
|
+
const want = teamRepos.map((r) => r.fingerprint);
|
|
263
|
+
if (!want.length) {
|
|
264
|
+
console.error(D(` "${m.name}" has no repos registered yet — the first member to connect from a clone registers it`));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const name = (fp) => { const r = teamRepos.find((x) => x.fingerprint === fp); return r?.origin ? r.origin.replace(/^.*[:/]([^:/]+)$/, "$1") : fp; };
|
|
268
|
+
const already = new Set(Object.entries(all.bindings ?? {}).filter(([, b]) => b.space === m.id).map(([k]) => k));
|
|
269
|
+
const unbound = new Set(all.unbound ?? []);
|
|
270
|
+
for (const fp of want.filter((f) => unbound.has(f) && !already.has(f)))
|
|
271
|
+
console.error(D(` ${name(fp)}: you unbound it on this machine — left so (pantheon use ${m.name} --here inside it to bind it again)`));
|
|
272
|
+
const todo = want.filter((fp) => !already.has(fp) && !unbound.has(fp));
|
|
273
|
+
if (!todo.length) {
|
|
274
|
+
console.error(`${C("✓")} every repo "${m.name}" works from is bound on this machine: ${want.map(name).join(", ")}`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
let recents = opts.recents;
|
|
278
|
+
const tty = !!process.stdin.isTTY && !!process.stderr.isTTY;
|
|
279
|
+
if (recents === undefined && tty) {
|
|
280
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
281
|
+
const a = ((await questionOrNull(rl, `Also look in the folders your editors and agents have opened? Only their paths are read, and nothing leaves this machine. [Y/n] `)) ?? "").trim().toLowerCase();
|
|
282
|
+
rl.close();
|
|
283
|
+
recents = a === "" || a.startsWith("y");
|
|
284
|
+
}
|
|
285
|
+
const { findTeamClones } = await import("./repo-find.js");
|
|
286
|
+
console.error(D(` looking for ${todo.map(name).join(", ")} on this machine…`));
|
|
287
|
+
let found = findTeamClones(todo, { recents: !!recents, ...(opts.extra ? { extra: opts.extra } : {}) });
|
|
288
|
+
let missing = todo.filter((fp) => !found.some((f) => f.repo.fingerprint === fp));
|
|
289
|
+
if (missing.length && tty) {
|
|
290
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
291
|
+
const a = ((await questionOrNull(rl, ` Not found: ${missing.map(name).join(", ")}. The folder your repos live in (Enter to skip): `)) ?? "").trim();
|
|
292
|
+
rl.close();
|
|
293
|
+
if (a) {
|
|
294
|
+
const more = findTeamClones(missing, { extra: [resolve(a.replace(/^~(?=$|\/)/, homedir()))] });
|
|
295
|
+
found = [...found, ...more];
|
|
296
|
+
missing = missing.filter((fp) => !more.some((f) => f.repo.fingerprint === fp));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const elsewhere = found.filter((f) => { const b = all.bindings?.[f.repo.fingerprint]; return !!b && b.space !== m.id; });
|
|
300
|
+
for (const f of elsewhere)
|
|
301
|
+
console.error(` ${D("–")} ${name(f.repo.fingerprint).padEnd(18)} ${f.repo.root} ${D(`bound to "${findMembership(all, all.bindings[f.repo.fingerprint].space)?.name ?? all.bindings[f.repo.fingerprint].space}" — left there (pantheon use ${m.name} --here inside it to move it)`)}`);
|
|
302
|
+
found = found.filter((f) => !elsewhere.includes(f));
|
|
303
|
+
for (const f of found)
|
|
304
|
+
console.error(` ${C("✓")} ${name(f.repo.fingerprint).padEnd(18)} ${f.repo.root} ${D(`(${f.how})`)}`);
|
|
305
|
+
for (const fp of missing)
|
|
306
|
+
console.error(` ${D("✗")} ${name(fp).padEnd(18)} ${D("not on this machine — clone it, then: pantheon repo find")}`);
|
|
307
|
+
if (!found.length)
|
|
308
|
+
return;
|
|
309
|
+
let go = !!opts.yes || !!opts.auto;
|
|
310
|
+
if (!go && tty) {
|
|
311
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
312
|
+
const a = ((await questionOrNull(rl, `Use ${found.length === 1 ? "it" : `these ${found.length}`} for "${m.name}"? [Y/n] `)) ?? "").trim().toLowerCase();
|
|
313
|
+
rl.close();
|
|
314
|
+
go = a === "" || a.startsWith("y");
|
|
315
|
+
}
|
|
316
|
+
if (!go) {
|
|
317
|
+
console.error(D(` nothing bound${tty ? "" : " — run it again with --yes to bind them"}`));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
for (const f of found)
|
|
321
|
+
bindRepo(f.repo, m.id);
|
|
322
|
+
const them = found.length === 1 ? "it" : "them";
|
|
323
|
+
console.error(`${B("bound")} ${found.map((f) => name(f.repo.fingerprint)).join(", ")} → "${m.name}" — agents opened in ${them} speak for the team, and your worker (when it is on) serves ${them}`);
|
|
324
|
+
}
|
|
325
|
+
async function bindReposIn(dir, all, m, yes, again) {
|
|
326
|
+
const found = (await import("./repo-find.js")).reposUnder(dir, 3);
|
|
327
|
+
const taken = found.filter((r) => { const b = all.bindings?.[r.fingerprint]; return !!b && b.space !== m.id; });
|
|
328
|
+
for (const r of taken)
|
|
329
|
+
console.error(D(` ${repoLabel(r)} is bound to "${findMembership(all, all.bindings[r.fingerprint].space)?.name ?? all.bindings[r.fingerprint].space}" — left there (pantheon use ${m.name} --here inside it to move it)`));
|
|
330
|
+
const unbound = new Set(all.unbound ?? []);
|
|
331
|
+
for (const r of found.filter((x) => unbound.has(x.fingerprint)))
|
|
332
|
+
console.error(D(` ${repoLabel(r)}: you unbound it on this machine — left so (pantheon use ${m.name} --here inside it to bind it again)`));
|
|
333
|
+
let inside = found.filter((r) => !taken.includes(r) && !unbound.has(r.fingerprint));
|
|
334
|
+
if (inside.length && !yes) {
|
|
335
|
+
console.error(` ${B("repos in it")} ${inside.map(repoLabel).join(", ")}`);
|
|
336
|
+
if (process.stdin.isTTY) {
|
|
337
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
338
|
+
const a = ((await questionOrNull(rl, ` Bind ${inside.length === 1 ? "it" : `these ${inside.length}`} to "${m.name}" and register ${inside.length === 1 ? "it" : "them"} with the team? [Y/n] `)) ?? "").trim().toLowerCase();
|
|
339
|
+
rl.close();
|
|
340
|
+
if (!(a === "" || a.startsWith("y")))
|
|
341
|
+
inside = [];
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
console.error(D(` not bound yet — to bind and register them with "${m.name}": ${again}`));
|
|
345
|
+
inside = [];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (!inside.length)
|
|
349
|
+
return 0;
|
|
350
|
+
for (const r of inside)
|
|
351
|
+
bindRepo(r, m.id);
|
|
352
|
+
const c = configFor(loadAll() ?? all, m, null);
|
|
353
|
+
const client = new PantheonClient(c, undefined, { role: "cli" });
|
|
354
|
+
const registered = [], failed = [];
|
|
355
|
+
try {
|
|
356
|
+
await client.connect();
|
|
357
|
+
for (const r of inside) {
|
|
358
|
+
if (client.repos.some((x) => x.fingerprint === r.fingerprint)) {
|
|
359
|
+
registered.push(repoLabel(r));
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
await client.bindRepo(r.fingerprint, repoShare(r)).then(() => registered.push(repoLabel(r)), (e) => failed.push(`${repoLabel(r)} (${e.message.split("\n")[0]})`));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
catch (e) {
|
|
366
|
+
failed.push(...inside.map((r) => `${repoLabel(r)} (${e.message.split("\n")[0]})`));
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
client.close();
|
|
370
|
+
}
|
|
371
|
+
console.error(` ${B("repos in it")} ${inside.map(repoLabel).join(", ")} — each bound to "${m.name}"${registered.length ? "" : D(" here")}`);
|
|
372
|
+
if (failed.length)
|
|
373
|
+
console.error(D(` not registered with the team yet: ${failed.join("; ")} — pantheon repo add, from each, when the relay is reachable`));
|
|
374
|
+
return inside.length;
|
|
375
|
+
}
|
|
376
|
+
async function bindReposHere(yes) {
|
|
377
|
+
const cwd = process.cwd();
|
|
378
|
+
if (repoInfo(cwd))
|
|
379
|
+
return;
|
|
380
|
+
const real = (d) => { try {
|
|
381
|
+
return realpathSync.native(d);
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
return resolve(d);
|
|
385
|
+
} };
|
|
386
|
+
const here = real(cwd);
|
|
387
|
+
if (here === real(homedir()) || dirname(here) === here)
|
|
388
|
+
return;
|
|
389
|
+
if (["Downloads", "Desktop", "Documents", "Library"].some((n) => here === join(real(homedir()), n)))
|
|
390
|
+
return;
|
|
391
|
+
const all = loadAll(), j = load();
|
|
392
|
+
const m = all && j ? findMembership(all, j.spaceId ?? j.space) ?? findMembership(all, j.space) : null;
|
|
393
|
+
if (!all || !m)
|
|
394
|
+
return;
|
|
395
|
+
const n = (await import("./repo-find.js")).reposUnder(cwd, 3).length;
|
|
396
|
+
if (!n)
|
|
397
|
+
return;
|
|
398
|
+
console.error("");
|
|
399
|
+
const many = n > 6 && !process.stdin.isTTY;
|
|
400
|
+
if (many) {
|
|
401
|
+
console.error(D(` this folder holds ${n} repos — to bind and register them all with "${m.name}": pantheon use ${m.name} --here --yes (or pantheon repo find, which binds only the team's own)`));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
await bindReposIn(cwd, all, m, yes, `pantheon use ${m.name} --here --yes`).catch((e) => console.error(D(` could not bind the repos here: ${e.message}`)));
|
|
405
|
+
}
|
|
406
|
+
async function workerAfterBinding(how) {
|
|
407
|
+
if (how === "off")
|
|
408
|
+
return;
|
|
409
|
+
const j = load();
|
|
410
|
+
if (!j?.spaceId || wakeMode(loadProfile(j)) === "no")
|
|
411
|
+
return;
|
|
412
|
+
const { serviceInstalled, servicesUnavailable } = await import("./service.js");
|
|
413
|
+
if (servicesUnavailable(process.platform) || serviceInstalled("worker"))
|
|
414
|
+
return;
|
|
415
|
+
const ws = (await import("./worker-fleet.js")).teamWorkspaces(loadAll(), j.spaceId);
|
|
416
|
+
if (!ws.length)
|
|
417
|
+
return;
|
|
418
|
+
try {
|
|
419
|
+
const { commandsChosen } = await import("./profile.js");
|
|
420
|
+
const commandsOn = ws.some((w) => w.kind === "repo") && commandsChosen() !== false;
|
|
421
|
+
installService("worker", ws[0].path, commandsOn ? ["--allow-bash"] : []);
|
|
422
|
+
console.error(` ${C("✓")} worker on — serves ${ws.map((w) => w.name).join(", ")} while you are away${wakeMode(loadProfile(j)) === "approved" ? "; you are asked the first time each teammate needs it" : ""}${commandsOn ? "; a run may run tests, commit and push to its own branch (edits only: pantheon setup --commands no)" : ""}`);
|
|
423
|
+
}
|
|
424
|
+
catch (e) {
|
|
425
|
+
console.error(D(` the worker did not install: ${e.message} — pantheon service install worker`));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const repoLabel = (r) => r.origin ?? basename(r.root);
|
|
429
|
+
const repoShare = (r) => (r.origin ? r.origin.replace(/^.*[:/]([^:/]+)$/, "$1") : basename(r.root));
|
|
430
|
+
const teamFlag = (cfg) => (flag("space") ? ` --space ${cfg.space}` : "");
|
|
231
431
|
function need() {
|
|
232
432
|
const r = resolveFor(process.cwd());
|
|
233
|
-
if (
|
|
433
|
+
if (flag("space") && !["start", "profile"].includes(cmd)) {
|
|
434
|
+
const f = loadAll();
|
|
435
|
+
const m = f && findMembership(f, flag("space"));
|
|
436
|
+
if (!f || !m)
|
|
437
|
+
die(`you are not in a team called "${flag("space")}". Yours: ${Object.values(f?.spaces ?? {}).map((x) => x.name).join(", ") || "none"}`);
|
|
438
|
+
return r.cfg && (r.cfg.spaceId === m.id || r.cfg.space === m.name) && r.reason !== "default" ? r.cfg : configFor(f, m, null);
|
|
439
|
+
}
|
|
440
|
+
if (r.cfg && r.cfg.space) {
|
|
441
|
+
if (r.autoBound && r.repo && r.cfg.spaceId) {
|
|
442
|
+
try {
|
|
443
|
+
bindRepo(r.repo, r.cfg.spaceId);
|
|
444
|
+
console.error(D(` ${r.repo.origin ?? r.repo.root} is one of "${r.cfg.space}"'s repos — bound here`));
|
|
445
|
+
}
|
|
446
|
+
catch { }
|
|
447
|
+
}
|
|
234
448
|
return r.cfg;
|
|
449
|
+
}
|
|
450
|
+
if (r.claimedBy?.length)
|
|
451
|
+
die(`this repo is listed by more than one of your teams (${r.claimedBy.join(", ")}) — pick one: pantheon use <team> --here (or name it: --space <team>)`);
|
|
452
|
+
if (r.repoUnknown)
|
|
453
|
+
die(`git did not answer in this folder, so which team this repo is bound to cannot be told — and a command for the wrong team is worse than none.\n Check that git works here (git status), then run it again — or name the team: --space <team>`);
|
|
235
454
|
if (r.reason === "unbound") {
|
|
236
455
|
const names = Object.values(loadAll()?.spaces ?? {}).map((m) => m.name).join(", ");
|
|
237
456
|
die(`this folder is not bound to one of your ${r.memberships} teams (${names}).\n From the team's repo: pantheon use <team> --here\n Or pick a default: pantheon use <team>`);
|
|
@@ -239,7 +458,7 @@ function need() {
|
|
|
239
458
|
if (r.reason === "stale")
|
|
240
459
|
die(`this repo is bound to a team this machine is no longer in (${r.binding?.space}).\n Rebind it: pantheon use <team> --here · or rejoin: pantheon join <invite>`);
|
|
241
460
|
if (r.cfg && !r.cfg.space)
|
|
242
|
-
die("signed in, but not on a team yet. Run
|
|
461
|
+
die(`${r.cfg.account ? "signed in" : "this machine has a key"}, but is not on a team yet. Run \`pantheon start\` (create one) or \`pantheon join <invite>\` (join one).`);
|
|
243
462
|
return die("not configured. Run `pantheon signup` then `pantheon start` (create a team), or `pantheon join <invite>` (join one).");
|
|
244
463
|
}
|
|
245
464
|
function targets() {
|
|
@@ -303,6 +522,17 @@ function printGranted(granted) {
|
|
|
303
522
|
console.error(` \u2022 ${g}`);
|
|
304
523
|
console.error(D(` Undo: pantheon hook uninstall · pantheon service uninstall worker`));
|
|
305
524
|
}
|
|
525
|
+
function printRelayNote(relay) {
|
|
526
|
+
let host = relay ?? "the relay";
|
|
527
|
+
try {
|
|
528
|
+
if (relay)
|
|
529
|
+
host = new URL(relay.replace(/^ws/, "http")).host;
|
|
530
|
+
}
|
|
531
|
+
catch { }
|
|
532
|
+
console.error(D(`\n Your team's messages go through ${host}. Whoever runs it can read them (they are not end-to-end encrypted), so treat the team like a shared channel;`));
|
|
533
|
+
console.error(D(` on a relay from 0.31 on, it cannot write or change one in a teammate's name without your machine noticing: every message is signed by its sender. Your own relay: pantheon relay`));
|
|
534
|
+
}
|
|
535
|
+
const HUD_ACTION_MAX_AGE_MS = 30_000;
|
|
306
536
|
function unattendedProject() {
|
|
307
537
|
if (argv.includes("--no-worker"))
|
|
308
538
|
return { project: null, how: "off" };
|
|
@@ -321,7 +551,23 @@ async function guidedSetup(cfg, project, opts = {}) {
|
|
|
321
551
|
const io = rl ? terminalIO(rl, (l) => console.error(l)) : null;
|
|
322
552
|
console.error(`\n${B("A few choices, then you're done.")}${io ? "" : Object.keys(preset).length ? D(" (applying the answers you passed)") : D(" (no terminal — taking the defaults)")}`);
|
|
323
553
|
try {
|
|
324
|
-
|
|
554
|
+
let serves = null;
|
|
555
|
+
if (!project && cfg.spaceId) {
|
|
556
|
+
try {
|
|
557
|
+
const ws = (await import("./worker-fleet.js")).teamWorkspaces(loadAll(), cfg.spaceId);
|
|
558
|
+
if (ws.length)
|
|
559
|
+
serves = { path: ws[0].path, repos: ws.some((w) => w.kind === "repo") };
|
|
560
|
+
}
|
|
561
|
+
catch { }
|
|
562
|
+
}
|
|
563
|
+
const { agentsNeedingUnsandboxed } = await import("./agents.js");
|
|
564
|
+
const r = await runGuide(io, { cfg, project, optOut: optOutsFrom(argv), serves, ...(opts.founder !== undefined ? { founder: opts.founder } : {}) }, {
|
|
565
|
+
unsandboxedAgents: () => { try {
|
|
566
|
+
return agentsNeedingUnsandboxed();
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
return [];
|
|
570
|
+
} },
|
|
325
571
|
publish: async (p) => {
|
|
326
572
|
const c = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
327
573
|
try {
|
|
@@ -377,21 +623,33 @@ async function workdirFor(cfg, hud) {
|
|
|
377
623
|
return pickWorkdir(bindings, hints, team);
|
|
378
624
|
}
|
|
379
625
|
async function fetchHistory(cfg, opts = {}) {
|
|
380
|
-
const
|
|
381
|
-
const q = `space=${encodeURIComponent(cfg.space)}&token=${encodeURIComponent(cfg.token)}`
|
|
382
|
-
+ (opts.goal ? `&goal=${encodeURIComponent(opts.goal)}` : opts.since !== undefined ? `&since=${opts.since}` : "");
|
|
383
|
-
const res = await fetch(`${base}/history?${q}`, { signal: AbortSignal.timeout(8000) });
|
|
626
|
+
const res = await signedRead(cfg, "/history", opts.goal ? { goal: opts.goal } : opts.since !== undefined ? { since: opts.since } : {}, { timeoutMs: 8000 });
|
|
384
627
|
if (!res.ok)
|
|
385
|
-
throw
|
|
628
|
+
throw await readFailure(cfg, res, "the ledger");
|
|
386
629
|
return (await res.json()).acts ?? [];
|
|
387
630
|
}
|
|
631
|
+
async function readFailure(cfg, res, what) {
|
|
632
|
+
const why = await res.json().then((j) => j.error).catch(() => undefined);
|
|
633
|
+
if (res.status === 401 || res.status === 403) {
|
|
634
|
+
const c = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
635
|
+
try {
|
|
636
|
+
await c.connect();
|
|
637
|
+
}
|
|
638
|
+
catch (e) {
|
|
639
|
+
return new Error(c.pending ?? connectFailure(e, cfg.relay));
|
|
640
|
+
}
|
|
641
|
+
finally {
|
|
642
|
+
c.close();
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return new Error(`relay answered ${res.status} for ${what}${why ? `: ${why}` : ""}`);
|
|
646
|
+
}
|
|
388
647
|
async function fetchAct(cfg, id, budgetMs = Number.POSITIVE_INFINITY) {
|
|
389
648
|
const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
|
|
390
|
-
const q = `space=${encodeURIComponent(cfg.space)}&token=${encodeURIComponent(cfg.token)}`;
|
|
391
649
|
const deadline = Date.now() + budgetMs;
|
|
392
650
|
const left = () => Math.max(1, Math.min(8000, deadline - Date.now()));
|
|
393
651
|
try {
|
|
394
|
-
const res = await
|
|
652
|
+
const res = await signedRead(cfg, "/act", { id }, { timeoutMs: left() });
|
|
395
653
|
if (res.ok) {
|
|
396
654
|
const body = await res.json();
|
|
397
655
|
if (body.act)
|
|
@@ -469,11 +727,36 @@ function reportWindows(installed) {
|
|
|
469
727
|
if (ended.length)
|
|
470
728
|
console.error(D(` ended ${ended.length} pantheon server${ended.length > 1 ? "s" : ""} left behind by windows that no longer exist`));
|
|
471
729
|
}
|
|
730
|
+
const AGENT_ROWS = new Set(["claude code", "cursor", "codex", "copilot", "vs code", "kiro"]);
|
|
731
|
+
const rowFailed = (status) => /^(failed|could not)/i.test(status);
|
|
732
|
+
function readiness(tools) {
|
|
733
|
+
const agents = tools.filter((t) => AGENT_ROWS.has(t.tool));
|
|
734
|
+
const attempted = agents.filter((t) => !/^skipped/i.test(t.status));
|
|
735
|
+
return { failed: tools.filter((t) => rowFailed(t.status)), wired: attempted.filter((t) => !rowFailed(t.status)).length, attempted: attempted.length };
|
|
736
|
+
}
|
|
472
737
|
function printReady(tools) {
|
|
473
738
|
const workerOn = tools.some((t) => t.tool === "worker" && t.status.startsWith("on"));
|
|
474
|
-
|
|
739
|
+
const r = readiness(tools);
|
|
740
|
+
if (r.failed.length) {
|
|
741
|
+
console.error(`\n\x1b[31m${B("Not everything was set up:")}\x1b[0m`);
|
|
742
|
+
for (const f of r.failed)
|
|
743
|
+
console.error(` ${f.tool.padEnd(14)}${f.status}`);
|
|
744
|
+
}
|
|
745
|
+
if (r.attempted && !r.wired) {
|
|
746
|
+
console.error(`\n\x1b[31m No agent on this machine is connected to pantheon.\x1b[0m ${D("Fix the lines above, then run: pantheon install --user")}`);
|
|
747
|
+
process.exitCode = 1;
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (!r.attempted) {
|
|
751
|
+
console.error(`\n${B("No agent CLI was found on this machine,")} so nothing is connected yet. ${D("Install Claude Code, Codex, Cursor, Copilot or Kiro, then run: pantheon install --user")}`);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if (r.failed.length)
|
|
755
|
+
console.error(`\n${B("Partly set.")} ${D("The agents that did not fail above connect once you restart them; fix the rest, then: pantheon install --user")}`);
|
|
756
|
+
else
|
|
757
|
+
console.error(`\n${B("You're set.")} Restart your agent once (quit and reopen) and it is connected.`);
|
|
475
758
|
if (workerOn)
|
|
476
|
-
console.error(D(` While you're away, a teammate you
|
|
759
|
+
console.error(D(` While you're away, a teammate you approve to wake this machine can hand work to it —`));
|
|
477
760
|
if (workerOn)
|
|
478
761
|
console.error(D(` isolated branch, no secrets in the worktree. Pause any time from the menu bar.`));
|
|
479
762
|
const oneTime = new Map();
|
|
@@ -497,9 +780,17 @@ function printReady(tools) {
|
|
|
497
780
|
console.error(D(` ${l}`));
|
|
498
781
|
}
|
|
499
782
|
}
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
const
|
|
783
|
+
const PLAIN = !!process.env["NO_COLOR"];
|
|
784
|
+
if (PLAIN) {
|
|
785
|
+
const SGR = /\x1b\[[0-9;]*m/g;
|
|
786
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
787
|
+
const write = stream.write.bind(stream);
|
|
788
|
+
stream.write = ((chunk, ...rest) => write(typeof chunk === "string" ? chunk.replace(SGR, "") : chunk, ...rest));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
const B = (s) => PLAIN ? s : `\x1b[1m${s}\x1b[0m`;
|
|
792
|
+
const D = (s) => PLAIN ? s : `\x1b[2m${s}\x1b[0m`;
|
|
793
|
+
const C = (s) => PLAIN ? s : `\x1b[36m${s}\x1b[0m`;
|
|
503
794
|
const HELP = `${B("pantheon")} — your agent and your teammates' agents, working together
|
|
504
795
|
|
|
505
796
|
${B("first time — verify your email once")}
|
|
@@ -521,14 +812,14 @@ ${B("getting going — two commands, from your project's repo if you have one")}
|
|
|
521
812
|
|
|
522
813
|
One command does everything: agents wired and pre-approved, wake-up hook (your
|
|
523
814
|
agent finishes what a peer is waiting for), worker (this machine answers the
|
|
524
|
-
team while you are away — on when run from a git repo; a peer still needs
|
|
525
|
-
|
|
815
|
+
team while you are away — on when run from a git repo; a peer still needs you
|
|
816
|
+
to approve them here to use it: pantheon wake allow), native decision alerts, menu bar. Opt-outs:
|
|
526
817
|
--no-worker, --no-hook, --no-notifications, --no-menubar.
|
|
527
818
|
Already a member, new machine or fresh pull? pantheon install --user does the same.
|
|
528
819
|
|
|
529
820
|
${B("several teams on one machine")}
|
|
530
821
|
pantheon spaces your teams, which repo is bound to which, and where this folder points
|
|
531
|
-
pantheon use <team> --here bind this repo to a team (agents opened here speak in it)
|
|
822
|
+
pantheon use <team> --here bind this repo or folder to a team (agents opened here speak in it); --unbind undoes it
|
|
532
823
|
pantheon repo add | list the repos a team works from; a delivery must come from one of them
|
|
533
824
|
(repo add --base <branch>: the branch workers cut from, else origin's default)
|
|
534
825
|
pantheon leave <team> drop one team from this machine (its token and repo bindings)
|
|
@@ -544,7 +835,10 @@ ${B("day to day")}
|
|
|
544
835
|
pantheon logs [-f] the relay's log
|
|
545
836
|
|
|
546
837
|
${B("control")}
|
|
547
|
-
pantheon admit <who> let someone in (--may-wake
|
|
838
|
+
pantheon admit <who> let someone in (--may-wake: the team lets them wake workers, and this machine allows them)
|
|
839
|
+
pantheon wake allow <who> let a teammate wake THIS machine; wake deny <who> takes it back; wake list shows who can
|
|
840
|
+
pantheon trust [<who>] the teammates' keys this machine checks their messages against; trust <who> adopts a changed one
|
|
841
|
+
(this machine only — nothing is sent to the team; --space <team> to pick one)
|
|
548
842
|
pantheon admit <who> --revoke remove them
|
|
549
843
|
pantheon worker stay answerable while you are away
|
|
550
844
|
(--agent claude-code|codex|cursor|copilot|kiro|auto, --persona <name>)
|
|
@@ -563,7 +857,7 @@ ${B("control")}
|
|
|
563
857
|
(auto-detects claude / codex / cursor / copilot / kiro on this machine)
|
|
564
858
|
pantheon spend woken runs on this machine, and what pantheon cannot see
|
|
565
859
|
pantheon leave remove what pantheon put on this machine
|
|
566
|
-
pantheon panic
|
|
860
|
+
pantheon panic emergency stop: ends the run in flight and the worker, and switches off wake rights in every team you are in
|
|
567
861
|
pantheon doctor [--share] check this machine end to end; --share prints it without names or paths, to paste to us
|
|
568
862
|
pantheon network [--proxy <url> | --direct | --clear] how this machine reaches the relay; save a proxy for every Pantheon process here
|
|
569
863
|
pantheon setup three questions — your name, your role, may a woken run execute commands —
|
|
@@ -575,14 +869,14 @@ ${B("control")}
|
|
|
575
869
|
|
|
576
870
|
${D("more: pantheon help --all")}
|
|
577
871
|
`;
|
|
578
|
-
const HELP_ALL = HELP + `
|
|
872
|
+
const HELP_ALL = HELP.replace(/\n[^\n]*more: pantheon help --all[^\n]*\n?$/, "\n") + `
|
|
579
873
|
${B("less common")}
|
|
580
874
|
pantheon relay run only the relay, in this terminal
|
|
581
875
|
pantheon service install [relay|worker|notifier|menubar] | restart | uninstall
|
|
582
876
|
run a long-lived component in the background, from login
|
|
583
877
|
pantheon members the full list, with key fingerprints
|
|
584
|
-
pantheon whoami your name and fingerprint
|
|
585
878
|
pantheon pending what a live agent window has not yet seen
|
|
879
|
+
pantheon goal open "<title>" --with <who> open a goal to work in (goal list · goal close <id> "<summary>")
|
|
586
880
|
pantheon say <ACT> <text> send one act by hand (NOTE, ASK, ANSWER, PROPOSE, ...)
|
|
587
881
|
replies pick the act they answer; --ref <id | seq | last> to choose
|
|
588
882
|
pantheon history the thread in full, with ids (--goal <id>, --from <who>, --last N, --short)
|
|
@@ -607,6 +901,31 @@ function usageFor(command, help) {
|
|
|
607
901
|
}
|
|
608
902
|
return out.length ? out.join("\n") : null;
|
|
609
903
|
}
|
|
904
|
+
const COMMAND_FLAGS = {
|
|
905
|
+
start: "--space <name> --relay <url> --me <name> --port <n> --ttl <minutes> --no-worker --no-hook --no-notifications --no-menubar --reset --yes\n and every setup answer as a flag (--name, --title, --wake, --commands, --alerts, --decides …): pantheon setup --questions",
|
|
906
|
+
join: "<invite link | line | code> --relay <url> --me <name> --no-worker --no-hook --no-notifications --no-menubar --reset --yes\n and every setup answer as a flag: pantheon setup --questions",
|
|
907
|
+
invite: "--list --revoke <token|all> --may-wake | --no-wake --ttl <minutes>",
|
|
908
|
+
admit: "<member> --may-wake | --no-wake --revoke --forget --yes",
|
|
909
|
+
wake: "allow <member> | deny <member> | list --space <team>",
|
|
910
|
+
trust: "[<member>] --yes --space <team>",
|
|
911
|
+
say: "<ACT> <text> --to <member>[,<member>] --goal <id> --ref <id | seq | last> --summary '<one line>' --domains <a,b> --branch --commit --remote (DELIVERED) --space <team>",
|
|
912
|
+
goal: "list | open \"<title>\" --with <member>[,…] [--acceptance \"…\"] [--deadline <ISO time>] [--list] | close <id> \"<summary>\"",
|
|
913
|
+
history: "--goal <id> --from <member> --last <n> --since <seq> --short --presence --json",
|
|
914
|
+
network: "--proxy <url> [--no-proxy <hosts>] | --direct | --clear",
|
|
915
|
+
leave: "[<team>] --yes --keep-identity --stop-relay --project <dir>",
|
|
916
|
+
stop: "[relay|worker|notifier|menubar|all] --port <n>",
|
|
917
|
+
logs: "[relay|worker|notifier|menubar] --follow --n <lines>",
|
|
918
|
+
worker: "--agent <claude-code|codex|cursor|copilot|kiro|auto> --persona <name> --project <dir> --allow-bash --max-runs <n> --max-runs-day <n> --grace <s> --once",
|
|
919
|
+
service: "install [relay|worker|notifier|menubar] | restart <kind> | uninstall <kind> --project <dir> --allow-bash",
|
|
920
|
+
doctor: "--share --project <dir>",
|
|
921
|
+
relay: "--port <n> --host <address> --data <dir>",
|
|
922
|
+
use: "<team> --here --yes · pantheon use --unbind (this folder or repo bound to no team again)",
|
|
923
|
+
repo: "add [--base <branch>] | list [--all] | find [<folder>] [--recents] [--yes]",
|
|
924
|
+
login: "--email <address> --relay <url> --label <device name>",
|
|
925
|
+
signup: "--email <address> --username <name> --relay <url> --label <device name>",
|
|
926
|
+
audit: "--space <team> --since <hours> --json",
|
|
927
|
+
setup: "--questions (and every answer as a flag, with --yes)",
|
|
928
|
+
};
|
|
610
929
|
const HELP_OPAQUE = new Set(["mcp", "hook", "pending", "tray-status", "help", "version", "-v", "--help", "-h"]);
|
|
611
930
|
function rememberWorkingProxy() {
|
|
612
931
|
try {
|
|
@@ -622,7 +941,54 @@ function rememberWorkingProxy() {
|
|
|
622
941
|
}
|
|
623
942
|
catch { }
|
|
624
943
|
}
|
|
944
|
+
async function teamStanding(all, m) {
|
|
945
|
+
const c = new PantheonClient(configFor(all, m, null), undefined, { role: "cli" });
|
|
946
|
+
try {
|
|
947
|
+
await Promise.race([c.connect(), new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), 8_000))]);
|
|
948
|
+
return { others: c.members.filter((x) => x.admitted && x.member !== all.member).map((x) => x.member), founder: !!c.members.find((x) => x.member === all.member)?.founder };
|
|
949
|
+
}
|
|
950
|
+
catch {
|
|
951
|
+
return { others: null, founder: false };
|
|
952
|
+
}
|
|
953
|
+
finally {
|
|
954
|
+
c.close();
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
function slipDistance(a, b) {
|
|
958
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
959
|
+
for (let j = 1; j <= b.length; j++)
|
|
960
|
+
d[0][j] = j;
|
|
961
|
+
for (let i = 1; i <= a.length; i++)
|
|
962
|
+
for (let j = 1; j <= b.length; j++) {
|
|
963
|
+
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
964
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1])
|
|
965
|
+
d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
|
|
966
|
+
}
|
|
967
|
+
return d[a.length][b.length];
|
|
968
|
+
}
|
|
969
|
+
const EXTRA_FLAGS = "--agent-cmd --approve-all --ask-before-sending --child --code --data --device --display --force --help --host --idle --keywords --local " +
|
|
970
|
+
"--no-continue --no-guide --no-notify --path --peek --poll --portable --publish --queued --quiet --remind --scratch --session --surface --token --unattended " +
|
|
971
|
+
"--until --user --version --all --json " +
|
|
972
|
+
"--name --title --wake --commands --unsandboxed --alerts --decides --playbook --base --responsibilities --human-required --agent-can-decide --capabilities";
|
|
973
|
+
const KNOWN_FLAGS = new Set([...`${Object.values(COMMAND_FLAGS).join(" ")} ${EXTRA_FLAGS}`.matchAll(/--([a-z][a-z0-9-]*)/g)].map((m) => m[1]));
|
|
974
|
+
function refuseUnknownFlags() {
|
|
975
|
+
if (["mcp", "hook", "tray-status", "_menubar"].includes(cmd))
|
|
976
|
+
return;
|
|
977
|
+
for (const a of argv.slice(1)) {
|
|
978
|
+
if (!/^--[a-z][a-z0-9-]*(?:=\S*)?$/i.test(a))
|
|
979
|
+
continue;
|
|
980
|
+
const name = a.slice(2).split("=")[0].toLowerCase();
|
|
981
|
+
if (KNOWN_FLAGS.has(name))
|
|
982
|
+
continue;
|
|
983
|
+
const own = [...(COMMAND_FLAGS[cmd] ?? "").matchAll(/--([a-z][a-z0-9-]*)/g)].map((m) => m[1]);
|
|
984
|
+
const rank = (ks) => ks.map((k) => ({ k, d: slipDistance(name, k) })).sort((x, y) => x.d - y.d)[0];
|
|
985
|
+
const mine = rank(own);
|
|
986
|
+
const near = mine && mine.d <= 2 ? mine : rank([...KNOWN_FLAGS]);
|
|
987
|
+
die(`unknown flag ${a}${near && near.d <= 2 ? ` — did you mean --${near.k}?` : ""}\n Nothing was run. pantheon ${cmd} --help shows its usage${cmd === "say" ? "; if it is part of your message, put the message in quotes" : ""}.`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
625
990
|
async function main() {
|
|
991
|
+
refuseUnknownFlags();
|
|
626
992
|
try {
|
|
627
993
|
await installNetwork();
|
|
628
994
|
}
|
|
@@ -641,11 +1007,15 @@ async function main() {
|
|
|
641
1007
|
}
|
|
642
1008
|
return interactiveOnboard();
|
|
643
1009
|
}
|
|
644
|
-
|
|
1010
|
+
const askedHelpOnly = argv.length === 2 && (argv[1] === "--help" || argv[1] === "-h") && ["pending", "hook"].includes(cmd);
|
|
1011
|
+
if (!HELP_OPAQUE.has(cmd) || askedHelpOnly) {
|
|
645
1012
|
const helpAt = cmd === "say" ? [argv[1]] : argv.slice(1);
|
|
646
1013
|
if (helpAt.some((a) => a === "--help" || a === "-h")) {
|
|
647
1014
|
const u = usageFor(cmd, HELP_ALL);
|
|
648
|
-
|
|
1015
|
+
const flags = COMMAND_FLAGS[cmd];
|
|
1016
|
+
const teamCmd = !["start", "join", "signup", "login", "verify", "whoami", "relay", "help", "version", "upgrade", "network", "ps", "stop", "service", "hook", "mcp", "pending", "tray-status"].includes(cmd);
|
|
1017
|
+
const shown = flags && teamCmd && !/--space/.test(flags) ? `${flags}\n --space <team> (the team to act on, when this folder is bound to another or none)` : flags;
|
|
1018
|
+
console.log(u || shown ? `${u ?? ` pantheon ${cmd}`}${shown ? `\n\n${B("flags")}\n ${shown}` : ""}\n\n${D("nothing was run · all commands: pantheon help --all")}` : HELP);
|
|
649
1019
|
return;
|
|
650
1020
|
}
|
|
651
1021
|
}
|
|
@@ -656,7 +1026,28 @@ async function main() {
|
|
|
656
1026
|
if (set !== undefined) {
|
|
657
1027
|
const p = normalizeProxy(set) ?? die(`"${redactText(set)}" is not a proxy address — use http://host:port (add user:password@ if it needs a login)`);
|
|
658
1028
|
saveAll({ ...all, network: { proxy: p, ...(flag("no-proxy") ? { no_proxy: flag("no-proxy") } : {}), source: "pantheon network", saved_at: Date.now() } });
|
|
659
|
-
|
|
1029
|
+
const knock = await (async () => {
|
|
1030
|
+
try {
|
|
1031
|
+
const u = new URL(p);
|
|
1032
|
+
const { connect } = await import("node:net");
|
|
1033
|
+
const port = Number(u.port) || (u.protocol === "https:" ? 443 : 80);
|
|
1034
|
+
return await new Promise((done) => {
|
|
1035
|
+
const sock = connect({ host: u.hostname, port, timeout: 3_000 });
|
|
1036
|
+
sock.once("connect", () => { sock.destroy(); done(null); });
|
|
1037
|
+
sock.once("timeout", () => { sock.destroy(); done("did not answer in 3s"); });
|
|
1038
|
+
sock.once("error", (e) => done(e.code === "ENOTFOUND" ? "its name did not resolve" : e.code === "ECONNREFUSED" ? "nothing is listening there" : e.message));
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
catch (e) {
|
|
1042
|
+
return e.message;
|
|
1043
|
+
}
|
|
1044
|
+
})();
|
|
1045
|
+
if (knock) {
|
|
1046
|
+
console.error(`${B("saved")} ${redactProxy(p)} — but right now ${knock}. Every Pantheon process here will try it; check the address, or undo: pantheon network --clear`);
|
|
1047
|
+
}
|
|
1048
|
+
else {
|
|
1049
|
+
console.error(`${B("saved")} every Pantheon process on this machine will go through ${redactProxy(p)} (it answered). pantheon doctor checks the relay through it.`);
|
|
1050
|
+
}
|
|
660
1051
|
return;
|
|
661
1052
|
}
|
|
662
1053
|
if (argv.includes("--direct")) {
|
|
@@ -686,13 +1077,14 @@ async function main() {
|
|
|
686
1077
|
die(setupFlags.errors.join("\n") + "\n pantheon setup --questions lists every answer");
|
|
687
1078
|
const wp = unattendedProject();
|
|
688
1079
|
requireSignInOnHostedRelay(flag("relay"));
|
|
1080
|
+
await requireSignInOnAccountsRelay(flag("relay"));
|
|
689
1081
|
console.error(`${B("pantheon start")}\n`);
|
|
690
1082
|
if (!flag("space") && !repoInfo(process.cwd()))
|
|
691
1083
|
console.error(D(` No git repo here, so the team will be named after this folder: "${teamNameFor(process.cwd())}". Pass --space <name> to choose.\n`));
|
|
692
1084
|
const r = await qsStart({
|
|
693
1085
|
port,
|
|
694
1086
|
dataDir: resolveDataDir(flag("data")).dir,
|
|
695
|
-
ttlMinutes:
|
|
1087
|
+
ttlMinutes: numFlag("ttl", 1440),
|
|
696
1088
|
reset: argv.includes("--reset"),
|
|
697
1089
|
...(flag("space") ? { space: flag("space") } : {}),
|
|
698
1090
|
...(flag("me") ? { me: flag("me") } : {}),
|
|
@@ -708,8 +1100,9 @@ async function main() {
|
|
|
708
1100
|
for (const t of r.tools)
|
|
709
1101
|
console.error(` ${t.tool.padEnd(14)}${t.status}`);
|
|
710
1102
|
for (const w of r.warnings)
|
|
711
|
-
console.error(`\x1b[33m ! ${w}\x1b[0m`);
|
|
1103
|
+
console.error(`\x1b[33m ! ${w}\x1b[0m` + (/NOT_SIGNED_IN/.test(w) ? signInHint("NOT_SIGNED_IN", r.cfg.relay) : ""));
|
|
712
1104
|
printGranted(r.granted);
|
|
1105
|
+
printRelayNote(r.cfg?.relay);
|
|
713
1106
|
rememberWorkingProxy();
|
|
714
1107
|
if (!r.claimed) {
|
|
715
1108
|
console.error(`\n\x1b[31m No team was created.\x1b[0m ${D("Fix the warning above, then run `pantheon start` again.")}`);
|
|
@@ -729,18 +1122,25 @@ async function main() {
|
|
|
729
1122
|
console.error(`\n${B("Send your teammate this one line — it sets up their whole machine:")}\n`);
|
|
730
1123
|
console.error(` ${C(`pantheon join ${r.invite}`)}\n`);
|
|
731
1124
|
}
|
|
732
|
-
console.error(D(` From
|
|
1125
|
+
console.error(D(` From the folder they work in — a clone of the team's repo, or any folder for docs and plans (no git needed). Single use, expires in ${Math.round(numFlag("ttl", 1440) / 60)}h.`));
|
|
733
1126
|
console.error(D(` Redeeming it admits them — no separate approval step. Need another? \`pantheon invite\``));
|
|
1127
|
+
console.error(r.inviteMayWake
|
|
1128
|
+
? ` ${B("It also lets them wake workers:")} ${D("their requests can start an agent run on this machine while you are away (you chose wake \"anyone\"). Take it back: pantheon admit <name> --revoke")}`
|
|
1129
|
+
: D(` It lets them join and talk to your agent. It does not let them wake this machine on its own: the first time their request needs it while you are away, you are asked (Always / Just this once / No). Approve ahead: pantheon wake allow <name>`));
|
|
734
1130
|
console.error(D(`\n Not technical? Forward them this instead, and their agent does the rest:`));
|
|
735
|
-
console.error(` ${D("Tell your agent:
|
|
1131
|
+
console.error(` ${D("Tell your agent: read")} ${C("https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${link ?? r.invite}`)}`);
|
|
736
1132
|
}
|
|
737
1133
|
if (!r.reach.shareable && r.reach.hint) {
|
|
738
1134
|
console.error(`\n\x1b[33m That address only works on this machine.\x1b[0m`);
|
|
739
1135
|
console.error(D(` ${r.reach.hint}`));
|
|
740
1136
|
}
|
|
1137
|
+
if (r.claimed) {
|
|
1138
|
+
await bindReposHere(argv.includes("--yes"));
|
|
1139
|
+
await workerAfterBinding(wp.how);
|
|
1140
|
+
}
|
|
741
1141
|
printReady(r.tools);
|
|
742
1142
|
if (r.claimed)
|
|
743
|
-
await guidedSetup(load() ?? r.cfg, repoInfo(process.cwd())?.root ?? null, { preset: setupFlags.preset, force: Object.keys(setupFlags.preset).length > 0 || argv.includes("--yes") });
|
|
1143
|
+
await guidedSetup(load() ?? r.cfg, repoInfo(process.cwd())?.root ?? null, { preset: setupFlags.preset, force: Object.keys(setupFlags.preset).length > 0 || argv.includes("--yes"), founder: true });
|
|
744
1144
|
if (r.relay) {
|
|
745
1145
|
console.error(D(`\nKeeping the relay running here. Ctrl-C stops it (your teammate goes offline too).`));
|
|
746
1146
|
await new Promise(() => { });
|
|
@@ -774,11 +1174,11 @@ async function main() {
|
|
|
774
1174
|
for (const i of open)
|
|
775
1175
|
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)")}`);
|
|
776
1176
|
if (open.length)
|
|
777
|
-
console.log(D(`\nrevoke one: pantheon invite --revoke <token or its last 6 characters
|
|
1177
|
+
console.log(D(`\nrevoke one: pantheon invite --revoke <token or its last 6 characters>${teamFlag(cfg)} · all of yours: pantheon invite --revoke all${teamFlag(cfg)}`));
|
|
778
1178
|
client.close();
|
|
779
1179
|
return;
|
|
780
1180
|
}
|
|
781
|
-
const ttl =
|
|
1181
|
+
const ttl = numFlag("ttl", 1440);
|
|
782
1182
|
const mayWake = argv.includes("--may-wake") ? true : argv.includes("--no-wake") ? false : wakeDefault(loadProfile(cfg));
|
|
783
1183
|
const inv = await client.makeInvite(mayWake, ttl).catch((e) => die(e.message));
|
|
784
1184
|
const raw = toInvite({ ...cfg, token: inv.url });
|
|
@@ -788,13 +1188,29 @@ async function main() {
|
|
|
788
1188
|
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)}`);
|
|
789
1189
|
console.error(D(`\nSingle use, expires ${new Date(inv.expires_at).toLocaleString()}.`));
|
|
790
1190
|
console.error(D(`For a teammate who is not technical, forward this and their agent does the rest:`));
|
|
791
|
-
console.error(` ${D("Tell your agent:
|
|
1191
|
+
console.error(` ${D("Tell your agent: read")} ${C("https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${link ?? raw}`)}`);
|
|
792
1192
|
client.close();
|
|
793
1193
|
return;
|
|
794
1194
|
}
|
|
795
1195
|
case "relay": {
|
|
1196
|
+
for (const s of [process.stdout, process.stderr])
|
|
1197
|
+
s.on("error", () => { });
|
|
796
1198
|
process.on("unhandledRejection", (e) => { console.error(`! unhandled rejection: ${e?.stack ?? String(e)}`); });
|
|
797
|
-
|
|
1199
|
+
for (const s of [process.stdout, process.stderr])
|
|
1200
|
+
s.on("error", () => { });
|
|
1201
|
+
let inHandler = false;
|
|
1202
|
+
process.on("uncaughtException", (e) => {
|
|
1203
|
+
if (e?.code === "EPIPE" || e?.code === "ERR_STREAM_DESTROYED" || inHandler)
|
|
1204
|
+
return;
|
|
1205
|
+
inHandler = true;
|
|
1206
|
+
try {
|
|
1207
|
+
console.error(`! uncaught exception (kept serving): ${e?.stack ?? String(e)}`);
|
|
1208
|
+
}
|
|
1209
|
+
catch { }
|
|
1210
|
+
finally {
|
|
1211
|
+
inHandler = false;
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
798
1214
|
const relay = new Relay({
|
|
799
1215
|
port: Number(flag("port", process.env.PORT ?? "8787")),
|
|
800
1216
|
host: flag("host", "0.0.0.0"),
|
|
@@ -805,7 +1221,7 @@ async function main() {
|
|
|
805
1221
|
console.error(D(`ledger: ${resolveDataDir(flag("data")).dir}`));
|
|
806
1222
|
console.error(D(`health: http://localhost:${port}/health`));
|
|
807
1223
|
console.error(D(`\nFor a colleague on another network, expose this port (ssh -R, cloudflared, tailscale)`));
|
|
808
|
-
console.error(D(`and
|
|
1224
|
+
console.error(D(`then, on your own machine: pantheon start --relay <this address>, and send them what pantheon invite prints. Ctrl-C to stop.`));
|
|
809
1225
|
let shuttingDown = false;
|
|
810
1226
|
const shutdown = async (sig) => {
|
|
811
1227
|
if (shuttingDown)
|
|
@@ -878,7 +1294,8 @@ async function main() {
|
|
|
878
1294
|
catch (e) {
|
|
879
1295
|
if (!looksLikeInviteCode(invite))
|
|
880
1296
|
die(e.message);
|
|
881
|
-
const
|
|
1297
|
+
const named = flag("relay") ?? process.env.PANTHEON_RELAY;
|
|
1298
|
+
const candidates = (named ? [named] : [load()?.relay, DEFAULT_RELAY]).filter((r) => !!r);
|
|
882
1299
|
parsed = await resolveInviteCode(invite, candidates).catch((err) => die(err.message.replace(/^INVITE_CODE_[A-Z]+:\s*/, "")));
|
|
883
1300
|
console.error(D(`That was just the code — it opens "${parsed.space}" on ${new URL(parsed.relay).host}. Joining that team.\n`));
|
|
884
1301
|
}
|
|
@@ -901,21 +1318,59 @@ async function main() {
|
|
|
901
1318
|
const m = e.message;
|
|
902
1319
|
if (/BAD_INVITE/.test(m)) {
|
|
903
1320
|
const body = m.replace(/^BAD_INVITE:\s*/, "");
|
|
904
|
-
die(/\n/.test(body) ? body : `${body}\n Ask your teammate for a fresh one: pantheon invite`);
|
|
1321
|
+
die(/\n/.test(body) || /fresh one|new invite/i.test(body) ? body : `${body}\n Ask your teammate for a fresh one: pantheon invite`);
|
|
905
1322
|
}
|
|
906
1323
|
if (/NO_SUCH_SPACE/.test(m))
|
|
907
|
-
die(`${m}\n
|
|
1324
|
+
die(`${m}\n The relay answered, but has no team by that name — it was deleted, or the link was copied from somewhere else. Ask your teammate for a fresh invite.`);
|
|
908
1325
|
if (/ECONNREFUSED|ENOTFOUND|timeout/i.test(m))
|
|
909
1326
|
die(`cannot reach the relay in that invite.\n ${m}\n Is their relay still running, and are you both on the same network/tailnet?`);
|
|
910
|
-
die(m);
|
|
1327
|
+
die(m + (/NOT_SIGNED_IN/.test(m) ? signInHint("NOT_SIGNED_IN", parsed.relay) : ""));
|
|
911
1328
|
});
|
|
912
1329
|
for (const t of r.tools)
|
|
913
1330
|
console.error(` ${t.tool.padEnd(14)}${t.status}`);
|
|
914
1331
|
for (const w of r.warnings)
|
|
915
|
-
console.error(`\x1b[33m ! ${w}\x1b[0m`);
|
|
1332
|
+
console.error(`\x1b[33m ! ${w}\x1b[0m` + (/NOT_SIGNED_IN/.test(w) ? signInHint("NOT_SIGNED_IN", parsed.relay) : ""));
|
|
916
1333
|
printGranted(r.granted);
|
|
1334
|
+
printRelayNote(r.cfg?.relay ?? parsed.relay);
|
|
917
1335
|
rememberWorkingProxy();
|
|
918
1336
|
printReady(r.tools);
|
|
1337
|
+
{
|
|
1338
|
+
const jc = load();
|
|
1339
|
+
const mode = jc ? wakeMode(loadProfile(jc)) : "anyone";
|
|
1340
|
+
console.error(D(mode === "anyone"
|
|
1341
|
+
? `\n Your teammates can hand work to this machine while you're away, so nothing waits on you. To approve each teammate first: pantheon setup --wake approved`
|
|
1342
|
+
: mode === "no"
|
|
1343
|
+
? `\n This machine is set not to be woken: teammates' requests wait for you. Change it: pantheon setup`
|
|
1344
|
+
: `\n The first time a teammate's request needs this machine while you are away, you are asked — "Always / Just this once / No" in the menu bar. Approve someone ahead: pantheon wake allow <name>`));
|
|
1345
|
+
}
|
|
1346
|
+
if (process.stdin.isTTY)
|
|
1347
|
+
await bindReposHere(false);
|
|
1348
|
+
{
|
|
1349
|
+
const j = load();
|
|
1350
|
+
let teamRepos = [];
|
|
1351
|
+
if (j) {
|
|
1352
|
+
const { repo: _r, repoOrigin: _o, ...plain } = j;
|
|
1353
|
+
const c = new PantheonClient(plain, undefined, { role: "cli" });
|
|
1354
|
+
try {
|
|
1355
|
+
await Promise.race([c.connect(), new Promise((_, no) => setTimeout(() => no(new Error("timeout")), 8000))]);
|
|
1356
|
+
await new Promise((res) => setTimeout(res, 150));
|
|
1357
|
+
teamRepos = c.repos.map((r) => ({ fingerprint: r.fingerprint, origin: r.origin ?? null }));
|
|
1358
|
+
}
|
|
1359
|
+
catch { }
|
|
1360
|
+
finally {
|
|
1361
|
+
c.close();
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
const all = loadAll();
|
|
1365
|
+
const m = all && j ? findMembership(all, j.spaceId ?? j.space) ?? findMembership(all, j.space) : null;
|
|
1366
|
+
if (!teamRepos.length && m?.repos?.length)
|
|
1367
|
+
teamRepos = m.repos.map((r) => ({ fingerprint: r.fp, origin: r.name }));
|
|
1368
|
+
if (all && m && teamRepos.length) {
|
|
1369
|
+
console.error("");
|
|
1370
|
+
await findAndBindRepos(all, m, teamRepos, { auto: !process.stdin.isTTY }).catch(() => { });
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
await workerAfterBinding(wp.how);
|
|
919
1374
|
{
|
|
920
1375
|
const joined = load();
|
|
921
1376
|
if (joined)
|
|
@@ -1070,6 +1525,8 @@ async function main() {
|
|
|
1070
1525
|
return;
|
|
1071
1526
|
}
|
|
1072
1527
|
case "setup": {
|
|
1528
|
+
if (inWokenRun() && !argv.includes("--questions"))
|
|
1529
|
+
die("a woken run cannot change this machine's setup — a person has to run this here");
|
|
1073
1530
|
if (argv.includes("--questions")) {
|
|
1074
1531
|
console.log(questionnaire());
|
|
1075
1532
|
return;
|
|
@@ -1104,15 +1561,20 @@ async function main() {
|
|
|
1104
1561
|
console.log(B("members"));
|
|
1105
1562
|
if (!peers.length)
|
|
1106
1563
|
console.log(` ${D("(nobody else yet — send them an invite from `pantheon invite`)")}`);
|
|
1564
|
+
const seens = peers.map((p) => p.seen_seq).filter((x) => x !== undefined && x < client.head);
|
|
1565
|
+
const since = seens.length ? Math.min(...seens) : null;
|
|
1566
|
+
const recent = since === null ? [] : await fetchHistory(cfg, { since }).catch(() => null);
|
|
1107
1567
|
for (const p of peers) {
|
|
1108
|
-
const
|
|
1568
|
+
const lag = p.seen_seq === undefined ? null : client.head - p.seen_seq;
|
|
1569
|
+
const behind = lag === null ? null : lag <= 0 ? 0
|
|
1570
|
+
: recent ? recent.filter((a) => a.seq > p.seen_seq && a.from === cfg.member && a.act !== "JOIN" && a.act !== "LEAVE" && !a.meta?.organization_profile).length : lag;
|
|
1109
1571
|
const reach = behind === null
|
|
1110
1572
|
? D(p.delivered_seq !== undefined && p.delivered_seq >= client.head
|
|
1111
1573
|
? "delivered, but no read-receipt — no wake-up hook on their machine"
|
|
1112
1574
|
: "no wake-up hook on their machine — nothing surfaces until their human types")
|
|
1113
1575
|
: behind > 0
|
|
1114
|
-
? `\x1b[
|
|
1115
|
-
: D("their agent is caught up");
|
|
1576
|
+
? `\x1b[33m${recent ? `their agent has not seen your last ${behind} act${behind === 1 ? "" : "s"}` : `their agent is ${behind} act${behind === 1 ? "" : "s"} behind the team`}\x1b[0m`
|
|
1577
|
+
: lag > 0 ? D("their agent has seen everything you sent") : D("their agent is caught up");
|
|
1116
1578
|
console.log(` ${p.online ? C("●") : D("○")} ${p.id.padEnd(14)} ${D(p.agent.padEnd(12))} ${reach}`);
|
|
1117
1579
|
}
|
|
1118
1580
|
const goals = [...client.goals.values()];
|
|
@@ -1135,6 +1597,12 @@ async function main() {
|
|
|
1135
1597
|
for (const o of human) {
|
|
1136
1598
|
const who = o.owner.replace(/^human:/, "");
|
|
1137
1599
|
const stale = Date.now() - o.created_at > 3_600_000;
|
|
1600
|
+
const askAct = client.seenAct(o.ref);
|
|
1601
|
+
const wakeAsk = askAct?.from === who ? askAct.meta?.wake_ask : undefined;
|
|
1602
|
+
if (who !== cfg.member && typeof wakeAsk === "string") {
|
|
1603
|
+
console.log(` ${who.padEnd(14)}${ageOf(o.created_at).padEnd(6)} ${D(`is being asked on their machine whether ${wakeAsk === cfg.member ? "you" : wakeAsk} may start work there while they are away`)}`);
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1138
1606
|
console.log(` ${stale ? "\x1b[33m" : C("")}${who.padEnd(14)}${ageOf(o.created_at).padEnd(6)}\x1b[0m ${o.body.split("\n")[0].slice(0, 110)} ${D(o.ref)}`);
|
|
1139
1607
|
if (who === cfg.member)
|
|
1140
1608
|
console.log(` ${D("your peer was told this decision was coming — answer it or say it is dropped")}`);
|
|
@@ -1175,7 +1643,7 @@ async function main() {
|
|
|
1175
1643
|
owed: client.myObligations().map((o) => ({
|
|
1176
1644
|
id: o.id, kind: o.kind, body: o.body, created_at: o.created_at,
|
|
1177
1645
|
})),
|
|
1178
|
-
recentRuns: recentRuns(86_400_000),
|
|
1646
|
+
recentRuns: recentRuns(86_400_000, undefined, 12, { teams: [client.spaceId, cfg.spaceId, cfg.space], member: cfg.member }),
|
|
1179
1647
|
}));
|
|
1180
1648
|
client.close();
|
|
1181
1649
|
}
|
|
@@ -1218,16 +1686,14 @@ async function main() {
|
|
|
1218
1686
|
console.log(` ${line}`);
|
|
1219
1687
|
console.log();
|
|
1220
1688
|
};
|
|
1221
|
-
const
|
|
1222
|
-
const q = `space=${encodeURIComponent(cfg.space)}&token=${encodeURIComponent(cfg.token)}`;
|
|
1223
|
-
const first = await fetch(`${base}/history?${q}`).catch((e) => die(describeNetError(e, cfg.relay)));
|
|
1689
|
+
const first = await signedRead(cfg, "/history").catch((e) => die(describeNetError(e, cfg.relay)));
|
|
1224
1690
|
if (!first.ok)
|
|
1225
|
-
die(
|
|
1691
|
+
die((await readFailure(cfg, first, "the ledger")).message);
|
|
1226
1692
|
let since = ((await first.json()).acts ?? []).at(-1)?.seq ?? 0;
|
|
1227
1693
|
console.log(`${B("watching")} ${cfg.space} ${D("(ctrl-c to stop)")}\n`);
|
|
1228
1694
|
for (;;) {
|
|
1229
1695
|
await new Promise((r) => setTimeout(r, LIMITS.watchPollMs));
|
|
1230
|
-
const res = await
|
|
1696
|
+
const res = await signedRead(cfg, "/history", { since }).catch(() => null);
|
|
1231
1697
|
if (!res?.ok)
|
|
1232
1698
|
continue;
|
|
1233
1699
|
for (const act of (await res.json()).acts ?? []) {
|
|
@@ -1242,6 +1708,8 @@ async function main() {
|
|
|
1242
1708
|
if (!report) {
|
|
1243
1709
|
if (argv.includes("--json"))
|
|
1244
1710
|
console.log(JSON.stringify({ seq: 0, acts: [], obligations: [] }));
|
|
1711
|
+
else if (process.stderr.isTTY)
|
|
1712
|
+
console.error(D(`nothing new for this session${load() ? "" : " — this folder resolves to no team (pantheon status says why)"}`));
|
|
1245
1713
|
return;
|
|
1246
1714
|
}
|
|
1247
1715
|
console.log(argv.includes("--json") ? JSON.stringify(report) : renderPending(report));
|
|
@@ -1276,7 +1744,8 @@ async function main() {
|
|
|
1276
1744
|
}
|
|
1277
1745
|
for (const a of picked)
|
|
1278
1746
|
console.log(formatAct(a, { short: argv.includes("--short"), ...(a.goal && titles.get(a.goal) ? { goalTitle: titles.get(a.goal) } : {}) }) + "\n");
|
|
1279
|
-
|
|
1747
|
+
const pool = argv.includes("--presence") ? acts.length : acts.filter((a) => a.act !== "JOIN" && a.act !== "LEAVE").length;
|
|
1748
|
+
console.log(D(`${picked.length} of ${pool} acts${argv.includes("--presence") ? " (joins and leaves included)" : ""} · --last N · --goal <id> · --from <member> · --short · --json · reply: pantheon say ANSWER "…" --ref <id or seq>`));
|
|
1280
1749
|
return;
|
|
1281
1750
|
}
|
|
1282
1751
|
case "show": {
|
|
@@ -1292,13 +1761,66 @@ async function main() {
|
|
|
1292
1761
|
}
|
|
1293
1762
|
return;
|
|
1294
1763
|
}
|
|
1764
|
+
case "goal": {
|
|
1765
|
+
const cfg = need();
|
|
1766
|
+
const sub = argv[1] ?? "list";
|
|
1767
|
+
const client = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
1768
|
+
await client.connect().catch((e) => die(connectFailure(e, cfg.relay)));
|
|
1769
|
+
try {
|
|
1770
|
+
if (sub === "list") {
|
|
1771
|
+
const open = [...client.goals.values()].filter((g) => g.state !== "CLOSED");
|
|
1772
|
+
if (!open.length)
|
|
1773
|
+
console.log(D("no open goals — pantheon goal open \"<what it is about>\" --with <member>"));
|
|
1774
|
+
for (const g of open)
|
|
1775
|
+
console.log(` ${g.id} ${g.state.padEnd(8)} ${g.title} ${D(`with ${g.members.join(", ")} · ${g.acts}/${g.budget} acts`)}`);
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
if (sub === "open") {
|
|
1779
|
+
const VALUE_FLAGS = new Set(["--with", "--acceptance", "--deadline", "--space"]);
|
|
1780
|
+
const title = argv.slice(2).filter((a, i, all) => !a.startsWith("--") && !(i > 0 && VALUE_FLAGS.has(all[i - 1]))).join(" ").trim()
|
|
1781
|
+
|| die('usage: pantheon goal open "<what it is about>" --with <member>[,<member>] [--acceptance "<what done looks like>"] [--deadline <ISO time>] [--list]');
|
|
1782
|
+
const withs = cleanList((flag("with") ?? "").split(","));
|
|
1783
|
+
const dl = flag("deadline");
|
|
1784
|
+
const deadline = dl ? Date.parse(dl) : undefined;
|
|
1785
|
+
if (dl && !Number.isFinite(deadline))
|
|
1786
|
+
die(`--deadline takes a time like 2026-10-01T17:00:00Z, not "${dl}"`);
|
|
1787
|
+
const g = await client.openGoal({ title, acceptance: flag("acceptance") ?? "", ...(withs.length ? { invite: withs } : {}),
|
|
1788
|
+
...(deadline ? { deadline } : {}), ...(argv.includes("--list") ? { kind: "worklist" } : {}) }).catch((e) => die(e.message));
|
|
1789
|
+
console.log(`opened ${g.id} "${title}"${withs.length ? ` with ${withs.join(", ")}` : ""}`);
|
|
1790
|
+
console.error(D(` now: pantheon say ASK "…" --to <member> --goal ${g.id}`));
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
if (sub === "close") {
|
|
1794
|
+
const id = argv[2] ?? die('usage: pantheon goal close <goal id> "<what was done>"');
|
|
1795
|
+
const summary = argv.slice(3).filter((a) => !a.startsWith("--")).join(" ").trim() || die("say what was done: it is the goal's record");
|
|
1796
|
+
const r = await client.send({ act: "CLOSE", body: summary, goal: id }).catch((e) => die(e.message));
|
|
1797
|
+
console.log(`closed ${id} (act ${r.id})`);
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
die("usage: pantheon goal list | open \"<title>\" --with <member> | close <id> \"<summary>\"");
|
|
1801
|
+
}
|
|
1802
|
+
finally {
|
|
1803
|
+
client.close();
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1295
1806
|
case "say": {
|
|
1296
1807
|
const cfg = need();
|
|
1297
1808
|
const act = argv[1]?.toUpperCase() ?? die("usage: pantheon say <ACT> <body> [--goal id] [--to a,b] [--ref <id | seq | last>] [--summary 'one line'] [--domains technical,gtm]");
|
|
1809
|
+
{
|
|
1810
|
+
const sayable = ACTS.filter((a) => a !== "JOIN" && a !== "LEAVE");
|
|
1811
|
+
if (!sayable.includes(act))
|
|
1812
|
+
die(`"${argv[1]}" is not an act. One of: ${sayable.join(", ")}`);
|
|
1813
|
+
}
|
|
1298
1814
|
const stop = argv.findIndex((a, i) => i >= 2 && a.startsWith("--"));
|
|
1299
1815
|
const body = argv.slice(2, stop === -1 ? undefined : stop).join(" ");
|
|
1300
1816
|
if (!body)
|
|
1301
1817
|
die("nothing to say — pantheon say NOTE 'your message here'");
|
|
1818
|
+
{
|
|
1819
|
+
const SAY_FLAGS = new Set([...COMMAND_FLAGS["say"].matchAll(/--([a-z][a-z-]*)/g)].map((m) => m[1]));
|
|
1820
|
+
const stray = argv.slice(stop === -1 ? argv.length : stop).find((a) => /^--[a-z]/i.test(a) && !SAY_FLAGS.has(a.slice(2).toLowerCase()) && a !== "--help");
|
|
1821
|
+
if (stray)
|
|
1822
|
+
die(`${stray} is not a flag of say, so it reads as part of your message — and the message would have stopped before it. Put the whole message in quotes: pantheon say ${act} "…"`);
|
|
1823
|
+
}
|
|
1302
1824
|
const summary = flag("summary");
|
|
1303
1825
|
const client = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
1304
1826
|
await client.connect().catch((e) => die(connectFailure(e, cfg.relay)));
|
|
@@ -1316,15 +1838,25 @@ async function main() {
|
|
|
1316
1838
|
let ref = flag("ref") ?? null;
|
|
1317
1839
|
if (ref !== null || ["ANSWER", "ACCEPT", "OBJECT"].includes(act)) {
|
|
1318
1840
|
let acts;
|
|
1841
|
+
let settled = null;
|
|
1842
|
+
let answered = new Set();
|
|
1319
1843
|
if (ref && ref !== "last") {
|
|
1320
1844
|
const hit = (await fetchAct(cfg, ref))?.act;
|
|
1321
1845
|
acts = hit ? [hit] : [];
|
|
1322
|
-
if (!hit && /^a_/.test(ref))
|
|
1323
|
-
console.error(D(` ${ref} is not in what this relay holds — sending it as given`));
|
|
1324
1846
|
}
|
|
1325
|
-
else
|
|
1847
|
+
else {
|
|
1326
1848
|
acts = await fetchHistory(cfg, goalArg ? { goal: goalArg } : {}).catch(() => []);
|
|
1849
|
+
answered = new Set(acts.filter((a) => a.from === cfg.member && a.ref).map((a) => a.ref));
|
|
1850
|
+
const closed = new Set([...client.goals.values()].filter((g) => g.state === "CLOSED").map((g) => g.id));
|
|
1851
|
+
const all = acts;
|
|
1852
|
+
acts = acts.filter((a) => !answered.has(a.id) && !(a.goal && closed.has(a.goal)));
|
|
1853
|
+
settled = all.filter((a) => a.from !== cfg.member && (answered.has(a.id) || (!!a.goal && closed.has(a.goal))) && (!a.to?.length || a.to.includes(cfg.member))).at(-1) ?? null;
|
|
1854
|
+
}
|
|
1327
1855
|
const rr = resolveRef(acts, ref, act, goalArg, cfg.member);
|
|
1856
|
+
if (rr.error && settled) {
|
|
1857
|
+
client.close();
|
|
1858
|
+
die(`${rr.error}\n The latest one to you, [${settled.seq}] ${settled.act} from ${settled.from}, is already ${answered.has(settled.id) ? "answered" : "in a closed goal"} — to answer it again: --ref ${settled.seq}`);
|
|
1859
|
+
}
|
|
1328
1860
|
if (rr.error) {
|
|
1329
1861
|
client.close();
|
|
1330
1862
|
die(rr.error);
|
|
@@ -1341,8 +1873,17 @@ async function main() {
|
|
|
1341
1873
|
ref,
|
|
1342
1874
|
...(summary ? { summary } : {}),
|
|
1343
1875
|
...(Object.keys(meta).length ? { meta } : {}),
|
|
1344
|
-
}).catch((e) => die(/SUMMARY_REQUIRED/.test(e.message) ? `${e.message}\n Add one: --summary 'the point they must not miss'`
|
|
1876
|
+
}).catch((e) => die(/SUMMARY_REQUIRED/.test(e.message) ? `${e.message}\n Add one: --summary 'the point they must not miss'`
|
|
1877
|
+
: /UNVERIFIABLE_DELIVERY/.test(e.message) ? `${e.message}\n From here: pantheon say DELIVERED "…" --branch <the pushed branch> --commit <its sha>`
|
|
1878
|
+
: /BAD_REF/.test(e.message) ? `${e.message}\n pantheon history lists the acts with their ids and [sequence numbers]; --ref takes either, or "last"`
|
|
1879
|
+
: /TO_REQUIRED/.test(e.message) ? `${e.message}\n Name who owes it: --to <member> (pantheon team lists them)`
|
|
1880
|
+
: /GOAL_REQUIRED|NO_SUCH_GOAL|GOAL_CLOSED/.test(e.message) ? `${e.message.replace(/ — open one with pantheon_open_goal/, "").replace(/pantheon_open_goal/g, "pantheon goal open")}\n Open one from here: pantheon goal open "<what it is about>" --with <member> then add --goal <id> (pantheon goal list shows the open ones)`
|
|
1881
|
+
: e.message));
|
|
1345
1882
|
console.log(`sent ${act} id=${r.id} seq=${r.seq}`);
|
|
1883
|
+
if (flag("space"))
|
|
1884
|
+
console.error(D(` in "${cfg.space}"`));
|
|
1885
|
+
else if (resolveFor(process.cwd()).reason === "default")
|
|
1886
|
+
console.error(D(` in "${cfg.space}" — your default team; this folder is bound to none (pantheon use <team> --here)`));
|
|
1346
1887
|
client.close();
|
|
1347
1888
|
return;
|
|
1348
1889
|
}
|
|
@@ -1374,7 +1915,7 @@ async function main() {
|
|
|
1374
1915
|
}
|
|
1375
1916
|
if (worker.running) {
|
|
1376
1917
|
console.log(` worker ${C("running")} ${worker.managedBy === "launchd" ? `service ${worker.label}` : "started by hand"} pid ${worker.pid}`);
|
|
1377
|
-
console.log(` ${D("a
|
|
1918
|
+
console.log(` ${D("a teammate approved on this machine (pantheon wake list) can start an agent run here")}`);
|
|
1378
1919
|
}
|
|
1379
1920
|
else {
|
|
1380
1921
|
console.log(` worker ${D("not running")} ${D("nobody can start an agent run on this machine")}`);
|
|
@@ -1453,9 +1994,10 @@ async function main() {
|
|
|
1453
1994
|
if (!["all", "relay", "worker", "notifier", "menubar"].includes(what))
|
|
1454
1995
|
die(`usage: pantheon stop [relay|worker|notifier|menubar|all]`);
|
|
1455
1996
|
const { stopMachine } = await import("./machine.js");
|
|
1997
|
+
const { killRecordedRun } = await import("./worker.js");
|
|
1456
1998
|
console.log(B("stopping"));
|
|
1457
1999
|
const everyTeam = Object.values(loadAll()?.spaces ?? {}).map((m) => ({ name: m.name, relay: m.relay }));
|
|
1458
|
-
const res = stopMachine(what, { port: Number(flag("port", "8787")), teams: everyTeam.length ? everyTeam : (load()?.relay ? [{ name: load().space, relay: load().relay }] : []) });
|
|
2000
|
+
const res = stopMachine(what, { port: Number(flag("port", "8787")), teams: everyTeam.length ? everyTeam : (load()?.relay ? [{ name: load().space, relay: load().relay }] : []), killRun: () => killRecordedRun(dirname(CONFIG_PATH)) });
|
|
1459
2001
|
for (const l of res.lines)
|
|
1460
2002
|
console.log(` ${l}`);
|
|
1461
2003
|
if (res.note)
|
|
@@ -1470,12 +2012,21 @@ async function main() {
|
|
|
1470
2012
|
die(`no ${kind} log found. ${process.platform === "linux" ? `If it runs as a service: journalctl --user -u pantheon-${kind}` : process.platform === "darwin" ? "If it runs as a service, the plist names the path" : "Services are not used on Windows"}; if you started it by hand, its output went to that terminal.`);
|
|
1471
2013
|
console.error(D(`${f} (${statFile(f)})\n`));
|
|
1472
2014
|
const follow = argv.includes("-f") || argv.includes("--follow");
|
|
1473
|
-
const {
|
|
1474
|
-
|
|
2015
|
+
const { tailLines, followFile } = await import("./machine.js");
|
|
2016
|
+
process.stdout.write(tailLines(f, Math.max(0, Number(flag("n", "40")) || 40)));
|
|
2017
|
+
if (follow)
|
|
2018
|
+
await followFile(f, (chunk) => process.stdout.write(chunk));
|
|
1475
2019
|
return;
|
|
1476
2020
|
}
|
|
1477
2021
|
case "hook": {
|
|
1478
2022
|
const sub = argv[1] ?? "status";
|
|
2023
|
+
if (inWokenRun() && !["install", "uninstall", "remove", "status"].includes(sub)) {
|
|
2024
|
+
if (sub === "cursor-prompt")
|
|
2025
|
+
console.log(JSON.stringify({ continue: true }));
|
|
2026
|
+
else if (sub === "cursor-stop")
|
|
2027
|
+
console.log("{}");
|
|
2028
|
+
process.exit(0);
|
|
2029
|
+
}
|
|
1479
2030
|
const hookCfg = resolveForHook();
|
|
1480
2031
|
const sessionOf = (raw) => {
|
|
1481
2032
|
let fromJson;
|
|
@@ -1573,7 +2124,8 @@ async function main() {
|
|
|
1573
2124
|
catch { }
|
|
1574
2125
|
try {
|
|
1575
2126
|
const { reportActivity } = await import("./pending.js");
|
|
1576
|
-
|
|
2127
|
+
if (hookCfg)
|
|
2128
|
+
await reportActivity({ cfg: hookCfg, session: `hook:${session}`, state: "busy" });
|
|
1577
2129
|
}
|
|
1578
2130
|
catch { }
|
|
1579
2131
|
console.log(JSON.stringify({ continue: true }));
|
|
@@ -1761,7 +2313,7 @@ async function main() {
|
|
|
1761
2313
|
client.close();
|
|
1762
2314
|
const bound = Object.values(all.bindings ?? {}).filter((b) => b.space === m.id).length;
|
|
1763
2315
|
removeMembership(m.id);
|
|
1764
|
-
console.error(`${B("deleted")} "${m.name}" — everyone in it was disconnected; its token${bound ? ` and ${bound}
|
|
2316
|
+
console.error(`${B("deleted")} "${m.name}" — everyone in it was disconnected; its token${bound ? ` and ${bound} folder binding${bound === 1 ? "" : "s"}` : ""} left this machine`);
|
|
1765
2317
|
return;
|
|
1766
2318
|
}
|
|
1767
2319
|
case "leave": {
|
|
@@ -1769,9 +2321,21 @@ async function main() {
|
|
|
1769
2321
|
const all = loadAll() ?? die("not configured");
|
|
1770
2322
|
const m = findMembership(all, argv[1]) ?? die(`you are not in a team called "${argv[1]}". Yours: ${Object.values(all.spaces ?? {}).map((x) => x.name).join(", ") || "none"}`);
|
|
1771
2323
|
const bound = Object.values(all.bindings ?? {}).filter((b) => b.space === m.id).map((b) => b.origin ?? b.path);
|
|
2324
|
+
const { others, founder } = await teamStanding(all, m);
|
|
2325
|
+
if (founder && others?.length === 0 && !argv.includes("--yes")) {
|
|
2326
|
+
die(`you founded "${m.name}" and nobody else is in it. Leaving keeps it on the relay — holding your plan's team slot — and takes away the only way to delete it.\n` +
|
|
2327
|
+
` Delete it instead: pantheon space delete ${m.name} --yes\n Leave it anyway: pantheon leave ${m.name} --yes`);
|
|
2328
|
+
}
|
|
1772
2329
|
removeMembership(m.id);
|
|
1773
|
-
console.error(`${B("left")} "${m.name}" — its token is gone from this machine${bound.length ? `, and ${bound.length}
|
|
1774
|
-
|
|
2330
|
+
console.error(`${B("left")} "${m.name}" — its token is gone from this machine${bound.length ? `, and ${bound.length} binding${bound.length === 1 ? "" : "s"} with it (${bound.join(", ")})` : ""}.`);
|
|
2331
|
+
if (others === null)
|
|
2332
|
+
console.error(D(`The relay could not be reached to check who else is in "${m.name}". If there are teammates, they still list you; one of them removes you with: pantheon admit ${all.member} --revoke`));
|
|
2333
|
+
else if (others.length)
|
|
2334
|
+
console.error(D(founder
|
|
2335
|
+
? `${others.join(", ")} ${others.length === 1 ? "is" : "are"} still in "${m.name}", with you as its founder — a founder cannot be removed by the others. To come back, ask one of them for an invite.`
|
|
2336
|
+
: `${others.join(", ")} still list you in "${m.name}". If you are not coming back, one of them runs: pantheon admit ${all.member} --revoke`));
|
|
2337
|
+
else
|
|
2338
|
+
console.error(D(`Nobody else was in "${m.name}"; it stays on the relay until its founder deletes it.`));
|
|
1775
2339
|
return;
|
|
1776
2340
|
}
|
|
1777
2341
|
const all = loadAll();
|
|
@@ -1780,24 +2344,43 @@ async function main() {
|
|
|
1780
2344
|
const plan = leavePlan(all);
|
|
1781
2345
|
const project = resolve(flag("project", "."));
|
|
1782
2346
|
const keepIdentity = argv.includes("--keep-identity");
|
|
1783
|
-
|
|
2347
|
+
const standing = new Map();
|
|
2348
|
+
if (all && !keepIdentity) {
|
|
2349
|
+
const ms = Object.values(all.spaces ?? {});
|
|
2350
|
+
const got = await Promise.all(ms.map((m) => teamStanding(all, m)));
|
|
2351
|
+
ms.forEach((m, i) => standing.set(m.name, got[i]));
|
|
2352
|
+
}
|
|
2353
|
+
const stranded = [...standing].filter(([, v]) => v.founder && v.others?.length === 0).map(([k]) => k);
|
|
2354
|
+
if (stranded.length && !argv.includes("--yes")) {
|
|
2355
|
+
die(`you founded ${stranded.map((t) => `"${t}"`).join(", ")} and nobody else is in ${stranded.length === 1 ? "it" : "them"}. Leaving keeps ${stranded.length === 1 ? "it" : "them"} on the relay — holding your plan's team slot — and takes away the only way to delete ${stranded.length === 1 ? "it" : "them"}.\n` +
|
|
2356
|
+
` Delete first: ${stranded.map((t) => `pantheon space delete ${t} --yes`).join(" · ")}\n Leave anyway: pantheon leave --yes`);
|
|
2357
|
+
}
|
|
2358
|
+
const orphaned = [...standing].filter(([, v]) => v.founder && (v.others?.length ?? 0) > 0).map(([k]) => k);
|
|
2359
|
+
if (orphaned.length && !argv.includes("--yes")) {
|
|
2360
|
+
die(`you founded ${orphaned.map((t) => `"${t}"`).join(", ")}, and others are in ${orphaned.length === 1 ? "it" : "them"}. Leaving removes this machine's key: you could not rejoin as yourself, and only a founder can remove a member or delete a team — nobody there could, ever again.\n` +
|
|
2361
|
+
` To step out but keep the way back: ${orphaned.map((t) => `pantheon leave ${t}`).join(" · ")} (keeps your key)\n Leave anyway: pantheon leave --yes`);
|
|
2362
|
+
}
|
|
2363
|
+
if (plan.teams.length && !keepIdentity && !argv.includes("--yes")) {
|
|
1784
2364
|
const list = plan.teams.map((t) => `"${t.name}"`).join(", ");
|
|
2365
|
+
const what = plan.teams.length > 1 ? `this machine is in ${plan.teams.length} teams — ${list}. Leaving removes its key, so it leaves ALL of them.` : `this removes Pantheon and this machine's key, so it leaves ${list}.`;
|
|
1785
2366
|
if (!process.stdin.isTTY)
|
|
1786
|
-
die(
|
|
2367
|
+
die(`${what}\n If you mean it: pantheon leave --yes${plan.teams.length > 1 ? "\n Just one team: pantheon leave <team>" : ""}`);
|
|
1787
2368
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1788
|
-
const answer = await
|
|
2369
|
+
const answer = (await questionOrNull(rl, `${what[0].toUpperCase()}${what.slice(1)} Type "yes" to continue: `)) ?? "";
|
|
1789
2370
|
rl.close();
|
|
1790
2371
|
if (answer.toLowerCase() !== "yes")
|
|
1791
|
-
die(`nothing was changed. Leave one team with: pantheon leave <team
|
|
2372
|
+
die(`nothing was changed.${plan.teams.length > 1 ? " Leave one team with: pantheon leave <team>" : ""}`);
|
|
1792
2373
|
}
|
|
1793
2374
|
console.error(`${B("leaving pantheon")}${plan.teams.length ? D(` — ${plan.teams.map((t) => t.name).join(", ")}`) : ""}\n`);
|
|
1794
2375
|
const { uninstallMachine, realUninstallIo } = await import("./machine.js");
|
|
1795
2376
|
const { removeUserMcpEntries } = await import("./install.js");
|
|
2377
|
+
const { killRecordedRun } = await import("./worker.js");
|
|
1796
2378
|
const done = uninstallMachine({ project, stopRelay: argv.includes("--stop-relay") }, realUninstallIo({
|
|
1797
2379
|
port: Number(flag("port", "8787")),
|
|
1798
2380
|
removeMcpEntries: () => removeUserMcpEntries({ project }),
|
|
1799
2381
|
hasIntent: () => readIntent() !== null,
|
|
1800
2382
|
clearIntent: () => clearIntent(),
|
|
2383
|
+
killRun: () => killRecordedRun(dirname(CONFIG_PATH)),
|
|
1801
2384
|
}));
|
|
1802
2385
|
for (const l of done.lines)
|
|
1803
2386
|
console.error(` ${l}`);
|
|
@@ -1858,6 +2441,20 @@ async function main() {
|
|
|
1858
2441
|
left.push(`${join(dir, "quarantine-kept")} — agent config an interrupted run held, set aside because the repo had moved on; check it, then delete the folder`);
|
|
1859
2442
|
}
|
|
1860
2443
|
}
|
|
2444
|
+
{
|
|
2445
|
+
const { removePantheonWorktrees, removeHomeLeftovers } = await import("./machine.js");
|
|
2446
|
+
const repos = [project, ...Object.values(all?.bindings ?? {}).map((b) => b.path).filter((p) => !!p)];
|
|
2447
|
+
const trees = removePantheonWorktrees(repos, [join(CONFIG_DIR, "worktrees"), ...repos.map((r) => join(r, ".pantheon", "worktrees"))]);
|
|
2448
|
+
removed.push(...trees.removed);
|
|
2449
|
+
left.push(...trees.left);
|
|
2450
|
+
try {
|
|
2451
|
+
rmSync(join(CONFIG_DIR, "worktrees"), { recursive: true, force: true });
|
|
2452
|
+
}
|
|
2453
|
+
catch { }
|
|
2454
|
+
const rest = removeHomeLeftovers(CONFIG_DIR, keepIdentity);
|
|
2455
|
+
removed.push(...rest.removed);
|
|
2456
|
+
left.push(...rest.left);
|
|
2457
|
+
}
|
|
1861
2458
|
console.error(`\n${B("Removed")}`);
|
|
1862
2459
|
for (const r of removed)
|
|
1863
2460
|
console.error(` ${C("✓")} ${r}`);
|
|
@@ -1868,9 +2465,20 @@ async function main() {
|
|
|
1868
2465
|
console.error(` • ${l}`);
|
|
1869
2466
|
console.error(` • ${D("files committed in your repos (.mcp.json, .cursor/mcp.json, .vscode/mcp.json, the brief in AGENTS.md / CLAUDE.md) — your team's; delete them in a commit if the whole team is leaving")}`);
|
|
1870
2467
|
console.error(` • ${D("the relay's ledger on disk, and anything you pushed to git")}`);
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
2468
|
+
{
|
|
2469
|
+
const name = me?.member ?? plan.member ?? "<your name>";
|
|
2470
|
+
const listed = plan.teams.filter((t) => { const s = standing.get(t.name); return !s || s.others === null || (s.others.length && !s.founder); }).map((t) => `"${t.name}"`);
|
|
2471
|
+
const founded = plan.teams.filter((t) => { const s = standing.get(t.name); return s?.founder && s.others?.length; }).map((t) => `"${t.name}"`);
|
|
2472
|
+
if (listed.length) {
|
|
2473
|
+
console.error(`\n ${D(`Your teammates may still list you in ${listed.join(", ")}. If you are not coming back, ask one in each to run:`)}`);
|
|
2474
|
+
console.error(` ${D(`pantheon admit ${name} --revoke`)}`);
|
|
2475
|
+
}
|
|
2476
|
+
if (founded.length)
|
|
2477
|
+
console.error(`\n ${D(`You founded ${founded.join(", ")}. With this machine's key gone you cannot rejoin as yourself, and nobody there can remove a member or delete the team — only its founder could.`)}`);
|
|
2478
|
+
}
|
|
2479
|
+
if (me?.account && !keepIdentity) {
|
|
2480
|
+
const { accountStillExists } = await import("./machine.js");
|
|
2481
|
+
console.error(`\n ${accountStillExists(me.account.email)}`);
|
|
1874
2482
|
}
|
|
1875
2483
|
console.error(`\n ${D(`If Pantheon let you down, one line on why to ${FEEDBACK_ADDRESS} would help more than you might think.`)}`);
|
|
1876
2484
|
return;
|
|
@@ -1884,6 +2492,12 @@ async function main() {
|
|
|
1884
2492
|
}
|
|
1885
2493
|
case "panic": {
|
|
1886
2494
|
console.error(`${B("PANIC — locking down this machine")}\n`);
|
|
2495
|
+
{
|
|
2496
|
+
const { killRecordedRun } = await import("./worker.js");
|
|
2497
|
+
const killed = killRecordedRun(dirname(CONFIG_PATH));
|
|
2498
|
+
if (killed)
|
|
2499
|
+
console.error(` ${killed}`);
|
|
2500
|
+
}
|
|
1887
2501
|
const { panicMachine, waitStopped } = await import("./machine.js");
|
|
1888
2502
|
const all = loadAll();
|
|
1889
2503
|
const teams = Object.values(all?.spaces ?? {});
|
|
@@ -1921,11 +2535,27 @@ async function main() {
|
|
|
1921
2535
|
await client.connect().catch((e) => die(client.pending ?? connectFailure(e, cfg.relay)));
|
|
1922
2536
|
await new Promise((r) => setTimeout(r, 300));
|
|
1923
2537
|
console.log(`${B(`who is in ${cfg.space}`)}\n`);
|
|
2538
|
+
const { loadPolicy, isApprovedWaker, relayKnowsAdmitters } = await import("./worker-policy.js");
|
|
2539
|
+
const { wakeMode } = await import("./profile.js");
|
|
2540
|
+
const pol = loadPolicy();
|
|
2541
|
+
const keys = [client.spaceId ?? cfg.spaceId, cfg.space];
|
|
2542
|
+
const carried = keys.some((k) => !!k && !!pol.wakersCarried?.[k]);
|
|
2543
|
+
const mode = wakeMode(loadProfile(cfg)) === "approved" && !carried && !relayKnowsAdmitters(client.members, cfg.member) ? "anyone" : wakeMode(loadProfile(cfg));
|
|
1924
2544
|
for (const e of client.members) {
|
|
1925
2545
|
const who = e.member.padEnd(16);
|
|
1926
2546
|
const you = e.member === cfg.member ? D(" (you)") : "";
|
|
1927
2547
|
if (e.admitted) {
|
|
1928
|
-
|
|
2548
|
+
const team = D(e.may_wake ? " team: may wake workers" : " team: may not wake workers");
|
|
2549
|
+
const here = e.member === cfg.member ? ""
|
|
2550
|
+
: mode === "no" ? D("cannot start work here — this machine is set not to be woken")
|
|
2551
|
+
: mode === "anyone" ? (e.may_wake ? "can start work here while you are away" : D("cannot start work here"))
|
|
2552
|
+
: isApprovedWaker(pol, keys, e.member)
|
|
2553
|
+
? (e.may_wake ? "can start work here while you are away (approved here)" : D("approved here, but the team does not let them wake workers yet"))
|
|
2554
|
+
: D(`cannot start work here — pantheon wake allow ${e.member}${teamFlag(cfg)}`);
|
|
2555
|
+
console.log(` ${C("in")} ${who}${here}${e.member === cfg.member ? "" : team}${you}`);
|
|
2556
|
+
}
|
|
2557
|
+
else if (e.revoked_at) {
|
|
2558
|
+
console.log(` ${D("removed")} ${who}${D(`removed${e.revoked_by ? ` by ${e.revoked_by}` : ""}, ${new Date(e.revoked_at).toLocaleString()}`)}`);
|
|
1929
2559
|
}
|
|
1930
2560
|
else {
|
|
1931
2561
|
console.log(` \x1b[33mwaiting\x1b[0m ${who}${D(`fingerprint ${e.fingerprint}`)}`);
|
|
@@ -1936,37 +2566,58 @@ async function main() {
|
|
|
1936
2566
|
return;
|
|
1937
2567
|
}
|
|
1938
2568
|
case "admit": {
|
|
2569
|
+
if (inWokenRun())
|
|
2570
|
+
die("a woken run cannot admit anyone or change who may wake this machine — a person has to run this here");
|
|
1939
2571
|
const cfg = need();
|
|
1940
|
-
const who = argv[1] ?? die("usage: pantheon admit <member> [--may-wake] [--revoke] [--forget]");
|
|
2572
|
+
const who = argv[1] ?? die("usage: pantheon admit <member> [--may-wake | --no-wake] [--revoke] [--forget] [--yes]");
|
|
2573
|
+
const forget = argv.includes("--forget");
|
|
2574
|
+
const revoke = argv.includes("--revoke");
|
|
2575
|
+
const askedWake = argv.includes("--may-wake") && !revoke && !forget;
|
|
2576
|
+
if (askedWake && !argv.includes("--yes") && !process.stdin.isTTY) {
|
|
2577
|
+
die(`--may-wake lets ${who} start agent runs for the team, and there is no terminal here for a person to confirm it — pass --yes to confirm (nothing was changed)`);
|
|
2578
|
+
}
|
|
1941
2579
|
const client = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
1942
2580
|
await client.connect().catch((e) => die(client.pending ?? connectFailure(e, cfg.relay)));
|
|
1943
2581
|
await new Promise((r) => setTimeout(r, 250));
|
|
1944
|
-
const forget = argv.includes("--forget");
|
|
1945
|
-
const revoke = argv.includes("--revoke");
|
|
1946
|
-
const mayWake = argv.includes("--may-wake");
|
|
1947
2582
|
const entry = client.members.find((e) => e.member === who);
|
|
1948
2583
|
if (!entry)
|
|
1949
2584
|
die(`"${who}" has never connected, so there is no key to admit. Have them run \`pantheon join\` first.`);
|
|
2585
|
+
const mayWake = askedWake ? true : argv.includes("--no-wake") ? false : !!(entry.admitted && entry.may_wake);
|
|
1950
2586
|
if (!revoke && !forget) {
|
|
1951
2587
|
console.error(`${B("confirm this fingerprint with them directly:")} ${C(entry.fingerprint)}`);
|
|
1952
|
-
if (
|
|
1953
|
-
|
|
1954
|
-
|
|
2588
|
+
if (askedWake) {
|
|
2589
|
+
const { COMMAND_GRANT_SCOPE } = await import("./agents.js");
|
|
2590
|
+
const { commandState } = await import("./command-state.js");
|
|
2591
|
+
const cmds = commandState({ project: process.cwd() });
|
|
2592
|
+
console.error(`\n${B(`--may-wake lets ${who} cause an agent to run on THIS machine (and on any teammate's that allows them — pantheon wake allow):`)}`);
|
|
2593
|
+
console.error(` ${C("can")} edit files in its own worktree of this repo, on a pantheon/* branch`);
|
|
2594
|
+
console.error(` ${C("can")} read files your agent's own rules let it read — for Codex that is the whole disk (its read-only sandbox limits writes, not reads)`);
|
|
1955
2595
|
console.error(` ${C("can")} reply to your peers as you, while you are away`);
|
|
1956
|
-
|
|
1957
|
-
|
|
2596
|
+
if (cmds.level === "off") {
|
|
2597
|
+
console.error(` ${D("cannot")} run commands — unless a person on this machine grants that (pantheon policy grant run-commands, or answering the worker's request here) or the worker runs with --allow-bash. Then: ${COMMAND_GRANT_SCOPE}`);
|
|
2598
|
+
}
|
|
2599
|
+
else {
|
|
2600
|
+
console.error(` ${C("can")} run commands: ${cmds.summary}. ${COMMAND_GRANT_SCOPE}`);
|
|
2601
|
+
}
|
|
2602
|
+
console.error(` ${D("cannot")} touch your own checkout's working tree, or merge anything`);
|
|
1958
2603
|
console.error(` ${D("limits")} one run at a time, 12 per hour, and only while a worker is running`);
|
|
1959
|
-
console.error(`\n ${D("
|
|
2604
|
+
console.error(`\n ${D("Runs happen only while a worker runs here (pantheon ps shows it). Undo: pantheon wake deny " + who + " (this machine) or pantheon admit " + who + " --no-wake (the team). Emergency: pantheon panic — it also switches off wake rights in every team you are in.")}`);
|
|
1960
2605
|
if (argv.includes("--yes")) {
|
|
1961
2606
|
console.error(` ${D("--yes given: granted without prompting.")}`);
|
|
1962
2607
|
}
|
|
1963
2608
|
else {
|
|
1964
2609
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1965
|
-
const ans = await
|
|
2610
|
+
const ans = (await questionOrNull(rl, `\ngrant that to ${who}? [y/N] `)) ?? "";
|
|
1966
2611
|
rl.close();
|
|
1967
|
-
if (!/^y(es)?$/i.test(ans
|
|
2612
|
+
if (!/^y(es)?$/i.test(ans)) {
|
|
2613
|
+
const keep = !!(entry.admitted && entry.may_wake);
|
|
2614
|
+
if (entry.admitted) {
|
|
2615
|
+
console.error(`\nnot granted. Nothing was changed.`);
|
|
2616
|
+
client.close();
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
1968
2619
|
console.error(`\nnot granted. Admitting ${who} without wake rights instead.`);
|
|
1969
|
-
await client.admit(who,
|
|
2620
|
+
await client.admit(who, keep, false).catch((e) => die(e.message));
|
|
1970
2621
|
console.log(`${C(who)} is in, but cannot start anything on this machine.`);
|
|
1971
2622
|
client.close();
|
|
1972
2623
|
return;
|
|
@@ -1975,13 +2626,27 @@ async function main() {
|
|
|
1975
2626
|
}
|
|
1976
2627
|
}
|
|
1977
2628
|
await client.admit(who, mayWake, revoke, forget).catch((e) => die(e.message));
|
|
2629
|
+
let local = "";
|
|
2630
|
+
const { setLocalWaker, loadPolicy: lp, isApprovedWaker: approved } = await import("./worker-policy.js");
|
|
2631
|
+
const wakesHere = approved(lp(), [client.spaceId ?? cfg.spaceId, cfg.space], who);
|
|
2632
|
+
if (askedWake || revoke || forget) {
|
|
2633
|
+
const r = setLocalWaker([client.spaceId ?? cfg.spaceId, cfg.space], who, askedWake);
|
|
2634
|
+
if (!r.ok && askedWake)
|
|
2635
|
+
local = D(`\n Not recorded on this machine: ${r.why}.`);
|
|
2636
|
+
if (r.ok && r.changed && !askedWake && wakesHere)
|
|
2637
|
+
local = D(`\n ${who} was also taken off this machine's wake list.`);
|
|
2638
|
+
}
|
|
1978
2639
|
console.log(forget
|
|
1979
2640
|
? `${C(who)} forgotten — the key ${D(entry.fingerprint)} is retired.\n` +
|
|
1980
2641
|
` They can now rejoin as "${who}" with a new key, keeping their goals and obligations.\n` +
|
|
1981
2642
|
` ${B("Check the new fingerprint with them")} once they are back: pantheon members`
|
|
1982
2643
|
: revoke
|
|
1983
|
-
? `${C(who)}
|
|
1984
|
-
:
|
|
2644
|
+
? `${C(who)} removed from "${cfg.space}" — their sessions were disconnected${local}`
|
|
2645
|
+
: askedWake
|
|
2646
|
+
? `${C(who)} ${entry.admitted ? `may now wake workers in "${cfg.space}"` : `admitted to "${cfg.space}", and may wake workers there`}, including on this machine${local}\n` +
|
|
2647
|
+
D(` Each teammate decides for their own machine: pantheon wake allow ${who}`)
|
|
2648
|
+
: `${C(who)} ${entry.admitted ? `was already in "${cfg.space}"` : `admitted to "${cfg.space}"`}${mayWake ? D(" (the team still lets them wake workers)") : ""}\n` +
|
|
2649
|
+
(wakesHere ? D(` ${who} may already wake this machine (pantheon wake list)`) : D(` To let ${who} wake THIS machine: pantheon wake allow ${who}${teamFlag(cfg)} (only here — nothing is sent to the team)`)));
|
|
1985
2650
|
client.close();
|
|
1986
2651
|
return;
|
|
1987
2652
|
}
|
|
@@ -2015,7 +2680,7 @@ async function main() {
|
|
|
2015
2680
|
const prior = identity();
|
|
2016
2681
|
const relay = flag("relay") ?? prior?.relay ?? process.env.PANTHEON_RELAY ?? DEFAULT_RELAY;
|
|
2017
2682
|
const kp = prior?.publicKey && prior.privateKey ? { publicKey: prior.publicKey, privateKey: prior.privateKey } : newKeypair();
|
|
2018
|
-
const label = flag("label") ??
|
|
2683
|
+
const label = flag("label") ?? deviceLabel();
|
|
2019
2684
|
const base = relay.replace(/^ws/, "http").replace(/\/+$/, "");
|
|
2020
2685
|
const body = purpose === "signup" ? { email, username, pubkey: kp.publicKey, label } : { email, pubkey: kp.publicKey, label };
|
|
2021
2686
|
const r = await fetch(`${base}/auth/${purpose}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })
|
|
@@ -2042,7 +2707,7 @@ async function main() {
|
|
|
2042
2707
|
let code = (argv[1] ?? flag("code") ?? "").trim();
|
|
2043
2708
|
if (!code && process.stdin.isTTY) {
|
|
2044
2709
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
2045
|
-
code = (await
|
|
2710
|
+
code = (await questionOrNull(rl, `Enter the 6-digit code sent to ${pending.email}: `)) ?? "";
|
|
2046
2711
|
rl.close();
|
|
2047
2712
|
}
|
|
2048
2713
|
if (!code)
|
|
@@ -2408,39 +3073,229 @@ async function main() {
|
|
|
2408
3073
|
client.close();
|
|
2409
3074
|
return;
|
|
2410
3075
|
}
|
|
3076
|
+
case "trust": {
|
|
3077
|
+
const { WOKEN_RUN_ENV } = await import("./worker-policy.js");
|
|
3078
|
+
const inRun = process.env[WOKEN_RUN_ENV] !== undefined || /[\\/]run-homes[\\/]/.test(process.env.PANTHEON_HOME ?? "");
|
|
3079
|
+
if (inRun && argv[1] && !argv[1].startsWith("--"))
|
|
3080
|
+
die("a woken run cannot trust a key — a person has to, on this machine");
|
|
3081
|
+
const cfg = need();
|
|
3082
|
+
const { loadPins, trustPending, updatePins } = await import("./pins.js");
|
|
3083
|
+
const { fingerprint } = await import("./identity.js");
|
|
3084
|
+
const c = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
3085
|
+
await Promise.race([c.connect().catch(() => { }), new Promise((r) => setTimeout(r, 5000))]);
|
|
3086
|
+
const team = c.pinKey();
|
|
3087
|
+
c.close();
|
|
3088
|
+
const p = loadPins();
|
|
3089
|
+
const t = p.teams[team];
|
|
3090
|
+
const who = argv[1] && !argv[1].startsWith("--") ? argv[1] : null;
|
|
3091
|
+
if (!who) {
|
|
3092
|
+
console.log(`${B("Teammates' keys this machine checks messages against")} ${D(`(${cfg.space}; messages ${t?.sigs ? "are" : "are not yet"} signed on this team's relay)`)}\n`);
|
|
3093
|
+
for (const [m, pin] of Object.entries(t?.members ?? {}).sort()) {
|
|
3094
|
+
console.log(` ${m.padEnd(16)} ${pin.fp}${m === cfg.member ? D(" (you)") : ""}${pin.pending ? `\n ${" ".repeat(16)} ${B("changed to")} ${pin.pending.fp} ${D(`— not trusted until you check it with ${m}: pantheon trust ${m}`)}` : ""}`);
|
|
3095
|
+
}
|
|
3096
|
+
if (!t || !Object.keys(t.members).length)
|
|
3097
|
+
console.log(D(" none yet — keys are pinned the first time this machine sees each teammate"));
|
|
3098
|
+
console.log(D(`\n Your own key: ${fingerprint(cfg.publicKey)} — read it to a teammate who asks you to confirm it.`));
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3101
|
+
const cur = t?.members[who];
|
|
3102
|
+
if (!cur)
|
|
3103
|
+
die(`no key for "${who}" is known on this machine in "${cfg.space}"`);
|
|
3104
|
+
if (!cur.pending) {
|
|
3105
|
+
console.log(`${C(who)}'s key is ${cur.fp} — nothing is waiting to be confirmed.`);
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
3108
|
+
console.log(`${B(`${who}'s key changed`)} (a new machine, or someone else):`);
|
|
3109
|
+
console.log(` was ${cur.fp}`);
|
|
3110
|
+
console.log(` now ${cur.pending.fp}`);
|
|
3111
|
+
console.log(D(` Ask ${who} to read theirs out (pantheon trust, last line) and compare. Only then trust it.`));
|
|
3112
|
+
let go = argv.includes("--yes");
|
|
3113
|
+
if (!go && process.stdin.isTTY) {
|
|
3114
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
3115
|
+
const a = ((await questionOrNull(rl, `Trust ${who}'s new key ${cur.pending.fp}? [y/N] `)) ?? "").trim().toLowerCase();
|
|
3116
|
+
rl.close();
|
|
3117
|
+
go = a === "y" || a === "yes";
|
|
3118
|
+
}
|
|
3119
|
+
if (!go) {
|
|
3120
|
+
console.log(D(process.stdin.isTTY ? " nothing changed" : ` nothing changed — after checking it with ${who}: pantheon trust ${who} --yes`));
|
|
3121
|
+
return;
|
|
3122
|
+
}
|
|
3123
|
+
let outcome = "ok";
|
|
3124
|
+
updatePins((fresh) => {
|
|
3125
|
+
if (fresh.corrupt) {
|
|
3126
|
+
outcome = "corrupt";
|
|
3127
|
+
return false;
|
|
3128
|
+
}
|
|
3129
|
+
if (fresh.teams[team]?.members[who]?.pending?.fp !== cur.pending.fp) {
|
|
3130
|
+
outcome = "moved";
|
|
3131
|
+
return false;
|
|
3132
|
+
}
|
|
3133
|
+
return !!trustPending(fresh, team, who);
|
|
3134
|
+
});
|
|
3135
|
+
if (outcome === "corrupt")
|
|
3136
|
+
die("keys.json could not be read, so nothing was changed — fix or remove it (pantheon doctor)");
|
|
3137
|
+
if (outcome === "moved")
|
|
3138
|
+
die(`${who}'s held-back key changed while you were deciding — run pantheon trust ${who} again`);
|
|
3139
|
+
console.log(`${C("trusted")} ${who}'s new key on this machine — their signed messages count again.`);
|
|
3140
|
+
return;
|
|
3141
|
+
}
|
|
3142
|
+
case "wake": {
|
|
3143
|
+
const { loadPolicy, setLocalWaker } = await import("./worker-policy.js");
|
|
3144
|
+
const { wakeMode } = await import("./profile.js");
|
|
3145
|
+
const sub = argv[1] ?? "list";
|
|
3146
|
+
const one = () => {
|
|
3147
|
+
if (!flag("space"))
|
|
3148
|
+
return need();
|
|
3149
|
+
const f = loadAll();
|
|
3150
|
+
const m = f && findMembership(f, flag("space"));
|
|
3151
|
+
if (!f || !m)
|
|
3152
|
+
die(`you are not in a team called "${flag("space")}"`);
|
|
3153
|
+
return configFor(f, m, null);
|
|
3154
|
+
};
|
|
3155
|
+
if (sub === "allow" || sub === "deny") {
|
|
3156
|
+
const who = argv[2] && !argv[2].startsWith("--") ? argv[2] : die(`usage: pantheon wake ${sub} <name> [--space <team>]`);
|
|
3157
|
+
const cfg = one();
|
|
3158
|
+
if (who === cfg.member)
|
|
3159
|
+
die("your own requests always reach your own worker; there is nothing to allow");
|
|
3160
|
+
let unchecked = false;
|
|
3161
|
+
let teamBlocks = false;
|
|
3162
|
+
if (sub === "allow") {
|
|
3163
|
+
const c = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
3164
|
+
const got = await Promise.race([c.connect().then(() => true, () => false), new Promise((r) => setTimeout(() => r(false), 3_000))]);
|
|
3165
|
+
if (got) {
|
|
3166
|
+
const names = c.members.map((m) => m.member);
|
|
3167
|
+
{
|
|
3168
|
+
const e = c.members.find((m) => m.member === who);
|
|
3169
|
+
teamBlocks = e?.admitted === true && !e.revoked_by && e.may_wake === false;
|
|
3170
|
+
}
|
|
3171
|
+
c.close();
|
|
3172
|
+
if (!names.includes(who))
|
|
3173
|
+
die(`"${who}" is not in "${cfg.space}". Its members: ${names.filter((n) => n !== cfg.member).join(", ") || "only you"}`);
|
|
3174
|
+
}
|
|
3175
|
+
else {
|
|
3176
|
+
try {
|
|
3177
|
+
c.close();
|
|
3178
|
+
}
|
|
3179
|
+
catch { }
|
|
3180
|
+
unchecked = true;
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
const r = setLocalWaker([cfg.spaceId, cfg.space], who, sub === "allow");
|
|
3184
|
+
if (!r.ok)
|
|
3185
|
+
die(r.why);
|
|
3186
|
+
const mode = wakeMode(loadProfile(cfg));
|
|
3187
|
+
console.log(sub === "allow"
|
|
3188
|
+
? `${C(who)} may wake this machine in ${cfg.space}${r.changed ? "" : D(" (already could)")}`
|
|
3189
|
+
: `${C(who)} can no longer wake this machine in ${cfg.space}${r.changed ? "" : D(" (could not before either)")}`);
|
|
3190
|
+
console.log(D(` Recorded on this machine only — nothing was sent to the team. Undo: pantheon wake ${sub === "allow" ? "deny" : "allow"} ${who}${flag("space") ? ` --space ${cfg.space}` : ""}`));
|
|
3191
|
+
if (unchecked)
|
|
3192
|
+
console.log(D(` The relay did not answer, so "${who}" was not checked against the team's members — check the spelling (pantheon team).`));
|
|
3193
|
+
if (sub === "allow") {
|
|
3194
|
+
if (mode === "anyone")
|
|
3195
|
+
console.log(D(` This machine is set to wake for anyone the team lets, so the list is used only if you choose "approved" (pantheon setup).`));
|
|
3196
|
+
if (mode === "no")
|
|
3197
|
+
console.log(D(` This machine is set not to be woken at all; the list applies once you choose "approved" (pantheon setup).`));
|
|
3198
|
+
if (teamBlocks) {
|
|
3199
|
+
const { markTeamFlagOnYes } = await import("./worker-policy.js");
|
|
3200
|
+
markTeamFlagOnYes(cfg.spaceId ?? cfg.space, who);
|
|
3201
|
+
}
|
|
3202
|
+
if (teamBlocks)
|
|
3203
|
+
console.log(` ${B("The team does not let")} ${C(who)} ${B("wake workers yet.")} ${D(`Your worker turns that on the first time ${who}'s request needs this machine (it is said when it does); or now: pantheon admit ${who} --may-wake${teamFlag(cfg)}`)}`);
|
|
3204
|
+
else if (unchecked)
|
|
3205
|
+
console.log(D(` The team must also let ${who} wake workers at all. If their requests are refused with NOT_ALLOWED_TO_WAKE, a member runs: pantheon admit ${who} --may-wake${teamFlag(cfg)}`));
|
|
3206
|
+
}
|
|
3207
|
+
return;
|
|
3208
|
+
}
|
|
3209
|
+
if (sub === "list") {
|
|
3210
|
+
const pol = loadPolicy();
|
|
3211
|
+
const f = loadAll();
|
|
3212
|
+
const teams = flag("space") ? [one()] : f && Object.keys(f.spaces ?? {}).length ? Object.values(f.spaces ?? {}).map((m) => configFor(f, m, null)) : [need()];
|
|
3213
|
+
console.log(`${B("Who may wake this machine")} ${D("(this machine's own list — pantheon wake allow|deny <name>)")}\n`);
|
|
3214
|
+
for (const t of teams) {
|
|
3215
|
+
const names = [...new Set([...(t.spaceId ? pol.wakers?.[t.spaceId] ?? [] : []), ...(pol.wakers?.[t.space] ?? [])])].sort();
|
|
3216
|
+
const key = t.spaceId ?? t.space;
|
|
3217
|
+
const carried = !!(pol.wakersCarried?.[key] || pol.wakersCarried?.[t.space]);
|
|
3218
|
+
const deferred = !!(pol.wakersDeferred?.[key] || pol.wakersDeferred?.[t.space]);
|
|
3219
|
+
const mem = Object.values(f?.spaces ?? {}).find((m) => m.id === t.spaceId || m.name === t.space);
|
|
3220
|
+
const pending = !carried && (deferred || (!!mem && !mem.since));
|
|
3221
|
+
const why = deferred
|
|
3222
|
+
? "when the worker last checked, the team's relay was older than 0.30 — until it is upgraded and the worker reconnects, the team's may_wake decides"
|
|
3223
|
+
: "from before 0.30 and not carried over yet — the worker does that when it next connects to a 0.30 relay; until then the team's may_wake decides";
|
|
3224
|
+
console.log(` ${t.space.padEnd(16)} ${names.length ? names.join(", ") : D("nobody")}${pending ? D(` (${why})`) : ""}`);
|
|
3225
|
+
}
|
|
3226
|
+
const mode = wakeMode(loadProfile(teams[0]));
|
|
3227
|
+
console.log(`\n ${D("setting")} ${mode === "approved" ? "only the people listed above" : mode === "anyone" ? "anyone the team lets wake workers (the list is kept for if you choose \"approved\")" : "nobody — this machine is set not to be woken"}${D(" (pantheon setup to change)")}`);
|
|
3228
|
+
return;
|
|
3229
|
+
}
|
|
3230
|
+
{
|
|
3231
|
+
const near = ["allow", "deny", "list"].map((k) => ({ k, d: slipDistance(sub, k) })).sort((x, y) => x.d - y.d)[0];
|
|
3232
|
+
die(`${near.d <= 2 ? `"${sub}" — did you mean: pantheon wake ${near.k}?\n` : ""}usage: pantheon wake allow <name> [--space <team>] · pantheon wake deny <name> [--space <team>] · pantheon wake list`);
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
2411
3235
|
case "policy": {
|
|
2412
|
-
const { loadPolicy, savePolicy, grant, revoke } = await import("./worker-policy.js");
|
|
3236
|
+
const { loadPolicy, savePolicy, grant, revoke, inWokenRun, CAPABILITY_ARGS } = await import("./worker-policy.js");
|
|
3237
|
+
const { COMMAND_GRANT_SCOPE } = await import("./agents.js");
|
|
2413
3238
|
const sub = argv[1];
|
|
2414
3239
|
const capArg = (argv[2] ?? "").replace(/-/g, "_");
|
|
3240
|
+
const caps = Object.keys(CAPABILITY_ARGS);
|
|
2415
3241
|
if (sub === "grant" || sub === "revoke") {
|
|
2416
|
-
|
|
2417
|
-
|
|
3242
|
+
const cap = caps.find((c) => c === capArg) ?? die(`unknown capability "${argv[2] ?? ""}" — capabilities: ${caps.map((c) => CAPABILITY_ARGS[c]).join(", ")}`);
|
|
3243
|
+
if (sub === "grant" && inWokenRun())
|
|
3244
|
+
die("a woken run cannot grant this machine anything — a person has to run this here");
|
|
2418
3245
|
const p = loadPolicy();
|
|
2419
|
-
if (sub === "grant")
|
|
2420
|
-
grant(p,
|
|
2421
|
-
}
|
|
3246
|
+
if (sub === "grant")
|
|
3247
|
+
grant(p, cap, "always", "policy-command");
|
|
2422
3248
|
else
|
|
2423
|
-
revoke(p,
|
|
3249
|
+
revoke(p, cap);
|
|
2424
3250
|
savePolicy(p);
|
|
2425
|
-
|
|
3251
|
+
const said = cap === "self_upgrade"
|
|
3252
|
+
? (sub === "grant" ? "— the worker keeps pantheon current with the relay" : "— the worker will ask in your approvals inbox when it falls behind")
|
|
3253
|
+
: cap === "unsandboxed_runs"
|
|
3254
|
+
? (sub === "grant"
|
|
3255
|
+
? "— Codex, Cursor and Copilot answer teammates' requests while you're away, so nobody waits on you. To have them wait for you instead: pantheon policy revoke unsandboxed-runs"
|
|
3256
|
+
: "— Codex, Cursor and Copilot leave teammates' requests for you to pick up when you're back; Claude Code still answers them. On again: pantheon policy grant unsandboxed-runs")
|
|
3257
|
+
: (sub === "grant" ? `— ${COMMAND_GRANT_SCOPE}` : "— woken runs are back to edits only");
|
|
3258
|
+
console.log(`${C(sub === "grant" ? "granted" : "revoked")} ${CAPABILITY_ARGS[cap]} ${D(said)}`);
|
|
2426
3259
|
return;
|
|
2427
3260
|
}
|
|
2428
3261
|
if (sub && sub !== "show")
|
|
2429
|
-
die(
|
|
3262
|
+
die(`usage: pantheon policy [show | grant <capability> | revoke <capability>] capabilities: ${caps.map((c) => CAPABILITY_ARGS[c]).join(", ")}`);
|
|
2430
3263
|
const p = loadPolicy();
|
|
2431
3264
|
const w = workerProc();
|
|
2432
3265
|
console.log(`${B("What this machine's worker may do")}\n`);
|
|
2433
3266
|
console.log(` worker ${w.running ? C("running") : D("not running")}`);
|
|
2434
3267
|
const su = p.grants.self_upgrade;
|
|
2435
3268
|
console.log(` self-upgrade ${su ? C(su.mode === "always" ? "granted" : "granted once") : D("not granted")}${su ? D(` (${su.by}, ${new Date(su.at).toLocaleDateString()})`) : D(" — it will ask in your approvals inbox when behind the relay")}`);
|
|
2436
|
-
const
|
|
2437
|
-
|
|
2438
|
-
|
|
3269
|
+
const rc = p.grants.run_commands;
|
|
3270
|
+
const { commandState, repoCommandsAtBase } = await import("./command-state.js");
|
|
3271
|
+
const cs = commandState({ project: resolve(".") });
|
|
3272
|
+
const { commandCaveat } = await import("./command-state.js");
|
|
3273
|
+
const caveat = cs.level !== "off" ? commandCaveat() : "";
|
|
3274
|
+
console.log(` run-commands ${rc ? C(rc.mode === "always" ? "granted" : "granted once") : D("not granted")}${rc ? D(` (${rc.by}, ${new Date(rc.at).toLocaleDateString()})`) : cs.allowBash ? D(" — not needed: the worker runs with --allow-bash") : D(" — woken runs edit files only; it asks when a run needed more")}`);
|
|
3275
|
+
const ur = p.grants.unsandboxed_runs;
|
|
3276
|
+
const { unsandboxedAllowed } = await import("./worker-policy.js");
|
|
3277
|
+
console.log(` unsandboxed ${unsandboxedAllowed(p)
|
|
3278
|
+
? C("on") + D(` (${ur ? `${ur.by}, ${new Date(ur.at).toLocaleDateString()}` : "the default"}) — Codex, Cursor and Copilot answer teammates while you're away (off: pantheon policy revoke unsandboxed-runs)`)
|
|
3279
|
+
: D("off (your choice) — Codex, Cursor and Copilot leave requests for when you're back; Claude Code still answers them (on: pantheon policy grant unsandboxed-runs)")}`);
|
|
3280
|
+
for (const cap of caps)
|
|
3281
|
+
if (p.asks[cap])
|
|
3282
|
+
console.log(` ${"\x1b[33mopen request\x1b[0m"} ${CAPABILITY_ARGS[cap]} — answer it here: pantheon approvals, or pantheon policy grant ${CAPABILITY_ARGS[cap]}`);
|
|
2439
3283
|
const pers = loadPersonas(resolve("."), undefined);
|
|
2440
|
-
console.log(` personas ${pers.length ? pers.map((x) => x.name).join(", ") + D(" (restricted mode — tighten-only)") : D("none — open mode; peers
|
|
2441
|
-
console.log(`
|
|
3284
|
+
console.log(` personas ${pers.length ? pers.map((x) => x.name).join(", ") + D(" (restricted mode — tighten-only)") : D("none — open mode; peers this machine approved to wake it can hand it any matched work")}`);
|
|
3285
|
+
console.log(` commands ${cs.level === "off" ? D(cs.summary) : C(cs.summary)}${caveat ? `\n \x1b[33m${caveat}\x1b[0m` : ""}`);
|
|
3286
|
+
if (cs.level !== "off")
|
|
3287
|
+
console.log(` ${D(COMMAND_GRANT_SCOPE)}`);
|
|
3288
|
+
{
|
|
3289
|
+
const { REPO_COMMANDS_FILE } = await import("./agents.js");
|
|
3290
|
+
const root = repoInfo(process.cwd())?.root;
|
|
3291
|
+
const rc2 = root ? repoCommandsAtBase(root) : { ref: null, ok: [], refused: [] };
|
|
3292
|
+
const at = rc2.ref ? D(` (as committed on ${rc2.ref})`) : "";
|
|
3293
|
+
console.log(` this repo ${root ? (rc2.ok.length ? `also allows ${rc2.ok.join(", ")}${at}` : D(`adds no commands${rc2.ref ? ` on ${rc2.ref}` : ""} — list extra ones, one per line, in ${REPO_COMMANDS_FILE} and commit it`)) : D("not in a git repo")}`);
|
|
3294
|
+
for (const r of rc2.refused)
|
|
3295
|
+
console.log(` ${D(`ignored "${r.line}" — ${r.why}`)}`);
|
|
3296
|
+
}
|
|
2442
3297
|
console.log(` runs ${D("12/hour, 40/day, one at a time, 2 failed attempts then parked (defaults; personas only lower them)")}`);
|
|
2443
|
-
console.log(`\n${D("Change it: pantheon policy grant|revoke self-upgrade · personas: pantheon persona init ·
|
|
3298
|
+
console.log(`\n${D("Change it: pantheon policy grant|revoke self-upgrade|run-commands|unsandboxed-runs · personas: pantheon persona init · who may wake this machine: pantheon wake allow|deny <who>")}`);
|
|
2444
3299
|
return;
|
|
2445
3300
|
}
|
|
2446
3301
|
case "explain": {
|
|
@@ -2448,13 +3303,36 @@ async function main() {
|
|
|
2448
3303
|
const CONFIG_DIR = dirname(CONFIG_PATH);
|
|
2449
3304
|
let hud = null;
|
|
2450
3305
|
if (argv.includes("--queued")) {
|
|
3306
|
+
const refuse = (why) => { console.log(`not sent: ${why}`); process.exit(1); };
|
|
3307
|
+
if (inWokenRun())
|
|
3308
|
+
refuse("a woken run cannot act for the menu bar — a person has to click it");
|
|
2451
3309
|
const f = join(CONFIG_DIR, "bin", ".hud-action.json");
|
|
3310
|
+
const { statSync } = await import("node:fs");
|
|
3311
|
+
let st = null;
|
|
3312
|
+
let raw = null;
|
|
2452
3313
|
try {
|
|
2453
|
-
|
|
3314
|
+
st = statSync(f);
|
|
3315
|
+
raw = readFileSync(f, "utf8");
|
|
2454
3316
|
rmSync(f, { force: true });
|
|
2455
3317
|
}
|
|
2456
3318
|
catch {
|
|
2457
|
-
|
|
3319
|
+
st = null;
|
|
3320
|
+
raw = null;
|
|
3321
|
+
}
|
|
3322
|
+
if (st && raw !== null) {
|
|
3323
|
+
const age = Date.now() - st.mtimeMs;
|
|
3324
|
+
const { runInFlight } = await import("./worker.js");
|
|
3325
|
+
const run = runInFlight(CONFIG_DIR);
|
|
3326
|
+
if (age > HUD_ACTION_MAX_AGE_MS)
|
|
3327
|
+
refuse(`that menu-bar action was written ${Math.round(age / 1000)}s ago — click it again`);
|
|
3328
|
+
if (run && st.mtimeMs >= run.at - 1_000)
|
|
3329
|
+
refuse("that menu-bar action was written while a woken run was working on this machine, so it is not trusted — click it again once the run is done");
|
|
3330
|
+
try {
|
|
3331
|
+
hud = JSON.parse(raw);
|
|
3332
|
+
}
|
|
3333
|
+
catch {
|
|
3334
|
+
hud = null;
|
|
3335
|
+
}
|
|
2458
3336
|
}
|
|
2459
3337
|
}
|
|
2460
3338
|
const cfg = (() => {
|
|
@@ -2506,7 +3384,7 @@ async function main() {
|
|
|
2506
3384
|
return;
|
|
2507
3385
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
2508
3386
|
try {
|
|
2509
|
-
const answer = (await
|
|
3387
|
+
const answer = ((await questionOrNull(rl, `\n${D("run these here now? [y/N] ")}`)) ?? "").toLowerCase();
|
|
2510
3388
|
if (answer !== "y" && answer !== "yes") {
|
|
2511
3389
|
console.error(D("left for you — the commands are above"));
|
|
2512
3390
|
return;
|
|
@@ -2565,6 +3443,12 @@ async function main() {
|
|
|
2565
3443
|
.catch((e) => notSent(e.message));
|
|
2566
3444
|
c2.close();
|
|
2567
3445
|
console.log(`sent ${kind} id=${sent.id} seq=${sent.seq}`);
|
|
3446
|
+
if (kind === "ANSWER") {
|
|
3447
|
+
const { applyLocalAnswer, describeLocalAnswer } = await import("./worker-policy.js");
|
|
3448
|
+
const g = applyLocalAnswer(ref, body);
|
|
3449
|
+
if (g)
|
|
3450
|
+
console.error(describeLocalAnswer(g));
|
|
3451
|
+
}
|
|
2568
3452
|
return;
|
|
2569
3453
|
}
|
|
2570
3454
|
if (hud?.action === "retract") {
|
|
@@ -2633,7 +3517,7 @@ async function main() {
|
|
|
2633
3517
|
const agent = pickExplainAgent(hud.tool ?? p0.agent.primary_surface, detectAvailable());
|
|
2634
3518
|
const goalId = a?.goal ?? o?.goal ?? null;
|
|
2635
3519
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
2636
|
-
const ask = (q) =>
|
|
3520
|
+
const ask = async (q) => (await questionOrNull(rl, q)) ?? "";
|
|
2637
3521
|
try {
|
|
2638
3522
|
for (;;) {
|
|
2639
3523
|
console.error(`\n${B("Next")}${agent ? ` ${C("1")} open in ${agent.id} with this loaded` : ""} ${C("2")} reply from here${goalId ? ` ${C("3")} show the whole thread` : ""} ${D("Enter) close")}`);
|
|
@@ -2675,6 +3559,12 @@ async function main() {
|
|
|
2675
3559
|
await c3.connect();
|
|
2676
3560
|
const sent = await c3.send({ act: kind, body: text, goal: o ? null : goalId, ref, to });
|
|
2677
3561
|
console.error(` ${C("sent")} ${kind} id=${sent.id}`);
|
|
3562
|
+
if (kind === "ANSWER") {
|
|
3563
|
+
const { applyLocalAnswer, describeLocalAnswer } = await import("./worker-policy.js");
|
|
3564
|
+
const g = applyLocalAnswer(ref, text);
|
|
3565
|
+
if (g)
|
|
3566
|
+
console.error(` ${describeLocalAnswer(g)}`);
|
|
3567
|
+
}
|
|
2678
3568
|
}
|
|
2679
3569
|
catch (e) {
|
|
2680
3570
|
console.error(` ${e.message}`);
|
|
@@ -2777,6 +3667,10 @@ async function main() {
|
|
|
2777
3667
|
const sent = await hit.client.send({ act: "ANSWER", body: answer, goal: null, ref: hit.o.ref, after_seq: hit.client.readSeq, ...(summary ? { summary } : {}) })
|
|
2778
3668
|
.catch((e) => { closeAll(); return die(/SUMMARY_REQUIRED/.test(e.message) ? `${e.message}\n Add one: --summary 'the point they must not miss'` : e.message); });
|
|
2779
3669
|
console.log(`${C("answered")} ${id}${conns.length > 1 ? ` in ${B(hit.cfg.space)}` : ""} — ledger act ${sent.id}`);
|
|
3670
|
+
const { applyLocalAnswer, describeLocalAnswer } = await import("./worker-policy.js");
|
|
3671
|
+
const g = applyLocalAnswer(hit.o.ref, answer);
|
|
3672
|
+
if (g)
|
|
3673
|
+
console.log(` ${describeLocalAnswer(g)}`);
|
|
2780
3674
|
closeAll();
|
|
2781
3675
|
return;
|
|
2782
3676
|
}
|
|
@@ -2785,10 +3679,21 @@ async function main() {
|
|
|
2785
3679
|
die("usage: pantheon approvals show <obligation-id>");
|
|
2786
3680
|
const hit = find(id) ?? die(`no decision ${id} is waiting on you`);
|
|
2787
3681
|
console.log(`${B("Decision required")} ${hit.o.id}${conns.length > 1 ? ` ${D("in " + hit.cfg.space)}` : ""}`);
|
|
3682
|
+
const full = hit.client.seenAct(hit.o.ref) ?? (await fetchAct(hit.cfg, hit.o.ref, 4000).catch(() => null))?.act;
|
|
2788
3683
|
console.log(` ${hit.o.body}`);
|
|
3684
|
+
if (full?.body && full.body.trim() !== hit.o.body.trim())
|
|
3685
|
+
console.log(`\n${full.body.split("\n").map((l) => ` ${l}`).join("\n")}`);
|
|
2789
3686
|
if (hit.o.goal)
|
|
2790
3687
|
console.log(` goal ${hit.client.goals.get(hit.o.goal)?.title ?? hit.o.goal}`);
|
|
2791
|
-
|
|
3688
|
+
const { localChoices } = await import("./tray.js");
|
|
3689
|
+
const choices = localChoices(hit.o.ref);
|
|
3690
|
+
if (choices) {
|
|
3691
|
+
console.log(`\nAnswer on this machine, one of:`);
|
|
3692
|
+
for (const c of choices)
|
|
3693
|
+
console.log(` ${c.label.padEnd(16)} ${C(`pantheon approvals answer ${hit.o.id} ${c.answer}`)}`);
|
|
3694
|
+
}
|
|
3695
|
+
else
|
|
3696
|
+
console.log(`\nAnswer: ${C(`pantheon approvals answer ${hit.o.id} 'your decision and rationale'`)}`);
|
|
2792
3697
|
closeAll();
|
|
2793
3698
|
return;
|
|
2794
3699
|
}
|
|
@@ -2796,8 +3701,11 @@ async function main() {
|
|
|
2796
3701
|
die("usage: pantheon approvals [list | show <id> | answer <id> <decision>]");
|
|
2797
3702
|
if (!all.length)
|
|
2798
3703
|
console.log(D(conns.length > 1 ? `no decisions waiting on you in any of your ${conns.length} teams` : "no decisions waiting on you"));
|
|
2799
|
-
|
|
2800
|
-
|
|
3704
|
+
const { localChoices: lc } = await import("./tray.js");
|
|
3705
|
+
for (const { o, cfg, client } of all) {
|
|
3706
|
+
const ch = lc(o.ref);
|
|
3707
|
+
console.log(`${C(o.id)} ${conns.length > 1 ? D(`[${cfg.space}] `) : ""}${client.goals.get(o.goal ?? "")?.title ?? (ch ? "This machine" : "No goal")}\n ${o.body}${ch ? D(`\n answer: ${ch.map((c) => c.answer).join(" / ")} (pantheon approvals show ${o.id})`) : ""}`);
|
|
3708
|
+
}
|
|
2801
3709
|
closeAll();
|
|
2802
3710
|
return;
|
|
2803
3711
|
}
|
|
@@ -2840,12 +3748,13 @@ async function main() {
|
|
|
2840
3748
|
return;
|
|
2841
3749
|
}
|
|
2842
3750
|
case "worker": {
|
|
3751
|
+
(await import("./machine.js")).recordWorkerPid();
|
|
2843
3752
|
const allCfg = loadAll();
|
|
2844
3753
|
const bindings = Object.values(allCfg?.bindings ?? {});
|
|
2845
3754
|
if (!flag("project") && bindings.length) {
|
|
2846
3755
|
const { WorkerFleet } = await import("./worker-fleet.js");
|
|
2847
3756
|
const fleet = new WorkerFleet({
|
|
2848
|
-
...(flag("agent-cmd") ? { agentCmd: flag("agent-cmd") } : { agent: flag("agent", "auto") }),
|
|
3757
|
+
...(flag("agent-cmd") ? { agentCmd: flag("agent-cmd"), agentCmdFromCli: true } : { agent: flag("agent", "auto") }),
|
|
2849
3758
|
...(flag("persona") ? { persona: flag("persona") } : {}),
|
|
2850
3759
|
continuations: !argv.includes("--no-continue"),
|
|
2851
3760
|
allowBash: argv.includes("--allow-bash"),
|
|
@@ -2862,10 +3771,10 @@ async function main() {
|
|
|
2862
3771
|
}),
|
|
2863
3772
|
log: (l) => console.error(l),
|
|
2864
3773
|
});
|
|
2865
|
-
console.error(`${B("pantheon worker")} ${D(`(${bindings.length} bound
|
|
3774
|
+
console.error(`${B("pantheon worker")} ${D(`(${bindings.length} bound folder${bindings.length === 1 ? "" : "s"} · ctrl-c to stop)`)}`);
|
|
2866
3775
|
if (argv.includes("--allow-bash")) {
|
|
2867
3776
|
console.error(`\x1b[33m --allow-bash: a woken run may execute commands. Only do this if you trust every\x1b[0m`);
|
|
2868
|
-
console.error(`\x1b[33m
|
|
3777
|
+
console.error(`\x1b[33m teammate approved to wake this machine (pantheon wake list), since their request is what starts the run.\x1b[0m`);
|
|
2869
3778
|
}
|
|
2870
3779
|
process.on("SIGINT", () => { fleet.stop(); process.exit(0); });
|
|
2871
3780
|
await fleet.run();
|
|
@@ -2876,7 +3785,7 @@ async function main() {
|
|
|
2876
3785
|
const worker = new Worker(cfg, {
|
|
2877
3786
|
project,
|
|
2878
3787
|
scratch: resolve(flag("scratch", join(project, ".pantheon", "worktrees"))),
|
|
2879
|
-
...(flag("agent-cmd") ? { agentCmd: flag("agent-cmd") } : { agent: flag("agent", "auto") }),
|
|
3788
|
+
...(flag("agent-cmd") ? { agentCmd: flag("agent-cmd"), agentCmdFromCli: true } : { agent: flag("agent", "auto") }),
|
|
2880
3789
|
...(flag("persona") ? { persona: flag("persona") } : {}),
|
|
2881
3790
|
continuations: !argv.includes("--no-continue"),
|
|
2882
3791
|
allowBash: argv.includes("--allow-bash"),
|
|
@@ -2896,7 +3805,7 @@ async function main() {
|
|
|
2896
3805
|
console.error(`${B("pantheon worker")} ${D("(ctrl-c to stop)")}`);
|
|
2897
3806
|
if (argv.includes("--allow-bash")) {
|
|
2898
3807
|
console.error(`\x1b[33m --allow-bash: a woken run may execute commands. Only do this if you trust every\x1b[0m`);
|
|
2899
|
-
console.error(`\x1b[33m
|
|
3808
|
+
console.error(`\x1b[33m teammate approved to wake this machine (pantheon wake list), since their request is what starts the run.\x1b[0m`);
|
|
2900
3809
|
}
|
|
2901
3810
|
process.on("SIGINT", () => { worker.stop(); process.exit(0); });
|
|
2902
3811
|
await worker.run();
|
|
@@ -3044,12 +3953,12 @@ async function main() {
|
|
|
3044
3953
|
}
|
|
3045
3954
|
for (const l of installService(kind, homedir(), extraN))
|
|
3046
3955
|
console.error(l);
|
|
3047
|
-
console.error(`\n${D("Clicking an alert opens the primary surface in `pantheon profile`. No agent run or
|
|
3956
|
+
console.error(`\n${D("Clicking an alert opens the primary surface in `pantheon profile`. No agent run or wake permission is involved.")}`);
|
|
3048
3957
|
return;
|
|
3049
3958
|
}
|
|
3050
3959
|
need();
|
|
3051
3960
|
const project = resolve(flag("project", "."));
|
|
3052
|
-
const extra = ["--project", project];
|
|
3961
|
+
const extra = flag("project") ? ["--project", project] : [];
|
|
3053
3962
|
if (argv.includes("--allow-bash"))
|
|
3054
3963
|
extra.push("--allow-bash");
|
|
3055
3964
|
if (argv.includes("--no-continue"))
|
|
@@ -3059,10 +3968,10 @@ async function main() {
|
|
|
3059
3968
|
extra.push(`--${f}`, flag(f));
|
|
3060
3969
|
}
|
|
3061
3970
|
console.error(`${B("installing the pantheon worker as a background service")}`);
|
|
3062
|
-
console.error(D(` project ${project}`));
|
|
3971
|
+
console.error(D(flag("project") ? ` project ${project} (this checkout only)` : ` serves every repo and folder bound to a team on this machine`));
|
|
3063
3972
|
for (const l of installService(kind, project, extra))
|
|
3064
3973
|
console.error(l);
|
|
3065
|
-
console.error(`\n${D("It only acts on requests from
|
|
3974
|
+
console.error(`\n${D("It only acts on requests from teammates approved on this machine — you are asked the first time each one needs it — one run at a time.")}`);
|
|
3066
3975
|
return;
|
|
3067
3976
|
}
|
|
3068
3977
|
case "spaces": {
|
|
@@ -3079,9 +3988,13 @@ async function main() {
|
|
|
3079
3988
|
const tags = [m.id === all.default_space ? C("default") : "", here.cfg?.spaceId === m.id || (here.cfg?.space === m.name && here.reason !== "none") ? C("← here") : ""].filter(Boolean).join(" ");
|
|
3080
3989
|
console.error(` ${B(m.name.padEnd(18))} ${D(new URL(m.relay).host)} ${tags}`);
|
|
3081
3990
|
for (const b of bound)
|
|
3082
|
-
console.error(` ${D("repo")} ${b.origin ?? b.path}${b.origin ? D(` (${b.path})`) : ""}`);
|
|
3991
|
+
console.error(` ${D(b.origin === null && !existsSync(join(b.path, ".git")) ? "folder" : "repo")} ${b.origin ?? b.path}${b.origin ? D(` (${b.path})`) : ""}`);
|
|
3083
3992
|
if (!bound.length)
|
|
3084
3993
|
console.error(` ${D("repo none bound — run `pantheon use " + m.name + " --here` from its checkout")}`);
|
|
3994
|
+
const boundFps = new Set(Object.entries(all.bindings ?? {}).filter(([, b]) => b.space === m.id).map(([k]) => k));
|
|
3995
|
+
const missing = (m.repos ?? []).filter((r) => !boundFps.has(r.fp)).map((r) => r.name);
|
|
3996
|
+
if (missing.length)
|
|
3997
|
+
console.error(` ${D(`not on this machine: ${missing.join(", ")} — pantheon repo find, once you have ${missing.length === 1 ? "it" : "them"} cloned`)}`);
|
|
3085
3998
|
}
|
|
3086
3999
|
const r = repoInfo(process.cwd());
|
|
3087
4000
|
console.error(`\n ${D("this folder:")} ${r ? (r.origin ?? r.root) : D("not a git repo")} ${D("→")} ${here.reason === "binding" ? `"${here.cfg.space}"` : here.reason === "single" ? `"${here.cfg.space}" (your only team)` : here.reason === "default" ? `unbound — humans get the default "${here.cfg.space}"; agents refuse` : here.reason === "stale" ? "bound to a team you have left" : "unbound"}`);
|
|
@@ -3089,10 +4002,53 @@ async function main() {
|
|
|
3089
4002
|
}
|
|
3090
4003
|
case "use": {
|
|
3091
4004
|
const all = loadAll() ?? die("not configured. Run `pantheon signup` first.");
|
|
3092
|
-
|
|
4005
|
+
if (argv.includes("--unbind")) {
|
|
4006
|
+
const { folderBinding } = await import("./config.js");
|
|
4007
|
+
const r = repoInfo(process.cwd());
|
|
4008
|
+
const fb = r ? null : folderBinding(all, process.cwd());
|
|
4009
|
+
const key = r ? r.fingerprint : fb && fb.path === realpathSync.native(process.cwd()) ? fb.key : null;
|
|
4010
|
+
if (fb && !key)
|
|
4011
|
+
die(`this folder is not bound itself — ${fb.path} is, and this folder is inside it. Run pantheon use --unbind there.`);
|
|
4012
|
+
const was = key ? all.bindings?.[key] : undefined;
|
|
4013
|
+
if (!key || !was || !unbindRepo(key))
|
|
4014
|
+
die(`${r ? "this repo" : "this folder"} is not bound to a team — nothing to undo`);
|
|
4015
|
+
console.error(`${B("unbound")} ${r ? (r.origin ?? r.root) : was.path} — it was "${findMembership(all, was.space)?.name ?? was.space}"; it now resolves as a folder bound to no team`);
|
|
4016
|
+
return;
|
|
4017
|
+
}
|
|
4018
|
+
const ref = argv[1] && !argv[1].startsWith("--") ? argv[1] : die("usage: pantheon use <team> [--here] # --here binds this repo to the team; without it, sets your default");
|
|
3093
4019
|
const m = findMembership(all, ref) ?? die(`you are not in a team called "${ref}". Yours: ${Object.values(all.spaces ?? {}).map((x) => x.name).join(", ") || "none"}`);
|
|
3094
4020
|
if (argv.includes("--here")) {
|
|
3095
|
-
const r = repoInfo(process.cwd())
|
|
4021
|
+
const r = repoInfo(process.cwd());
|
|
4022
|
+
if (!r) {
|
|
4023
|
+
for (let d = resolve(process.cwd());; d = dirname(d)) {
|
|
4024
|
+
if (existsSync(join(d, ".git")))
|
|
4025
|
+
die(`this folder is inside a git repo (${d}), but git did not answer — check that git works here (git status), then run this again.`);
|
|
4026
|
+
if (dirname(d) === d)
|
|
4027
|
+
break;
|
|
4028
|
+
}
|
|
4029
|
+
const real = (d) => { try {
|
|
4030
|
+
return realpathSync.native(d);
|
|
4031
|
+
}
|
|
4032
|
+
catch {
|
|
4033
|
+
return resolve(d);
|
|
4034
|
+
} };
|
|
4035
|
+
const here = real(process.cwd());
|
|
4036
|
+
if ((here === real(homedir()) || dirname(here) === here) && !argv.includes("--yes")) {
|
|
4037
|
+
die(`this is ${dirname(here) === here ? "the root of the disk" : "your home folder"}: binding it puts every folder under it in "${m.name}", and an agent opened anywhere there would speak for that team.\n Bind a project folder instead, or if you mean it: pantheon use ${m.name} --here --yes`);
|
|
4038
|
+
}
|
|
4039
|
+
if ((await import("./repo-find.js")).holdsRepo(process.cwd())) {
|
|
4040
|
+
const n = await bindReposIn(process.cwd(), all, m, argv.includes("--yes"), `pantheon use ${m.name} --here --yes`);
|
|
4041
|
+
if (!n)
|
|
4042
|
+
console.error(D(` nothing bound — the folder itself is not bound, since it holds repos`));
|
|
4043
|
+
else
|
|
4044
|
+
console.error(D(` Agents opened in those repos now speak as you in "${m.name}". Restart any that are open.`));
|
|
4045
|
+
return;
|
|
4046
|
+
}
|
|
4047
|
+
const { replaced } = bindFolder(process.cwd(), m.id);
|
|
4048
|
+
console.error(`${B("bound")} this folder → "${m.name}"${replaced ? D(` (was: ${findMembership(all, replaced.space)?.name ?? replaced.space})`) : ""}`);
|
|
4049
|
+
console.error(D(` Agents opened here, or in any folder below, now speak as you in "${m.name}". Restart any that are open.`));
|
|
4050
|
+
return;
|
|
4051
|
+
}
|
|
3096
4052
|
const { replaced } = bindRepo(r, m.id);
|
|
3097
4053
|
console.error(`${B("bound")} ${r.origin ?? r.root} → "${m.name}"${replaced ? D(` (was: ${findMembership(all, replaced.space)?.name ?? replaced.space})`) : ""}`);
|
|
3098
4054
|
if (!r.shared)
|
|
@@ -3119,6 +4075,15 @@ async function main() {
|
|
|
3119
4075
|
const { repo: _r, repoOrigin: _o, ...plain } = cfg;
|
|
3120
4076
|
const client = new PantheonClient(plain, undefined, { role: "cli" });
|
|
3121
4077
|
await client.connect().catch((e) => die(client.pending ?? connectFailure(e, plain.relay)));
|
|
4078
|
+
if (sub === "find") {
|
|
4079
|
+
await new Promise((res) => setTimeout(res, 150));
|
|
4080
|
+
const all = loadAll() ?? die("not configured");
|
|
4081
|
+
const m = findMembership(all, cfg.spaceId ?? cfg.space) ?? findMembership(all, cfg.space) ?? die(`not in "${cfg.space}" on this machine`);
|
|
4082
|
+
const where = argv[2] && !argv[2].startsWith("--") ? resolve(argv[2]) : null;
|
|
4083
|
+
client.close();
|
|
4084
|
+
await findAndBindRepos(all, m, client.repos, { ...(where ? { extra: [where] } : {}), recents: argv.includes("--recents") ? true : undefined, yes: argv.includes("--yes") });
|
|
4085
|
+
return;
|
|
4086
|
+
}
|
|
3122
4087
|
if (sub === "add") {
|
|
3123
4088
|
const r = repoInfo(flag("path") ?? process.cwd()) ?? die("not inside a git repo");
|
|
3124
4089
|
const base = flag("base") ?? null;
|
|
@@ -3154,12 +4119,26 @@ async function main() {
|
|
|
3154
4119
|
case "version":
|
|
3155
4120
|
console.log(VERSION);
|
|
3156
4121
|
return;
|
|
3157
|
-
case "help":
|
|
3158
|
-
|
|
4122
|
+
case "help": {
|
|
4123
|
+
const topic = argv[1] && argv[1] !== "all" && !argv[1].startsWith("-") ? argv[1] : null;
|
|
4124
|
+
if (topic && (usageFor(topic, HELP_ALL) || COMMAND_FLAGS[topic])) {
|
|
4125
|
+
console.log(`${usageFor(topic, HELP_ALL) ?? ` pantheon ${topic}`}${COMMAND_FLAGS[topic] ? `\n\n${B("flags")}\n ${COMMAND_FLAGS[topic]}` : ""}`);
|
|
4126
|
+
return;
|
|
4127
|
+
}
|
|
4128
|
+
console.log(argv.includes("--all") || argv[1] === "all" ? HELP_ALL : HELP);
|
|
4129
|
+
return;
|
|
4130
|
+
}
|
|
4131
|
+
case "--help":
|
|
4132
|
+
case "-h":
|
|
4133
|
+
console.log(argv.includes("--all") ? HELP_ALL : HELP);
|
|
3159
4134
|
return;
|
|
3160
|
-
default:
|
|
3161
|
-
|
|
3162
|
-
|
|
4135
|
+
default: {
|
|
4136
|
+
const known = [...new Set([...HELP_ALL.matchAll(/pantheon ([a-z][a-z-]*)/g)].map((m) => m[1]))];
|
|
4137
|
+
const near = known.map((k) => ({ k, d: slipDistance(cmd, k) })).sort((x, y) => x.d - y.d)[0];
|
|
4138
|
+
console.error(`unknown command "${cmd}"${near && near.d <= 2 ? ` — did you mean: pantheon ${near.k}?` : ""}`);
|
|
4139
|
+
console.error(D("all commands: pantheon help · everything: pantheon help --all"));
|
|
4140
|
+
process.exit(1);
|
|
4141
|
+
}
|
|
3163
4142
|
}
|
|
3164
4143
|
}
|
|
3165
4144
|
main().catch((e) => die(describeNetError(e, (() => { try {
|