@join-pantheon/cli 0.29.1 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -22
- package/bin/pantheon.js +12 -0
- package/dist/accounts.js +151 -26
- package/dist/agents.js +197 -14
- package/dist/claude-confine.js +283 -0
- package/dist/cli.js +725 -120
- package/dist/client.js +37 -2
- package/dist/command-state.js +79 -0
- package/dist/config.js +211 -10
- package/dist/doctor.js +103 -13
- package/dist/grants.js +1 -1
- package/dist/history-view.js +2 -1
- package/dist/home-guard.js +210 -0
- package/dist/hooks-install.js +2 -1
- package/dist/identity.js +24 -0
- package/dist/install.js +76 -26
- package/dist/kiro-files.js +2 -2
- package/dist/ledger.js +111 -28
- package/dist/machine.js +494 -32
- package/dist/mcp-supervisor.js +25 -6
- package/dist/mcp-windows.js +47 -25
- package/dist/mcp.js +25 -10
- package/dist/network.js +39 -8
- package/dist/notify.js +17 -6
- package/dist/pending.js +8 -14
- package/dist/persona.js +21 -0
- package/dist/profile.js +4 -1
- package/dist/quickstart.js +82 -14
- package/dist/reads.js +30 -0
- package/dist/refusal.js +14 -1
- package/dist/relay.js +1143 -180
- package/dist/roster-file.js +169 -23
- package/dist/run-failure.js +83 -3
- package/dist/secret-guard.js +141 -0
- package/dist/service.js +84 -34
- package/dist/setup-guide.js +28 -9
- package/dist/spend.js +6 -2
- package/dist/store.js +94 -10
- package/dist/text-file.js +74 -0
- package/dist/tray.js +11 -2
- package/dist/update-check.js +2 -0
- package/dist/which.js +69 -12
- package/dist/worker-policy.js +137 -3
- package/dist/worker.js +702 -81
- package/helper/macos/menubar.swift +15 -1
- package/package.json +2 -2
package/dist/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";
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { 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";
|
|
@@ -21,7 +21,7 @@ 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
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";
|
|
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,20 @@ function sameHost(a, b) {
|
|
|
228
258
|
return false;
|
|
229
259
|
}
|
|
230
260
|
}
|
|
261
|
+
const teamFlag = (cfg) => (flag("space") ? ` --space ${cfg.space}` : "");
|
|
231
262
|
function need() {
|
|
232
263
|
const r = resolveFor(process.cwd());
|
|
264
|
+
if (flag("space") && !["start", "profile"].includes(cmd)) {
|
|
265
|
+
const f = loadAll();
|
|
266
|
+
const m = f && findMembership(f, flag("space"));
|
|
267
|
+
if (!f || !m)
|
|
268
|
+
die(`you are not in a team called "${flag("space")}". Yours: ${Object.values(f?.spaces ?? {}).map((x) => x.name).join(", ") || "none"}`);
|
|
269
|
+
return r.cfg && (r.cfg.spaceId === m.id || r.cfg.space === m.name) && r.reason !== "default" ? r.cfg : configFor(f, m, null);
|
|
270
|
+
}
|
|
233
271
|
if (r.cfg && r.cfg.space)
|
|
234
272
|
return r.cfg;
|
|
273
|
+
if (r.repoUnknown)
|
|
274
|
+
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
275
|
if (r.reason === "unbound") {
|
|
236
276
|
const names = Object.values(loadAll()?.spaces ?? {}).map((m) => m.name).join(", ");
|
|
237
277
|
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 +279,7 @@ function need() {
|
|
|
239
279
|
if (r.reason === "stale")
|
|
240
280
|
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
281
|
if (r.cfg && !r.cfg.space)
|
|
242
|
-
die("signed in, but not on a team yet. Run
|
|
282
|
+
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
283
|
return die("not configured. Run `pantheon signup` then `pantheon start` (create a team), or `pantheon join <invite>` (join one).");
|
|
244
284
|
}
|
|
245
285
|
function targets() {
|
|
@@ -303,6 +343,7 @@ function printGranted(granted) {
|
|
|
303
343
|
console.error(` \u2022 ${g}`);
|
|
304
344
|
console.error(D(` Undo: pantheon hook uninstall · pantheon service uninstall worker`));
|
|
305
345
|
}
|
|
346
|
+
const HUD_ACTION_MAX_AGE_MS = 30_000;
|
|
306
347
|
function unattendedProject() {
|
|
307
348
|
if (argv.includes("--no-worker"))
|
|
308
349
|
return { project: null, how: "off" };
|
|
@@ -377,21 +418,33 @@ async function workdirFor(cfg, hud) {
|
|
|
377
418
|
return pickWorkdir(bindings, hints, team);
|
|
378
419
|
}
|
|
379
420
|
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) });
|
|
421
|
+
const res = await signedRead(cfg, "/history", opts.goal ? { goal: opts.goal } : opts.since !== undefined ? { since: opts.since } : {}, { timeoutMs: 8000 });
|
|
384
422
|
if (!res.ok)
|
|
385
|
-
throw
|
|
423
|
+
throw await readFailure(cfg, res, "the ledger");
|
|
386
424
|
return (await res.json()).acts ?? [];
|
|
387
425
|
}
|
|
426
|
+
async function readFailure(cfg, res, what) {
|
|
427
|
+
const why = await res.json().then((j) => j.error).catch(() => undefined);
|
|
428
|
+
if (res.status === 401 || res.status === 403) {
|
|
429
|
+
const c = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
430
|
+
try {
|
|
431
|
+
await c.connect();
|
|
432
|
+
}
|
|
433
|
+
catch (e) {
|
|
434
|
+
return new Error(c.pending ?? connectFailure(e, cfg.relay));
|
|
435
|
+
}
|
|
436
|
+
finally {
|
|
437
|
+
c.close();
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return new Error(`relay answered ${res.status} for ${what}${why ? `: ${why}` : ""}`);
|
|
441
|
+
}
|
|
388
442
|
async function fetchAct(cfg, id, budgetMs = Number.POSITIVE_INFINITY) {
|
|
389
443
|
const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
|
|
390
|
-
const q = `space=${encodeURIComponent(cfg.space)}&token=${encodeURIComponent(cfg.token)}`;
|
|
391
444
|
const deadline = Date.now() + budgetMs;
|
|
392
445
|
const left = () => Math.max(1, Math.min(8000, deadline - Date.now()));
|
|
393
446
|
try {
|
|
394
|
-
const res = await
|
|
447
|
+
const res = await signedRead(cfg, "/act", { id }, { timeoutMs: left() });
|
|
395
448
|
if (res.ok) {
|
|
396
449
|
const body = await res.json();
|
|
397
450
|
if (body.act)
|
|
@@ -469,11 +522,36 @@ function reportWindows(installed) {
|
|
|
469
522
|
if (ended.length)
|
|
470
523
|
console.error(D(` ended ${ended.length} pantheon server${ended.length > 1 ? "s" : ""} left behind by windows that no longer exist`));
|
|
471
524
|
}
|
|
525
|
+
const AGENT_ROWS = new Set(["claude code", "cursor", "codex", "copilot", "vs code", "kiro"]);
|
|
526
|
+
const rowFailed = (status) => /^(failed|could not)/i.test(status);
|
|
527
|
+
function readiness(tools) {
|
|
528
|
+
const agents = tools.filter((t) => AGENT_ROWS.has(t.tool));
|
|
529
|
+
const attempted = agents.filter((t) => !/^skipped/i.test(t.status));
|
|
530
|
+
return { failed: tools.filter((t) => rowFailed(t.status)), wired: attempted.filter((t) => !rowFailed(t.status)).length, attempted: attempted.length };
|
|
531
|
+
}
|
|
472
532
|
function printReady(tools) {
|
|
473
533
|
const workerOn = tools.some((t) => t.tool === "worker" && t.status.startsWith("on"));
|
|
474
|
-
|
|
534
|
+
const r = readiness(tools);
|
|
535
|
+
if (r.failed.length) {
|
|
536
|
+
console.error(`\n\x1b[31m${B("Not everything was set up:")}\x1b[0m`);
|
|
537
|
+
for (const f of r.failed)
|
|
538
|
+
console.error(` ${f.tool.padEnd(14)}${f.status}`);
|
|
539
|
+
}
|
|
540
|
+
if (r.attempted && !r.wired) {
|
|
541
|
+
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")}`);
|
|
542
|
+
process.exitCode = 1;
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (!r.attempted) {
|
|
546
|
+
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")}`);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (r.failed.length)
|
|
550
|
+
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")}`);
|
|
551
|
+
else
|
|
552
|
+
console.error(`\n${B("You're set.")} Restart your agent once (quit and reopen) and it is connected.`);
|
|
475
553
|
if (workerOn)
|
|
476
|
-
console.error(D(` While you're away, a teammate you
|
|
554
|
+
console.error(D(` While you're away, a teammate you approve to wake this machine can hand work to it —`));
|
|
477
555
|
if (workerOn)
|
|
478
556
|
console.error(D(` isolated branch, no secrets in the worktree. Pause any time from the menu bar.`));
|
|
479
557
|
const oneTime = new Map();
|
|
@@ -497,9 +575,17 @@ function printReady(tools) {
|
|
|
497
575
|
console.error(D(` ${l}`));
|
|
498
576
|
}
|
|
499
577
|
}
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
const
|
|
578
|
+
const PLAIN = !!process.env["NO_COLOR"];
|
|
579
|
+
if (PLAIN) {
|
|
580
|
+
const SGR = /\x1b\[[0-9;]*m/g;
|
|
581
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
582
|
+
const write = stream.write.bind(stream);
|
|
583
|
+
stream.write = ((chunk, ...rest) => write(typeof chunk === "string" ? chunk.replace(SGR, "") : chunk, ...rest));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const B = (s) => PLAIN ? s : `\x1b[1m${s}\x1b[0m`;
|
|
587
|
+
const D = (s) => PLAIN ? s : `\x1b[2m${s}\x1b[0m`;
|
|
588
|
+
const C = (s) => PLAIN ? s : `\x1b[36m${s}\x1b[0m`;
|
|
503
589
|
const HELP = `${B("pantheon")} — your agent and your teammates' agents, working together
|
|
504
590
|
|
|
505
591
|
${B("first time — verify your email once")}
|
|
@@ -521,14 +607,14 @@ ${B("getting going — two commands, from your project's repo if you have one")}
|
|
|
521
607
|
|
|
522
608
|
One command does everything: agents wired and pre-approved, wake-up hook (your
|
|
523
609
|
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
|
-
|
|
610
|
+
team while you are away — on when run from a git repo; a peer still needs you
|
|
611
|
+
to approve them here to use it: pantheon wake allow), native decision alerts, menu bar. Opt-outs:
|
|
526
612
|
--no-worker, --no-hook, --no-notifications, --no-menubar.
|
|
527
613
|
Already a member, new machine or fresh pull? pantheon install --user does the same.
|
|
528
614
|
|
|
529
615
|
${B("several teams on one machine")}
|
|
530
616
|
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)
|
|
617
|
+
pantheon use <team> --here bind this repo or folder to a team (agents opened here speak in it); --unbind undoes it
|
|
532
618
|
pantheon repo add | list the repos a team works from; a delivery must come from one of them
|
|
533
619
|
(repo add --base <branch>: the branch workers cut from, else origin's default)
|
|
534
620
|
pantheon leave <team> drop one team from this machine (its token and repo bindings)
|
|
@@ -544,7 +630,9 @@ ${B("day to day")}
|
|
|
544
630
|
pantheon logs [-f] the relay's log
|
|
545
631
|
|
|
546
632
|
${B("control")}
|
|
547
|
-
pantheon admit <who> let someone in (--may-wake
|
|
633
|
+
pantheon admit <who> let someone in (--may-wake: the team lets them wake workers, and this machine allows them)
|
|
634
|
+
pantheon wake allow <who> let a teammate wake THIS machine; wake deny <who> takes it back; wake list shows who can
|
|
635
|
+
(this machine only — nothing is sent to the team; --space <team> to pick one)
|
|
548
636
|
pantheon admit <who> --revoke remove them
|
|
549
637
|
pantheon worker stay answerable while you are away
|
|
550
638
|
(--agent claude-code|codex|cursor|copilot|kiro|auto, --persona <name>)
|
|
@@ -575,14 +663,14 @@ ${B("control")}
|
|
|
575
663
|
|
|
576
664
|
${D("more: pantheon help --all")}
|
|
577
665
|
`;
|
|
578
|
-
const HELP_ALL = HELP + `
|
|
666
|
+
const HELP_ALL = HELP.replace(/\n[^\n]*more: pantheon help --all[^\n]*\n?$/, "\n") + `
|
|
579
667
|
${B("less common")}
|
|
580
668
|
pantheon relay run only the relay, in this terminal
|
|
581
669
|
pantheon service install [relay|worker|notifier|menubar] | restart | uninstall
|
|
582
670
|
run a long-lived component in the background, from login
|
|
583
671
|
pantheon members the full list, with key fingerprints
|
|
584
|
-
pantheon whoami your name and fingerprint
|
|
585
672
|
pantheon pending what a live agent window has not yet seen
|
|
673
|
+
pantheon goal open "<title>" --with <who> open a goal to work in (goal list · goal close <id> "<summary>")
|
|
586
674
|
pantheon say <ACT> <text> send one act by hand (NOTE, ASK, ANSWER, PROPOSE, ...)
|
|
587
675
|
replies pick the act they answer; --ref <id | seq | last> to choose
|
|
588
676
|
pantheon history the thread in full, with ids (--goal <id>, --from <who>, --last N, --short)
|
|
@@ -607,6 +695,30 @@ function usageFor(command, help) {
|
|
|
607
695
|
}
|
|
608
696
|
return out.length ? out.join("\n") : null;
|
|
609
697
|
}
|
|
698
|
+
const COMMAND_FLAGS = {
|
|
699
|
+
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",
|
|
700
|
+
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",
|
|
701
|
+
invite: "--list --revoke <token|all> --may-wake | --no-wake --ttl <minutes>",
|
|
702
|
+
admit: "<member> --may-wake | --no-wake --revoke --forget --yes",
|
|
703
|
+
wake: "allow <member> | deny <member> | list --space <team>",
|
|
704
|
+
say: "<ACT> <text> --to <member>[,<member>] --goal <id> --ref <id | seq | last> --summary '<one line>' --domains <a,b> --branch --commit --remote (DELIVERED) --space <team>",
|
|
705
|
+
goal: "list | open \"<title>\" --with <member>[,…] [--acceptance \"…\"] [--deadline <ISO time>] [--list] | close <id> \"<summary>\"",
|
|
706
|
+
history: "--goal <id> --from <member> --last <n> --since <seq> --short --presence --json",
|
|
707
|
+
network: "--proxy <url> [--no-proxy <hosts>] | --direct | --clear",
|
|
708
|
+
leave: "[<team>] --yes --keep-identity --stop-relay --project <dir>",
|
|
709
|
+
stop: "[relay|worker|notifier|menubar|all] --port <n>",
|
|
710
|
+
logs: "[relay|worker|notifier|menubar] --follow --n <lines>",
|
|
711
|
+
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",
|
|
712
|
+
service: "install [relay|worker|notifier|menubar] | restart <kind> | uninstall <kind> --project <dir> --allow-bash",
|
|
713
|
+
doctor: "--share --project <dir>",
|
|
714
|
+
relay: "--port <n> --host <address> --data <dir>",
|
|
715
|
+
use: "<team> --here --yes · pantheon use --unbind (this folder or repo bound to no team again)",
|
|
716
|
+
repo: "add [--base <branch>] | list [--all]",
|
|
717
|
+
login: "--email <address> --relay <url> --label <device name>",
|
|
718
|
+
signup: "--email <address> --username <name> --relay <url> --label <device name>",
|
|
719
|
+
audit: "--space <team> --since <hours> --json",
|
|
720
|
+
setup: "--questions (and every answer as a flag, with --yes)",
|
|
721
|
+
};
|
|
610
722
|
const HELP_OPAQUE = new Set(["mcp", "hook", "pending", "tray-status", "help", "version", "-v", "--help", "-h"]);
|
|
611
723
|
function rememberWorkingProxy() {
|
|
612
724
|
try {
|
|
@@ -622,7 +734,54 @@ function rememberWorkingProxy() {
|
|
|
622
734
|
}
|
|
623
735
|
catch { }
|
|
624
736
|
}
|
|
737
|
+
async function teamStanding(all, m) {
|
|
738
|
+
const c = new PantheonClient(configFor(all, m, null), undefined, { role: "cli" });
|
|
739
|
+
try {
|
|
740
|
+
await Promise.race([c.connect(), new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), 8_000))]);
|
|
741
|
+
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 };
|
|
742
|
+
}
|
|
743
|
+
catch {
|
|
744
|
+
return { others: null, founder: false };
|
|
745
|
+
}
|
|
746
|
+
finally {
|
|
747
|
+
c.close();
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
function slipDistance(a, b) {
|
|
751
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
752
|
+
for (let j = 1; j <= b.length; j++)
|
|
753
|
+
d[0][j] = j;
|
|
754
|
+
for (let i = 1; i <= a.length; i++)
|
|
755
|
+
for (let j = 1; j <= b.length; j++) {
|
|
756
|
+
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));
|
|
757
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1])
|
|
758
|
+
d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
|
|
759
|
+
}
|
|
760
|
+
return d[a.length][b.length];
|
|
761
|
+
}
|
|
762
|
+
const EXTRA_FLAGS = "--agent-cmd --approve-all --ask-before-sending --child --code --data --device --display --force --help --host --idle --keywords --local " +
|
|
763
|
+
"--no-continue --no-guide --no-notify --path --peek --poll --portable --publish --queued --quiet --remind --scratch --session --surface --token --unattended " +
|
|
764
|
+
"--until --user --version --all --json " +
|
|
765
|
+
"--name --title --wake --commands --alerts --decides --playbook --base --responsibilities --human-required --agent-can-decide --capabilities";
|
|
766
|
+
const KNOWN_FLAGS = new Set([...`${Object.values(COMMAND_FLAGS).join(" ")} ${EXTRA_FLAGS}`.matchAll(/--([a-z][a-z0-9-]*)/g)].map((m) => m[1]));
|
|
767
|
+
function refuseUnknownFlags() {
|
|
768
|
+
if (["mcp", "hook", "tray-status", "_menubar"].includes(cmd))
|
|
769
|
+
return;
|
|
770
|
+
for (const a of argv.slice(1)) {
|
|
771
|
+
if (!/^--[a-z][a-z0-9-]*(?:=\S*)?$/i.test(a))
|
|
772
|
+
continue;
|
|
773
|
+
const name = a.slice(2).split("=")[0].toLowerCase();
|
|
774
|
+
if (KNOWN_FLAGS.has(name))
|
|
775
|
+
continue;
|
|
776
|
+
const own = [...(COMMAND_FLAGS[cmd] ?? "").matchAll(/--([a-z][a-z0-9-]*)/g)].map((m) => m[1]);
|
|
777
|
+
const rank = (ks) => ks.map((k) => ({ k, d: slipDistance(name, k) })).sort((x, y) => x.d - y.d)[0];
|
|
778
|
+
const mine = rank(own);
|
|
779
|
+
const near = mine && mine.d <= 2 ? mine : rank([...KNOWN_FLAGS]);
|
|
780
|
+
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" : ""}.`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
625
783
|
async function main() {
|
|
784
|
+
refuseUnknownFlags();
|
|
626
785
|
try {
|
|
627
786
|
await installNetwork();
|
|
628
787
|
}
|
|
@@ -641,11 +800,15 @@ async function main() {
|
|
|
641
800
|
}
|
|
642
801
|
return interactiveOnboard();
|
|
643
802
|
}
|
|
644
|
-
|
|
803
|
+
const askedHelpOnly = argv.length === 2 && (argv[1] === "--help" || argv[1] === "-h") && ["pending", "hook"].includes(cmd);
|
|
804
|
+
if (!HELP_OPAQUE.has(cmd) || askedHelpOnly) {
|
|
645
805
|
const helpAt = cmd === "say" ? [argv[1]] : argv.slice(1);
|
|
646
806
|
if (helpAt.some((a) => a === "--help" || a === "-h")) {
|
|
647
807
|
const u = usageFor(cmd, HELP_ALL);
|
|
648
|
-
|
|
808
|
+
const flags = COMMAND_FLAGS[cmd];
|
|
809
|
+
const teamCmd = !["start", "join", "signup", "login", "verify", "whoami", "relay", "help", "version", "upgrade", "network", "ps", "stop", "service", "hook", "mcp", "pending", "tray-status"].includes(cmd);
|
|
810
|
+
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;
|
|
811
|
+
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
812
|
return;
|
|
650
813
|
}
|
|
651
814
|
}
|
|
@@ -656,7 +819,28 @@ async function main() {
|
|
|
656
819
|
if (set !== undefined) {
|
|
657
820
|
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
821
|
saveAll({ ...all, network: { proxy: p, ...(flag("no-proxy") ? { no_proxy: flag("no-proxy") } : {}), source: "pantheon network", saved_at: Date.now() } });
|
|
659
|
-
|
|
822
|
+
const knock = await (async () => {
|
|
823
|
+
try {
|
|
824
|
+
const u = new URL(p);
|
|
825
|
+
const { connect } = await import("node:net");
|
|
826
|
+
const port = Number(u.port) || (u.protocol === "https:" ? 443 : 80);
|
|
827
|
+
return await new Promise((done) => {
|
|
828
|
+
const sock = connect({ host: u.hostname, port, timeout: 3_000 });
|
|
829
|
+
sock.once("connect", () => { sock.destroy(); done(null); });
|
|
830
|
+
sock.once("timeout", () => { sock.destroy(); done("did not answer in 3s"); });
|
|
831
|
+
sock.once("error", (e) => done(e.code === "ENOTFOUND" ? "its name did not resolve" : e.code === "ECONNREFUSED" ? "nothing is listening there" : e.message));
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
catch (e) {
|
|
835
|
+
return e.message;
|
|
836
|
+
}
|
|
837
|
+
})();
|
|
838
|
+
if (knock) {
|
|
839
|
+
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`);
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
console.error(`${B("saved")} every Pantheon process on this machine will go through ${redactProxy(p)} (it answered). pantheon doctor checks the relay through it.`);
|
|
843
|
+
}
|
|
660
844
|
return;
|
|
661
845
|
}
|
|
662
846
|
if (argv.includes("--direct")) {
|
|
@@ -686,13 +870,14 @@ async function main() {
|
|
|
686
870
|
die(setupFlags.errors.join("\n") + "\n pantheon setup --questions lists every answer");
|
|
687
871
|
const wp = unattendedProject();
|
|
688
872
|
requireSignInOnHostedRelay(flag("relay"));
|
|
873
|
+
await requireSignInOnAccountsRelay(flag("relay"));
|
|
689
874
|
console.error(`${B("pantheon start")}\n`);
|
|
690
875
|
if (!flag("space") && !repoInfo(process.cwd()))
|
|
691
876
|
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
877
|
const r = await qsStart({
|
|
693
878
|
port,
|
|
694
879
|
dataDir: resolveDataDir(flag("data")).dir,
|
|
695
|
-
ttlMinutes:
|
|
880
|
+
ttlMinutes: numFlag("ttl", 1440),
|
|
696
881
|
reset: argv.includes("--reset"),
|
|
697
882
|
...(flag("space") ? { space: flag("space") } : {}),
|
|
698
883
|
...(flag("me") ? { me: flag("me") } : {}),
|
|
@@ -708,7 +893,7 @@ async function main() {
|
|
|
708
893
|
for (const t of r.tools)
|
|
709
894
|
console.error(` ${t.tool.padEnd(14)}${t.status}`);
|
|
710
895
|
for (const w of r.warnings)
|
|
711
|
-
console.error(`\x1b[33m ! ${w}\x1b[0m`);
|
|
896
|
+
console.error(`\x1b[33m ! ${w}\x1b[0m` + (/NOT_SIGNED_IN/.test(w) ? signInHint("NOT_SIGNED_IN", r.cfg.relay) : ""));
|
|
712
897
|
printGranted(r.granted);
|
|
713
898
|
rememberWorkingProxy();
|
|
714
899
|
if (!r.claimed) {
|
|
@@ -729,8 +914,11 @@ async function main() {
|
|
|
729
914
|
console.error(`\n${B("Send your teammate this one line — it sets up their whole machine:")}\n`);
|
|
730
915
|
console.error(` ${C(`pantheon join ${r.invite}`)}\n`);
|
|
731
916
|
}
|
|
732
|
-
console.error(D(` From their project's repo if they have one — a repo is only needed to hand over code or to be woken. Single use, expires in ${Math.round(
|
|
917
|
+
console.error(D(` From their project's repo if they have one — a repo is only needed to hand over code or to be woken. Single use, expires in ${Math.round(numFlag("ttl", 1440) / 60)}h.`));
|
|
733
918
|
console.error(D(` Redeeming it admits them — no separate approval step. Need another? \`pantheon invite\``));
|
|
919
|
+
console.error(r.inviteMayWake
|
|
920
|
+
? ` ${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")}`
|
|
921
|
+
: D(` It lets them join and talk to your agent. It does NOT let them wake this machine: approve a person on this machine with pantheon wake allow <name>.`));
|
|
734
922
|
console.error(D(`\n Not technical? Forward them this instead, and their agent does the rest:`));
|
|
735
923
|
console.error(` ${D("Tell your agent: run")} ${C("curl -fsSL https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${link ?? r.invite}`)}`);
|
|
736
924
|
}
|
|
@@ -774,11 +962,11 @@ async function main() {
|
|
|
774
962
|
for (const i of open)
|
|
775
963
|
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
964
|
if (open.length)
|
|
777
|
-
console.log(D(`\nrevoke one: pantheon invite --revoke <token or its last 6 characters
|
|
965
|
+
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
966
|
client.close();
|
|
779
967
|
return;
|
|
780
968
|
}
|
|
781
|
-
const ttl =
|
|
969
|
+
const ttl = numFlag("ttl", 1440);
|
|
782
970
|
const mayWake = argv.includes("--may-wake") ? true : argv.includes("--no-wake") ? false : wakeDefault(loadProfile(cfg));
|
|
783
971
|
const inv = await client.makeInvite(mayWake, ttl).catch((e) => die(e.message));
|
|
784
972
|
const raw = toInvite({ ...cfg, token: inv.url });
|
|
@@ -793,8 +981,24 @@ async function main() {
|
|
|
793
981
|
return;
|
|
794
982
|
}
|
|
795
983
|
case "relay": {
|
|
984
|
+
for (const s of [process.stdout, process.stderr])
|
|
985
|
+
s.on("error", () => { });
|
|
796
986
|
process.on("unhandledRejection", (e) => { console.error(`! unhandled rejection: ${e?.stack ?? String(e)}`); });
|
|
797
|
-
|
|
987
|
+
for (const s of [process.stdout, process.stderr])
|
|
988
|
+
s.on("error", () => { });
|
|
989
|
+
let inHandler = false;
|
|
990
|
+
process.on("uncaughtException", (e) => {
|
|
991
|
+
if (e?.code === "EPIPE" || e?.code === "ERR_STREAM_DESTROYED" || inHandler)
|
|
992
|
+
return;
|
|
993
|
+
inHandler = true;
|
|
994
|
+
try {
|
|
995
|
+
console.error(`! uncaught exception (kept serving): ${e?.stack ?? String(e)}`);
|
|
996
|
+
}
|
|
997
|
+
catch { }
|
|
998
|
+
finally {
|
|
999
|
+
inHandler = false;
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
798
1002
|
const relay = new Relay({
|
|
799
1003
|
port: Number(flag("port", process.env.PORT ?? "8787")),
|
|
800
1004
|
host: flag("host", "0.0.0.0"),
|
|
@@ -805,7 +1009,7 @@ async function main() {
|
|
|
805
1009
|
console.error(D(`ledger: ${resolveDataDir(flag("data")).dir}`));
|
|
806
1010
|
console.error(D(`health: http://localhost:${port}/health`));
|
|
807
1011
|
console.error(D(`\nFor a colleague on another network, expose this port (ssh -R, cloudflared, tailscale)`));
|
|
808
|
-
console.error(D(`and
|
|
1012
|
+
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
1013
|
let shuttingDown = false;
|
|
810
1014
|
const shutdown = async (sig) => {
|
|
811
1015
|
if (shuttingDown)
|
|
@@ -878,7 +1082,8 @@ async function main() {
|
|
|
878
1082
|
catch (e) {
|
|
879
1083
|
if (!looksLikeInviteCode(invite))
|
|
880
1084
|
die(e.message);
|
|
881
|
-
const
|
|
1085
|
+
const named = flag("relay") ?? process.env.PANTHEON_RELAY;
|
|
1086
|
+
const candidates = (named ? [named] : [load()?.relay, DEFAULT_RELAY]).filter((r) => !!r);
|
|
882
1087
|
parsed = await resolveInviteCode(invite, candidates).catch((err) => die(err.message.replace(/^INVITE_CODE_[A-Z]+:\s*/, "")));
|
|
883
1088
|
console.error(D(`That was just the code — it opens "${parsed.space}" on ${new URL(parsed.relay).host}. Joining that team.\n`));
|
|
884
1089
|
}
|
|
@@ -901,21 +1106,28 @@ async function main() {
|
|
|
901
1106
|
const m = e.message;
|
|
902
1107
|
if (/BAD_INVITE/.test(m)) {
|
|
903
1108
|
const body = m.replace(/^BAD_INVITE:\s*/, "");
|
|
904
|
-
die(/\n/.test(body) ? body : `${body}\n Ask your teammate for a fresh one: pantheon invite`);
|
|
1109
|
+
die(/\n/.test(body) || /fresh one|new invite/i.test(body) ? body : `${body}\n Ask your teammate for a fresh one: pantheon invite`);
|
|
905
1110
|
}
|
|
906
1111
|
if (/NO_SUCH_SPACE/.test(m))
|
|
907
|
-
die(`${m}\n
|
|
1112
|
+
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
1113
|
if (/ECONNREFUSED|ENOTFOUND|timeout/i.test(m))
|
|
909
1114
|
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);
|
|
1115
|
+
die(m + (/NOT_SIGNED_IN/.test(m) ? signInHint("NOT_SIGNED_IN", parsed.relay) : ""));
|
|
911
1116
|
});
|
|
912
1117
|
for (const t of r.tools)
|
|
913
1118
|
console.error(` ${t.tool.padEnd(14)}${t.status}`);
|
|
914
1119
|
for (const w of r.warnings)
|
|
915
|
-
console.error(`\x1b[33m ! ${w}\x1b[0m`);
|
|
1120
|
+
console.error(`\x1b[33m ! ${w}\x1b[0m` + (/NOT_SIGNED_IN/.test(w) ? signInHint("NOT_SIGNED_IN", parsed.relay) : ""));
|
|
916
1121
|
printGranted(r.granted);
|
|
917
1122
|
rememberWorkingProxy();
|
|
918
1123
|
printReady(r.tools);
|
|
1124
|
+
{
|
|
1125
|
+
const jc = load();
|
|
1126
|
+
const mode = jc ? loadProfile(jc).agent.wake_default ?? "approved" : "approved";
|
|
1127
|
+
console.error(D(mode === "anyone"
|
|
1128
|
+
? `\n You chose wake "anyone": teammates this team lets wake workers can start an agent run on this machine. Change it: pantheon setup`
|
|
1129
|
+
: `\n Nobody on the team can wake this machine until you approve them, here: pantheon wake allow <name>${mode === "no" ? "" : " (or choose \"anyone\" below)"}.`));
|
|
1130
|
+
}
|
|
919
1131
|
{
|
|
920
1132
|
const joined = load();
|
|
921
1133
|
if (joined)
|
|
@@ -1070,6 +1282,8 @@ async function main() {
|
|
|
1070
1282
|
return;
|
|
1071
1283
|
}
|
|
1072
1284
|
case "setup": {
|
|
1285
|
+
if (inWokenRun() && !argv.includes("--questions"))
|
|
1286
|
+
die("a woken run cannot change this machine's setup — a person has to run this here");
|
|
1073
1287
|
if (argv.includes("--questions")) {
|
|
1074
1288
|
console.log(questionnaire());
|
|
1075
1289
|
return;
|
|
@@ -1104,15 +1318,20 @@ async function main() {
|
|
|
1104
1318
|
console.log(B("members"));
|
|
1105
1319
|
if (!peers.length)
|
|
1106
1320
|
console.log(` ${D("(nobody else yet — send them an invite from `pantheon invite`)")}`);
|
|
1321
|
+
const seens = peers.map((p) => p.seen_seq).filter((x) => x !== undefined && x < client.head);
|
|
1322
|
+
const since = seens.length ? Math.min(...seens) : null;
|
|
1323
|
+
const recent = since === null ? [] : await fetchHistory(cfg, { since }).catch(() => null);
|
|
1107
1324
|
for (const p of peers) {
|
|
1108
|
-
const
|
|
1325
|
+
const lag = p.seen_seq === undefined ? null : client.head - p.seen_seq;
|
|
1326
|
+
const behind = lag === null ? null : lag <= 0 ? 0
|
|
1327
|
+
: recent ? recent.filter((a) => a.seq > p.seen_seq && a.from === cfg.member && a.act !== "JOIN" && a.act !== "LEAVE").length : lag;
|
|
1109
1328
|
const reach = behind === null
|
|
1110
1329
|
? D(p.delivered_seq !== undefined && p.delivered_seq >= client.head
|
|
1111
1330
|
? "delivered, but no read-receipt — no wake-up hook on their machine"
|
|
1112
1331
|
: "no wake-up hook on their machine — nothing surfaces until their human types")
|
|
1113
1332
|
: behind > 0
|
|
1114
|
-
? `\x1b[
|
|
1115
|
-
: D("their agent is caught up");
|
|
1333
|
+
? `\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`
|
|
1334
|
+
: lag > 0 ? D("their agent has seen everything you sent") : D("their agent is caught up");
|
|
1116
1335
|
console.log(` ${p.online ? C("●") : D("○")} ${p.id.padEnd(14)} ${D(p.agent.padEnd(12))} ${reach}`);
|
|
1117
1336
|
}
|
|
1118
1337
|
const goals = [...client.goals.values()];
|
|
@@ -1175,7 +1394,7 @@ async function main() {
|
|
|
1175
1394
|
owed: client.myObligations().map((o) => ({
|
|
1176
1395
|
id: o.id, kind: o.kind, body: o.body, created_at: o.created_at,
|
|
1177
1396
|
})),
|
|
1178
|
-
recentRuns: recentRuns(86_400_000),
|
|
1397
|
+
recentRuns: recentRuns(86_400_000, undefined, 12, { teams: [client.spaceId, cfg.spaceId, cfg.space], member: cfg.member }),
|
|
1179
1398
|
}));
|
|
1180
1399
|
client.close();
|
|
1181
1400
|
}
|
|
@@ -1218,16 +1437,14 @@ async function main() {
|
|
|
1218
1437
|
console.log(` ${line}`);
|
|
1219
1438
|
console.log();
|
|
1220
1439
|
};
|
|
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)));
|
|
1440
|
+
const first = await signedRead(cfg, "/history").catch((e) => die(describeNetError(e, cfg.relay)));
|
|
1224
1441
|
if (!first.ok)
|
|
1225
|
-
die(
|
|
1442
|
+
die((await readFailure(cfg, first, "the ledger")).message);
|
|
1226
1443
|
let since = ((await first.json()).acts ?? []).at(-1)?.seq ?? 0;
|
|
1227
1444
|
console.log(`${B("watching")} ${cfg.space} ${D("(ctrl-c to stop)")}\n`);
|
|
1228
1445
|
for (;;) {
|
|
1229
1446
|
await new Promise((r) => setTimeout(r, LIMITS.watchPollMs));
|
|
1230
|
-
const res = await
|
|
1447
|
+
const res = await signedRead(cfg, "/history", { since }).catch(() => null);
|
|
1231
1448
|
if (!res?.ok)
|
|
1232
1449
|
continue;
|
|
1233
1450
|
for (const act of (await res.json()).acts ?? []) {
|
|
@@ -1242,6 +1459,8 @@ async function main() {
|
|
|
1242
1459
|
if (!report) {
|
|
1243
1460
|
if (argv.includes("--json"))
|
|
1244
1461
|
console.log(JSON.stringify({ seq: 0, acts: [], obligations: [] }));
|
|
1462
|
+
else if (process.stderr.isTTY)
|
|
1463
|
+
console.error(D(`nothing new for this session${load() ? "" : " — this folder resolves to no team (pantheon status says why)"}`));
|
|
1245
1464
|
return;
|
|
1246
1465
|
}
|
|
1247
1466
|
console.log(argv.includes("--json") ? JSON.stringify(report) : renderPending(report));
|
|
@@ -1276,7 +1495,8 @@ async function main() {
|
|
|
1276
1495
|
}
|
|
1277
1496
|
for (const a of picked)
|
|
1278
1497
|
console.log(formatAct(a, { short: argv.includes("--short"), ...(a.goal && titles.get(a.goal) ? { goalTitle: titles.get(a.goal) } : {}) }) + "\n");
|
|
1279
|
-
|
|
1498
|
+
const pool = argv.includes("--presence") ? acts.length : acts.filter((a) => a.act !== "JOIN" && a.act !== "LEAVE").length;
|
|
1499
|
+
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
1500
|
return;
|
|
1281
1501
|
}
|
|
1282
1502
|
case "show": {
|
|
@@ -1292,13 +1512,66 @@ async function main() {
|
|
|
1292
1512
|
}
|
|
1293
1513
|
return;
|
|
1294
1514
|
}
|
|
1515
|
+
case "goal": {
|
|
1516
|
+
const cfg = need();
|
|
1517
|
+
const sub = argv[1] ?? "list";
|
|
1518
|
+
const client = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
1519
|
+
await client.connect().catch((e) => die(connectFailure(e, cfg.relay)));
|
|
1520
|
+
try {
|
|
1521
|
+
if (sub === "list") {
|
|
1522
|
+
const open = [...client.goals.values()].filter((g) => g.state !== "CLOSED");
|
|
1523
|
+
if (!open.length)
|
|
1524
|
+
console.log(D("no open goals — pantheon goal open \"<what it is about>\" --with <member>"));
|
|
1525
|
+
for (const g of open)
|
|
1526
|
+
console.log(` ${g.id} ${g.state.padEnd(8)} ${g.title} ${D(`with ${g.members.join(", ")} · ${g.acts}/${g.budget} acts`)}`);
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
if (sub === "open") {
|
|
1530
|
+
const VALUE_FLAGS = new Set(["--with", "--acceptance", "--deadline", "--space"]);
|
|
1531
|
+
const title = argv.slice(2).filter((a, i, all) => !a.startsWith("--") && !(i > 0 && VALUE_FLAGS.has(all[i - 1]))).join(" ").trim()
|
|
1532
|
+
|| die('usage: pantheon goal open "<what it is about>" --with <member>[,<member>] [--acceptance "<what done looks like>"] [--deadline <ISO time>] [--list]');
|
|
1533
|
+
const withs = cleanList((flag("with") ?? "").split(","));
|
|
1534
|
+
const dl = flag("deadline");
|
|
1535
|
+
const deadline = dl ? Date.parse(dl) : undefined;
|
|
1536
|
+
if (dl && !Number.isFinite(deadline))
|
|
1537
|
+
die(`--deadline takes a time like 2026-10-01T17:00:00Z, not "${dl}"`);
|
|
1538
|
+
const g = await client.openGoal({ title, acceptance: flag("acceptance") ?? "", ...(withs.length ? { invite: withs } : {}),
|
|
1539
|
+
...(deadline ? { deadline } : {}), ...(argv.includes("--list") ? { kind: "worklist" } : {}) }).catch((e) => die(e.message));
|
|
1540
|
+
console.log(`opened ${g.id} "${title}"${withs.length ? ` with ${withs.join(", ")}` : ""}`);
|
|
1541
|
+
console.error(D(` now: pantheon say ASK "…" --to <member> --goal ${g.id}`));
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
if (sub === "close") {
|
|
1545
|
+
const id = argv[2] ?? die('usage: pantheon goal close <goal id> "<what was done>"');
|
|
1546
|
+
const summary = argv.slice(3).filter((a) => !a.startsWith("--")).join(" ").trim() || die("say what was done: it is the goal's record");
|
|
1547
|
+
const r = await client.send({ act: "CLOSE", body: summary, goal: id }).catch((e) => die(e.message));
|
|
1548
|
+
console.log(`closed ${id} (act ${r.id})`);
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
die("usage: pantheon goal list | open \"<title>\" --with <member> | close <id> \"<summary>\"");
|
|
1552
|
+
}
|
|
1553
|
+
finally {
|
|
1554
|
+
client.close();
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1295
1557
|
case "say": {
|
|
1296
1558
|
const cfg = need();
|
|
1297
1559
|
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]");
|
|
1560
|
+
{
|
|
1561
|
+
const sayable = ACTS.filter((a) => a !== "JOIN" && a !== "LEAVE");
|
|
1562
|
+
if (!sayable.includes(act))
|
|
1563
|
+
die(`"${argv[1]}" is not an act. One of: ${sayable.join(", ")}`);
|
|
1564
|
+
}
|
|
1298
1565
|
const stop = argv.findIndex((a, i) => i >= 2 && a.startsWith("--"));
|
|
1299
1566
|
const body = argv.slice(2, stop === -1 ? undefined : stop).join(" ");
|
|
1300
1567
|
if (!body)
|
|
1301
1568
|
die("nothing to say — pantheon say NOTE 'your message here'");
|
|
1569
|
+
{
|
|
1570
|
+
const SAY_FLAGS = new Set([...COMMAND_FLAGS["say"].matchAll(/--([a-z][a-z-]*)/g)].map((m) => m[1]));
|
|
1571
|
+
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");
|
|
1572
|
+
if (stray)
|
|
1573
|
+
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} "…"`);
|
|
1574
|
+
}
|
|
1302
1575
|
const summary = flag("summary");
|
|
1303
1576
|
const client = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
1304
1577
|
await client.connect().catch((e) => die(connectFailure(e, cfg.relay)));
|
|
@@ -1316,15 +1589,25 @@ async function main() {
|
|
|
1316
1589
|
let ref = flag("ref") ?? null;
|
|
1317
1590
|
if (ref !== null || ["ANSWER", "ACCEPT", "OBJECT"].includes(act)) {
|
|
1318
1591
|
let acts;
|
|
1592
|
+
let settled = null;
|
|
1593
|
+
let answered = new Set();
|
|
1319
1594
|
if (ref && ref !== "last") {
|
|
1320
1595
|
const hit = (await fetchAct(cfg, ref))?.act;
|
|
1321
1596
|
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
1597
|
}
|
|
1325
|
-
else
|
|
1598
|
+
else {
|
|
1326
1599
|
acts = await fetchHistory(cfg, goalArg ? { goal: goalArg } : {}).catch(() => []);
|
|
1600
|
+
answered = new Set(acts.filter((a) => a.from === cfg.member && a.ref).map((a) => a.ref));
|
|
1601
|
+
const closed = new Set([...client.goals.values()].filter((g) => g.state === "CLOSED").map((g) => g.id));
|
|
1602
|
+
const all = acts;
|
|
1603
|
+
acts = acts.filter((a) => !answered.has(a.id) && !(a.goal && closed.has(a.goal)));
|
|
1604
|
+
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;
|
|
1605
|
+
}
|
|
1327
1606
|
const rr = resolveRef(acts, ref, act, goalArg, cfg.member);
|
|
1607
|
+
if (rr.error && settled) {
|
|
1608
|
+
client.close();
|
|
1609
|
+
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}`);
|
|
1610
|
+
}
|
|
1328
1611
|
if (rr.error) {
|
|
1329
1612
|
client.close();
|
|
1330
1613
|
die(rr.error);
|
|
@@ -1341,8 +1624,17 @@ async function main() {
|
|
|
1341
1624
|
ref,
|
|
1342
1625
|
...(summary ? { summary } : {}),
|
|
1343
1626
|
...(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'`
|
|
1627
|
+
}).catch((e) => die(/SUMMARY_REQUIRED/.test(e.message) ? `${e.message}\n Add one: --summary 'the point they must not miss'`
|
|
1628
|
+
: /UNVERIFIABLE_DELIVERY/.test(e.message) ? `${e.message}\n From here: pantheon say DELIVERED "…" --branch <the pushed branch> --commit <its sha>`
|
|
1629
|
+
: /BAD_REF/.test(e.message) ? `${e.message}\n pantheon history lists the acts with their ids and [sequence numbers]; --ref takes either, or "last"`
|
|
1630
|
+
: /TO_REQUIRED/.test(e.message) ? `${e.message}\n Name who owes it: --to <member> (pantheon team lists them)`
|
|
1631
|
+
: /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)`
|
|
1632
|
+
: e.message));
|
|
1345
1633
|
console.log(`sent ${act} id=${r.id} seq=${r.seq}`);
|
|
1634
|
+
if (flag("space"))
|
|
1635
|
+
console.error(D(` in "${cfg.space}"`));
|
|
1636
|
+
else if (resolveFor(process.cwd()).reason === "default")
|
|
1637
|
+
console.error(D(` in "${cfg.space}" — your default team; this folder is bound to none (pantheon use <team> --here)`));
|
|
1346
1638
|
client.close();
|
|
1347
1639
|
return;
|
|
1348
1640
|
}
|
|
@@ -1374,7 +1666,7 @@ async function main() {
|
|
|
1374
1666
|
}
|
|
1375
1667
|
if (worker.running) {
|
|
1376
1668
|
console.log(` worker ${C("running")} ${worker.managedBy === "launchd" ? `service ${worker.label}` : "started by hand"} pid ${worker.pid}`);
|
|
1377
|
-
console.log(` ${D("a
|
|
1669
|
+
console.log(` ${D("a teammate approved on this machine (pantheon wake list) can start an agent run here")}`);
|
|
1378
1670
|
}
|
|
1379
1671
|
else {
|
|
1380
1672
|
console.log(` worker ${D("not running")} ${D("nobody can start an agent run on this machine")}`);
|
|
@@ -1453,9 +1745,10 @@ async function main() {
|
|
|
1453
1745
|
if (!["all", "relay", "worker", "notifier", "menubar"].includes(what))
|
|
1454
1746
|
die(`usage: pantheon stop [relay|worker|notifier|menubar|all]`);
|
|
1455
1747
|
const { stopMachine } = await import("./machine.js");
|
|
1748
|
+
const { killRecordedRun } = await import("./worker.js");
|
|
1456
1749
|
console.log(B("stopping"));
|
|
1457
1750
|
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 }] : []) });
|
|
1751
|
+
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
1752
|
for (const l of res.lines)
|
|
1460
1753
|
console.log(` ${l}`);
|
|
1461
1754
|
if (res.note)
|
|
@@ -1470,12 +1763,21 @@ async function main() {
|
|
|
1470
1763
|
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
1764
|
console.error(D(`${f} (${statFile(f)})\n`));
|
|
1472
1765
|
const follow = argv.includes("-f") || argv.includes("--follow");
|
|
1473
|
-
const {
|
|
1474
|
-
|
|
1766
|
+
const { tailLines, followFile } = await import("./machine.js");
|
|
1767
|
+
process.stdout.write(tailLines(f, Math.max(0, Number(flag("n", "40")) || 40)));
|
|
1768
|
+
if (follow)
|
|
1769
|
+
await followFile(f, (chunk) => process.stdout.write(chunk));
|
|
1475
1770
|
return;
|
|
1476
1771
|
}
|
|
1477
1772
|
case "hook": {
|
|
1478
1773
|
const sub = argv[1] ?? "status";
|
|
1774
|
+
if (inWokenRun() && !["install", "uninstall", "remove", "status"].includes(sub)) {
|
|
1775
|
+
if (sub === "cursor-prompt")
|
|
1776
|
+
console.log(JSON.stringify({ continue: true }));
|
|
1777
|
+
else if (sub === "cursor-stop")
|
|
1778
|
+
console.log("{}");
|
|
1779
|
+
process.exit(0);
|
|
1780
|
+
}
|
|
1479
1781
|
const hookCfg = resolveForHook();
|
|
1480
1782
|
const sessionOf = (raw) => {
|
|
1481
1783
|
let fromJson;
|
|
@@ -1761,7 +2063,7 @@ async function main() {
|
|
|
1761
2063
|
client.close();
|
|
1762
2064
|
const bound = Object.values(all.bindings ?? {}).filter((b) => b.space === m.id).length;
|
|
1763
2065
|
removeMembership(m.id);
|
|
1764
|
-
console.error(`${B("deleted")} "${m.name}" — everyone in it was disconnected; its token${bound ? ` and ${bound}
|
|
2066
|
+
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
2067
|
return;
|
|
1766
2068
|
}
|
|
1767
2069
|
case "leave": {
|
|
@@ -1769,9 +2071,21 @@ async function main() {
|
|
|
1769
2071
|
const all = loadAll() ?? die("not configured");
|
|
1770
2072
|
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
2073
|
const bound = Object.values(all.bindings ?? {}).filter((b) => b.space === m.id).map((b) => b.origin ?? b.path);
|
|
2074
|
+
const { others, founder } = await teamStanding(all, m);
|
|
2075
|
+
if (founder && others?.length === 0 && !argv.includes("--yes")) {
|
|
2076
|
+
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` +
|
|
2077
|
+
` Delete it instead: pantheon space delete ${m.name} --yes\n Leave it anyway: pantheon leave ${m.name} --yes`);
|
|
2078
|
+
}
|
|
1772
2079
|
removeMembership(m.id);
|
|
1773
|
-
console.error(`${B("left")} "${m.name}" — its token is gone from this machine${bound.length ? `, and ${bound.length}
|
|
1774
|
-
|
|
2080
|
+
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(", ")})` : ""}.`);
|
|
2081
|
+
if (others === null)
|
|
2082
|
+
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`));
|
|
2083
|
+
else if (others.length)
|
|
2084
|
+
console.error(D(founder
|
|
2085
|
+
? `${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.`
|
|
2086
|
+
: `${others.join(", ")} still list you in "${m.name}". If you are not coming back, one of them runs: pantheon admit ${all.member} --revoke`));
|
|
2087
|
+
else
|
|
2088
|
+
console.error(D(`Nobody else was in "${m.name}"; it stays on the relay until its founder deletes it.`));
|
|
1775
2089
|
return;
|
|
1776
2090
|
}
|
|
1777
2091
|
const all = loadAll();
|
|
@@ -1780,24 +2094,43 @@ async function main() {
|
|
|
1780
2094
|
const plan = leavePlan(all);
|
|
1781
2095
|
const project = resolve(flag("project", "."));
|
|
1782
2096
|
const keepIdentity = argv.includes("--keep-identity");
|
|
1783
|
-
|
|
2097
|
+
const standing = new Map();
|
|
2098
|
+
if (all && !keepIdentity) {
|
|
2099
|
+
const ms = Object.values(all.spaces ?? {});
|
|
2100
|
+
const got = await Promise.all(ms.map((m) => teamStanding(all, m)));
|
|
2101
|
+
ms.forEach((m, i) => standing.set(m.name, got[i]));
|
|
2102
|
+
}
|
|
2103
|
+
const stranded = [...standing].filter(([, v]) => v.founder && v.others?.length === 0).map(([k]) => k);
|
|
2104
|
+
if (stranded.length && !argv.includes("--yes")) {
|
|
2105
|
+
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` +
|
|
2106
|
+
` Delete first: ${stranded.map((t) => `pantheon space delete ${t} --yes`).join(" · ")}\n Leave anyway: pantheon leave --yes`);
|
|
2107
|
+
}
|
|
2108
|
+
const orphaned = [...standing].filter(([, v]) => v.founder && (v.others?.length ?? 0) > 0).map(([k]) => k);
|
|
2109
|
+
if (orphaned.length && !argv.includes("--yes")) {
|
|
2110
|
+
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` +
|
|
2111
|
+
` To step out but keep the way back: ${orphaned.map((t) => `pantheon leave ${t}`).join(" · ")} (keeps your key)\n Leave anyway: pantheon leave --yes`);
|
|
2112
|
+
}
|
|
2113
|
+
if (plan.teams.length && !keepIdentity && !argv.includes("--yes")) {
|
|
1784
2114
|
const list = plan.teams.map((t) => `"${t.name}"`).join(", ");
|
|
2115
|
+
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
2116
|
if (!process.stdin.isTTY)
|
|
1786
|
-
die(
|
|
2117
|
+
die(`${what}\n If you mean it: pantheon leave --yes${plan.teams.length > 1 ? "\n Just one team: pantheon leave <team>" : ""}`);
|
|
1787
2118
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1788
|
-
const answer = await
|
|
2119
|
+
const answer = (await questionOrNull(rl, `${what[0].toUpperCase()}${what.slice(1)} Type "yes" to continue: `)) ?? "";
|
|
1789
2120
|
rl.close();
|
|
1790
2121
|
if (answer.toLowerCase() !== "yes")
|
|
1791
|
-
die(`nothing was changed. Leave one team with: pantheon leave <team
|
|
2122
|
+
die(`nothing was changed.${plan.teams.length > 1 ? " Leave one team with: pantheon leave <team>" : ""}`);
|
|
1792
2123
|
}
|
|
1793
2124
|
console.error(`${B("leaving pantheon")}${plan.teams.length ? D(` — ${plan.teams.map((t) => t.name).join(", ")}`) : ""}\n`);
|
|
1794
2125
|
const { uninstallMachine, realUninstallIo } = await import("./machine.js");
|
|
1795
2126
|
const { removeUserMcpEntries } = await import("./install.js");
|
|
2127
|
+
const { killRecordedRun } = await import("./worker.js");
|
|
1796
2128
|
const done = uninstallMachine({ project, stopRelay: argv.includes("--stop-relay") }, realUninstallIo({
|
|
1797
2129
|
port: Number(flag("port", "8787")),
|
|
1798
2130
|
removeMcpEntries: () => removeUserMcpEntries({ project }),
|
|
1799
2131
|
hasIntent: () => readIntent() !== null,
|
|
1800
2132
|
clearIntent: () => clearIntent(),
|
|
2133
|
+
killRun: () => killRecordedRun(dirname(CONFIG_PATH)),
|
|
1801
2134
|
}));
|
|
1802
2135
|
for (const l of done.lines)
|
|
1803
2136
|
console.error(` ${l}`);
|
|
@@ -1858,6 +2191,20 @@ async function main() {
|
|
|
1858
2191
|
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
2192
|
}
|
|
1860
2193
|
}
|
|
2194
|
+
{
|
|
2195
|
+
const { removePantheonWorktrees, removeHomeLeftovers } = await import("./machine.js");
|
|
2196
|
+
const repos = [project, ...Object.values(all?.bindings ?? {}).map((b) => b.path).filter((p) => !!p)];
|
|
2197
|
+
const trees = removePantheonWorktrees(repos, [join(CONFIG_DIR, "worktrees"), ...repos.map((r) => join(r, ".pantheon", "worktrees"))]);
|
|
2198
|
+
removed.push(...trees.removed);
|
|
2199
|
+
left.push(...trees.left);
|
|
2200
|
+
try {
|
|
2201
|
+
rmSync(join(CONFIG_DIR, "worktrees"), { recursive: true, force: true });
|
|
2202
|
+
}
|
|
2203
|
+
catch { }
|
|
2204
|
+
const rest = removeHomeLeftovers(CONFIG_DIR, keepIdentity);
|
|
2205
|
+
removed.push(...rest.removed);
|
|
2206
|
+
left.push(...rest.left);
|
|
2207
|
+
}
|
|
1861
2208
|
console.error(`\n${B("Removed")}`);
|
|
1862
2209
|
for (const r of removed)
|
|
1863
2210
|
console.error(` ${C("✓")} ${r}`);
|
|
@@ -1868,9 +2215,20 @@ async function main() {
|
|
|
1868
2215
|
console.error(` • ${l}`);
|
|
1869
2216
|
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
2217
|
console.error(` • ${D("the relay's ledger on disk, and anything you pushed to git")}`);
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
2218
|
+
{
|
|
2219
|
+
const name = me?.member ?? plan.member ?? "<your name>";
|
|
2220
|
+
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}"`);
|
|
2221
|
+
const founded = plan.teams.filter((t) => { const s = standing.get(t.name); return s?.founder && s.others?.length; }).map((t) => `"${t.name}"`);
|
|
2222
|
+
if (listed.length) {
|
|
2223
|
+
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:`)}`);
|
|
2224
|
+
console.error(` ${D(`pantheon admit ${name} --revoke`)}`);
|
|
2225
|
+
}
|
|
2226
|
+
if (founded.length)
|
|
2227
|
+
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.`)}`);
|
|
2228
|
+
}
|
|
2229
|
+
if (me?.account && !keepIdentity) {
|
|
2230
|
+
const { accountStillExists } = await import("./machine.js");
|
|
2231
|
+
console.error(`\n ${accountStillExists(me.account.email)}`);
|
|
1874
2232
|
}
|
|
1875
2233
|
console.error(`\n ${D(`If Pantheon let you down, one line on why to ${FEEDBACK_ADDRESS} would help more than you might think.`)}`);
|
|
1876
2234
|
return;
|
|
@@ -1884,6 +2242,12 @@ async function main() {
|
|
|
1884
2242
|
}
|
|
1885
2243
|
case "panic": {
|
|
1886
2244
|
console.error(`${B("PANIC — locking down this machine")}\n`);
|
|
2245
|
+
{
|
|
2246
|
+
const { killRecordedRun } = await import("./worker.js");
|
|
2247
|
+
const killed = killRecordedRun(dirname(CONFIG_PATH));
|
|
2248
|
+
if (killed)
|
|
2249
|
+
console.error(` ${killed}`);
|
|
2250
|
+
}
|
|
1887
2251
|
const { panicMachine, waitStopped } = await import("./machine.js");
|
|
1888
2252
|
const all = loadAll();
|
|
1889
2253
|
const teams = Object.values(all?.spaces ?? {});
|
|
@@ -1921,11 +2285,27 @@ async function main() {
|
|
|
1921
2285
|
await client.connect().catch((e) => die(client.pending ?? connectFailure(e, cfg.relay)));
|
|
1922
2286
|
await new Promise((r) => setTimeout(r, 300));
|
|
1923
2287
|
console.log(`${B(`who is in ${cfg.space}`)}\n`);
|
|
2288
|
+
const { loadPolicy, isApprovedWaker, relayKnowsAdmitters } = await import("./worker-policy.js");
|
|
2289
|
+
const { wakeMode } = await import("./profile.js");
|
|
2290
|
+
const pol = loadPolicy();
|
|
2291
|
+
const keys = [client.spaceId ?? cfg.spaceId, cfg.space];
|
|
2292
|
+
const carried = keys.some((k) => !!k && !!pol.wakersCarried?.[k]);
|
|
2293
|
+
const mode = wakeMode(loadProfile(cfg)) === "approved" && !carried && !relayKnowsAdmitters(client.members, cfg.member) ? "anyone" : wakeMode(loadProfile(cfg));
|
|
1924
2294
|
for (const e of client.members) {
|
|
1925
2295
|
const who = e.member.padEnd(16);
|
|
1926
2296
|
const you = e.member === cfg.member ? D(" (you)") : "";
|
|
1927
2297
|
if (e.admitted) {
|
|
1928
|
-
|
|
2298
|
+
const team = D(e.may_wake ? " team: may wake workers" : " team: may not wake workers");
|
|
2299
|
+
const here = e.member === cfg.member ? ""
|
|
2300
|
+
: mode === "no" ? D("cannot start work here — this machine is set not to be woken")
|
|
2301
|
+
: mode === "anyone" ? (e.may_wake ? "can start work here while you are away" : D("cannot start work here"))
|
|
2302
|
+
: isApprovedWaker(pol, keys, e.member)
|
|
2303
|
+
? (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"))
|
|
2304
|
+
: D(`cannot start work here — pantheon wake allow ${e.member}${teamFlag(cfg)}`);
|
|
2305
|
+
console.log(` ${C("in")} ${who}${here}${e.member === cfg.member ? "" : team}${you}`);
|
|
2306
|
+
}
|
|
2307
|
+
else if (e.revoked_at) {
|
|
2308
|
+
console.log(` ${D("removed")} ${who}${D(`removed${e.revoked_by ? ` by ${e.revoked_by}` : ""}, ${new Date(e.revoked_at).toLocaleString()}`)}`);
|
|
1929
2309
|
}
|
|
1930
2310
|
else {
|
|
1931
2311
|
console.log(` \x1b[33mwaiting\x1b[0m ${who}${D(`fingerprint ${e.fingerprint}`)}`);
|
|
@@ -1936,25 +2316,40 @@ async function main() {
|
|
|
1936
2316
|
return;
|
|
1937
2317
|
}
|
|
1938
2318
|
case "admit": {
|
|
2319
|
+
if (inWokenRun())
|
|
2320
|
+
die("a woken run cannot admit anyone or change who may wake this machine — a person has to run this here");
|
|
1939
2321
|
const cfg = need();
|
|
1940
|
-
const who = argv[1] ?? die("usage: pantheon admit <member> [--may-wake] [--revoke] [--forget]");
|
|
2322
|
+
const who = argv[1] ?? die("usage: pantheon admit <member> [--may-wake | --no-wake] [--revoke] [--forget] [--yes]");
|
|
2323
|
+
const forget = argv.includes("--forget");
|
|
2324
|
+
const revoke = argv.includes("--revoke");
|
|
2325
|
+
const askedWake = argv.includes("--may-wake") && !revoke && !forget;
|
|
2326
|
+
if (askedWake && !argv.includes("--yes") && !process.stdin.isTTY) {
|
|
2327
|
+
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)`);
|
|
2328
|
+
}
|
|
1941
2329
|
const client = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
1942
2330
|
await client.connect().catch((e) => die(client.pending ?? connectFailure(e, cfg.relay)));
|
|
1943
2331
|
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
2332
|
const entry = client.members.find((e) => e.member === who);
|
|
1948
2333
|
if (!entry)
|
|
1949
2334
|
die(`"${who}" has never connected, so there is no key to admit. Have them run \`pantheon join\` first.`);
|
|
2335
|
+
const mayWake = askedWake ? true : argv.includes("--no-wake") ? false : !!(entry.admitted && entry.may_wake);
|
|
1950
2336
|
if (!revoke && !forget) {
|
|
1951
2337
|
console.error(`${B("confirm this fingerprint with them directly:")} ${C(entry.fingerprint)}`);
|
|
1952
|
-
if (
|
|
1953
|
-
|
|
1954
|
-
|
|
2338
|
+
if (askedWake) {
|
|
2339
|
+
const { COMMAND_GRANT_SCOPE } = await import("./agents.js");
|
|
2340
|
+
const { commandState } = await import("./command-state.js");
|
|
2341
|
+
const cmds = commandState({ project: process.cwd() });
|
|
2342
|
+
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):`)}`);
|
|
2343
|
+
console.error(` ${C("can")} edit files in its own worktree of this repo, on a pantheon/* branch`);
|
|
2344
|
+
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
2345
|
console.error(` ${C("can")} reply to your peers as you, while you are away`);
|
|
1956
|
-
|
|
1957
|
-
|
|
2346
|
+
if (cmds.level === "off") {
|
|
2347
|
+
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}`);
|
|
2348
|
+
}
|
|
2349
|
+
else {
|
|
2350
|
+
console.error(` ${C("can")} run commands: ${cmds.summary}. ${COMMAND_GRANT_SCOPE}`);
|
|
2351
|
+
}
|
|
2352
|
+
console.error(` ${D("cannot")} touch your own checkout's working tree, or merge anything`);
|
|
1958
2353
|
console.error(` ${D("limits")} one run at a time, 12 per hour, and only while a worker is running`);
|
|
1959
2354
|
console.error(`\n ${D("Nothing happens at all unless you run `pantheon worker`. Undo instantly: pantheon panic")}`);
|
|
1960
2355
|
if (argv.includes("--yes")) {
|
|
@@ -1962,11 +2357,17 @@ async function main() {
|
|
|
1962
2357
|
}
|
|
1963
2358
|
else {
|
|
1964
2359
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1965
|
-
const ans = await
|
|
2360
|
+
const ans = (await questionOrNull(rl, `\ngrant that to ${who}? [y/N] `)) ?? "";
|
|
1966
2361
|
rl.close();
|
|
1967
|
-
if (!/^y(es)?$/i.test(ans
|
|
2362
|
+
if (!/^y(es)?$/i.test(ans)) {
|
|
2363
|
+
const keep = !!(entry.admitted && entry.may_wake);
|
|
2364
|
+
if (entry.admitted) {
|
|
2365
|
+
console.error(`\nnot granted. Nothing was changed.`);
|
|
2366
|
+
client.close();
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
1968
2369
|
console.error(`\nnot granted. Admitting ${who} without wake rights instead.`);
|
|
1969
|
-
await client.admit(who,
|
|
2370
|
+
await client.admit(who, keep, false).catch((e) => die(e.message));
|
|
1970
2371
|
console.log(`${C(who)} is in, but cannot start anything on this machine.`);
|
|
1971
2372
|
client.close();
|
|
1972
2373
|
return;
|
|
@@ -1975,13 +2376,27 @@ async function main() {
|
|
|
1975
2376
|
}
|
|
1976
2377
|
}
|
|
1977
2378
|
await client.admit(who, mayWake, revoke, forget).catch((e) => die(e.message));
|
|
2379
|
+
let local = "";
|
|
2380
|
+
const { setLocalWaker, loadPolicy: lp, isApprovedWaker: approved } = await import("./worker-policy.js");
|
|
2381
|
+
const wakesHere = approved(lp(), [client.spaceId ?? cfg.spaceId, cfg.space], who);
|
|
2382
|
+
if (askedWake || revoke || forget) {
|
|
2383
|
+
const r = setLocalWaker([client.spaceId ?? cfg.spaceId, cfg.space], who, askedWake);
|
|
2384
|
+
if (!r.ok && askedWake)
|
|
2385
|
+
local = D(`\n Not recorded on this machine: ${r.why}.`);
|
|
2386
|
+
if (r.ok && r.changed && !askedWake && wakesHere)
|
|
2387
|
+
local = D(`\n ${who} was also taken off this machine's wake list.`);
|
|
2388
|
+
}
|
|
1978
2389
|
console.log(forget
|
|
1979
2390
|
? `${C(who)} forgotten — the key ${D(entry.fingerprint)} is retired.\n` +
|
|
1980
2391
|
` They can now rejoin as "${who}" with a new key, keeping their goals and obligations.\n` +
|
|
1981
2392
|
` ${B("Check the new fingerprint with them")} once they are back: pantheon members`
|
|
1982
2393
|
: revoke
|
|
1983
|
-
? `${C(who)}
|
|
1984
|
-
:
|
|
2394
|
+
? `${C(who)} removed from "${cfg.space}" — their sessions were disconnected${local}`
|
|
2395
|
+
: askedWake
|
|
2396
|
+
? `${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` +
|
|
2397
|
+
D(` Each teammate decides for their own machine: pantheon wake allow ${who}`)
|
|
2398
|
+
: `${C(who)} ${entry.admitted ? `was already in "${cfg.space}"` : `admitted to "${cfg.space}"`}${mayWake ? D(" (the team still lets them wake workers)") : ""}\n` +
|
|
2399
|
+
(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
2400
|
client.close();
|
|
1986
2401
|
return;
|
|
1987
2402
|
}
|
|
@@ -2015,7 +2430,7 @@ async function main() {
|
|
|
2015
2430
|
const prior = identity();
|
|
2016
2431
|
const relay = flag("relay") ?? prior?.relay ?? process.env.PANTHEON_RELAY ?? DEFAULT_RELAY;
|
|
2017
2432
|
const kp = prior?.publicKey && prior.privateKey ? { publicKey: prior.publicKey, privateKey: prior.privateKey } : newKeypair();
|
|
2018
|
-
const label = flag("label") ??
|
|
2433
|
+
const label = flag("label") ?? deviceLabel();
|
|
2019
2434
|
const base = relay.replace(/^ws/, "http").replace(/\/+$/, "");
|
|
2020
2435
|
const body = purpose === "signup" ? { email, username, pubkey: kp.publicKey, label } : { email, pubkey: kp.publicKey, label };
|
|
2021
2436
|
const r = await fetch(`${base}/auth/${purpose}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })
|
|
@@ -2042,7 +2457,7 @@ async function main() {
|
|
|
2042
2457
|
let code = (argv[1] ?? flag("code") ?? "").trim();
|
|
2043
2458
|
if (!code && process.stdin.isTTY) {
|
|
2044
2459
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
2045
|
-
code = (await
|
|
2460
|
+
code = (await questionOrNull(rl, `Enter the 6-digit code sent to ${pending.email}: `)) ?? "";
|
|
2046
2461
|
rl.close();
|
|
2047
2462
|
}
|
|
2048
2463
|
if (!code)
|
|
@@ -2408,39 +2823,140 @@ async function main() {
|
|
|
2408
2823
|
client.close();
|
|
2409
2824
|
return;
|
|
2410
2825
|
}
|
|
2826
|
+
case "wake": {
|
|
2827
|
+
const { loadPolicy, setLocalWaker } = await import("./worker-policy.js");
|
|
2828
|
+
const { wakeMode } = await import("./profile.js");
|
|
2829
|
+
const sub = argv[1] ?? "list";
|
|
2830
|
+
const one = () => {
|
|
2831
|
+
if (!flag("space"))
|
|
2832
|
+
return need();
|
|
2833
|
+
const f = loadAll();
|
|
2834
|
+
const m = f && findMembership(f, flag("space"));
|
|
2835
|
+
if (!f || !m)
|
|
2836
|
+
die(`you are not in a team called "${flag("space")}"`);
|
|
2837
|
+
return configFor(f, m, null);
|
|
2838
|
+
};
|
|
2839
|
+
if (sub === "allow" || sub === "deny") {
|
|
2840
|
+
const who = argv[2] && !argv[2].startsWith("--") ? argv[2] : die(`usage: pantheon wake ${sub} <name> [--space <team>]`);
|
|
2841
|
+
const cfg = one();
|
|
2842
|
+
if (who === cfg.member)
|
|
2843
|
+
die("your own requests always reach your own worker; there is nothing to allow");
|
|
2844
|
+
let unchecked = false;
|
|
2845
|
+
if (sub === "allow") {
|
|
2846
|
+
const c = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
2847
|
+
const got = await Promise.race([c.connect().then(() => true, () => false), new Promise((r) => setTimeout(() => r(false), 3_000))]);
|
|
2848
|
+
if (got) {
|
|
2849
|
+
const names = c.members.map((m) => m.member);
|
|
2850
|
+
c.close();
|
|
2851
|
+
if (!names.includes(who))
|
|
2852
|
+
die(`"${who}" is not in "${cfg.space}". Its members: ${names.filter((n) => n !== cfg.member).join(", ") || "only you"}`);
|
|
2853
|
+
}
|
|
2854
|
+
else {
|
|
2855
|
+
try {
|
|
2856
|
+
c.close();
|
|
2857
|
+
}
|
|
2858
|
+
catch { }
|
|
2859
|
+
unchecked = true;
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
const r = setLocalWaker([cfg.spaceId, cfg.space], who, sub === "allow");
|
|
2863
|
+
if (!r.ok)
|
|
2864
|
+
die(r.why);
|
|
2865
|
+
const mode = wakeMode(loadProfile(cfg));
|
|
2866
|
+
console.log(sub === "allow"
|
|
2867
|
+
? `${C(who)} may wake this machine in ${cfg.space}${r.changed ? "" : D(" (already could)")}`
|
|
2868
|
+
: `${C(who)} can no longer wake this machine in ${cfg.space}${r.changed ? "" : D(" (could not before either)")}`);
|
|
2869
|
+
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}` : ""}`));
|
|
2870
|
+
if (unchecked)
|
|
2871
|
+
console.log(D(` The relay did not answer, so "${who}" was not checked against the team's members — check the spelling (pantheon team).`));
|
|
2872
|
+
if (sub === "allow") {
|
|
2873
|
+
if (mode === "anyone")
|
|
2874
|
+
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).`));
|
|
2875
|
+
if (mode === "no")
|
|
2876
|
+
console.log(D(` This machine is set not to be woken at all; the list applies once you choose "approved" (pantheon setup).`));
|
|
2877
|
+
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)}`));
|
|
2878
|
+
}
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
if (sub === "list") {
|
|
2882
|
+
const pol = loadPolicy();
|
|
2883
|
+
const f = loadAll();
|
|
2884
|
+
const teams = flag("space") ? [one()] : f && Object.keys(f.spaces ?? {}).length ? Object.values(f.spaces ?? {}).map((m) => configFor(f, m, null)) : [need()];
|
|
2885
|
+
console.log(`${B("Who may wake this machine")} ${D("(this machine's own list — pantheon wake allow|deny <name>)")}\n`);
|
|
2886
|
+
for (const t of teams) {
|
|
2887
|
+
const names = [...new Set([...(t.spaceId ? pol.wakers?.[t.spaceId] ?? [] : []), ...(pol.wakers?.[t.space] ?? [])])].sort();
|
|
2888
|
+
const key = t.spaceId ?? t.space;
|
|
2889
|
+
const carried = !!(pol.wakersCarried?.[key] || pol.wakersCarried?.[t.space]);
|
|
2890
|
+
const deferred = !!(pol.wakersDeferred?.[key] || pol.wakersDeferred?.[t.space]);
|
|
2891
|
+
const mem = Object.values(f?.spaces ?? {}).find((m) => m.id === t.spaceId || m.name === t.space);
|
|
2892
|
+
const pending = !carried && (deferred || (!!mem && !mem.since));
|
|
2893
|
+
const why = deferred
|
|
2894
|
+
? "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"
|
|
2895
|
+
: "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";
|
|
2896
|
+
console.log(` ${t.space.padEnd(16)} ${names.length ? names.join(", ") : D("nobody")}${pending ? D(` (${why})`) : ""}`);
|
|
2897
|
+
}
|
|
2898
|
+
const mode = wakeMode(loadProfile(teams[0]));
|
|
2899
|
+
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)")}`);
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2902
|
+
{
|
|
2903
|
+
const near = ["allow", "deny", "list"].map((k) => ({ k, d: slipDistance(sub, k) })).sort((x, y) => x.d - y.d)[0];
|
|
2904
|
+
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`);
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2411
2907
|
case "policy": {
|
|
2412
|
-
const { loadPolicy, savePolicy, grant, revoke } = await import("./worker-policy.js");
|
|
2908
|
+
const { loadPolicy, savePolicy, grant, revoke, inWokenRun, CAPABILITY_ARGS } = await import("./worker-policy.js");
|
|
2909
|
+
const { COMMAND_GRANT_SCOPE } = await import("./agents.js");
|
|
2413
2910
|
const sub = argv[1];
|
|
2414
2911
|
const capArg = (argv[2] ?? "").replace(/-/g, "_");
|
|
2912
|
+
const caps = Object.keys(CAPABILITY_ARGS);
|
|
2415
2913
|
if (sub === "grant" || sub === "revoke") {
|
|
2416
|
-
|
|
2417
|
-
|
|
2914
|
+
const cap = caps.find((c) => c === capArg) ?? die(`unknown capability "${argv[2] ?? ""}" — capabilities: ${caps.map((c) => CAPABILITY_ARGS[c]).join(", ")}`);
|
|
2915
|
+
if (sub === "grant" && inWokenRun())
|
|
2916
|
+
die("a woken run cannot grant this machine anything — a person has to run this here");
|
|
2418
2917
|
const p = loadPolicy();
|
|
2419
|
-
if (sub === "grant")
|
|
2420
|
-
grant(p,
|
|
2421
|
-
}
|
|
2918
|
+
if (sub === "grant")
|
|
2919
|
+
grant(p, cap, "always", "policy-command");
|
|
2422
2920
|
else
|
|
2423
|
-
revoke(p,
|
|
2921
|
+
revoke(p, cap);
|
|
2424
2922
|
savePolicy(p);
|
|
2425
|
-
|
|
2923
|
+
const said = cap === "self_upgrade"
|
|
2924
|
+
? (sub === "grant" ? "— the worker keeps pantheon current with the relay" : "— the worker will ask in your approvals inbox when it falls behind")
|
|
2925
|
+
: (sub === "grant" ? `— ${COMMAND_GRANT_SCOPE}` : "— woken runs are back to edits only");
|
|
2926
|
+
console.log(`${C(sub === "grant" ? "granted" : "revoked")} ${CAPABILITY_ARGS[cap]} ${D(said)}`);
|
|
2426
2927
|
return;
|
|
2427
2928
|
}
|
|
2428
2929
|
if (sub && sub !== "show")
|
|
2429
|
-
die(
|
|
2930
|
+
die(`usage: pantheon policy [show | grant <capability> | revoke <capability>] capabilities: ${caps.map((c) => CAPABILITY_ARGS[c]).join(", ")}`);
|
|
2430
2931
|
const p = loadPolicy();
|
|
2431
2932
|
const w = workerProc();
|
|
2432
2933
|
console.log(`${B("What this machine's worker may do")}\n`);
|
|
2433
2934
|
console.log(` worker ${w.running ? C("running") : D("not running")}`);
|
|
2434
2935
|
const su = p.grants.self_upgrade;
|
|
2435
2936
|
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
|
-
|
|
2937
|
+
const rc = p.grants.run_commands;
|
|
2938
|
+
const { commandState, repoCommandsAtBase } = await import("./command-state.js");
|
|
2939
|
+
const cs = commandState({ project: resolve(".") });
|
|
2940
|
+
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")}`);
|
|
2941
|
+
for (const cap of caps)
|
|
2942
|
+
if (p.asks[cap])
|
|
2943
|
+
console.log(` ${"\x1b[33mopen request\x1b[0m"} ${CAPABILITY_ARGS[cap]} — answer it here: pantheon approvals, or pantheon policy grant ${CAPABILITY_ARGS[cap]}`);
|
|
2439
2944
|
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(`
|
|
2945
|
+
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")}`);
|
|
2946
|
+
console.log(` commands ${cs.level === "off" ? D(cs.summary) : C(cs.summary)}`);
|
|
2947
|
+
if (cs.level !== "off")
|
|
2948
|
+
console.log(` ${D(COMMAND_GRANT_SCOPE)}`);
|
|
2949
|
+
{
|
|
2950
|
+
const { REPO_COMMANDS_FILE } = await import("./agents.js");
|
|
2951
|
+
const root = repoInfo(process.cwd())?.root;
|
|
2952
|
+
const rc2 = root ? repoCommandsAtBase(root) : { ref: null, ok: [], refused: [] };
|
|
2953
|
+
const at = rc2.ref ? D(` (as committed on ${rc2.ref})`) : "";
|
|
2954
|
+
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")}`);
|
|
2955
|
+
for (const r of rc2.refused)
|
|
2956
|
+
console.log(` ${D(`ignored "${r.line}" — ${r.why}`)}`);
|
|
2957
|
+
}
|
|
2442
2958
|
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 ·
|
|
2959
|
+
console.log(`\n${D("Change it: pantheon policy grant|revoke self-upgrade|run-commands · personas: pantheon persona init · who may wake this machine: pantheon wake allow|deny <who>")}`);
|
|
2444
2960
|
return;
|
|
2445
2961
|
}
|
|
2446
2962
|
case "explain": {
|
|
@@ -2448,13 +2964,36 @@ async function main() {
|
|
|
2448
2964
|
const CONFIG_DIR = dirname(CONFIG_PATH);
|
|
2449
2965
|
let hud = null;
|
|
2450
2966
|
if (argv.includes("--queued")) {
|
|
2967
|
+
const refuse = (why) => { console.log(`not sent: ${why}`); process.exit(1); };
|
|
2968
|
+
if (inWokenRun())
|
|
2969
|
+
refuse("a woken run cannot act for the menu bar — a person has to click it");
|
|
2451
2970
|
const f = join(CONFIG_DIR, "bin", ".hud-action.json");
|
|
2971
|
+
const { statSync } = await import("node:fs");
|
|
2972
|
+
let st = null;
|
|
2973
|
+
let raw = null;
|
|
2452
2974
|
try {
|
|
2453
|
-
|
|
2975
|
+
st = statSync(f);
|
|
2976
|
+
raw = readFileSync(f, "utf8");
|
|
2454
2977
|
rmSync(f, { force: true });
|
|
2455
2978
|
}
|
|
2456
2979
|
catch {
|
|
2457
|
-
|
|
2980
|
+
st = null;
|
|
2981
|
+
raw = null;
|
|
2982
|
+
}
|
|
2983
|
+
if (st && raw !== null) {
|
|
2984
|
+
const age = Date.now() - st.mtimeMs;
|
|
2985
|
+
const { runInFlight } = await import("./worker.js");
|
|
2986
|
+
const run = runInFlight(CONFIG_DIR);
|
|
2987
|
+
if (age > HUD_ACTION_MAX_AGE_MS)
|
|
2988
|
+
refuse(`that menu-bar action was written ${Math.round(age / 1000)}s ago — click it again`);
|
|
2989
|
+
if (run && st.mtimeMs >= run.at - 1_000)
|
|
2990
|
+
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");
|
|
2991
|
+
try {
|
|
2992
|
+
hud = JSON.parse(raw);
|
|
2993
|
+
}
|
|
2994
|
+
catch {
|
|
2995
|
+
hud = null;
|
|
2996
|
+
}
|
|
2458
2997
|
}
|
|
2459
2998
|
}
|
|
2460
2999
|
const cfg = (() => {
|
|
@@ -2506,7 +3045,7 @@ async function main() {
|
|
|
2506
3045
|
return;
|
|
2507
3046
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
2508
3047
|
try {
|
|
2509
|
-
const answer = (await
|
|
3048
|
+
const answer = ((await questionOrNull(rl, `\n${D("run these here now? [y/N] ")}`)) ?? "").toLowerCase();
|
|
2510
3049
|
if (answer !== "y" && answer !== "yes") {
|
|
2511
3050
|
console.error(D("left for you — the commands are above"));
|
|
2512
3051
|
return;
|
|
@@ -2565,6 +3104,12 @@ async function main() {
|
|
|
2565
3104
|
.catch((e) => notSent(e.message));
|
|
2566
3105
|
c2.close();
|
|
2567
3106
|
console.log(`sent ${kind} id=${sent.id} seq=${sent.seq}`);
|
|
3107
|
+
if (kind === "ANSWER") {
|
|
3108
|
+
const { applyLocalAnswer, describeLocalAnswer } = await import("./worker-policy.js");
|
|
3109
|
+
const g = applyLocalAnswer(ref, body);
|
|
3110
|
+
if (g)
|
|
3111
|
+
console.error(describeLocalAnswer(g));
|
|
3112
|
+
}
|
|
2568
3113
|
return;
|
|
2569
3114
|
}
|
|
2570
3115
|
if (hud?.action === "retract") {
|
|
@@ -2633,7 +3178,7 @@ async function main() {
|
|
|
2633
3178
|
const agent = pickExplainAgent(hud.tool ?? p0.agent.primary_surface, detectAvailable());
|
|
2634
3179
|
const goalId = a?.goal ?? o?.goal ?? null;
|
|
2635
3180
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
2636
|
-
const ask = (q) =>
|
|
3181
|
+
const ask = async (q) => (await questionOrNull(rl, q)) ?? "";
|
|
2637
3182
|
try {
|
|
2638
3183
|
for (;;) {
|
|
2639
3184
|
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 +3220,12 @@ async function main() {
|
|
|
2675
3220
|
await c3.connect();
|
|
2676
3221
|
const sent = await c3.send({ act: kind, body: text, goal: o ? null : goalId, ref, to });
|
|
2677
3222
|
console.error(` ${C("sent")} ${kind} id=${sent.id}`);
|
|
3223
|
+
if (kind === "ANSWER") {
|
|
3224
|
+
const { applyLocalAnswer, describeLocalAnswer } = await import("./worker-policy.js");
|
|
3225
|
+
const g = applyLocalAnswer(ref, text);
|
|
3226
|
+
if (g)
|
|
3227
|
+
console.error(` ${describeLocalAnswer(g)}`);
|
|
3228
|
+
}
|
|
2678
3229
|
}
|
|
2679
3230
|
catch (e) {
|
|
2680
3231
|
console.error(` ${e.message}`);
|
|
@@ -2777,6 +3328,10 @@ async function main() {
|
|
|
2777
3328
|
const sent = await hit.client.send({ act: "ANSWER", body: answer, goal: null, ref: hit.o.ref, after_seq: hit.client.readSeq, ...(summary ? { summary } : {}) })
|
|
2778
3329
|
.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
3330
|
console.log(`${C("answered")} ${id}${conns.length > 1 ? ` in ${B(hit.cfg.space)}` : ""} — ledger act ${sent.id}`);
|
|
3331
|
+
const { applyLocalAnswer, describeLocalAnswer } = await import("./worker-policy.js");
|
|
3332
|
+
const g = applyLocalAnswer(hit.o.ref, answer);
|
|
3333
|
+
if (g)
|
|
3334
|
+
console.log(` ${describeLocalAnswer(g)}`);
|
|
2780
3335
|
closeAll();
|
|
2781
3336
|
return;
|
|
2782
3337
|
}
|
|
@@ -2840,8 +3395,9 @@ async function main() {
|
|
|
2840
3395
|
return;
|
|
2841
3396
|
}
|
|
2842
3397
|
case "worker": {
|
|
3398
|
+
(await import("./machine.js")).recordWorkerPid();
|
|
2843
3399
|
const allCfg = loadAll();
|
|
2844
|
-
const bindings = Object.
|
|
3400
|
+
const bindings = Object.entries(allCfg?.bindings ?? {}).filter(([k]) => !k.startsWith("d_")).map(([, b]) => b);
|
|
2845
3401
|
if (!flag("project") && bindings.length) {
|
|
2846
3402
|
const { WorkerFleet } = await import("./worker-fleet.js");
|
|
2847
3403
|
const fleet = new WorkerFleet({
|
|
@@ -2865,7 +3421,7 @@ async function main() {
|
|
|
2865
3421
|
console.error(`${B("pantheon worker")} ${D(`(${bindings.length} bound repo${bindings.length === 1 ? "" : "s"} · ctrl-c to stop)`)}`);
|
|
2866
3422
|
if (argv.includes("--allow-bash")) {
|
|
2867
3423
|
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
|
|
3424
|
+
console.error(`\x1b[33m teammate approved to wake this machine (pantheon wake list), since their request is what starts the run.\x1b[0m`);
|
|
2869
3425
|
}
|
|
2870
3426
|
process.on("SIGINT", () => { fleet.stop(); process.exit(0); });
|
|
2871
3427
|
await fleet.run();
|
|
@@ -2896,7 +3452,7 @@ async function main() {
|
|
|
2896
3452
|
console.error(`${B("pantheon worker")} ${D("(ctrl-c to stop)")}`);
|
|
2897
3453
|
if (argv.includes("--allow-bash")) {
|
|
2898
3454
|
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
|
|
3455
|
+
console.error(`\x1b[33m teammate approved to wake this machine (pantheon wake list), since their request is what starts the run.\x1b[0m`);
|
|
2900
3456
|
}
|
|
2901
3457
|
process.on("SIGINT", () => { worker.stop(); process.exit(0); });
|
|
2902
3458
|
await worker.run();
|
|
@@ -3044,7 +3600,7 @@ async function main() {
|
|
|
3044
3600
|
}
|
|
3045
3601
|
for (const l of installService(kind, homedir(), extraN))
|
|
3046
3602
|
console.error(l);
|
|
3047
|
-
console.error(`\n${D("Clicking an alert opens the primary surface in `pantheon profile`. No agent run or
|
|
3603
|
+
console.error(`\n${D("Clicking an alert opens the primary surface in `pantheon profile`. No agent run or wake permission is involved.")}`);
|
|
3048
3604
|
return;
|
|
3049
3605
|
}
|
|
3050
3606
|
need();
|
|
@@ -3062,7 +3618,7 @@ async function main() {
|
|
|
3062
3618
|
console.error(D(` project ${project}`));
|
|
3063
3619
|
for (const l of installService(kind, project, extra))
|
|
3064
3620
|
console.error(l);
|
|
3065
|
-
console.error(`\n${D("It only acts on requests from
|
|
3621
|
+
console.error(`\n${D("It only acts on requests from teammates approved on this machine (pantheon wake allow <name>), one run at a time.")}`);
|
|
3066
3622
|
return;
|
|
3067
3623
|
}
|
|
3068
3624
|
case "spaces": {
|
|
@@ -3079,7 +3635,7 @@ async function main() {
|
|
|
3079
3635
|
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
3636
|
console.error(` ${B(m.name.padEnd(18))} ${D(new URL(m.relay).host)} ${tags}`);
|
|
3081
3637
|
for (const b of bound)
|
|
3082
|
-
console.error(` ${D("repo")} ${b.origin ?? b.path}${b.origin ? D(` (${b.path})`) : ""}`);
|
|
3638
|
+
console.error(` ${D(b.origin === null && !existsSync(join(b.path, ".git")) ? "folder" : "repo")} ${b.origin ?? b.path}${b.origin ? D(` (${b.path})`) : ""}`);
|
|
3083
3639
|
if (!bound.length)
|
|
3084
3640
|
console.error(` ${D("repo none bound — run `pantheon use " + m.name + " --here` from its checkout")}`);
|
|
3085
3641
|
}
|
|
@@ -3089,10 +3645,45 @@ async function main() {
|
|
|
3089
3645
|
}
|
|
3090
3646
|
case "use": {
|
|
3091
3647
|
const all = loadAll() ?? die("not configured. Run `pantheon signup` first.");
|
|
3092
|
-
|
|
3648
|
+
if (argv.includes("--unbind")) {
|
|
3649
|
+
const { folderBinding } = await import("./config.js");
|
|
3650
|
+
const r = repoInfo(process.cwd());
|
|
3651
|
+
const fb = r ? null : folderBinding(all, process.cwd());
|
|
3652
|
+
const key = r ? r.fingerprint : fb && fb.path === realpathSync.native(process.cwd()) ? fb.key : null;
|
|
3653
|
+
if (fb && !key)
|
|
3654
|
+
die(`this folder is not bound itself — ${fb.path} is, and this folder is inside it. Run pantheon use --unbind there.`);
|
|
3655
|
+
const was = key ? all.bindings?.[key] : undefined;
|
|
3656
|
+
if (!key || !was || !unbindRepo(key))
|
|
3657
|
+
die(`${r ? "this repo" : "this folder"} is not bound to a team — nothing to undo`);
|
|
3658
|
+
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`);
|
|
3659
|
+
return;
|
|
3660
|
+
}
|
|
3661
|
+
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
3662
|
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
3663
|
if (argv.includes("--here")) {
|
|
3095
|
-
const r = repoInfo(process.cwd())
|
|
3664
|
+
const r = repoInfo(process.cwd());
|
|
3665
|
+
if (!r) {
|
|
3666
|
+
for (let d = resolve(process.cwd());; d = dirname(d)) {
|
|
3667
|
+
if (existsSync(join(d, ".git")))
|
|
3668
|
+
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.`);
|
|
3669
|
+
if (dirname(d) === d)
|
|
3670
|
+
break;
|
|
3671
|
+
}
|
|
3672
|
+
const real = (d) => { try {
|
|
3673
|
+
return realpathSync.native(d);
|
|
3674
|
+
}
|
|
3675
|
+
catch {
|
|
3676
|
+
return resolve(d);
|
|
3677
|
+
} };
|
|
3678
|
+
const here = real(process.cwd());
|
|
3679
|
+
if ((here === real(homedir()) || dirname(here) === here) && !argv.includes("--yes")) {
|
|
3680
|
+
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`);
|
|
3681
|
+
}
|
|
3682
|
+
const { replaced } = bindFolder(process.cwd(), m.id);
|
|
3683
|
+
console.error(`${B("bound")} this folder (not a git repo) → "${m.name}"${replaced ? D(` (was: ${findMembership(all, replaced.space)?.name ?? replaced.space})`) : ""}`);
|
|
3684
|
+
console.error(D(` Agents opened here, or below here, now speak as you in "${m.name}". Delivering code needs a git repo. Restart any that are open.`));
|
|
3685
|
+
return;
|
|
3686
|
+
}
|
|
3096
3687
|
const { replaced } = bindRepo(r, m.id);
|
|
3097
3688
|
console.error(`${B("bound")} ${r.origin ?? r.root} → "${m.name}"${replaced ? D(` (was: ${findMembership(all, replaced.space)?.name ?? replaced.space})`) : ""}`);
|
|
3098
3689
|
if (!r.shared)
|
|
@@ -3154,12 +3745,26 @@ async function main() {
|
|
|
3154
3745
|
case "version":
|
|
3155
3746
|
console.log(VERSION);
|
|
3156
3747
|
return;
|
|
3157
|
-
case "help":
|
|
3158
|
-
|
|
3748
|
+
case "help": {
|
|
3749
|
+
const topic = argv[1] && argv[1] !== "all" && !argv[1].startsWith("-") ? argv[1] : null;
|
|
3750
|
+
if (topic && (usageFor(topic, HELP_ALL) || COMMAND_FLAGS[topic])) {
|
|
3751
|
+
console.log(`${usageFor(topic, HELP_ALL) ?? ` pantheon ${topic}`}${COMMAND_FLAGS[topic] ? `\n\n${B("flags")}\n ${COMMAND_FLAGS[topic]}` : ""}`);
|
|
3752
|
+
return;
|
|
3753
|
+
}
|
|
3754
|
+
console.log(argv.includes("--all") || argv[1] === "all" ? HELP_ALL : HELP);
|
|
3755
|
+
return;
|
|
3756
|
+
}
|
|
3757
|
+
case "--help":
|
|
3758
|
+
case "-h":
|
|
3759
|
+
console.log(argv.includes("--all") ? HELP_ALL : HELP);
|
|
3159
3760
|
return;
|
|
3160
|
-
default:
|
|
3161
|
-
|
|
3162
|
-
|
|
3761
|
+
default: {
|
|
3762
|
+
const known = [...new Set([...HELP_ALL.matchAll(/pantheon ([a-z][a-z-]*)/g)].map((m) => m[1]))];
|
|
3763
|
+
const near = known.map((k) => ({ k, d: slipDistance(cmd, k) })).sort((x, y) => x.d - y.d)[0];
|
|
3764
|
+
console.error(`unknown command "${cmd}"${near && near.d <= 2 ? ` — did you mean: pantheon ${near.k}?` : ""}`);
|
|
3765
|
+
console.error(D("all commands: pantheon help · everything: pantheon help --all"));
|
|
3766
|
+
process.exit(1);
|
|
3767
|
+
}
|
|
3163
3768
|
}
|
|
3164
3769
|
}
|
|
3165
3770
|
main().catch((e) => die(describeNetError(e, (() => { try {
|