@join-pantheon/cli 0.31.1 → 0.31.2-rc.1
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 +4 -2
- package/dist/agent-outage.js +87 -0
- package/dist/agents.js +59 -20
- package/dist/claude-confine.js +21 -4
- package/dist/cli.js +254 -53
- package/dist/client.js +3 -3
- package/dist/config.js +1 -0
- package/dist/dbus.js +418 -0
- package/dist/decision-ui.js +89 -0
- package/dist/doctor.js +3 -3
- package/dist/email.js +21 -0
- package/dist/file-lock.js +3 -3
- package/dist/git-safe.js +274 -0
- package/dist/hooks-install.js +6 -1
- package/dist/install.js +21 -0
- package/dist/kiro-files.js +20 -4
- package/dist/mcp-instructions.js +2 -2
- package/dist/nonces.js +5 -3
- package/dist/notice.js +2 -1
- package/dist/notifier.js +1 -1
- package/dist/onboard-defaults.js +18 -0
- package/dist/persona.js +28 -2
- package/dist/plans.js +12 -3
- package/dist/profile.js +23 -5
- package/dist/relay.js +59 -16
- package/dist/roster-file.js +3 -0
- package/dist/run-failure.js +2 -1
- package/dist/safe-write.js +55 -0
- package/dist/setup-guide.js +9 -1
- package/dist/spend.js +65 -5
- package/dist/tray.js +11 -3
- package/dist/worker.js +890 -280
- package/helper/macos/menubar.swift +198 -28
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ process.emitWarning = ((warning, ...rest) => {
|
|
|
7
7
|
return _emitWarning(warning, ...rest);
|
|
8
8
|
});
|
|
9
9
|
import { randomBytes } from "node:crypto";
|
|
10
|
+
import { gitConfigValue, suggestUsername } from "./onboard-defaults.js";
|
|
10
11
|
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
11
12
|
import { basename, dirname, join, resolve } from "node:path";
|
|
12
13
|
import { PantheonClient } from "./client.js";
|
|
@@ -14,13 +15,13 @@ import { agentSpawn, CmdLineUnsafe } from "./which.js";
|
|
|
14
15
|
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
16
|
import { fmtLimit } from "./plans.js";
|
|
16
17
|
import { validEmail, validUsername } from "./accounts.js";
|
|
17
|
-
import { install, launcher, installForUser } from "./install.js";
|
|
18
|
+
import { install, launcher, installForUser, windowsRepoOverrides } from "./install.js";
|
|
18
19
|
import { Relay } from "./relay.js";
|
|
19
20
|
import { runMcp } from "./mcp.js";
|
|
20
21
|
import { Worker } from "./worker.js";
|
|
21
22
|
import { DEFAULT_REMINDER_MS, ensureNativeNotifier, HumanNotifier, runNotifiers, sendDesktopNotification } from "./notifier.js";
|
|
22
23
|
import { desktopUnavailable, hostEnv } from "./host-env.js";
|
|
23
|
-
import { AGENT_SURFACES, applyProfileInit, cleanList, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile, wakeDefault, wakeMode } from "./profile.js";
|
|
24
|
+
import { AGENT_SURFACES, applyProfileInit, cleanList, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile, teamProfile, wakeDefault, wakeMode } from "./profile.js";
|
|
24
25
|
import { runGuide, terminalIO, questionOrNull, parseSetupFlags, questionnaire, optOutsFrom } from "./setup-guide.js";
|
|
25
26
|
import { selectActs, formatAct, resolveRef } from "./history-view.js";
|
|
26
27
|
import { doctor, render, shareBlock } from "./doctor.js";
|
|
@@ -69,7 +70,7 @@ async function signedFetch(cfg, method, path, params = {}) {
|
|
|
69
70
|
}
|
|
70
71
|
return res;
|
|
71
72
|
}
|
|
72
|
-
function requireSignInOnHostedRelay(relayFlag) {
|
|
73
|
+
async function requireSignInOnHostedRelay(relayFlag) {
|
|
73
74
|
const cfg = identity();
|
|
74
75
|
if (cfg?.account)
|
|
75
76
|
return;
|
|
@@ -77,6 +78,13 @@ function requireSignInOnHostedRelay(relayFlag) {
|
|
|
77
78
|
const hosted = relay === DEFAULT_RELAY && !relayFlag && !process.env.PANTHEON_RELAY;
|
|
78
79
|
if (!hosted)
|
|
79
80
|
return;
|
|
81
|
+
if (process.stdin.isTTY) {
|
|
82
|
+
if (await interactiveOnboard(relay, { starting: true }) && identity()?.account) {
|
|
83
|
+
console.error("");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
die("not signed in yet.\n When the code arrives: pantheon verify <code>\n Then run pantheon start again.");
|
|
87
|
+
}
|
|
80
88
|
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.`);
|
|
81
89
|
}
|
|
82
90
|
async function requireSignInOnAccountsRelay(relayFlag) {
|
|
@@ -91,17 +99,24 @@ async function requireSignInOnAccountsRelay(relayFlag) {
|
|
|
91
99
|
catch {
|
|
92
100
|
return;
|
|
93
101
|
}
|
|
102
|
+
if (hasAccounts && process.stdin.isTTY) {
|
|
103
|
+
if (await interactiveOnboard(relayFlag, { starting: true }) && identity()?.account) {
|
|
104
|
+
console.error("");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
die("not signed in yet.\n When the code arrives: pantheon verify <code>\n Then run this same pantheon start again.");
|
|
108
|
+
}
|
|
94
109
|
if (hasAccounts)
|
|
95
110
|
die(`sign in first — ${relayFlag} verifies email.${signInHint("NOT_SIGNED_IN", relayFlag)}\n Then run this same pantheon start again.`);
|
|
96
111
|
}
|
|
97
|
-
async function interactiveOnboard(relayArg) {
|
|
112
|
+
async function interactiveOnboard(relayArg, opts = {}) {
|
|
98
113
|
const prior = identity();
|
|
99
114
|
const relay = relayArg ?? prior?.relay ?? process.env.PANTHEON_RELAY ?? DEFAULT_RELAY;
|
|
100
115
|
const base = relay.replace(/^ws/, "http").replace(/\/+$/, "");
|
|
101
116
|
if (!process.stdin.isTTY) {
|
|
102
117
|
console.error(`${B("pantheon")} — sign in to get started:`);
|
|
103
118
|
console.error(` ${C("pantheon signup --email you@work.com --username you")} then ${C("pantheon verify <code>")}`);
|
|
104
|
-
return;
|
|
119
|
+
return false;
|
|
105
120
|
}
|
|
106
121
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
107
122
|
const ask = async (q) => {
|
|
@@ -117,10 +132,16 @@ async function interactiveOnboard(relayArg) {
|
|
|
117
132
|
const postJson = async (path, body) => fetch(`${base}${path}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })
|
|
118
133
|
.then(async (x) => ({ status: x.status, j: await x.json() }));
|
|
119
134
|
try {
|
|
120
|
-
console.error(
|
|
135
|
+
console.error(opts.joining
|
|
136
|
+
? `${B("Welcome to Pantheon.")} You've been invited to "${opts.joining}". First, sign this machine in — your email and a 6-digit code, nothing else.\n`
|
|
137
|
+
: opts.starting
|
|
138
|
+
? `${B("Welcome to Pantheon.")} First, sign this machine in — your email and a 6-digit code, nothing else. Then your team is created.\n`
|
|
139
|
+
: `${B("Welcome to pantheon.")} Let's get this machine signed in.\n`);
|
|
140
|
+
const gitEmail = gitConfigValue("user.email");
|
|
141
|
+
const emailGuess = validEmail(gitEmail) ? gitEmail : "";
|
|
121
142
|
let email = "";
|
|
122
143
|
while (!email) {
|
|
123
|
-
const e = await ask(
|
|
144
|
+
const e = (await ask(` Your work email${emailGuess ? ` [${emailGuess}]` : ""}: `)) || emailGuess;
|
|
124
145
|
if (validEmail(e))
|
|
125
146
|
email = e;
|
|
126
147
|
else
|
|
@@ -129,8 +150,9 @@ async function interactiveOnboard(relayArg) {
|
|
|
129
150
|
let started = null;
|
|
130
151
|
let mode = "signup";
|
|
131
152
|
let signupUsername = "";
|
|
153
|
+
let userGuess = suggestUsername(email);
|
|
132
154
|
while (!started || started.status !== 200) {
|
|
133
|
-
const u = await ask(
|
|
155
|
+
const u = (await ask(` Pick a username${userGuess ? ` [${userGuess}]` : ""}: `)) || userGuess;
|
|
134
156
|
if (!validUsername(u.toLowerCase())) {
|
|
135
157
|
console.error(D(" 2–32 chars: letters, digits, . _ - (not at the ends)"));
|
|
136
158
|
continue;
|
|
@@ -138,25 +160,28 @@ async function interactiveOnboard(relayArg) {
|
|
|
138
160
|
signupUsername = u;
|
|
139
161
|
started = await postJson("/auth/signup", { email, username: u, pubkey: kp.publicKey, label }).catch((e) => { console.error(D(` could not reach the relay: ${e.message}`)); return null; });
|
|
140
162
|
if (!started)
|
|
141
|
-
return;
|
|
163
|
+
return false;
|
|
142
164
|
if (started.status === 404) {
|
|
143
165
|
console.error(`\n ${D("This relay is invite-based (no email accounts). Get started with:")}`);
|
|
144
166
|
console.error(` ${C("pantheon start")} ${D("(create a team)")} or ${C("pantheon join <invite>")}`);
|
|
145
|
-
return;
|
|
167
|
+
return false;
|
|
146
168
|
}
|
|
147
169
|
if (started.status === 200)
|
|
148
170
|
break;
|
|
149
171
|
if (started.j.reason === "email_taken") {
|
|
150
172
|
console.error(D(` that email already has an account — signing this device in instead…`));
|
|
151
|
-
started = await postJson("/auth/login", { email, pubkey: kp.publicKey, label });
|
|
173
|
+
started = await postJson("/auth/login", { email, pubkey: kp.publicKey, label }).catch((e) => { console.error(D(` could not reach the relay: ${e.message}`)); return null; });
|
|
174
|
+
if (!started)
|
|
175
|
+
return false;
|
|
152
176
|
mode = "login";
|
|
153
177
|
break;
|
|
154
178
|
}
|
|
155
179
|
console.error(D(` ${started.j.error ?? "try a different username"}`));
|
|
180
|
+
userGuess = suggestUsername(email, Math.floor(10 + Math.random() * 90));
|
|
156
181
|
}
|
|
157
182
|
if (!started || started.status !== 200) {
|
|
158
183
|
console.error(D(` ${started?.j.error ?? "sign-up failed"}`));
|
|
159
|
-
return;
|
|
184
|
+
return false;
|
|
160
185
|
}
|
|
161
186
|
const authUser = mode === "signup" ? signupUsername : undefined;
|
|
162
187
|
save({ ...(prior ?? {}), relay, member: signupUsername || prior?.member || "me",
|
|
@@ -182,7 +207,7 @@ async function interactiveOnboard(relayArg) {
|
|
|
182
207
|
console.error(D(` could not resend: ${rs?.j.error ?? "the relay did not send an email"} — try again in a minute.`));
|
|
183
208
|
continue;
|
|
184
209
|
}
|
|
185
|
-
const v = await postJson("/auth/verify", { email, code, pubkey: kp.publicKey });
|
|
210
|
+
const v = await postJson("/auth/verify", { email, code, pubkey: kp.publicKey }).catch((e) => ({ status: 0, j: { error: `could not reach the relay (${e.message}) — try the same code again` } }));
|
|
186
211
|
if (v.status === 200) {
|
|
187
212
|
const acct = v.j.account;
|
|
188
213
|
const cfg = { ...(prior ?? {}), relay, member: acct.username, space: prior?.space ?? "", token: prior?.token ?? "",
|
|
@@ -191,14 +216,17 @@ async function interactiveOnboard(relayArg) {
|
|
|
191
216
|
delete cfg.pendingAuth;
|
|
192
217
|
save(cfg);
|
|
193
218
|
console.error(`\n ${C("You're in")} — signed in as ${B(acct.username)} <${acct.email}> on this machine.\n`);
|
|
219
|
+
if (opts.joining || opts.starting)
|
|
220
|
+
return true;
|
|
194
221
|
console.error(` ${B("Next:")} ${C("pantheon start")} ${D("(create a team, get a link to send)")}`);
|
|
195
222
|
console.error(` ${C("pantheon join <invite-link>")} ${D("(join a teammate's team)")}`);
|
|
196
|
-
return;
|
|
223
|
+
return true;
|
|
197
224
|
}
|
|
198
225
|
const spent = v.j.reason === "expired" || v.j.reason === "too_many_attempts" || v.j.tries_left === 0;
|
|
199
226
|
console.error(D(` ${v.j.error ?? "that code is not right"}${spent ? " — type 'resend' for a new code" : " — try again"}`));
|
|
200
227
|
}
|
|
201
228
|
console.error(D("\n Too many tries. Run `pantheon` again to restart, or `pantheon verify <code>` once you have it."));
|
|
229
|
+
return false;
|
|
202
230
|
}
|
|
203
231
|
finally {
|
|
204
232
|
rl.close();
|
|
@@ -532,6 +560,20 @@ function printRelayNote(relay) {
|
|
|
532
560
|
console.error(D(`\n Your team's messages go through ${host}. Whoever runs it can read them (they are not end-to-end encrypted), so treat the team like a shared channel;`));
|
|
533
561
|
console.error(D(` on a relay from 0.31 on, it cannot write or change one in a teammate's name without your machine noticing: every message is signed by its sender. Your own relay: pantheon relay`));
|
|
534
562
|
}
|
|
563
|
+
function windowsOverridesForBoundRepos() {
|
|
564
|
+
const dirs = Object.values(loadAll()?.bindings ?? {}).map((b) => b.path).filter((p) => !!p && existsSync(p));
|
|
565
|
+
for (const r of windowsRepoOverrides(dirs))
|
|
566
|
+
console.error(` ${D("claude code in")} ${r.dir} ${D(r.status)}`);
|
|
567
|
+
}
|
|
568
|
+
function printJoinSteps(invite) {
|
|
569
|
+
console.error(`\n${B("For a teammate who'd rather not type commands, forward this whole block:")}\n`);
|
|
570
|
+
console.error(D(` You're invited to my Pantheon team. Paste these two lines into a terminal`));
|
|
571
|
+
console.error(D(` (Terminal on a Mac, Command Prompt on Windows), one at a time:`));
|
|
572
|
+
console.error(` ${C("npm i -g @join-pantheon/cli")}`);
|
|
573
|
+
console.error(` ${C(`pantheon join ${invite}`)}`);
|
|
574
|
+
console.error(D(` The second line asks for your email, sends you a 6-digit code, and sets up`));
|
|
575
|
+
console.error(D(` the coding agents on your computer. Needs Node.js 22.13 or newer: nodejs.org.`));
|
|
576
|
+
}
|
|
535
577
|
const HUD_ACTION_MAX_AGE_MS = 30_000;
|
|
536
578
|
function unattendedProject() {
|
|
537
579
|
if (argv.includes("--no-worker"))
|
|
@@ -644,6 +686,24 @@ async function readFailure(cfg, res, what) {
|
|
|
644
686
|
}
|
|
645
687
|
return new Error(`relay answered ${res.status} for ${what}${why ? `: ${why}` : ""}`);
|
|
646
688
|
}
|
|
689
|
+
async function decisionBriefFor(cfg, client, o) {
|
|
690
|
+
const { decisionBrief, decisionTitle, stripCeremony } = await import("./decision-ui.js");
|
|
691
|
+
const act = client.seenAct(o.ref) ?? (await fetchAct(cfg, o.ref, 6000).catch(() => null))?.act ?? null;
|
|
692
|
+
const g = o.goal ? client.goals.get(o.goal) : undefined;
|
|
693
|
+
const thread = o.goal ? await fetchHistory(cfg, { goal: o.goal }).catch(() => []) : [];
|
|
694
|
+
const recent = o.goal
|
|
695
|
+
? selectActs(thread, { goal: o.goal, last: 7 }).filter((a) => a.id !== o.ref).slice(-6)
|
|
696
|
+
.map((a) => ({ from: a.from, act: a.act, text: a.summary || a.body, at: a.ts }))
|
|
697
|
+
: [];
|
|
698
|
+
const from = stripCeremony(o.body).from ?? act?.from;
|
|
699
|
+
return decisionBrief({
|
|
700
|
+
team: cfg.space, decisionId: o.id, ref: o.ref, title: decisionTitle(o.body),
|
|
701
|
+
from: !from || from === cfg.member ? "your agent" : from,
|
|
702
|
+
question: act?.body ?? o.body,
|
|
703
|
+
goal: g ? { id: g.id, title: g.title, ...(g.acceptance ? { acceptance: g.acceptance } : {}) } : null,
|
|
704
|
+
recent,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
647
707
|
async function fetchAct(cfg, id, budgetMs = Number.POSITIVE_INFINITY) {
|
|
648
708
|
const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
|
|
649
709
|
const deadline = Date.now() + budgetMs;
|
|
@@ -1005,7 +1065,8 @@ async function main() {
|
|
|
1005
1065
|
console.error(`${B("pantheon")} — signed in as ${C(cfg.account.username)}. ${D(`Your plate: pantheon brief · all commands: pantheon help`)}`);
|
|
1006
1066
|
return;
|
|
1007
1067
|
}
|
|
1008
|
-
|
|
1068
|
+
await interactiveOnboard();
|
|
1069
|
+
return;
|
|
1009
1070
|
}
|
|
1010
1071
|
const askedHelpOnly = argv.length === 2 && (argv[1] === "--help" || argv[1] === "-h") && ["pending", "hook"].includes(cmd);
|
|
1011
1072
|
if (!HELP_OPAQUE.has(cmd) || askedHelpOnly) {
|
|
@@ -1076,7 +1137,7 @@ async function main() {
|
|
|
1076
1137
|
if (setupFlags.errors.length)
|
|
1077
1138
|
die(setupFlags.errors.join("\n") + "\n pantheon setup --questions lists every answer");
|
|
1078
1139
|
const wp = unattendedProject();
|
|
1079
|
-
requireSignInOnHostedRelay(flag("relay"));
|
|
1140
|
+
await requireSignInOnHostedRelay(flag("relay"));
|
|
1080
1141
|
await requireSignInOnAccountsRelay(flag("relay"));
|
|
1081
1142
|
console.error(`${B("pantheon start")}\n`);
|
|
1082
1143
|
if (!flag("space") && !repoInfo(process.cwd()))
|
|
@@ -1127,8 +1188,7 @@ async function main() {
|
|
|
1127
1188
|
console.error(r.inviteMayWake
|
|
1128
1189
|
? ` ${B("It also lets them wake workers:")} ${D("their requests can start an agent run on this machine while you are away (you chose wake \"anyone\"). Take it back: pantheon admit <name> --revoke")}`
|
|
1129
1190
|
: D(` It lets them join and talk to your agent. It does not let them wake this machine on its own: the first time their request needs it while you are away, you are asked (Always / Just this once / No). Approve ahead: pantheon wake allow <name>`));
|
|
1130
|
-
|
|
1131
|
-
console.error(` ${D("Tell your agent: read")} ${C("https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${link ?? r.invite}`)}`);
|
|
1191
|
+
printJoinSteps(link ?? r.invite);
|
|
1132
1192
|
}
|
|
1133
1193
|
if (!r.reach.shareable && r.reach.hint) {
|
|
1134
1194
|
console.error(`\n\x1b[33m That address only works on this machine.\x1b[0m`);
|
|
@@ -1137,6 +1197,7 @@ async function main() {
|
|
|
1137
1197
|
if (r.claimed) {
|
|
1138
1198
|
await bindReposHere(argv.includes("--yes"));
|
|
1139
1199
|
await workerAfterBinding(wp.how);
|
|
1200
|
+
windowsOverridesForBoundRepos();
|
|
1140
1201
|
}
|
|
1141
1202
|
printReady(r.tools);
|
|
1142
1203
|
if (r.claimed)
|
|
@@ -1187,8 +1248,9 @@ async function main() {
|
|
|
1187
1248
|
if (link)
|
|
1188
1249
|
console.error(`\n${D("Or send this — it unfurls in chat with the team's name, and pantheon join accepts it too:")}\n ${C(link)}`);
|
|
1189
1250
|
console.error(D(`\nSingle use, expires ${new Date(inv.expires_at).toLocaleString()}.`));
|
|
1190
|
-
|
|
1191
|
-
|
|
1251
|
+
if (inv.note)
|
|
1252
|
+
console.error(`\x1b[33m ! ${inv.note}\x1b[0m`);
|
|
1253
|
+
printJoinSteps(link ?? raw);
|
|
1192
1254
|
client.close();
|
|
1193
1255
|
return;
|
|
1194
1256
|
}
|
|
@@ -1299,12 +1361,18 @@ async function main() {
|
|
|
1299
1361
|
parsed = await resolveInviteCode(invite, candidates).catch((err) => die(err.message.replace(/^INVITE_CODE_[A-Z]+:\s*/, "")));
|
|
1300
1362
|
console.error(D(`That was just the code — it opens "${parsed.space}" on ${new URL(parsed.relay).host}. Joining that team.\n`));
|
|
1301
1363
|
}
|
|
1364
|
+
if (!identity()?.account && sameHost(parsed.relay, DEFAULT_RELAY) && process.stdin.isTTY) {
|
|
1365
|
+
const ok = await interactiveOnboard(parsed.relay, { joining: parsed.space });
|
|
1366
|
+
if (!ok || !identity()?.account)
|
|
1367
|
+
die(`not signed in yet — the invite is still good.\n When the code arrives: pantheon verify <code>\n Then run this same \`pantheon join\` command again.`);
|
|
1368
|
+
console.error("");
|
|
1369
|
+
}
|
|
1302
1370
|
if (!identity()?.account && sameHost(parsed.relay, DEFAULT_RELAY)) {
|
|
1303
1371
|
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 this same \`pantheon join\` command again — the invite is still good.`);
|
|
1304
1372
|
}
|
|
1305
1373
|
const wp = unattendedProject();
|
|
1306
1374
|
console.error(`${B("pantheon join")}\n`);
|
|
1307
|
-
const
|
|
1375
|
+
const joinOnce = () => qsJoin({
|
|
1308
1376
|
invite: parsed,
|
|
1309
1377
|
...(flag("me") ? { me: flag("me") } : {}),
|
|
1310
1378
|
...(argv.includes("--reset") ? { reset: true } : {}),
|
|
@@ -1314,6 +1382,15 @@ async function main() {
|
|
|
1314
1382
|
unattended: wp.project,
|
|
1315
1383
|
workerHow: wp.how,
|
|
1316
1384
|
log: (l) => console.error(l),
|
|
1385
|
+
});
|
|
1386
|
+
const r = await joinOnce().catch(async (e) => {
|
|
1387
|
+
if (/NOT_SIGNED_IN/.test(e.message) && process.stdin.isTTY && !identity()?.account) {
|
|
1388
|
+
if (await interactiveOnboard(parsed.relay, { joining: parsed.space }) && identity()?.account) {
|
|
1389
|
+
console.error("");
|
|
1390
|
+
return joinOnce();
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
throw e;
|
|
1317
1394
|
}).catch((e) => {
|
|
1318
1395
|
const m = e.message;
|
|
1319
1396
|
if (/BAD_INVITE/.test(m)) {
|
|
@@ -1371,6 +1448,7 @@ async function main() {
|
|
|
1371
1448
|
}
|
|
1372
1449
|
}
|
|
1373
1450
|
await workerAfterBinding(wp.how);
|
|
1451
|
+
windowsOverridesForBoundRepos();
|
|
1374
1452
|
{
|
|
1375
1453
|
const joined = load();
|
|
1376
1454
|
if (joined)
|
|
@@ -2673,8 +2751,10 @@ async function main() {
|
|
|
2673
2751
|
case "signup":
|
|
2674
2752
|
case "login": {
|
|
2675
2753
|
const purpose = cmd;
|
|
2676
|
-
if (!flag("email") && process.stdin.isTTY)
|
|
2677
|
-
|
|
2754
|
+
if (!flag("email") && process.stdin.isTTY) {
|
|
2755
|
+
await interactiveOnboard(flag("relay"));
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2678
2758
|
const email = flag("email") ?? die(`usage: pantheon ${purpose} --email you@work.com${purpose === "signup" ? " --username <name>" : ""} [--relay wss://...]`);
|
|
2679
2759
|
const username = purpose === "signup" ? (flag("username") ?? die("usage: pantheon signup --email you@work.com --username <name>")) : undefined;
|
|
2680
2760
|
const prior = identity();
|
|
@@ -3001,7 +3081,8 @@ async function main() {
|
|
|
3001
3081
|
if (to === cfg.member)
|
|
3002
3082
|
die("you cannot delegate authority to yourself");
|
|
3003
3083
|
const matches = (rule, domain) => rule === domain || rule === "*" || (rule.endsWith(".*") && (domain === rule.slice(0, -2) || domain.startsWith(rule.slice(0, -1))));
|
|
3004
|
-
const
|
|
3084
|
+
const teamAuthority = teamProfile(profile, cfg.spaceId, cfg.space).authority;
|
|
3085
|
+
const owns = (domain) => Object.keys(teamAuthority).some((rule) => matches(rule, domain));
|
|
3005
3086
|
const notOwned = domains.filter((d) => !owns(d));
|
|
3006
3087
|
if (notOwned.length)
|
|
3007
3088
|
die(`you cannot delegate authority you do not own: ${notOwned.join(", ")}`);
|
|
@@ -3480,7 +3561,23 @@ async function main() {
|
|
|
3480
3561
|
return;
|
|
3481
3562
|
}
|
|
3482
3563
|
if (hud?.action === "details") {
|
|
3483
|
-
|
|
3564
|
+
let tcfg = cfg;
|
|
3565
|
+
let found = hud.act ? await fetchAct(cfg, hud.act, 8000).catch(() => null) : null;
|
|
3566
|
+
if (hud.act && !found) {
|
|
3567
|
+
const f = loadAll();
|
|
3568
|
+
for (const m of Object.values(f?.spaces ?? {})) {
|
|
3569
|
+
const c = configFor(f, m, null);
|
|
3570
|
+
if (c.space === cfg.space)
|
|
3571
|
+
continue;
|
|
3572
|
+
const x = await fetchAct(c, hud.act, 6000).catch(() => null);
|
|
3573
|
+
if (x) {
|
|
3574
|
+
found = x;
|
|
3575
|
+
tcfg = c;
|
|
3576
|
+
break;
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
const wd = await workdirFor(tcfg, { ...hud, space: tcfg.space }).catch(() => null);
|
|
3484
3581
|
if (wd?.path) {
|
|
3485
3582
|
try {
|
|
3486
3583
|
process.chdir(wd.path);
|
|
@@ -3489,21 +3586,40 @@ async function main() {
|
|
|
3489
3586
|
}
|
|
3490
3587
|
if (wd)
|
|
3491
3588
|
console.error(D(` ${wd.note}`));
|
|
3492
|
-
const a =
|
|
3589
|
+
const a = found?.act;
|
|
3590
|
+
const goalTitle = found?.goalTitle;
|
|
3493
3591
|
let o;
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3592
|
+
let owed = [];
|
|
3593
|
+
{
|
|
3594
|
+
const c2 = new PantheonClient(tcfg, undefined, { role: "cli" });
|
|
3595
|
+
try {
|
|
3596
|
+
await c2.connect();
|
|
3597
|
+
if (hud.decision)
|
|
3598
|
+
o = c2.obligations.find((x) => x.id === hud.decision);
|
|
3599
|
+
const g = a?.goal ?? o?.goal ?? null;
|
|
3600
|
+
owed = c2.obligations.filter((x) => g && x.goal === g && (x.owner === tcfg.member || x.owner === `human:${tcfg.member}`));
|
|
3601
|
+
}
|
|
3602
|
+
catch { }
|
|
3603
|
+
finally {
|
|
3604
|
+
c2.close();
|
|
3605
|
+
}
|
|
3499
3606
|
}
|
|
3607
|
+
const req = a?.ref ? (await fetchAct(tcfg, a.ref, 6000).catch(() => null))?.act : undefined;
|
|
3608
|
+
const mine = !!a && a.from === tcfg.member;
|
|
3609
|
+
const peer = mine ? (req?.from ?? "a teammate") : (a?.from ?? "a teammate");
|
|
3610
|
+
const meta = (a?.meta ?? {});
|
|
3611
|
+
const block = (t, max) => {
|
|
3612
|
+
const ls = t.trim().split("\n");
|
|
3613
|
+
return ls.slice(0, max).map((l) => ` ${l}`).join("\n") + (ls.length > max ? `\n ${D(`… ${ls.length - max} more lines — 3 shows the whole thread`)}` : "");
|
|
3614
|
+
};
|
|
3500
3615
|
if (a) {
|
|
3501
|
-
console.log(
|
|
3502
|
-
if (
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3616
|
+
console.log(`${B(mine ? `Your worker answered ${peer}` : `${a.from} sent ${a.act === "ASK" ? "a question" : a.act === "DELIVERED" ? "a delivery" : "a message"}`)}${D(` · team ${tcfg.space}${goalTitle ? ` · "${goalTitle}"` : ""} · ${ago(a.ts)}`)}`);
|
|
3617
|
+
if (req)
|
|
3618
|
+
console.log(`\n${B(mine ? `What ${peer} asked` : "In reply to")}\n${block(req.body, 12)}`);
|
|
3619
|
+
console.log(`\n${B(mine ? `What it replied (${a.act === "DELIVERED" ? "delivered" : a.act === "ANSWER" ? "answer" : a.act.toLowerCase()})` : "What they said")}\n${block(a.body, 20)}`);
|
|
3620
|
+
if (meta.branch)
|
|
3621
|
+
console.log(`\n${B("The work")}\n branch ${C(meta.branch)}${meta.commit ? ` at ${meta.commit.slice(0, 12)}` : ""}\n ${D(`review it here: git log -p ${meta.branch}`)}`);
|
|
3622
|
+
console.log(`\n${B("Owed by you")}${owed.length ? "\n" + owed.map((x) => ` • ${x.body}`).join("\n") : ` ${D("nothing in this goal right now")}`}`);
|
|
3507
3623
|
}
|
|
3508
3624
|
else if (!o)
|
|
3509
3625
|
console.log(D("that act is no longer in what the relay holds"));
|
|
@@ -3526,7 +3642,26 @@ async function main() {
|
|
|
3526
3642
|
break;
|
|
3527
3643
|
if (choice === "1" && agent) {
|
|
3528
3644
|
rl.close();
|
|
3529
|
-
|
|
3645
|
+
let prompt = o ? dp(o.id, dt(o.body)) : "";
|
|
3646
|
+
if (o) {
|
|
3647
|
+
const c3 = new PantheonClient(tcfg, undefined, { role: "cli" });
|
|
3648
|
+
try {
|
|
3649
|
+
await c3.connect();
|
|
3650
|
+
prompt = await decisionBriefFor(tcfg, c3, o);
|
|
3651
|
+
}
|
|
3652
|
+
catch { }
|
|
3653
|
+
finally {
|
|
3654
|
+
c3.close();
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
else {
|
|
3658
|
+
const { runBrief } = await import("./decision-ui.js");
|
|
3659
|
+
prompt = runBrief({ team: tcfg.space, peer, goal: goalTitle ? { title: goalTitle } : null, request: req?.body ?? "(the request is no longer held by the relay)",
|
|
3660
|
+
reply: mine ? { act: a.act, text: a.body } : null, ...(meta.branch ? { branch: meta.branch } : {}), ...(meta.commit ? { commit: meta.commit } : {}),
|
|
3661
|
+
owedByMe: owed.map((x) => x.body), ok: true });
|
|
3662
|
+
if (!mine)
|
|
3663
|
+
prompt = prompt.replace(/^Pantheon: my worker handled .*$/m, `Pantheon: ${a.from}'s ${a.act.toLowerCase()} in "${tcfg.space}"`);
|
|
3664
|
+
}
|
|
3530
3665
|
console.error(D(`opening ${agent.id}…`));
|
|
3531
3666
|
await launchAgent(agent.binary, (p) => launchArgs(agent.id, p, sessionName(prompt)), prompt);
|
|
3532
3667
|
return;
|
|
@@ -3606,6 +3741,19 @@ async function main() {
|
|
|
3606
3741
|
await launchAgent(bin, (p) => [p], hud.prompt, cwd ?? undefined);
|
|
3607
3742
|
return;
|
|
3608
3743
|
}
|
|
3744
|
+
if (hud.decision) {
|
|
3745
|
+
const cl = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
3746
|
+
try {
|
|
3747
|
+
await cl.connect();
|
|
3748
|
+
const o = cl.obligations.find((x) => x.id === hud.decision);
|
|
3749
|
+
if (o)
|
|
3750
|
+
hud.prompt = await decisionBriefFor(cfg, cl, o);
|
|
3751
|
+
}
|
|
3752
|
+
catch { }
|
|
3753
|
+
finally {
|
|
3754
|
+
cl.close();
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3609
3757
|
if (hud.prompt) {
|
|
3610
3758
|
const agent = pickExplainAgent(hud.tool ?? p.agent.primary_surface, detectAvailable());
|
|
3611
3759
|
if (!agent)
|
|
@@ -3624,24 +3772,50 @@ async function main() {
|
|
|
3624
3772
|
catch { }
|
|
3625
3773
|
}
|
|
3626
3774
|
}
|
|
3627
|
-
const
|
|
3628
|
-
|
|
3629
|
-
const
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3775
|
+
const every = (() => { const f = loadAll(); return f && Object.keys(f.spaces ?? {}).length ? Object.values(f.spaces).map((m) => configFor(f, m, null)) : [cfg]; })();
|
|
3776
|
+
const open = [];
|
|
3777
|
+
const clients = [];
|
|
3778
|
+
for (const tc of every) {
|
|
3779
|
+
const cl = new PantheonClient(tc, undefined, { role: "cli" });
|
|
3780
|
+
try {
|
|
3781
|
+
await cl.connect();
|
|
3782
|
+
}
|
|
3783
|
+
catch {
|
|
3784
|
+
cl.close();
|
|
3785
|
+
continue;
|
|
3786
|
+
}
|
|
3787
|
+
clients.push(cl);
|
|
3788
|
+
for (const o of cl.obligations)
|
|
3789
|
+
if (o.kind === "HUMAN" && o.owner === `human:${tc.member}`)
|
|
3790
|
+
open.push({ cfg: tc, client: cl, o });
|
|
3791
|
+
}
|
|
3792
|
+
const closeAll = () => { for (const c of clients)
|
|
3793
|
+
c.close(); };
|
|
3794
|
+
const one = id ? open.find((x) => x.o.id === id) : open.length === 1 ? open[0] : undefined;
|
|
3795
|
+
if (id && !one) {
|
|
3796
|
+
closeAll();
|
|
3797
|
+
die(`no decision ${id} is waiting on you in ${every.length > 1 ? `any of your ${every.length} teams` : `"${cfg.space}"`}`);
|
|
3798
|
+
}
|
|
3799
|
+
let prompt = null;
|
|
3800
|
+
let cwd;
|
|
3801
|
+
if (one) {
|
|
3802
|
+
prompt = await decisionBriefFor(one.cfg, one.client, one.o).catch(() => dp(one.o.id, dt(one.o.body)));
|
|
3803
|
+
cwd = (await workdirFor(one.cfg, { action: "open", space: one.cfg.space, ...(one.o.goal ? { goal: one.o.goal } : {}) }).catch(() => null))?.path ?? undefined;
|
|
3804
|
+
}
|
|
3805
|
+
else if (open.length > 1)
|
|
3806
|
+
prompt = adp(open.length);
|
|
3807
|
+
closeAll();
|
|
3635
3808
|
if (!prompt) {
|
|
3636
3809
|
console.log(D("no decisions waiting on you — nothing to explain"));
|
|
3637
3810
|
return;
|
|
3638
3811
|
}
|
|
3639
|
-
const agent = pickExplainAgent(loadProfile(cfg).agent.primary_surface, detectAvailable());
|
|
3812
|
+
const agent = pickExplainAgent(loadProfile(one?.cfg ?? cfg).agent.primary_surface, detectAvailable());
|
|
3640
3813
|
if (!agent)
|
|
3641
3814
|
die("no agent CLI found on this machine (claude, cursor-agent, codex, copilot, or kiro-cli)");
|
|
3642
|
-
console.error(D(`opening ${agent.id} with the question…`));
|
|
3815
|
+
console.error(D(`opening ${agent.id} with the question${one ? ` (team "${one.cfg.space}")` : ""}…`));
|
|
3643
3816
|
const { spawnSync } = await import("node:child_process");
|
|
3644
|
-
|
|
3817
|
+
const finalPrompt = prompt;
|
|
3818
|
+
await launchAgent(agent.binary, (p) => launchArgs(agent.id, p, sessionName(finalPrompt)), finalPrompt, cwd);
|
|
3645
3819
|
return;
|
|
3646
3820
|
}
|
|
3647
3821
|
case "approvals": {
|
|
@@ -3817,10 +3991,31 @@ async function main() {
|
|
|
3817
3991
|
return;
|
|
3818
3992
|
}
|
|
3819
3993
|
case "tray-detail": {
|
|
3820
|
-
const key = argv[1] ?? die("usage: pantheon tray-detail <act id>");
|
|
3821
|
-
const
|
|
3994
|
+
const key = argv[1] ?? die("usage: pantheon tray-detail <act id> [--space <team>]");
|
|
3995
|
+
const fileNow = loadAll();
|
|
3996
|
+
const named = flag("space") && fileNow ? findMembership(fileNow, flag("space")) : undefined;
|
|
3997
|
+
const tryCfgs = [
|
|
3998
|
+
...(named && fileNow ? [configFor(fileNow, named, null)] : []),
|
|
3999
|
+
...(fileNow ? Object.values(fileNow.spaces ?? {}).filter((m) => m !== named).map((m) => configFor(fileNow, m, null)) : []),
|
|
4000
|
+
];
|
|
4001
|
+
if (!tryCfgs.length) {
|
|
4002
|
+
const c0 = load();
|
|
4003
|
+
if (c0)
|
|
4004
|
+
tryCfgs.push(c0);
|
|
4005
|
+
}
|
|
4006
|
+
if (!tryCfgs.length)
|
|
4007
|
+
die("not joined to a team");
|
|
3822
4008
|
const { humanize } = await import("./humanize.js");
|
|
3823
|
-
|
|
4009
|
+
let found = null;
|
|
4010
|
+
let cfgNow = tryCfgs[0];
|
|
4011
|
+
for (const c of tryCfgs) {
|
|
4012
|
+
const x = await fetchAct(c, key, 8000).catch(() => null);
|
|
4013
|
+
if (x) {
|
|
4014
|
+
found = x;
|
|
4015
|
+
cfgNow = c;
|
|
4016
|
+
break;
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
3824
4019
|
if (!found) {
|
|
3825
4020
|
console.log(JSON.stringify({ ok: false }));
|
|
3826
4021
|
return;
|
|
@@ -3833,8 +4028,12 @@ async function main() {
|
|
|
3833
4028
|
c2.close();
|
|
3834
4029
|
}
|
|
3835
4030
|
const { headline, blocks } = humanize(a.body, { ...(goalTitle ? { goalTitle } : {}), from: a.from });
|
|
4031
|
+
const asker = a.from !== cfgNow.member ? a.from : a.sig?.role === "run" ? "your worker" : "your agent";
|
|
4032
|
+
const { answerOptions } = await import("./decision-ui.js");
|
|
4033
|
+
const options = ["ESCALATE", "HUMAN_REQUIRED", "ASK"].includes(a.act) ? answerOptions(a.body) : [];
|
|
3836
4034
|
console.log(JSON.stringify({
|
|
3837
|
-
ok: true, id: a.id, act: a.act, from: a.from, ts: a.ts,
|
|
4035
|
+
ok: true, id: a.id, act: a.act, from: a.from, ts: a.ts, asker, team: cfgNow.space,
|
|
4036
|
+
...(options.length ? { options } : {}),
|
|
3838
4037
|
...(a.summary ? { summary: a.summary } : {}),
|
|
3839
4038
|
...(goalTitle ? { goal_title: goalTitle } : {}),
|
|
3840
4039
|
headline, blocks, raw: a.body,
|
|
@@ -4062,6 +4261,7 @@ async function main() {
|
|
|
4062
4261
|
await client.connect().then(() => client.bindRepo(r.fingerprint, r.origin)).catch((e) => console.error(D(` (could not register the repo with the relay: ${e.message} — run \`pantheon repo add\` when it is reachable)`)));
|
|
4063
4262
|
client.close();
|
|
4064
4263
|
}
|
|
4264
|
+
windowsOverridesForBoundRepos();
|
|
4065
4265
|
console.error(D(` Agents opened in this repo now speak as you in "${m.name}". Restart any that are open.`));
|
|
4066
4266
|
}
|
|
4067
4267
|
else {
|
|
@@ -4092,6 +4292,7 @@ async function main() {
|
|
|
4092
4292
|
await client.bindRepo(r.fingerprint, r.origin, base).catch((e) => die(e.message));
|
|
4093
4293
|
if (cfg.spaceId)
|
|
4094
4294
|
bindRepo(r, cfg.spaceId);
|
|
4295
|
+
windowsOverridesForBoundRepos();
|
|
4095
4296
|
console.error(`${B("registered")} ${r.origin ?? r.root} as a repo "${cfg.space}" works from${base ? ` — workers cut from ${C(base)}` : ""}${r.shared ? "" : D(" (no origin remote — deliveries from it cannot be fetched)")}`);
|
|
4096
4297
|
}
|
|
4097
4298
|
else {
|
package/dist/client.js
CHANGED
|
@@ -247,7 +247,7 @@ export class PantheonClient {
|
|
|
247
247
|
this.invitesList = msg.invites;
|
|
248
248
|
break;
|
|
249
249
|
case "invite":
|
|
250
|
-
this.inviteUrl = { url: msg.url, expires_at: msg.expires_at };
|
|
250
|
+
this.inviteUrl = { url: msg.url, expires_at: msg.expires_at, ...(typeof msg.note === "string" ? { note: msg.note } : {}) };
|
|
251
251
|
break;
|
|
252
252
|
case "welcome":
|
|
253
253
|
if (this.cfg.spaceId && msg.space_id && msg.space_id !== this.cfg.spaceId) {
|
|
@@ -622,7 +622,7 @@ export class PantheonClient {
|
|
|
622
622
|
}
|
|
623
623
|
const r = verifyAct(rec, [this.cfg.spaceId, this.spaceId, this.cfg.space], pin?.fp, pin?.key);
|
|
624
624
|
if (trusted(r.trust) && rec.sig && !this.verifiedIds.has(rec.id)) {
|
|
625
|
-
this.verifiedIds.set(rec.id, { member: rec.from, nonce: rec.sig.nonce });
|
|
625
|
+
this.verifiedIds.set(rec.id, { member: rec.from, nonce: rec.sig.nonce, sig: rec.sig.s });
|
|
626
626
|
if (this.verifiedIds.size > 5000)
|
|
627
627
|
this.verifiedIds.delete(this.verifiedIds.keys().next().value);
|
|
628
628
|
}
|
|
@@ -660,7 +660,7 @@ export class PantheonClient {
|
|
|
660
660
|
async noteAnswered(ref) {
|
|
661
661
|
const known = this.verifiedIds.get(ref);
|
|
662
662
|
if (known) {
|
|
663
|
-
markAnswered(this.pinTeam, known.member, known.nonce, ref);
|
|
663
|
+
markAnswered(this.pinTeam, known.member, known.nonce, ref, undefined, known.sig);
|
|
664
664
|
return true;
|
|
665
665
|
}
|
|
666
666
|
let r = this.seen.get(ref) ?? null;
|
package/dist/config.js
CHANGED
|
@@ -599,6 +599,7 @@ export function toJoinLink(cfg) {
|
|
|
599
599
|
export function fromInvite(invite) {
|
|
600
600
|
const clean = invite
|
|
601
601
|
.replace(/\u001b\[[0-9;]*m/g, "")
|
|
602
|
+
.replace(/[\r\n\t]+/g, " ")
|
|
602
603
|
.replace(/[\u0000-\u001f\u007f]/g, "")
|
|
603
604
|
.trim()
|
|
604
605
|
.replace(/^['"`]|['"`]$/g, "")
|