@join-pantheon/cli 0.33.1 → 0.33.2
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 +6 -5
- package/dist/accounts.js +10 -0
- package/dist/agents.js +11 -0
- package/dist/board.js +55 -9
- package/dist/claude-confine.js +1 -1
- package/dist/cli.js +556 -89
- package/dist/client.js +8 -4
- package/dist/config.js +18 -4
- package/dist/continued.js +5 -0
- package/dist/desktop-app.js +103 -0
- package/dist/doctor.js +84 -32
- package/dist/folder-guard.js +12 -0
- package/dist/folder-snapshot.js +432 -0
- package/dist/install.js +15 -8
- package/dist/machine.js +84 -4
- package/dist/managed.js +2 -2
- package/dist/mcp.js +5 -3
- package/dist/network.js +156 -0
- package/dist/notice-linux.js +6 -5
- package/dist/notice-windows.js +2 -2
- package/dist/notice.js +13 -0
- package/dist/notifier.js +29 -2
- package/dist/notify.js +11 -0
- package/dist/onboard-defaults.js +37 -1
- package/dist/pins.js +7 -1
- package/dist/plans.js +23 -0
- package/dist/protocol.js +5 -0
- package/dist/quickstart.js +17 -11
- package/dist/refusal.js +4 -4
- package/dist/relay.js +332 -22
- package/dist/repo-find.js +6 -3
- package/dist/roster-file.js +50 -5
- package/dist/tray.js +12 -1
- package/dist/uninstall.js +38 -3
- package/dist/uri-handler.js +3 -2
- package/dist/verify-act.js +17 -0
- package/dist/worker-fleet.js +140 -5
- package/dist/worker.js +201 -63
- package/installer/install.ps1 +251 -15
- package/installer/install.sh +147 -18
- package/package.json +1 -1
- package/helper/macos/prebuilt/manifest.json +0 -29
- package/helper/macos/prebuilt/pantheon-hud +0 -0
- package/helper/macos/prebuilt/pantheon-tray +0 -0
package/dist/cli.js
CHANGED
|
@@ -7,13 +7,13 @@ process.emitWarning = ((warning, ...rest) => {
|
|
|
7
7
|
return _emitWarning(warning, ...rest);
|
|
8
8
|
});
|
|
9
9
|
import { randomBytes } from "node:crypto";
|
|
10
|
-
import { gitConfigValue,
|
|
11
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { emailSuggestion, gitConfigValue, isGithubNoreply, pantheonCommand, signinCommand, usernameSuggestion } from "./onboard-defaults.js";
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
13
13
|
import { PantheonClient } from "./client.js";
|
|
14
14
|
import { agentSpawn, CmdLineUnsafe } from "./which.js";
|
|
15
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";
|
|
16
|
-
import { fmtLimit } from "./plans.js";
|
|
16
|
+
import { fmtLimit, lastSlotNotice, ownedTeamsIn, spaceLimitAdvice } from "./plans.js";
|
|
17
17
|
import { validEmail, validUsername } from "./accounts.js";
|
|
18
18
|
import { install, launcher, installForUser, windowsRepoOverrides, agentAsksOnce } from "./install.js";
|
|
19
19
|
import { Relay } from "./relay.js";
|
|
@@ -32,14 +32,14 @@ import { assess } from "./untrusted.js";
|
|
|
32
32
|
import { report as spendReport, recentRuns } from "./spend.js";
|
|
33
33
|
import { renderBrief, renderTeam } from "./brief.js";
|
|
34
34
|
import { resolveDataDir, saveAll } from "./config.js";
|
|
35
|
-
import { currentPlan, installNetwork, redactProxy, redactText, normalizeProxy, proxyToRemember, describeNetError } from "./network.js";
|
|
35
|
+
import { currentPlan, installNetwork, redactProxy, redactText, normalizeProxy, proxyToRemember, describeNetError, describeRelayFailure, checkRelayCertificate, interceptedText, interceptionExplanation, relayJson } from "./network.js";
|
|
36
36
|
import { createInterface } from "node:readline";
|
|
37
37
|
import { homedir } from "node:os";
|
|
38
|
-
import { removeHook, decideStop, writeIntent, clearIntent, readIntent, installHooks, agentHookStates } from "./notify.js";
|
|
38
|
+
import { removeHook, decideStop, writeIntent, clearIntent, readIntent, repairNote, installHooks, agentHookStates } from "./notify.js";
|
|
39
39
|
import { pending, render as renderPending } from "./pending.js";
|
|
40
40
|
import { signedRead } from "./reads.js";
|
|
41
|
-
import { start as qsStart, join as qsJoin, wireUp, teamNameFor, deviceLabel } from "./quickstart.js";
|
|
42
|
-
import { folderRefusalHere, isTemporaryRefusal } from "./folder-guard.js";
|
|
41
|
+
import { start as qsStart, join as qsJoin, wireUp, teamNameFor, deviceLabel, bindableFolder } from "./quickstart.js";
|
|
42
|
+
import { folderRefusalHere, isTemporaryRefusal, projectFolderAnswer } from "./folder-guard.js";
|
|
43
43
|
import { installService, serviceInstalled, serviceLogFile, uninstallService } from "./service.js";
|
|
44
44
|
import { LEGACY_TASK_NAME, hostAlive, hostFiles, readHostState, recordedTaskName } from "./win-host.js";
|
|
45
45
|
import { ACTS, LIMITS } from "./protocol.js";
|
|
@@ -88,9 +88,9 @@ async function requireSignInOnHostedRelay(relayFlag) {
|
|
|
88
88
|
console.error("");
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
|
-
die("not signed in yet.\n When the code arrives: pantheon verify <code
|
|
91
|
+
die("not signed in yet.\n When the code arrives: pantheon verify <code> (check spam if it is not in your inbox)\n Then run pantheon start again.");
|
|
92
92
|
}
|
|
93
|
-
die(`sign in first — this relay verifies email.\n
|
|
93
|
+
die(`sign in first — this relay verifies email.\n${signInSteps().join("\n")}\n Then run pantheon start again.\n ${SIGNIN_FOR_AGENTS}`);
|
|
94
94
|
}
|
|
95
95
|
async function requireSignInOnAccountsRelay(relayFlag) {
|
|
96
96
|
if (!relayFlag || identity()?.account)
|
|
@@ -109,7 +109,7 @@ async function requireSignInOnAccountsRelay(relayFlag) {
|
|
|
109
109
|
console.error("");
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
112
|
-
die("not signed in yet.\n When the code arrives: pantheon verify <code
|
|
112
|
+
die("not signed in yet.\n When the code arrives: pantheon verify <code> (check spam if it is not in your inbox)\n Then run this same pantheon start again.");
|
|
113
113
|
}
|
|
114
114
|
if (hasAccounts)
|
|
115
115
|
die(`sign in first — ${relayFlag} verifies email.${signInHint("NOT_SIGNED_IN", relayFlag)}\n Then run this same pantheon start again.`);
|
|
@@ -120,14 +120,15 @@ async function interactiveOnboard(relayArg, opts = {}) {
|
|
|
120
120
|
const base = relay.replace(/^ws/, "http").replace(/\/+$/, "");
|
|
121
121
|
if (!process.stdin.isTTY) {
|
|
122
122
|
console.error(`${B("pantheon")} — sign in to get started:`);
|
|
123
|
-
|
|
123
|
+
for (const l of signInSteps())
|
|
124
|
+
console.error(l);
|
|
124
125
|
return false;
|
|
125
126
|
}
|
|
126
127
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
127
128
|
const ask = async (q) => {
|
|
128
129
|
const a = await questionOrNull(rl, q);
|
|
129
130
|
if (a === null) {
|
|
130
|
-
console.error(D("\n cancelled. Pick up where you left off: pantheon
|
|
131
|
+
console.error(D("\n cancelled. Pick up where you left off: pantheon signin"));
|
|
131
132
|
process.exit(1);
|
|
132
133
|
}
|
|
133
134
|
return a;
|
|
@@ -135,84 +136,117 @@ async function interactiveOnboard(relayArg, opts = {}) {
|
|
|
135
136
|
const kp = prior?.publicKey && prior.privateKey ? { publicKey: prior.publicKey, privateKey: prior.privateKey } : newKeypair();
|
|
136
137
|
const label = deviceLabel();
|
|
137
138
|
const postJson = async (path, body) => fetch(`${base}${path}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })
|
|
138
|
-
.then(async (x) => ({ status: x.status, j: await x
|
|
139
|
+
.then(async (x) => ({ status: x.status, j: await relayJson(x, `${base}${path}`) }));
|
|
140
|
+
const unreachable = async (e) => { console.error(D(` could not reach the relay: ${await describeRelayFailure(e, relay)}`)); return null; };
|
|
139
141
|
try {
|
|
140
142
|
console.error(opts.joining
|
|
141
143
|
? `${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`
|
|
142
144
|
: opts.starting
|
|
143
145
|
? `${B("Welcome to Pantheon.")} First, sign this machine in — your email and a 6-digit code, nothing else. Then your team is created.\n`
|
|
144
|
-
: `${B("Welcome to pantheon.")} Let's get this machine signed in.\n`);
|
|
146
|
+
: `${B("Welcome to pantheon.")} Let's get this machine signed in — your email and a 6-digit code, nothing else.\n`);
|
|
145
147
|
const gitEmail = gitConfigValue("user.email");
|
|
146
|
-
const
|
|
148
|
+
const pend = prior?.pendingAuth;
|
|
149
|
+
const emailGuess = pend?.email && validEmail(pend.email) ? pend.email : emailSuggestion(gitEmail);
|
|
147
150
|
let email = "";
|
|
148
151
|
while (!email) {
|
|
149
152
|
const e = (await ask(` Your work email${emailGuess ? ` [${emailGuess}]` : ""}: `)) || emailGuess;
|
|
150
|
-
if (validEmail(e))
|
|
151
|
-
email = e;
|
|
152
|
-
else
|
|
153
|
+
if (!validEmail(e)) {
|
|
153
154
|
console.error(D(" that doesn't look like an email — try again"));
|
|
154
|
-
}
|
|
155
|
-
let started = null;
|
|
156
|
-
let mode = "signup";
|
|
157
|
-
let signupUsername = "";
|
|
158
|
-
let userGuess = suggestUsername(email);
|
|
159
|
-
while (!started || started.status !== 200) {
|
|
160
|
-
const u = (await ask(` Pick a username${userGuess ? ` [${userGuess}]` : ""}: `)) || userGuess;
|
|
161
|
-
if (!validUsername(u.toLowerCase())) {
|
|
162
|
-
console.error(D(" 2–32 chars: letters, digits, . _ - (not at the ends)"));
|
|
163
155
|
continue;
|
|
164
156
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return false;
|
|
169
|
-
if (started.status === 404) {
|
|
170
|
-
console.error(`\n ${D("This relay is invite-based (no email accounts). Get started with:")}`);
|
|
171
|
-
console.error(` ${C("pantheon start")} ${D("(create a team)")} or ${C("pantheon join <invite>")}`);
|
|
172
|
-
return false;
|
|
157
|
+
if (isGithubNoreply(e)) {
|
|
158
|
+
console.error(D(" that is GitHub's no-reply address: mail sent there goes nowhere, so the code would never arrive — use the email you read"));
|
|
159
|
+
continue;
|
|
173
160
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
161
|
+
email = e;
|
|
162
|
+
}
|
|
163
|
+
const send = (m, username) => postJson(`/auth/${m}`, m === "signup" ? { email, username, pubkey: kp.publicKey, label } : { email, pubkey: kp.publicKey, label })
|
|
164
|
+
.catch(unreachable);
|
|
165
|
+
const justSent = (r) => r.j.reason === "too_soon";
|
|
166
|
+
let mode = "login";
|
|
167
|
+
let signupUsername = "";
|
|
168
|
+
const recentPend = pend && pend.email === email && typeof pend.at === "number" && Date.now() - pend.at < 9 * 60_000 && prior?.publicKey === kp.publicKey ? pend : null;
|
|
169
|
+
let started = recentPend ? { status: 200, j: { reused: true } } : await send("login");
|
|
170
|
+
if (recentPend) {
|
|
171
|
+
mode = recentPend.purpose;
|
|
172
|
+
signupUsername = recentPend.username ?? "";
|
|
173
|
+
}
|
|
174
|
+
if (!started)
|
|
175
|
+
return false;
|
|
176
|
+
const noAccount = started.status === 404 && (started.j.reason === "no_such_account" || /no active account/i.test(String(started.j.error ?? "")));
|
|
177
|
+
if (started.status === 404 && !noAccount) {
|
|
178
|
+
console.error(`\n ${D("This relay is invite-based (no email accounts). Get started with:")}`);
|
|
179
|
+
console.error(` ${C("pantheon start")} ${D("(create a team)")} or ${C("pantheon join <invite>")}`);
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
if (noAccount) {
|
|
183
|
+
mode = "signup";
|
|
184
|
+
console.error(D(` no account for ${email} yet — one more question to make it.`));
|
|
185
|
+
let userGuess = (pend?.purpose === "signup" && pend.email === email && pend.username) || usernameSuggestion(email, gitEmail);
|
|
186
|
+
for (;;) {
|
|
187
|
+
const u = (await ask(` Pick a username${userGuess ? ` [${userGuess}]` : ""}: `)) || userGuess;
|
|
188
|
+
if (!validUsername(u.toLowerCase())) {
|
|
189
|
+
console.error(D(" 2–32 chars: letters, digits, . _ - (not at the ends)"));
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
signupUsername = u;
|
|
193
|
+
started = await send("signup", u);
|
|
179
194
|
if (!started)
|
|
180
195
|
return false;
|
|
181
|
-
|
|
182
|
-
|
|
196
|
+
if (started.status === 200 || justSent(started))
|
|
197
|
+
break;
|
|
198
|
+
if (started.j.reason === "email_taken") {
|
|
199
|
+
console.error(D(` that email already has an account — signing this device in instead…`));
|
|
200
|
+
started = await send("login");
|
|
201
|
+
if (!started)
|
|
202
|
+
return false;
|
|
203
|
+
mode = "login";
|
|
204
|
+
signupUsername = "";
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
if (started.j.reason !== "username_taken" && started.j.reason !== "bad_username")
|
|
208
|
+
break;
|
|
209
|
+
console.error(D(` ${started.j.error ?? "try a different username"}`));
|
|
210
|
+
userGuess = usernameSuggestion(email, gitEmail, Math.floor(10 + Math.random() * 90));
|
|
183
211
|
}
|
|
184
|
-
console.error(D(` ${started.j.error ?? "try a different username"}`));
|
|
185
|
-
userGuess = suggestUsername(email, Math.floor(10 + Math.random() * 90));
|
|
186
212
|
}
|
|
187
|
-
|
|
188
|
-
|
|
213
|
+
const already = justSent(started);
|
|
214
|
+
if (started.status !== 200 && !already) {
|
|
215
|
+
console.error(D(` ${started.j.error ?? "sign-in failed"}`));
|
|
189
216
|
return false;
|
|
190
217
|
}
|
|
191
218
|
const authUser = mode === "signup" ? signupUsername : undefined;
|
|
192
219
|
save({ ...(prior ?? {}), relay, member: signupUsername || prior?.member || "me",
|
|
193
220
|
space: prior?.space ?? "", token: prior?.token ?? "",
|
|
194
221
|
publicKey: kp.publicKey, privateKey: kp.privateKey, agent: prior?.agent ?? "unknown",
|
|
195
|
-
pendingAuth: { email, ...(authUser ? { username: authUser } : {}), purpose: mode } });
|
|
196
|
-
if (started.j.
|
|
197
|
-
console.error(`\n ${
|
|
222
|
+
pendingAuth: { email, ...(authUser ? { username: authUser } : {}), purpose: mode, at: recentPend?.at ?? (already && pend?.email === email && pend.at ? pend.at : Date.now()) } });
|
|
223
|
+
if (started.j.reused) {
|
|
224
|
+
console.error(`\n ${C("A code was sent")} to ${B(email)} ${Math.max(1, Math.round((Date.now() - recentPend.at) / 60_000))} min ago — enter that one, or type 'resend' for a new one. ${SPAM_HINT}\n`);
|
|
225
|
+
}
|
|
226
|
+
else if (already && pend?.email !== email) {
|
|
227
|
+
console.error(`\n ${D("A code for this email was asked for a moment ago from another device; that one will not work here.")} Wait a minute, then type 'resend'. ${SPAM_HINT}\n`);
|
|
228
|
+
}
|
|
229
|
+
else if (already) {
|
|
230
|
+
console.error(`\n ${C("A code was sent")} to ${B(email)} a moment ago — use that one. ${SPAM_HINT}\n`);
|
|
231
|
+
}
|
|
232
|
+
else if (started.j.sent === false && started.j.via === "resend") {
|
|
233
|
+
console.error(`\n ${D("The relay could not send the email just now.")} Type 'resend' to retry, or run ${C("pantheon signin")} again shortly.\n`);
|
|
198
234
|
}
|
|
199
235
|
else {
|
|
200
236
|
const via = started.j.via === "resend" ? "" : `\n ${D("(this is a dev relay — your code is printed in its log)")}`;
|
|
201
|
-
console.error(`\n ${C("Sent a 6-digit code")} to ${B(email)}. Check your inbox
|
|
237
|
+
console.error(`\n ${C("Sent a 6-digit code")} to ${B(email)}. Check your inbox. ${SPAM_HINT}${via}\n`);
|
|
202
238
|
}
|
|
203
239
|
for (let attempt = 0; attempt < 6; attempt++) {
|
|
204
|
-
const code = await ask(" Enter the code (or 'resend'): ");
|
|
240
|
+
const code = await ask(" Enter the code (not in your inbox? check spam · or type 'resend'): ");
|
|
205
241
|
if (/^resend$/i.test(code)) {
|
|
206
|
-
const rs = await
|
|
207
|
-
? { email, username: signupUsername, pubkey: kp.publicKey, label }
|
|
208
|
-
: { email, pubkey: kp.publicKey, label }).catch(() => null);
|
|
242
|
+
const rs = await send(mode, mode === "signup" ? signupUsername : undefined);
|
|
209
243
|
if (rs && rs.status === 200 && rs.j.sent !== false)
|
|
210
|
-
console.error(D(
|
|
244
|
+
console.error(D(` sent a fresh code. ${SPAM_HINT}`));
|
|
211
245
|
else
|
|
212
246
|
console.error(D(` could not resend: ${rs?.j.error ?? "the relay did not send an email"} — try again in a minute.`));
|
|
213
247
|
continue;
|
|
214
248
|
}
|
|
215
|
-
const v = await postJson("/auth/verify", { email, code, pubkey: kp.publicKey }).catch((e) => ({ status: 0, j: { error: `could not reach the relay (${e
|
|
249
|
+
const v = await postJson("/auth/verify", { email, code, pubkey: kp.publicKey }).catch(async (e) => ({ status: 0, j: { error: `could not reach the relay (${await describeRelayFailure(e, relay)}) — try the same code again` } }));
|
|
216
250
|
if (v.status === 200) {
|
|
217
251
|
const acct = v.j.account;
|
|
218
252
|
const cfg = { ...(prior ?? {}), relay, member: acct.username, space: prior?.space ?? "", token: prior?.token ?? "",
|
|
@@ -223,6 +257,11 @@ async function interactiveOnboard(relayArg, opts = {}) {
|
|
|
223
257
|
console.error(`\n ${C("You're in")} — signed in as ${B(acct.username)} <${acct.email}> on this machine.\n`);
|
|
224
258
|
if (opts.joining || opts.starting)
|
|
225
259
|
return true;
|
|
260
|
+
if (opts.signin) {
|
|
261
|
+
console.error(` ${B("Signed in — tell your agent to continue;")} it will run the join or start again.`);
|
|
262
|
+
console.error(D(` (On your own? ${C("pantheon start")} creates a team; ${C("pantheon join <invite-link>")} joins one.)`));
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
226
265
|
console.error(` ${B("Next:")} ${C("pantheon start")} ${D("(create a team, get a link to send)")}`);
|
|
227
266
|
console.error(` ${C("pantheon join <invite-link>")} ${D("(join a teammate's team)")}`);
|
|
228
267
|
return true;
|
|
@@ -230,13 +269,22 @@ async function interactiveOnboard(relayArg, opts = {}) {
|
|
|
230
269
|
const spent = v.j.reason === "expired" || v.j.reason === "too_many_attempts" || v.j.tries_left === 0;
|
|
231
270
|
console.error(D(` ${v.j.error ?? "that code is not right"}${spent ? " — type 'resend' for a new code" : " — try again"}`));
|
|
232
271
|
}
|
|
233
|
-
console.error(D("\n Too many tries. Run `pantheon` again to restart, or `pantheon verify <code>` once you have it."));
|
|
272
|
+
console.error(D("\n Too many tries. Run `pantheon signin` again to restart, or `pantheon verify <code>` once you have it."));
|
|
234
273
|
return false;
|
|
235
274
|
}
|
|
236
275
|
finally {
|
|
237
276
|
rl.close();
|
|
238
277
|
}
|
|
239
278
|
}
|
|
279
|
+
const SIGNIN_FOR_AGENTS = "A coding agent that cannot sign you in hands you the signin line to run yourself, and runs this again once you say you're signed in.";
|
|
280
|
+
const SPAM_HINT = "Not there in a minute? Check your spam folder.";
|
|
281
|
+
function signInSteps() {
|
|
282
|
+
return [
|
|
283
|
+
` ${C(signinCommand(managedLayout()?.launcher))} ${D("in a terminal of your own: your email, then the emailed code, in one go")}`,
|
|
284
|
+
` ${D("or two commands:")} ${C("pantheon signup --email you@work.com --username you")} ${D("(an account already: pantheon login --email you@work.com)")}`,
|
|
285
|
+
` ${C("pantheon verify <code>")} ${D("(the code is emailed — check spam)")}`,
|
|
286
|
+
];
|
|
287
|
+
}
|
|
240
288
|
function ago(t) {
|
|
241
289
|
const m = Math.round((Date.now() - t) / 60_000);
|
|
242
290
|
if (m < 1)
|
|
@@ -458,6 +506,123 @@ async function workerAfterBinding(how) {
|
|
|
458
506
|
console.error(D(` the worker did not install: ${e.message} — pantheon service install worker`));
|
|
459
507
|
}
|
|
460
508
|
}
|
|
509
|
+
async function bindProjectFromScratch() {
|
|
510
|
+
const cwd = process.cwd();
|
|
511
|
+
if (repoInfo(cwd) || !isTemporaryRefusal(folderRefusalHere(cwd)))
|
|
512
|
+
return;
|
|
513
|
+
const all = loadAll(), j = load();
|
|
514
|
+
const m = all && j ? findMembership(all, j.spaceId ?? j.space) ?? findMembership(all, j.space) : null;
|
|
515
|
+
if (!all || !m)
|
|
516
|
+
return;
|
|
517
|
+
if (Object.values(all.bindings ?? {}).some((b) => b.space === m.id))
|
|
518
|
+
return;
|
|
519
|
+
const line = pantheonCommand(managedLayout()?.launcher, `use ${m.name} --here`);
|
|
520
|
+
const later = () => {
|
|
521
|
+
console.error(` From your project folder (the repo or folder you work in), run: ${C(line)}`);
|
|
522
|
+
console.error(D(` The worker stays off until then, and agents opened there do not speak for "${m.name}" yet.`));
|
|
523
|
+
};
|
|
524
|
+
console.error(`
|
|
525
|
+
${B("Your project folder.")} ${D(`This is a temporary folder, so nothing in it was bound to "${m.name}".`)}`);
|
|
526
|
+
if (!process.stdin.isTTY) {
|
|
527
|
+
later();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
for (let tries = 0; tries < 3; tries++) {
|
|
531
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
532
|
+
const a = ((await questionOrNull(rl, ` The folder you work in for "${m.name}" — a repo or a docs folder (Enter to skip): `)) ?? "").trim();
|
|
533
|
+
rl.close();
|
|
534
|
+
if (!a)
|
|
535
|
+
break;
|
|
536
|
+
const why = await bindProjectFolder(projectFolderAnswer(a, homedir(), cwd), m);
|
|
537
|
+
if (!why)
|
|
538
|
+
return;
|
|
539
|
+
console.error(D(` ${why} — try another, or press Enter to skip`));
|
|
540
|
+
}
|
|
541
|
+
later();
|
|
542
|
+
}
|
|
543
|
+
async function bindProjectFolder(dir, m) {
|
|
544
|
+
try {
|
|
545
|
+
if (!statSync(dir).isDirectory())
|
|
546
|
+
return `${dir} is not a folder`;
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
return `there is no folder at ${dir}`;
|
|
550
|
+
}
|
|
551
|
+
const all = loadAll() ?? die("not configured");
|
|
552
|
+
const r = repoInfo(dir);
|
|
553
|
+
if (r) {
|
|
554
|
+
const held = all.bindings?.[r.fingerprint];
|
|
555
|
+
if (held && held.space !== m.id) {
|
|
556
|
+
const other = findMembership(all, held.space)?.name ?? held.space;
|
|
557
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
558
|
+
const a = ((await questionOrNull(rl, ` ${r.origin ?? r.root} works for "${other}" now. Move it to "${m.name}"? [y/N] `)) ?? "").trim().toLowerCase();
|
|
559
|
+
rl.close();
|
|
560
|
+
if (a !== "y" && a !== "yes")
|
|
561
|
+
return `left with "${other}"`;
|
|
562
|
+
}
|
|
563
|
+
const { replaced } = bindRepo(r, m.id);
|
|
564
|
+
console.error(` ${B("bound")} ${r.origin ?? r.root} → "${m.name}"${replaced ? D(` (was: ${findMembership(all, replaced.space)?.name ?? replaced.space})`) : ""}`);
|
|
565
|
+
if (!r.shared)
|
|
566
|
+
console.error(D(" this repo has no origin remote — deliveries from it cannot be fetched by teammates"));
|
|
567
|
+
const { repo: _r, repoOrigin: _o, ...plain } = configFor(all, m, null);
|
|
568
|
+
const client = new PantheonClient(plain, undefined, { role: "cli" });
|
|
569
|
+
try {
|
|
570
|
+
await client.connect();
|
|
571
|
+
if (!client.repos.some((x) => x.fingerprint === r.fingerprint))
|
|
572
|
+
await client.bindRepo(r.fingerprint, repoShare(r));
|
|
573
|
+
}
|
|
574
|
+
catch (e) {
|
|
575
|
+
console.error(D(` (could not register the repo with the team: ${e.message.split("\n")[0]} — pantheon repo add, from it, when the relay is reachable)`));
|
|
576
|
+
}
|
|
577
|
+
finally {
|
|
578
|
+
client.close();
|
|
579
|
+
}
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
const refused = folderRefusalHere(dir);
|
|
583
|
+
if (refused)
|
|
584
|
+
return refused.why.replace(/;.*$/, "");
|
|
585
|
+
if ((await import("./repo-find.js")).holdsRepo(dir)) {
|
|
586
|
+
const n = await bindReposIn(dir, all, m, false, `pantheon use ${m.name} --here --yes`);
|
|
587
|
+
return n ? null : "nothing in it was bound";
|
|
588
|
+
}
|
|
589
|
+
if (!bindableFolder(dir))
|
|
590
|
+
return `${dir} holds another team's folder, so it cannot be bound whole`;
|
|
591
|
+
bindFolder(dir, m.id);
|
|
592
|
+
console.error(` ${B("bound")} ${dir} → "${m.name}" ${D("(agents opened there speak in it; no git needed)")}`);
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
async function lastTeamNotice(cfg) {
|
|
596
|
+
if (!cfg.account)
|
|
597
|
+
return;
|
|
598
|
+
const all = loadAll();
|
|
599
|
+
const m = Object.values(all?.spaces ?? {}).find((x) => sameHost(x.relay, cfg.relay));
|
|
600
|
+
if (!all || !m)
|
|
601
|
+
return;
|
|
602
|
+
const c = new PantheonClient(configFor(all, m, null), undefined, { role: "cli" });
|
|
603
|
+
try {
|
|
604
|
+
await Promise.race([c.connect(), new Promise((_, no) => setTimeout(() => no(new Error("timeout")), 5_000))]);
|
|
605
|
+
const say = lastSlotNotice(c.plan);
|
|
606
|
+
if (say)
|
|
607
|
+
console.error(`\x1b[33m ! ${say}\x1b[0m`);
|
|
608
|
+
}
|
|
609
|
+
catch { }
|
|
610
|
+
finally {
|
|
611
|
+
c.dispose();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
async function teamLimitAdvice(warning) {
|
|
615
|
+
const owned = ownedTeamsIn(warning);
|
|
616
|
+
const all = loadAll();
|
|
617
|
+
const teams = await Promise.all(owned.map(async (name) => {
|
|
618
|
+
const m = all ? findMembership(all, name) : null;
|
|
619
|
+
if (!all || !m)
|
|
620
|
+
return { name, here: false, alone: null };
|
|
621
|
+
const { others } = await teamStanding(all, m);
|
|
622
|
+
return { name, here: true, alone: others === null ? null : others.length === 0 };
|
|
623
|
+
}));
|
|
624
|
+
return spaceLimitAdvice(teams);
|
|
625
|
+
}
|
|
461
626
|
const repoLabel = (r) => r.origin ?? basename(r.root);
|
|
462
627
|
const repoShare = (r) => (r.origin ? r.origin.replace(/^.*[:/]([^:/]+)$/, "$1") : basename(r.root));
|
|
463
628
|
const teamFlag = (cfg) => (flag("space") ? ` --space ${cfg.space}` : "");
|
|
@@ -533,7 +698,11 @@ async function launchAgent(binary, args, prompt, cwd) {
|
|
|
533
698
|
sp = agentSpawn(binary, args(`Read the file ${rel.split(/[\\/]/).join("/")} in this folder and do what it asks.`));
|
|
534
699
|
}
|
|
535
700
|
try {
|
|
536
|
-
spawnSync(sp.command, sp.args, { stdio: "inherit", ...(cwd ? { cwd } : {}), ...sp.options });
|
|
701
|
+
const r = spawnSync(sp.command, sp.args, { stdio: "inherit", ...(cwd ? { cwd } : {}), ...sp.options });
|
|
702
|
+
if (r.error || (r.status ?? 0) !== 0) {
|
|
703
|
+
console.error(`\n${B("the agent did not start, or stopped with an error")} ${D(`(${r.error ? r.error.message.split("\n")[0] : `exit ${r.status}`}: ${binary})`)}`);
|
|
704
|
+
process.exitCode = r.status || 1;
|
|
705
|
+
}
|
|
537
706
|
}
|
|
538
707
|
finally {
|
|
539
708
|
if (handoff)
|
|
@@ -543,6 +712,36 @@ async function launchAgent(binary, args, prompt, cwd) {
|
|
|
543
712
|
catch { }
|
|
544
713
|
}
|
|
545
714
|
}
|
|
715
|
+
async function claudeAppInstead(surface, prompt) {
|
|
716
|
+
if (surface !== "claude-code")
|
|
717
|
+
return false;
|
|
718
|
+
const { detectAvailable } = await import("./agents.js");
|
|
719
|
+
if (detectAvailable().some((a) => a.backend.id === "claude-code"))
|
|
720
|
+
return false;
|
|
721
|
+
const { findClaudeApp, explainInClaudeApp } = await import("./desktop-app.js");
|
|
722
|
+
const app = findClaudeApp();
|
|
723
|
+
if (!app)
|
|
724
|
+
return false;
|
|
725
|
+
const r = explainInClaudeApp(prompt, app);
|
|
726
|
+
if (!r.ok) {
|
|
727
|
+
console.error(D(` ${r.says} — opening an agent CLI instead`));
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
console.error(r.says);
|
|
731
|
+
if (/could not be copied/.test(r.says))
|
|
732
|
+
console.log(`\n${prompt}\n`);
|
|
733
|
+
return true;
|
|
734
|
+
}
|
|
735
|
+
async function workerServesLines() {
|
|
736
|
+
try {
|
|
737
|
+
const { workerServesReport } = await import("./worker-fleet.js");
|
|
738
|
+
const { detectAvailable } = await import("./agents.js");
|
|
739
|
+
return workerServesReport(loadAll(), detectAvailable()).map((r) => (r.warn ? `\x1b[33m${r.line}\x1b[0m` : D(r.line)));
|
|
740
|
+
}
|
|
741
|
+
catch {
|
|
742
|
+
return [];
|
|
743
|
+
}
|
|
744
|
+
}
|
|
546
745
|
function needStrict() {
|
|
547
746
|
const r = resolveFor(process.cwd(), { strict: true });
|
|
548
747
|
return r.cfg && r.cfg.space ? r.cfg : null;
|
|
@@ -847,7 +1046,8 @@ const C = (s) => PLAIN ? s : `\x1b[36m${s}\x1b[0m`;
|
|
|
847
1046
|
const HELP = `${B("pantheon")} — your agent and your teammates' agents, working together
|
|
848
1047
|
|
|
849
1048
|
${B("first time — verify your email once")}
|
|
850
|
-
pantheon
|
|
1049
|
+
pantheon signin in a terminal: your email, then the emailed code (check spam)
|
|
1050
|
+
pantheon signup --email you@work.com --username you or by flags: get a 6-digit code by email (check spam)
|
|
851
1051
|
pantheon verify <code> confirm it — you're in
|
|
852
1052
|
pantheon login --email you@work.com a second machine, same account
|
|
853
1053
|
pantheon whoami · pantheon sessions who you are; your devices & sessions
|
|
@@ -893,6 +1093,8 @@ ${B("control")}
|
|
|
893
1093
|
pantheon trust [<who>] the teammates' keys this machine checks their messages against; trust <who> adopts a changed one
|
|
894
1094
|
(this machine only — nothing is sent to the team; --space <team> to pick one)
|
|
895
1095
|
pantheon admit <who> --revoke remove them
|
|
1096
|
+
pantheon recover [<team>] lost the machine your seat was on? sign in on this one and move it here (a teammate confirms,
|
|
1097
|
+
or it waits a day); recover confirm|cancel <who> is the teammate's side
|
|
896
1098
|
pantheon worker stay answerable while you are away
|
|
897
1099
|
(--agent claude-code|codex|cursor|copilot|kiro|auto, --persona <name>)
|
|
898
1100
|
pantheon policy what this machine's worker may do, in one screen (grant/revoke self-upgrade)
|
|
@@ -995,6 +1197,7 @@ const COMMAND_FLAGS = {
|
|
|
995
1197
|
admit: "<member> --may-wake | --no-wake --revoke --forget --yes",
|
|
996
1198
|
wake: "allow <member> | deny <member> | list --space <team>",
|
|
997
1199
|
trust: "[<member>] --yes --space <team>",
|
|
1200
|
+
recover: "[<team>] | withdraw <team> | confirm <member> | cancel <member> [--yes] --space <team> --no-worker --no-hook --no-notifications --no-menubar",
|
|
998
1201
|
say: "<ACT> <text> --to <member>[,<member>] --goal <id> --ref <id | seq | last> --summary '<one line>' --domains <a,b> --branch --commit --remote (DELIVERED) --space <team>",
|
|
999
1202
|
goal: "list | open \"<title>\" --with <member>[,…] [--acceptance \"…\"] [--deadline <ISO time>] [--list] | close <id> \"<summary>\"",
|
|
1000
1203
|
history: "--goal <id> --from <member> --last <n> --since <seq> --short --presence --json",
|
|
@@ -1010,10 +1213,11 @@ const COMMAND_FLAGS = {
|
|
|
1010
1213
|
use: "<team> --here --yes · pantheon use --unbind (this folder or repo bound to no team again)",
|
|
1011
1214
|
repo: "add [--base <branch>] | list [--all] | find [<folder>] [--recents] [--yes]",
|
|
1012
1215
|
login: "--email <address> --relay <url> --label <device name>",
|
|
1216
|
+
signin: "--relay <url>",
|
|
1013
1217
|
signup: "--email <address> --username <name> --relay <url> --label <device name>",
|
|
1014
1218
|
audit: "--space <team> --since <hours> --json",
|
|
1015
1219
|
setup: "--questions (and every answer as a flag, with --yes)",
|
|
1016
|
-
board: "--open --url --watch --keep --stop",
|
|
1220
|
+
board: "--open --url --watch --keep --stop --focus <id>",
|
|
1017
1221
|
};
|
|
1018
1222
|
const HELP_OPAQUE = new Set(["mcp", "hook", "pending", "tray-status", "help", "version", "-v", "--help", "-h"]);
|
|
1019
1223
|
function rememberWorkingProxy() {
|
|
@@ -1104,7 +1308,7 @@ async function main() {
|
|
|
1104
1308
|
if (helpAt.some((a) => a === "--help" || a === "-h")) {
|
|
1105
1309
|
const u = usageFor(cmd, HELP_ALL);
|
|
1106
1310
|
const flags = COMMAND_FLAGS[cmd];
|
|
1107
|
-
const teamCmd = !["start", "join", "signup", "login", "verify", "whoami", "relay", "help", "version", "upgrade", "network", "ps", "stop", "service", "hook", "mcp", "pending", "tray-status", "uninstall"].includes(cmd);
|
|
1311
|
+
const teamCmd = !["start", "join", "signup", "signin", "login", "verify", "whoami", "relay", "help", "version", "upgrade", "network", "ps", "stop", "service", "hook", "mcp", "pending", "tray-status", "uninstall"].includes(cmd);
|
|
1108
1312
|
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;
|
|
1109
1313
|
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);
|
|
1110
1314
|
return;
|
|
@@ -1186,12 +1390,17 @@ async function main() {
|
|
|
1186
1390
|
menubar: !argv.includes("--no-menubar"),
|
|
1187
1391
|
unattended: wp.project,
|
|
1188
1392
|
workerHow: wp.how,
|
|
1393
|
+
beforeCreate: lastTeamNotice,
|
|
1189
1394
|
log: (l) => console.error(l),
|
|
1190
1395
|
});
|
|
1191
1396
|
for (const t of r.tools)
|
|
1192
1397
|
console.error(` ${t.tool.padEnd(14)}${t.status}`);
|
|
1193
1398
|
for (const w of r.warnings)
|
|
1194
1399
|
console.error(`\x1b[33m ! ${w}\x1b[0m` + (/NOT_SIGNED_IN/.test(w) ? signInHint("NOT_SIGNED_IN", r.cfg.relay) : ""));
|
|
1400
|
+
const planWarning = r.claimed ? undefined : r.warnings.find((w) => /PLAN_LIMIT/.test(w));
|
|
1401
|
+
if (planWarning)
|
|
1402
|
+
for (const l of await teamLimitAdvice(planWarning))
|
|
1403
|
+
console.error(` ${l}`);
|
|
1195
1404
|
printGranted(r.granted);
|
|
1196
1405
|
printRelayNote(r.cfg?.relay);
|
|
1197
1406
|
rememberWorkingProxy();
|
|
@@ -1225,6 +1434,7 @@ async function main() {
|
|
|
1225
1434
|
console.error(D(` ${r.reach.hint}`));
|
|
1226
1435
|
}
|
|
1227
1436
|
if (r.claimed) {
|
|
1437
|
+
await bindProjectFromScratch();
|
|
1228
1438
|
await bindReposHere(argv.includes("--yes"));
|
|
1229
1439
|
await workerAfterBinding(wp.how);
|
|
1230
1440
|
windowsOverridesForBoundRepos();
|
|
@@ -1389,17 +1599,25 @@ async function main() {
|
|
|
1389
1599
|
die(e.message);
|
|
1390
1600
|
const named = flag("relay") ?? process.env.PANTHEON_RELAY;
|
|
1391
1601
|
const candidates = (named ? [named] : [load()?.relay, DEFAULT_RELAY]).filter((r) => !!r);
|
|
1392
|
-
parsed = await resolveInviteCode(invite, candidates).catch((err) =>
|
|
1602
|
+
parsed = await resolveInviteCode(invite, candidates).catch(async (err) => {
|
|
1603
|
+
const c = /INVITE_CODE_UNREACHABLE/.test(err.message) && err.nonJsonFrom ? await checkRelayCertificate(err.nonJsonFrom).catch(() => null) : null;
|
|
1604
|
+
const rest = err.message.replace(/^INVITE_CODE_[A-Z]+:\s*/, "");
|
|
1605
|
+
if (c?.publicCa === false) {
|
|
1606
|
+
const x = interceptionExplanation(c);
|
|
1607
|
+
die(`${x.why} — ${x.fix}.\n${rest.split("\n").slice(1).join("\n")}`);
|
|
1608
|
+
}
|
|
1609
|
+
return die(rest);
|
|
1610
|
+
});
|
|
1393
1611
|
console.error(D(`That was just the code — it opens "${parsed.space}" on ${new URL(parsed.relay).host}. Joining that team.\n`));
|
|
1394
1612
|
}
|
|
1395
1613
|
if (!identity()?.account && sameHost(parsed.relay, DEFAULT_RELAY) && process.stdin.isTTY) {
|
|
1396
1614
|
const ok = await interactiveOnboard(parsed.relay, { joining: parsed.space });
|
|
1397
1615
|
if (!ok || !identity()?.account)
|
|
1398
|
-
die(`not signed in yet — the invite is still good.\n When the code arrives: pantheon verify <code
|
|
1616
|
+
die(`not signed in yet — the invite is still good.\n When the code arrives: pantheon verify <code> (check spam if it is not in your inbox)\n Then run this same \`pantheon join\` command again.`);
|
|
1399
1617
|
console.error("");
|
|
1400
1618
|
}
|
|
1401
1619
|
if (!identity()?.account && sameHost(parsed.relay, DEFAULT_RELAY)) {
|
|
1402
|
-
die(`sign in first — this relay verifies email.\n
|
|
1620
|
+
die(`sign in first — this relay verifies email.\n${signInSteps().join("\n")}\n Then run this same \`pantheon join\` command again — the invite is still good.\n ${SIGNIN_FOR_AGENTS}`);
|
|
1403
1621
|
}
|
|
1404
1622
|
const wp = unattendedProject();
|
|
1405
1623
|
console.error(`${B("pantheon join")}\n`);
|
|
@@ -1422,8 +1640,11 @@ async function main() {
|
|
|
1422
1640
|
}
|
|
1423
1641
|
}
|
|
1424
1642
|
throw e;
|
|
1425
|
-
}).catch((e) => {
|
|
1643
|
+
}).catch(async (e) => {
|
|
1426
1644
|
const m = e.message;
|
|
1645
|
+
const intercepted = await interceptedText(e, parsed.relay);
|
|
1646
|
+
if (intercepted)
|
|
1647
|
+
die(intercepted);
|
|
1427
1648
|
if (/BAD_INVITE/.test(m)) {
|
|
1428
1649
|
const body = m.replace(/^BAD_INVITE:\s*/, "");
|
|
1429
1650
|
die(/\n/.test(body) || /fresh one|new invite/i.test(body) ? body : `${body}\n Ask your teammate for a fresh one: pantheon invite`);
|
|
@@ -1478,6 +1699,7 @@ async function main() {
|
|
|
1478
1699
|
await findAndBindRepos(all, m, teamRepos, { auto: !process.stdin.isTTY }).catch(() => { });
|
|
1479
1700
|
}
|
|
1480
1701
|
}
|
|
1702
|
+
await bindProjectFromScratch();
|
|
1481
1703
|
await workerAfterBinding(wp.how);
|
|
1482
1704
|
windowsOverridesForBoundRepos();
|
|
1483
1705
|
{
|
|
@@ -2027,6 +2249,8 @@ async function main() {
|
|
|
2027
2249
|
const worker = workerProc();
|
|
2028
2250
|
const notifier = notifierProc();
|
|
2029
2251
|
const cfg = load();
|
|
2252
|
+
const client = cfg ? new PantheonClient(cfg, undefined, { role: "cli" }) : null;
|
|
2253
|
+
const upP = client ? client.connect().then(() => true, () => false) : Promise.resolve(false);
|
|
2030
2254
|
console.log(B("this machine"));
|
|
2031
2255
|
if (relay.running) {
|
|
2032
2256
|
const how = relay.managedBy === "launchd"
|
|
@@ -2044,8 +2268,16 @@ async function main() {
|
|
|
2044
2268
|
}
|
|
2045
2269
|
}
|
|
2046
2270
|
else {
|
|
2047
|
-
|
|
2048
|
-
|
|
2271
|
+
const { relayRowsWhenNoneHere, relayReachable } = await import("./machine.js");
|
|
2272
|
+
const relays = [...new Set([...Object.values(loadAll()?.spaces ?? {}).map((m) => m.relay), ...(cfg?.relay ? [cfg.relay] : [])])];
|
|
2273
|
+
const reach = Object.fromEntries(await Promise.all(relays.map(async (u) => [u,
|
|
2274
|
+
await Promise.race([u === cfg?.relay ? upP : relayReachable(u), new Promise((r) => setTimeout(() => r(null), 5_000))])])));
|
|
2275
|
+
const rows = relayRowsWhenNoneHere(relays, port, reach);
|
|
2276
|
+
for (const [i, r] of rows.entries()) {
|
|
2277
|
+
const head = i === 0 ? " relay " : " ";
|
|
2278
|
+
const named = r.line !== "not running";
|
|
2279
|
+
console.log(`${head}${named ? r.line : D(r.line)}${i === 0 && relay.label && !named ? D(` (service ${relay.label} is installed but down)`) : ""}${r.dim ? ` ${D(r.dim)}` : ""}`);
|
|
2280
|
+
}
|
|
2049
2281
|
}
|
|
2050
2282
|
if (worker.running) {
|
|
2051
2283
|
console.log(` worker ${C("running")} ${worker.managedBy === "launchd" ? `service ${worker.label}` : worker.managedBy === "host" ? `background host (${worker.label})` : "started by hand"} pid ${worker.pid}`);
|
|
@@ -2055,6 +2287,8 @@ async function main() {
|
|
|
2055
2287
|
const paused = worker.managedBy === "host" && existsSync(hostFiles().disabled("worker"));
|
|
2056
2288
|
console.log(` worker ${D(paused ? "paused" : "not running")} ${D(paused ? "pantheon service start worker resumes it" : "nobody can start an agent run on this machine")}`);
|
|
2057
2289
|
}
|
|
2290
|
+
for (const l of await workerServesLines())
|
|
2291
|
+
console.log(` ${l}`);
|
|
2058
2292
|
const menubar = menubarProc();
|
|
2059
2293
|
if (process.platform === "win32") {
|
|
2060
2294
|
const st = readHostState();
|
|
@@ -2086,7 +2320,7 @@ async function main() {
|
|
|
2086
2320
|
}
|
|
2087
2321
|
console.log(`\n${B("wired into")}`);
|
|
2088
2322
|
for (const w of wiring())
|
|
2089
|
-
console.log(` ${w.tool.padEnd(14)}${w.state
|
|
2323
|
+
console.log(` ${w.tool.padEnd(14)}${w.state.startsWith("not wired") ? D(w.state) : w.state}`);
|
|
2090
2324
|
const hk = agentHookStates();
|
|
2091
2325
|
const intent = readIntent();
|
|
2092
2326
|
if (!hk.length) {
|
|
@@ -2099,20 +2333,21 @@ async function main() {
|
|
|
2099
2333
|
? `\x1b[33mpartial\x1b[0m — ${summary}. Re-run: pantheon hook install`
|
|
2100
2334
|
: `on — ${summary}`}`);
|
|
2101
2335
|
}
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
console.log(` ${"".padEnd(14)}${
|
|
2336
|
+
const repaired = repairNote(intent?.repairs);
|
|
2337
|
+
if (repaired) {
|
|
2338
|
+
console.log(` ${"".padEnd(14)}${repaired.recurring ? `\x1b[33m${repaired.said}\x1b[0m` : D(repaired.said)}`);
|
|
2339
|
+
if (repaired.why)
|
|
2340
|
+
console.log(` ${"".padEnd(14)}${D(repaired.why)}`);
|
|
2105
2341
|
}
|
|
2106
2342
|
const pers = loadPersonas(process.cwd());
|
|
2107
2343
|
if (pers.length) {
|
|
2108
2344
|
console.log(` ${"personas".padEnd(14)}${pers.map((p) => p.name).join(", ")} ${D("worker takes only matched work — pantheon persona list")}`);
|
|
2109
2345
|
}
|
|
2110
|
-
if (!cfg) {
|
|
2346
|
+
if (!cfg || !client) {
|
|
2111
2347
|
console.log(`\n${D("not joined to a space yet — run: pantheon start")}`);
|
|
2112
2348
|
return;
|
|
2113
2349
|
}
|
|
2114
|
-
const
|
|
2115
|
-
const up = await client.connect().then(() => true).catch(() => false);
|
|
2350
|
+
const up = await upP;
|
|
2116
2351
|
console.log(`\n${B("space")} ${cfg.space} you are ${C(cfg.member)}`);
|
|
2117
2352
|
if (!up) {
|
|
2118
2353
|
console.log(` ${D(client.pending ? "you are not admitted here right now" : "cannot reach the relay from this machine")}`);
|
|
@@ -2881,10 +3116,10 @@ async function main() {
|
|
|
2881
3116
|
console.log(`account ${C(cfg.account.username)} ${D(`<${cfg.account.email}>`)} ${D("verified")}`);
|
|
2882
3117
|
}
|
|
2883
3118
|
else if (cfg.pendingAuth) {
|
|
2884
|
-
console.log(`account ${D(`not signed in yet — a code was sent to ${cfg.pendingAuth.email}: pantheon verify <code>`)}`);
|
|
3119
|
+
console.log(`account ${D(`not signed in yet — a code was sent to ${cfg.pendingAuth.email} (check spam): pantheon verify <code>`)}`);
|
|
2885
3120
|
}
|
|
2886
3121
|
else {
|
|
2887
|
-
console.log(`account ${D("not signed in")} ${D("— pantheon signup / pantheon login")}`);
|
|
3122
|
+
console.log(`account ${D("not signed in")} ${D("— pantheon signin (in a terminal), or pantheon signup / pantheon login")}`);
|
|
2888
3123
|
}
|
|
2889
3124
|
console.log(`member ${C(cfg.member)}`);
|
|
2890
3125
|
const teams = Object.values(loadAll()?.spaces ?? {}).map((m) => m.name);
|
|
@@ -2894,6 +3129,26 @@ async function main() {
|
|
|
2894
3129
|
console.log(D(`\nManage your devices and sessions: pantheon sessions`));
|
|
2895
3130
|
return;
|
|
2896
3131
|
}
|
|
3132
|
+
case "signin": {
|
|
3133
|
+
const relayArg = flag("relay");
|
|
3134
|
+
const had = identity();
|
|
3135
|
+
if (had?.account && (!relayArg || sameHost(relayArg, had.relay))) {
|
|
3136
|
+
console.error(`${C("already signed in")} as ${B(had.account.username)} <${had.account.email}> on this machine.`);
|
|
3137
|
+
console.error(D(` A different account? Sign this machine out first: pantheon logout — then pantheon signin again.`));
|
|
3138
|
+
console.error(` ${B("Tell your agent to continue;")} it will run the join or start again.`);
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
3141
|
+
if (!process.stdin.isTTY) {
|
|
3142
|
+
die(`pantheon signin asks for your email and then the code it emails you, so it needs a terminal to type into.\n` +
|
|
3143
|
+
` Run it in a terminal window of your own: ${pantheonCommand(managedLayout()?.launcher, `signin${relayArg ? ` --relay ${relayArg}` : ""}`)}\n` +
|
|
3144
|
+
` Without a terminal, two commands do the same:\n` +
|
|
3145
|
+
` pantheon signup --email you@work.com --username you${relayArg ? ` --relay ${relayArg}` : ""} (an account already: pantheon login --email you@work.com${relayArg ? ` --relay ${relayArg}` : ""})\n` +
|
|
3146
|
+
` pantheon verify <code> (the code is emailed — check spam if it is not in your inbox)`);
|
|
3147
|
+
}
|
|
3148
|
+
if (!await interactiveOnboard(relayArg, { signin: true }))
|
|
3149
|
+
process.exit(1);
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
2897
3152
|
case "signup":
|
|
2898
3153
|
case "login": {
|
|
2899
3154
|
const purpose = cmd;
|
|
@@ -2910,20 +3165,20 @@ async function main() {
|
|
|
2910
3165
|
const base = relay.replace(/^ws/, "http").replace(/\/+$/, "");
|
|
2911
3166
|
const body = purpose === "signup" ? { email, username, pubkey: kp.publicKey, label } : { email, pubkey: kp.publicKey, label };
|
|
2912
3167
|
const r = await fetch(`${base}/auth/${purpose}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })
|
|
2913
|
-
.then(async (x) => ({ status: x.status, j: await x
|
|
2914
|
-
.catch((e) => die(
|
|
3168
|
+
.then(async (x) => ({ status: x.status, j: await relayJson(x, `${base}/auth/${purpose}`) }))
|
|
3169
|
+
.catch(async (e) => die(await describeRelayFailure(e, relay)));
|
|
2915
3170
|
if (r.status !== 200)
|
|
2916
3171
|
die(String(r.j.error ?? `signup failed (${r.status})`));
|
|
2917
3172
|
save({ ...(prior ?? {}), relay, member: username ?? prior?.member ?? "me", space: prior?.space ?? "", token: prior?.token ?? "",
|
|
2918
3173
|
publicKey: kp.publicKey, privateKey: kp.privateKey, agent: prior?.agent ?? "unknown",
|
|
2919
|
-
pendingAuth: { email, ...(username ? { username } : {}), purpose } });
|
|
3174
|
+
pendingAuth: { email, ...(username ? { username } : {}), purpose, at: Date.now() } });
|
|
2920
3175
|
if (r.j.sent === false && r.j.via === "resend") {
|
|
2921
3176
|
console.error(`${B("signup accepted, but the email could not be sent just now.")}`);
|
|
2922
3177
|
console.error(`Retry in a minute: ${C(`pantheon ${purpose} --email ${email}${username ? ` --username ${username}` : ""}`)}`);
|
|
2923
3178
|
return;
|
|
2924
3179
|
}
|
|
2925
3180
|
const via = r.j.via === "resend" ? "" : D(" (dev relay: the code is in the relay log)");
|
|
2926
|
-
console.error(`${B("check your email")} — we sent a 6-digit code to ${C(email)}
|
|
3181
|
+
console.error(`${B("check your email")} — we sent a 6-digit code to ${C(email)}. ${SPAM_HINT}${via}`);
|
|
2927
3182
|
console.error(`Then run: ${C("pantheon verify <code>")}`);
|
|
2928
3183
|
return;
|
|
2929
3184
|
}
|
|
@@ -2933,15 +3188,15 @@ async function main() {
|
|
|
2933
3188
|
let code = (argv[1] ?? flag("code") ?? "").trim();
|
|
2934
3189
|
if (!code && process.stdin.isTTY) {
|
|
2935
3190
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
2936
|
-
code = (await questionOrNull(rl, `Enter the 6-digit code sent to ${pending.email}: `)) ?? "";
|
|
3191
|
+
code = (await questionOrNull(rl, `Enter the 6-digit code sent to ${pending.email} (not in your inbox? check spam): `)) ?? "";
|
|
2937
3192
|
rl.close();
|
|
2938
3193
|
}
|
|
2939
3194
|
if (!code)
|
|
2940
3195
|
die("usage: pantheon verify <6-digit-code>");
|
|
2941
3196
|
const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
|
|
2942
3197
|
const r = await fetch(`${base}/auth/verify`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email: pending.email, code, pubkey: cfg.publicKey }) })
|
|
2943
|
-
.then(async (x) => ({ status: x.status, j: await x
|
|
2944
|
-
.catch((e) => die(
|
|
3198
|
+
.then(async (x) => ({ status: x.status, j: await relayJson(x, `${base}/auth/verify`) }))
|
|
3199
|
+
.catch(async (e) => die(await describeRelayFailure(e, cfg.relay)));
|
|
2945
3200
|
if (r.status !== 200)
|
|
2946
3201
|
die(String(r.j.error ?? "verification failed"));
|
|
2947
3202
|
const acct = r.j.account;
|
|
@@ -3300,6 +3555,172 @@ async function main() {
|
|
|
3300
3555
|
client.close();
|
|
3301
3556
|
return;
|
|
3302
3557
|
}
|
|
3558
|
+
case "recover": {
|
|
3559
|
+
if (inWokenRun())
|
|
3560
|
+
die("a woken run cannot move a seat or vouch for one — a person has to, on their own machine");
|
|
3561
|
+
const sub = argv[1] && !argv[1].startsWith("--") ? argv[1] : null;
|
|
3562
|
+
if (sub === "confirm" || sub === "cancel") {
|
|
3563
|
+
const who = argv[2] && !argv[2].startsWith("--") ? argv[2] : die(`usage: pantheon recover ${sub} <member> [--space <team>]`);
|
|
3564
|
+
const cfg = need();
|
|
3565
|
+
const client = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
3566
|
+
await client.connect().catch((e) => die(client.pending ?? connectFailure(e, cfg.relay)));
|
|
3567
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
3568
|
+
const e = client.members.find((x) => x.member === who);
|
|
3569
|
+
if (!e?.recovery) {
|
|
3570
|
+
client.close();
|
|
3571
|
+
die(`no move of ${who}'s seat is waiting in "${cfg.space}"`);
|
|
3572
|
+
}
|
|
3573
|
+
const { fp, label, requested_at, due_at } = e.recovery;
|
|
3574
|
+
console.log(`${B(`${who} asked to move their seat in ${cfg.space} to a new device`)}`);
|
|
3575
|
+
console.log(` device "${label}"`);
|
|
3576
|
+
console.log(` new key ${C(fp)}`);
|
|
3577
|
+
console.log(D(` asked ${ago(requested_at)} ago, after signing in with the code emailed to their account; it moves on its own ${new Date(due_at).toLocaleString()} unless someone cancels`));
|
|
3578
|
+
if (sub === "confirm") {
|
|
3579
|
+
if (who === cfg.member) {
|
|
3580
|
+
client.close();
|
|
3581
|
+
die("a seat move is confirmed by a teammate, not by the person whose seat it is");
|
|
3582
|
+
}
|
|
3583
|
+
console.log(D(`\n Check it with ${who} over a call or a channel you trust: they run pantheon whoami on the new machine and read you "this device". Confirm only when it matches.`));
|
|
3584
|
+
}
|
|
3585
|
+
else if (who === cfg.member) {
|
|
3586
|
+
console.log(D(`\n If you did not ask for this, someone has signed in to your email account: cancel it, then secure that account.`));
|
|
3587
|
+
}
|
|
3588
|
+
let go = false;
|
|
3589
|
+
if (sub === "confirm") {
|
|
3590
|
+
if (!process.stdin.isTTY) {
|
|
3591
|
+
client.close();
|
|
3592
|
+
die(`confirming is yours to do, in a terminal: pantheon recover confirm ${who}${teamFlag(cfg)} — after ${who} reads you the key from their new machine`);
|
|
3593
|
+
}
|
|
3594
|
+
const last = fp.split("-").at(-1);
|
|
3595
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
3596
|
+
const a = ((await questionOrNull(rl, `Type the last group of the key ${who} reads you (4 characters), or press Enter to stop: `)) ?? "").trim().toLowerCase();
|
|
3597
|
+
rl.close();
|
|
3598
|
+
if (a && a !== last) {
|
|
3599
|
+
client.close();
|
|
3600
|
+
die(`that does not match the key waiting (…-${last}). Nothing changed — check again with ${who}.`);
|
|
3601
|
+
}
|
|
3602
|
+
go = a === last;
|
|
3603
|
+
}
|
|
3604
|
+
else {
|
|
3605
|
+
go = argv.includes("--yes");
|
|
3606
|
+
if (!go && process.stdin.isTTY) {
|
|
3607
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
3608
|
+
const a = ((await questionOrNull(rl, `Cancel the move of ${who}'s seat? [y/N] `)) ?? "").trim().toLowerCase();
|
|
3609
|
+
rl.close();
|
|
3610
|
+
go = a === "y" || a === "yes";
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
if (!go) {
|
|
3614
|
+
client.close();
|
|
3615
|
+
console.log(D(process.stdin.isTTY || sub === "confirm" ? " nothing changed" : ` nothing changed — to cancel it: pantheon recover cancel ${who} --yes${teamFlag(cfg)}`));
|
|
3616
|
+
return;
|
|
3617
|
+
}
|
|
3618
|
+
const body = sub === "confirm"
|
|
3619
|
+
? `I checked with ${who}: the new key ${fp} is theirs. Confirming the move of their seat.`
|
|
3620
|
+
: `Cancelling the move of ${who}'s seat to the key ${fp}${who === cfg.member ? " — I did not ask for it" : ""}.`;
|
|
3621
|
+
await client.send({ act: "NOTE", body, to: null, meta: { recovery: { member: who, fp, action: sub, ...(sub === "confirm" ? { from_fp: e.fingerprint } : {}) } } })
|
|
3622
|
+
.catch((err) => { client.close(); die(err.message); });
|
|
3623
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
3624
|
+
const { loadPins: lp, pinFor: pf } = await import("./pins.js");
|
|
3625
|
+
const pinned = pf(lp(), client.pinKey(), who)?.fp === fp;
|
|
3626
|
+
client.close();
|
|
3627
|
+
console.log(sub === "confirm"
|
|
3628
|
+
? `${C("confirmed")} — ${who}'s seat is on their new device${pinned ? ", and this machine trusts the new key" : ""}. Teammates' machines take it from your signed confirmation.`
|
|
3629
|
+
: `${C("cancelled")} — ${who}'s seat stays on its current key. The team was told.`);
|
|
3630
|
+
return;
|
|
3631
|
+
}
|
|
3632
|
+
const id = identity() ?? die("not set up on this machine — sign in first, in a terminal: pantheon signin");
|
|
3633
|
+
if (!id.account)
|
|
3634
|
+
die("sign in on this machine first, in a terminal: pantheon signin — the code emailed to you is what proves the seat is yours");
|
|
3635
|
+
const acct = id.account;
|
|
3636
|
+
const base0 = id.relay.replace(/^ws/, "http").replace(/\/+$/, "");
|
|
3637
|
+
const call = (method, path, params = {}) => signedFetch(id, method, path, params)
|
|
3638
|
+
.then(async (x) => ({ status: x.status, j: await relayJson(x, `${base0}${path}`) }))
|
|
3639
|
+
.catch(async (e) => die(await describeRelayFailure(e, id.relay)));
|
|
3640
|
+
const noRecovery = (j) => /no accounts/.test(String(j.error ?? ""))
|
|
3641
|
+
? "this relay has no accounts, so seats cannot be recovered by email here. A teammate retires the old key instead (pantheon admit <you> --forget) and invites you again."
|
|
3642
|
+
: "this relay is older than 0.33.2 and cannot move a seat yet — ask its operator to upgrade it, or have a teammate retire the old key (pantheon admit <you> --forget) and invite you again.";
|
|
3643
|
+
if (sub === "withdraw") {
|
|
3644
|
+
const team = argv[2] ?? die("usage: pantheon recover withdraw <team>");
|
|
3645
|
+
const r = await call("POST", "/account/recover/withdraw", { space: team });
|
|
3646
|
+
if (r.status !== 200)
|
|
3647
|
+
die(String(r.j.error ?? `the relay said ${r.status}`));
|
|
3648
|
+
console.log(`${C("withdrawn")} — your seat in ${team} stays where it was.`);
|
|
3649
|
+
return;
|
|
3650
|
+
}
|
|
3651
|
+
if (!sub) {
|
|
3652
|
+
const r = await call("GET", "/account/recover");
|
|
3653
|
+
if (r.status === 404)
|
|
3654
|
+
die(noRecovery(r.j));
|
|
3655
|
+
if (r.status !== 200)
|
|
3656
|
+
die(String(r.j.error ?? `the relay said ${r.status}`));
|
|
3657
|
+
const seats = (r.j.seats ?? []);
|
|
3658
|
+
console.log(`${B(`Your seats as ${acct.username}`)} ${D(`(this device: ${fingerprint(id.publicKey)})`)}\n`);
|
|
3659
|
+
if (!seats.length)
|
|
3660
|
+
console.log(D(" none — you are in no team on this relay yet"));
|
|
3661
|
+
for (const s of seats) {
|
|
3662
|
+
const name = s.space.padEnd(18);
|
|
3663
|
+
const line = s.state === "here" ? D("on this machine") :
|
|
3664
|
+
s.state === "ready" ? `moved here — save it on this machine: ${C(`pantheon recover ${s.space}`)}` :
|
|
3665
|
+
s.state === "waiting" ? `waiting — moves ${new Date(s.due_at).toLocaleString()} unless someone cancels; a teammate can confirm it sooner` :
|
|
3666
|
+
s.state === "movable" ? `on another of your devices (${s.fp}) — move it here: ${C(`pantheon recover ${s.space}`)}${D(s.alone ? " (you are alone in it: at once)" : " (a teammate confirms, or it waits a day)")}` :
|
|
3667
|
+
D(`its key ${s.fp} was never signed in to your account here, so it cannot be moved by email — a teammate who has checked it is you: pantheon admit ${acct.username} --forget, then a new invite`);
|
|
3668
|
+
console.log(` ${name}${line}${s.founder ? D(" (founder)") : ""}`);
|
|
3669
|
+
}
|
|
3670
|
+
return;
|
|
3671
|
+
}
|
|
3672
|
+
const team = sub;
|
|
3673
|
+
let r = await call("POST", "/account/recover", { space: team });
|
|
3674
|
+
if (r.status === 403 && r.j.reason === "sign_in_again" && process.stdin.isTTY) {
|
|
3675
|
+
console.error(`${B("First, prove it is you")} — a code goes to ${C(acct.email)} (look in spam too if it is not there in a minute).`);
|
|
3676
|
+
const base = id.relay.replace(/^ws/, "http").replace(/\/+$/, "");
|
|
3677
|
+
const post = (path, body) => fetch(`${base}${path}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })
|
|
3678
|
+
.then(async (x) => ({ status: x.status, j: await relayJson(x, `${base}${path}`) }))
|
|
3679
|
+
.catch(async (e) => die(await describeRelayFailure(e, id.relay)));
|
|
3680
|
+
const sent = await post("/auth/login", { email: acct.email, pubkey: id.publicKey, label: deviceLabel() });
|
|
3681
|
+
if (sent.status !== 200)
|
|
3682
|
+
die(String(sent.j.error ?? `the relay said ${sent.status}`));
|
|
3683
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
3684
|
+
let ok = false;
|
|
3685
|
+
for (let i = 0; i < 3 && !ok; i++) {
|
|
3686
|
+
const code = ((await questionOrNull(rl, `Enter the 6-digit code sent to ${acct.email}: `)) ?? "").trim();
|
|
3687
|
+
if (!code)
|
|
3688
|
+
break;
|
|
3689
|
+
const v = await post("/auth/verify", { email: acct.email, code, pubkey: id.publicKey });
|
|
3690
|
+
if (v.status === 200)
|
|
3691
|
+
ok = true;
|
|
3692
|
+
else
|
|
3693
|
+
console.error(` ${String(v.j.error ?? "that code did not work")}`);
|
|
3694
|
+
}
|
|
3695
|
+
rl.close();
|
|
3696
|
+
if (!ok)
|
|
3697
|
+
die(`not verified — run pantheon recover ${team} again for a new code`);
|
|
3698
|
+
r = await call("POST", "/account/recover", { space: team });
|
|
3699
|
+
}
|
|
3700
|
+
if (r.status === 404 && !r.j.reason)
|
|
3701
|
+
die(noRecovery(r.j));
|
|
3702
|
+
if (r.status >= 400)
|
|
3703
|
+
die(String(r.j.error ?? `the relay said ${r.status}`));
|
|
3704
|
+
if (r.j.state === "waiting") {
|
|
3705
|
+
const others = (r.j.others ?? []);
|
|
3706
|
+
console.log(`${B(`Asked: your seat in ${team} moves to this machine`)} ${D(`(new key ${String(r.j.fp)})`)}`);
|
|
3707
|
+
console.log(` It moves when a teammate confirms it — ${others.join(", ") || "your teammates"} ${others.length === 1 ? "was" : "were"} told — or on its own ${new Date(Number(r.j.due_at)).toLocaleString()} if nobody cancels.`);
|
|
3708
|
+
console.log(` Speed it up: ask one of them to check your key with you (read them "this device" from pantheon whoami ) and run ${C(`pantheon recover confirm ${acct.username} --space ${team}`)}`);
|
|
3709
|
+
console.log(D(` Once it has moved, run pantheon recover ${team} again here to save the team on this machine.`));
|
|
3710
|
+
return;
|
|
3711
|
+
}
|
|
3712
|
+
const token = String(r.j.invite ?? "");
|
|
3713
|
+
if (r.j.state !== "moved" || !token)
|
|
3714
|
+
die(`unexpected answer from the relay: ${JSON.stringify(r.j).slice(0, 200)}`);
|
|
3715
|
+
const how = r.j.how === "alone" ? " (you are alone in it)" : r.j.how === "confirmed" ? " (a teammate confirmed it)" : r.j.how === "waited" ? " (nobody cancelled in the time it waited)" : "";
|
|
3716
|
+
console.error(`${C("moved")} — your seat in ${team} is on this machine now${how}. Saving the team here, as a join does:\n`);
|
|
3717
|
+
console.error(D(` If the old machine is lost or not yours any more, sign it out of your account too: pantheon sessions (then pantheon sessions revoke <device-id> --device)\n`));
|
|
3718
|
+
const { spawnSync: run } = await import("node:child_process");
|
|
3719
|
+
const invite = toInvite({ ...id, space: String(r.j.space ?? team), token });
|
|
3720
|
+
const keep = argv.slice(2).filter((a) => /^--(no-worker|no-hook|no-notifications|no-menubar|yes)$/.test(a));
|
|
3721
|
+
const res = run(process.execPath, [process.argv[1], "join", invite, ...keep], { stdio: "inherit" });
|
|
3722
|
+
process.exit(res.status ?? 1);
|
|
3723
|
+
}
|
|
3303
3724
|
case "trust": {
|
|
3304
3725
|
const { WOKEN_RUN_ENV } = await import("./worker-policy.js");
|
|
3305
3726
|
const inRun = process.env[WOKEN_RUN_ENV] !== undefined || /[\\/]run-homes[\\/]/.test(process.env.PANTHEON_HOME ?? "");
|
|
@@ -3561,8 +3982,8 @@ async function main() {
|
|
|
3561
3982
|
home: uriHome,
|
|
3562
3983
|
wokenRun: inWokenRun(),
|
|
3563
3984
|
runQueued: (actionId) => spawnSync(process.execPath, self(["explain", "--queued", "--only-action", "--action-id", actionId]), { stdio: "inherit", env: childEnv }).status ?? 1,
|
|
3564
|
-
openBoard: hasBoard ? () => new Promise((done) => {
|
|
3565
|
-
const c = spawn(process.execPath, self(["board", "--open"]), { detached: true, stdio: "ignore", windowsHide: true, env: childEnv });
|
|
3985
|
+
openBoard: hasBoard ? (focus) => new Promise((done) => {
|
|
3986
|
+
const c = spawn(process.execPath, self(["board", "--open", ...(focus ? ["--focus", focus] : [])]), { detached: true, stdio: "ignore", windowsHide: true, env: childEnv });
|
|
3566
3987
|
const t = setTimeout(() => { c.unref(); done(true); }, 4_000);
|
|
3567
3988
|
c.on("error", () => { clearTimeout(t); done(false); });
|
|
3568
3989
|
c.on("exit", (status) => { clearTimeout(t); done(status === 0); });
|
|
@@ -3794,6 +4215,42 @@ async function main() {
|
|
|
3794
4215
|
console.log(`sent RETRACT id=${sent.id} seq=${sent.seq}`);
|
|
3795
4216
|
return;
|
|
3796
4217
|
}
|
|
4218
|
+
if (hud?.action === "wake") {
|
|
4219
|
+
const notSent = (why) => { console.log(`not sent: ${why}`); process.exit(1); };
|
|
4220
|
+
const { loadPolicy, setLocalWaker, markTeamFlagOnYes, applyLocalAnswer, describeLocalAnswer } = await import("./worker-policy.js");
|
|
4221
|
+
const ref = hud.act;
|
|
4222
|
+
const wa = ref ? loadPolicy().wakeAsks?.[ref] : undefined;
|
|
4223
|
+
if (!wa || !hud.member || wa.member !== hud.member)
|
|
4224
|
+
notSent("that question is no longer open on this machine — pantheon wake list shows who may wake it");
|
|
4225
|
+
if (![cfg.spaceId, cfg.space].includes(wa.team))
|
|
4226
|
+
notSent(`that question is about the team "${wa.team}", not "${hud.space ?? cfg.space}" — answer it from that team's board`);
|
|
4227
|
+
const allow = hud.allow === true;
|
|
4228
|
+
const r = setLocalWaker(allow ? [wa.team] : [wa.team, cfg.spaceId, cfg.space], wa.member, allow);
|
|
4229
|
+
if (!r.ok)
|
|
4230
|
+
notSent(r.why);
|
|
4231
|
+
if (allow && wa.teamBlocked)
|
|
4232
|
+
markTeamFlagOnYes(wa.team, wa.member);
|
|
4233
|
+
const body = allow ? "allow always" : "deny";
|
|
4234
|
+
const g = applyLocalAnswer(ref, body);
|
|
4235
|
+
if (g)
|
|
4236
|
+
console.error(describeLocalAnswer(g));
|
|
4237
|
+
const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
4238
|
+
try {
|
|
4239
|
+
await c2.connect();
|
|
4240
|
+
const o = c2.obligations.find((x) => x.id === hud.decision || x.ref === ref);
|
|
4241
|
+
if (o) {
|
|
4242
|
+
const sent = await c2.send({ act: "ANSWER", ref: ref, goal: null, after_seq: c2.readSeq, body, meta: { surface: "menubar" } });
|
|
4243
|
+
console.log(`sent ANSWER id=${sent.id} seq=${sent.seq}`);
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
catch (e) {
|
|
4247
|
+
console.error(D(` recorded on this machine; the question could not be marked answered on the relay (${e.message.split("\n")[0]}) — it leaves your board once it is`));
|
|
4248
|
+
}
|
|
4249
|
+
finally {
|
|
4250
|
+
c2.close();
|
|
4251
|
+
}
|
|
4252
|
+
return;
|
|
4253
|
+
}
|
|
3797
4254
|
if (hud?.action === "done") {
|
|
3798
4255
|
const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
3799
4256
|
await c2.connect().catch((e) => die(connectFailure(e, cfg.relay)));
|
|
@@ -4005,6 +4462,8 @@ async function main() {
|
|
|
4005
4462
|
}
|
|
4006
4463
|
}
|
|
4007
4464
|
if (hud.prompt) {
|
|
4465
|
+
if (await claudeAppInstead(hud.tool ?? p.agent.primary_surface, hud.prompt))
|
|
4466
|
+
return;
|
|
4008
4467
|
const agent = pickExplainAgent(hud.tool ?? p.agent.primary_surface, detectAvailable());
|
|
4009
4468
|
if (!agent)
|
|
4010
4469
|
die("no agent CLI found on this machine (claude, cursor-agent, codex, copilot, or kiro-cli)");
|
|
@@ -4059,6 +4518,8 @@ async function main() {
|
|
|
4059
4518
|
console.log(D("no decisions waiting on you — nothing to explain"));
|
|
4060
4519
|
return;
|
|
4061
4520
|
}
|
|
4521
|
+
if (await claudeAppInstead(loadProfile(one?.cfg ?? cfg).agent.primary_surface, prompt))
|
|
4522
|
+
return;
|
|
4062
4523
|
const agent = pickExplainAgent(loadProfile(one?.cfg ?? cfg).agent.primary_surface, detectAvailable());
|
|
4063
4524
|
if (!agent)
|
|
4064
4525
|
die("no agent CLI found on this machine (claude, cursor-agent, codex, copilot, or kiro-cli)");
|
|
@@ -4175,7 +4636,7 @@ async function main() {
|
|
|
4175
4636
|
(await import("./machine.js")).recordWorkerPid();
|
|
4176
4637
|
const allCfg = loadAll();
|
|
4177
4638
|
const bindings = Object.values(allCfg?.bindings ?? {});
|
|
4178
|
-
if (!flag("project")
|
|
4639
|
+
if (!flag("project")) {
|
|
4179
4640
|
const { WorkerFleet } = await import("./worker-fleet.js");
|
|
4180
4641
|
const fleet = new WorkerFleet({
|
|
4181
4642
|
...(flag("agent-cmd") ? { agentCmd: flag("agent-cmd"), agentCmdFromCli: true } : { agent: flag("agent", "auto") }),
|
|
@@ -4195,7 +4656,7 @@ async function main() {
|
|
|
4195
4656
|
}),
|
|
4196
4657
|
log: (l) => console.error(l),
|
|
4197
4658
|
});
|
|
4198
|
-
console.error(`${B("pantheon worker")} ${D(`(${bindings.length} bound folder${bindings.length === 1 ? "" : "s"} · ctrl-c to stop)`)}`);
|
|
4659
|
+
console.error(`${B("pantheon worker")} ${D(`(${bindings.length ? `${bindings.length} bound folder${bindings.length === 1 ? "" : "s"}` : "nothing bound yet — a folder bound with pantheon use <team> --here is served as soon as it is"} · ctrl-c to stop)`)}`);
|
|
4199
4660
|
if (argv.includes("--allow-bash")) {
|
|
4200
4661
|
console.error(`\x1b[33m --allow-bash: a woken run may execute commands. Only do this if you trust every\x1b[0m`);
|
|
4201
4662
|
console.error(`\x1b[33m teammate approved to wake this machine (pantheon wake list), since their request is what starts the run.\x1b[0m`);
|
|
@@ -4264,7 +4725,11 @@ async function main() {
|
|
|
4264
4725
|
if (!argv.includes("--open"))
|
|
4265
4726
|
return;
|
|
4266
4727
|
}
|
|
4267
|
-
const
|
|
4728
|
+
const focus = flag("focus");
|
|
4729
|
+
if (focus !== undefined && !b.FOCUS_RE.test(focus))
|
|
4730
|
+
die("--focus takes an act or decision id (a_… or o_…)");
|
|
4731
|
+
const plain = argv.includes("--url") ? await b.boardLinkFor(r.state).catch((e) => die(e.message)) : r.url;
|
|
4732
|
+
const url = focus && r.state.version === VERSION ? b.focusLink(plain, focus) : plain;
|
|
4268
4733
|
const opened = argv.includes("--no-browser") ? false : await b.openBrowser(url).catch(() => false);
|
|
4269
4734
|
console.error(`${B("pantheon board")} ${D(r.reused ? "(already running)" : "(started)")}`);
|
|
4270
4735
|
if (!opened)
|
|
@@ -4272,6 +4737,8 @@ async function main() {
|
|
|
4272
4737
|
else
|
|
4273
4738
|
console.error(D(" opened in your browser · pantheon board --url prints its address · --watch shows it in this terminal"));
|
|
4274
4739
|
console.error(D(r.state.keep ? " it stays running" : " it stops itself after 30 minutes with no open tab"));
|
|
4740
|
+
if (!opened && argv.includes("--open") && !argv.includes("--no-browser"))
|
|
4741
|
+
process.exit(1);
|
|
4275
4742
|
return;
|
|
4276
4743
|
}
|
|
4277
4744
|
case "tray": {
|
|
@@ -4694,7 +5161,7 @@ async function main() {
|
|
|
4694
5161
|
}
|
|
4695
5162
|
}
|
|
4696
5163
|
}
|
|
4697
|
-
main().catch((e) => die(
|
|
5164
|
+
main().catch(async (e) => die(await describeRelayFailure(e, (() => { try {
|
|
4698
5165
|
return load()?.relay;
|
|
4699
5166
|
}
|
|
4700
5167
|
catch {
|