@join-pantheon/cli 0.24.4 → 0.25.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 +26 -1
- package/dist/admin-stats.js +106 -0
- package/dist/cli.js +77 -10
- package/dist/client.js +2 -0
- package/dist/config.js +28 -0
- package/dist/decision-ui.js +31 -9
- package/dist/doctor.js +118 -62
- package/dist/feedback.js +3 -0
- package/dist/grants.js +38 -17
- package/dist/humanize.js +2 -0
- package/dist/install.js +97 -21
- package/dist/machine.js +12 -5
- package/dist/mcp-instructions.js +85 -0
- package/dist/mcp-supervisor.js +2 -1
- package/dist/mcp.js +47 -82
- package/dist/measure.js +142 -0
- package/dist/notice.js +7 -2
- package/dist/relay.js +86 -2
- package/dist/roster-file.js +17 -0
- package/dist/run-failure.js +97 -0
- package/dist/service.js +9 -1
- package/dist/setup-guide.js +30 -2
- package/dist/tools.js +9 -2
- package/dist/tray.js +10 -3
- package/dist/upgrade.js +11 -1
- package/dist/worker-policy.js +3 -1
- package/dist/worker.js +107 -13
- package/helper/macos/main.swift +22 -1
- package/helper/macos/menubar.swift +51 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,6 +87,19 @@ asks you in chat, and passes your answers back as flags — to `pantheon setup -
|
|
|
87
87
|
`pantheon start` / `pantheon join`, which apply them the moment the team exists — so nobody has to
|
|
88
88
|
open a terminal. Restart your agent once and ask it *"check pantheon_status"*.
|
|
89
89
|
|
|
90
|
+
One thing the setup grants that you should know about: your own agent runs the Pantheon tools
|
|
91
|
+
without a per-call prompt, in Claude Code, Cursor and Codex, so it can coordinate for you without
|
|
92
|
+
stopping to ask. For Codex that is one line, `default_tools_approval_mode = "approve"`, written under
|
|
93
|
+
`[mcp_servers.pantheon]` in `~/.codex/config.toml` — it covers Pantheon's tools only, never the shell or
|
|
94
|
+
any other server. The install output prints that exact line, the defaults screen states the grant,
|
|
95
|
+
`pantheon doctor` shows it, and `pantheon leave` removes it. If you would rather approve each message
|
|
96
|
+
your agent sends in your name, `pantheon setup --ask-before-sending` (or the same flag on start/join)
|
|
97
|
+
makes reads silent and prompts for send, open goal and claim; `--approve-all` switches back.
|
|
98
|
+
|
|
99
|
+
If all you were sent is the code — `pantheon join iIF-uHY8…` with no team or link — that works
|
|
100
|
+
too: the relay names the team the code opens, and the join proceeds. A spent or expired code says
|
|
101
|
+
so and asks for a fresh invite; a relay that cannot be reached asks you for the whole line instead.
|
|
102
|
+
|
|
90
103
|
Already a member — new laptop, or just pulled a newer build? The same full setup is one command:
|
|
91
104
|
`pantheon install --user` (from your project's repo if you have one).
|
|
92
105
|
|
|
@@ -166,6 +179,18 @@ Checks node, your keypair, the MCP entry, whether the repo has a remote (`DELIVE
|
|
|
166
179
|
whether the relay is reachable **from this machine**, whether a plain `ws://` is crossing the public
|
|
167
180
|
internet, and whether you were actually admitted. Non-zero exit means don't start yet.
|
|
168
181
|
|
|
182
|
+
It also measures what Pantheon costs your agent, on this machine rather than by estimate: it starts
|
|
183
|
+
the MCP server the way your agent does and times its answer (`mcp startup`, typically ~0.2s), reports
|
|
184
|
+
the instruction text every turn carries (`mcp context`), and lists how many tools the server
|
|
185
|
+
registers against how many each agent on this machine has been told to allow (`mcp tools`). If an
|
|
186
|
+
allow-list has drifted — a tool that would prompt on every call, or one allowed against your
|
|
187
|
+
`--ask-before-sending` choice — the line names the tool, and `pantheon install --user` fixes it.
|
|
188
|
+
|
|
189
|
+
If something is wrong and you want to tell us, `pantheon doctor --share` prints the same run with
|
|
190
|
+
nothing identifying in it — no member or team names, no teammates, no paths, no relay address, no
|
|
191
|
+
fingerprint — ready to paste into an email to sid@joinpantheon.network. Nothing is ever sent by the
|
|
192
|
+
product itself; you read it, and you decide.
|
|
193
|
+
|
|
169
194
|
<details><summary>The long way round, if you want to control each piece</summary>
|
|
170
195
|
|
|
171
196
|
`pantheon relay` · `pantheon init` · `pantheon join` · `pantheon members` · `pantheon admit <who> --may-wake`
|
|
@@ -462,7 +487,7 @@ pantheon join <line> join a teammate's team from their line
|
|
|
462
487
|
pantheon invite another single-use line (third person, second machine)
|
|
463
488
|
pantheon status · brief · team who is here, what needs you, who is in the space
|
|
464
489
|
pantheon watch live tail of everything the delegates say to each other
|
|
465
|
-
pantheon doctor
|
|
490
|
+
pantheon doctor [--share] preflight: node, keys, MCP entry, relay, membership, what pantheon costs your agent; --share = paste-safe
|
|
466
491
|
pantheon worker / service stay answerable while you are away (install as a service)
|
|
467
492
|
pantheon persona list / init / show — what a woken worker may do
|
|
468
493
|
pantheon spaces / use / repo several teams on one machine — what is bound where; bind this repo; a team's repos
|
package/dist/admin-stats.js
CHANGED
|
@@ -113,4 +113,110 @@ export function feed(records, f = {}) {
|
|
|
113
113
|
const rows = more ? hits.slice(0, limit) : hits;
|
|
114
114
|
return { rows, next: more ? rows[rows.length - 1].seq : null };
|
|
115
115
|
}
|
|
116
|
+
export const VERDICT_ORDER = ["went quiet", "never started", "new", "active", "idle"];
|
|
117
|
+
export function versionBehind(pkg, relay) {
|
|
118
|
+
const a = pkg.split(".").map(Number), b = relay.split(".").map(Number);
|
|
119
|
+
for (let i = 0; i < 3; i++) {
|
|
120
|
+
const x = a[i] ?? 0, y = b[i] ?? 0;
|
|
121
|
+
if (Number.isNaN(x) || Number.isNaN(y))
|
|
122
|
+
return false;
|
|
123
|
+
if (x !== y)
|
|
124
|
+
return x < y;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
export const SELF_UPGRADE_SINCE = "0.24.0";
|
|
129
|
+
export function stuckBehind(pkg, relay) {
|
|
130
|
+
if (!versionBehind(pkg, relay))
|
|
131
|
+
return false;
|
|
132
|
+
if (versionBehind(pkg, SELF_UPGRADE_SINCE))
|
|
133
|
+
return true;
|
|
134
|
+
const [aMaj = 0, aMin = 0] = pkg.split(".").map(Number), [bMaj = 0, bMin = 0] = relay.split(".").map(Number);
|
|
135
|
+
return aMaj < bMaj || bMin - aMin >= 2;
|
|
136
|
+
}
|
|
137
|
+
const daysAgo = (ts, now) => Math.max(0, Math.floor((now - ts) / DAY));
|
|
138
|
+
export function spaceHealth(input) {
|
|
139
|
+
const now = input.now ?? Date.now();
|
|
140
|
+
const admitted = input.members.filter((m) => m.admitted);
|
|
141
|
+
const messages = input.records.filter((r) => r.act !== "JOIN" && r.act !== "LEAVE");
|
|
142
|
+
let messages_7d = 0, messages_prev_21d = 0, last_message = null;
|
|
143
|
+
const days = new Set();
|
|
144
|
+
for (const r of messages) {
|
|
145
|
+
const d = daysAgo(r.ts, now);
|
|
146
|
+
if (d < 7)
|
|
147
|
+
messages_7d++;
|
|
148
|
+
else if (d < 28) {
|
|
149
|
+
messages_prev_21d++;
|
|
150
|
+
days.add(d);
|
|
151
|
+
}
|
|
152
|
+
if (last_message === null || r.ts > last_message)
|
|
153
|
+
last_message = r.ts;
|
|
154
|
+
}
|
|
155
|
+
const seenAt = (m) => m.last_seen ?? m.first_seen;
|
|
156
|
+
const last_seen = admitted.length ? Math.max(...admitted.map(seenAt)) : null;
|
|
157
|
+
const born = input.members.length ? Math.min(...input.members.map((m) => m.first_seen)) : now;
|
|
158
|
+
const age = daysAgo(born, now);
|
|
159
|
+
const versions = {};
|
|
160
|
+
const behind = [];
|
|
161
|
+
for (const m of admitted) {
|
|
162
|
+
if (!m.last_pkg)
|
|
163
|
+
continue;
|
|
164
|
+
versions[m.last_pkg] = (versions[m.last_pkg] ?? 0) + 1;
|
|
165
|
+
if (stuckBehind(m.last_pkg, input.relayPkg))
|
|
166
|
+
behind.push({ member: m.member, pkg: m.last_pkg, days: daysAgo(seenAt(m), now) });
|
|
167
|
+
}
|
|
168
|
+
behind.sort((a, b) => (versionBehind(a.pkg, b.pkg) ? -1 : versionBehind(b.pkg, a.pkg) ? 1 : a.days - b.days));
|
|
169
|
+
const newest = admitted.filter((m) => m.last_pkg).sort((a, b) => seenAt(b) - seenAt(a))[0];
|
|
170
|
+
const seenSays = last_seen === null ? "" : `; last seen ${daysAgo(last_seen, now)}d ago${newest ? ` on ${newest.last_pkg}` : ""}`;
|
|
171
|
+
let verdict, says;
|
|
172
|
+
if (input.online > 0 || messages_7d > 0) {
|
|
173
|
+
verdict = "active";
|
|
174
|
+
says = `${messages_7d} message${messages_7d === 1 ? "" : "s"} this week${input.online ? `, ${input.online} online now` : ""}`;
|
|
175
|
+
}
|
|
176
|
+
else if (days.size >= 3) {
|
|
177
|
+
verdict = "went quiet";
|
|
178
|
+
says = `${messages_prev_21d} messages on ${days.size} days in the three weeks before, none this week${seenSays}`;
|
|
179
|
+
}
|
|
180
|
+
else if (age < 7) {
|
|
181
|
+
verdict = "new";
|
|
182
|
+
says = `set up ${age}d ago${messages.length ? "" : ", no message yet"}`;
|
|
183
|
+
}
|
|
184
|
+
else if (messages.length === 0 && age < 28) {
|
|
185
|
+
verdict = "never started";
|
|
186
|
+
says = `set up ${age}d ago, not one message${seenSays}`;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
verdict = "idle";
|
|
190
|
+
const since = last_message ?? last_seen;
|
|
191
|
+
says = since === null ? "nothing on record" : `quiet for ${daysAgo(since, now)}d${seenSays}`;
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
name: input.name, verdict, says, members: admitted.length, online: input.online,
|
|
195
|
+
messages_7d, messages_prev_21d, active_days_prev_21d: days.size, last_message, last_seen, versions, behind,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
export function fleetReport(rows, relayPkg, now = Date.now()) {
|
|
199
|
+
const date = new Date(now).toISOString().slice(0, 10);
|
|
200
|
+
const counts = VERDICT_ORDER.map((v) => [v, rows.filter((r) => r.verdict === v).length]).filter(([, n]) => n > 0);
|
|
201
|
+
const lines = [
|
|
202
|
+
`pantheon relay ${relayPkg} — the week to ${date}`,
|
|
203
|
+
`${rows.length} space${rows.length === 1 ? "" : "s"}${counts.map(([v, n]) => ` · ${n} ${v}`).join("")}`,
|
|
204
|
+
];
|
|
205
|
+
const sorted = [...rows].sort((a, b) => VERDICT_ORDER.indexOf(a.verdict) - VERDICT_ORDER.indexOf(b.verdict) || b.messages_7d - a.messages_7d || a.name.localeCompare(b.name));
|
|
206
|
+
const width = Math.max(4, ...sorted.map((r) => r.name.length));
|
|
207
|
+
for (const r of sorted)
|
|
208
|
+
lines.push(`${r.verdict.padEnd(13)} ${r.name.padEnd(width)} ${r.says}`);
|
|
209
|
+
const builds = {};
|
|
210
|
+
const behind = [];
|
|
211
|
+
for (const r of rows) {
|
|
212
|
+
for (const [v, n] of Object.entries(r.versions))
|
|
213
|
+
builds[v] = (builds[v] ?? 0) + n;
|
|
214
|
+
for (const b of r.behind)
|
|
215
|
+
behind.push(`${b.member} in ${r.name} on ${b.pkg}, ${b.days}d ago`);
|
|
216
|
+
}
|
|
217
|
+
const spread = Object.keys(builds).sort((a, b) => (versionBehind(a, b) ? 1 : versionBehind(b, a) ? -1 : 0)).map((v) => `${v} ×${builds[v]}`).join(" · ");
|
|
218
|
+
if (spread)
|
|
219
|
+
lines.push(`builds in use: ${spread}${behind.length ? ` (behind the relay: ${behind.join("; ")})` : ""}`);
|
|
220
|
+
return lines.join("\n");
|
|
221
|
+
}
|
|
116
222
|
//# sourceMappingURL=admin-stats.js.map
|
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 { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { basename, dirname, join, resolve } from "node:path";
|
|
12
12
|
import { PantheonClient } from "./client.js";
|
|
13
|
-
import { fromInvite, load, save, toInvite, toJoinLink, resolveFor, loadAll, repoInfo, bindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY, resolveForHook } from "./config.js";
|
|
13
|
+
import { fromInvite, looksLikeInviteCode, resolveInviteCode, load, save, toInvite, toJoinLink, resolveFor, loadAll, repoInfo, bindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY, resolveForHook } from "./config.js";
|
|
14
14
|
import { fmtLimit } from "./plans.js";
|
|
15
15
|
import { validEmail, validUsername } from "./accounts.js";
|
|
16
|
-
import { install, launcher } from "./install.js";
|
|
16
|
+
import { install, launcher, installForUser, removeCodexUserScope } from "./install.js";
|
|
17
17
|
import { Relay } from "./relay.js";
|
|
18
18
|
import { runMcp } from "./mcp.js";
|
|
19
19
|
import { Worker } from "./worker.js";
|
|
@@ -22,7 +22,8 @@ import { desktopUnavailable, hostEnv } from "./host-env.js";
|
|
|
22
22
|
import { AGENT_SURFACES, applyProfileInit, cleanList, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile, wakeDefault } from "./profile.js";
|
|
23
23
|
import { runGuide, terminalIO, parseSetupFlags, questionnaire } from "./setup-guide.js";
|
|
24
24
|
import { selectActs, formatAct, resolveRef } from "./history-view.js";
|
|
25
|
-
import { doctor, render } from "./doctor.js";
|
|
25
|
+
import { doctor, render, shareBlock } from "./doctor.js";
|
|
26
|
+
import { FEEDBACK_ADDRESS } from "./feedback.js";
|
|
26
27
|
import { menubarProc, notifierProc, relayProc, workerProc, wiring, logFileFor, dataSummary, stopProc, statFile } from "./machine.js";
|
|
27
28
|
import { assess } from "./untrusted.js";
|
|
28
29
|
import { report as spendReport, recentRuns } from "./spend.js";
|
|
@@ -500,7 +501,7 @@ ${B("control")}
|
|
|
500
501
|
pantheon spend woken runs on this machine, and what pantheon cannot see
|
|
501
502
|
pantheon leave remove what pantheon put on this machine
|
|
502
503
|
pantheon panic cut off all unattended work on this machine, now
|
|
503
|
-
pantheon doctor
|
|
504
|
+
pantheon doctor [--share] check this machine end to end; --share prints it without names or paths, to paste to us
|
|
504
505
|
pantheon setup three questions — your name, your role, may a woken run execute commands —
|
|
505
506
|
then one screen of defaults to keep or change: who may wake this machine,
|
|
506
507
|
alerts, what you personally decide, playbook, base branch. Re-run any time.
|
|
@@ -727,7 +728,7 @@ async function main() {
|
|
|
727
728
|
return;
|
|
728
729
|
}
|
|
729
730
|
case "join": {
|
|
730
|
-
const invite = argv[1] ?? die("usage: pantheon join <invite> (pantheons://host/space#token,
|
|
731
|
+
const invite = argv[1] ?? die("usage: pantheon join <invite> (pantheons://host/space#token, https://www.joinpantheon.network/join/<team>#<token>, or just the code) [setup answers as flags — pantheon setup --questions]");
|
|
731
732
|
const setupFlags = parseSetupFlags(argv);
|
|
732
733
|
if (setupFlags.errors.length)
|
|
733
734
|
die(setupFlags.errors.join("\n") + "\n pantheon setup --questions lists every answer");
|
|
@@ -736,7 +737,11 @@ async function main() {
|
|
|
736
737
|
parsed = fromInvite(invite);
|
|
737
738
|
}
|
|
738
739
|
catch (e) {
|
|
739
|
-
|
|
740
|
+
if (!looksLikeInviteCode(invite))
|
|
741
|
+
die(e.message);
|
|
742
|
+
const candidates = [flag("relay"), process.env.PANTHEON_RELAY, load()?.relay, DEFAULT_RELAY].filter((r) => !!r);
|
|
743
|
+
parsed = await resolveInviteCode(invite, candidates).catch((err) => die(err.message.replace(/^INVITE_CODE_[A-Z]+:\s*/, "")));
|
|
744
|
+
console.error(D(`That was just the code — it opens "${parsed.space}" on ${new URL(parsed.relay).host}. Joining that team.\n`));
|
|
740
745
|
}
|
|
741
746
|
if (!load()?.account && sameHost(parsed.relay, DEFAULT_RELAY)) {
|
|
742
747
|
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.`);
|
|
@@ -810,6 +815,16 @@ async function main() {
|
|
|
810
815
|
console.error(`\n${B("Restart your agent")}, then ask it: "check pantheon_status and introduce yourself to the team".`);
|
|
811
816
|
return;
|
|
812
817
|
}
|
|
818
|
+
case "rewire": {
|
|
819
|
+
const rows = installForUser().map((t) => ({ tool: t.tool, status: t.status }));
|
|
820
|
+
if (argv.includes("--json")) {
|
|
821
|
+
console.log(JSON.stringify(rows));
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
for (const r of rows)
|
|
825
|
+
console.error(` ${r.tool.padEnd(14)}${r.status}`);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
813
828
|
case "upgrade": {
|
|
814
829
|
const { upgrade } = await import("./upgrade.js");
|
|
815
830
|
console.error(`${B("pantheon upgrade")}\n`);
|
|
@@ -1603,6 +1618,11 @@ async function main() {
|
|
|
1603
1618
|
const { revokeToolPermissions } = await import("./grants.js");
|
|
1604
1619
|
for (const l of revokeToolPermissions())
|
|
1605
1620
|
console.error(` perms:${l}`);
|
|
1621
|
+
{
|
|
1622
|
+
const l = removeCodexUserScope();
|
|
1623
|
+
if (l)
|
|
1624
|
+
console.error(` perms:${l}`);
|
|
1625
|
+
}
|
|
1606
1626
|
const { unlinkSync, existsSync: exists, rmSync } = await import("node:fs");
|
|
1607
1627
|
if (cfg && exists(CONFIG_PATH)) {
|
|
1608
1628
|
if (argv.includes("--keep-identity")) {
|
|
@@ -1620,12 +1640,13 @@ async function main() {
|
|
|
1620
1640
|
else {
|
|
1621
1641
|
console.error(` config: none to remove`);
|
|
1622
1642
|
}
|
|
1623
|
-
console.error(`\n ${D("Not touched: the MCP
|
|
1624
|
-
console.error(` ${D("the relay's ledger on disk, and anything you pushed to git.")}`);
|
|
1643
|
+
console.error(`\n ${D("Not touched: the MCP entries in Claude Code and Cursor (pantheon install wrote them; remove them there),")}`);
|
|
1644
|
+
console.error(` ${D("the relay's ledger on disk, and anything you pushed to git. Codex's entry and approval line were removed above.")}`);
|
|
1625
1645
|
if (cfg) {
|
|
1626
1646
|
console.error(`\n ${D(`Your teammates still list you in "${cfg.space}". Ask one of them to run:`)}`);
|
|
1627
1647
|
console.error(` ${D(`pantheon admit ${cfg.member} --revoke`)}`);
|
|
1628
1648
|
}
|
|
1649
|
+
console.error(`\n ${D(`If Pantheon let you down, one line on why to ${FEEDBACK_ADDRESS} would help more than you might think.`)}`);
|
|
1629
1650
|
return;
|
|
1630
1651
|
}
|
|
1631
1652
|
case "spend": {
|
|
@@ -2201,6 +2222,46 @@ async function main() {
|
|
|
2201
2222
|
hud = null;
|
|
2202
2223
|
}
|
|
2203
2224
|
}
|
|
2225
|
+
if (hud?.action === "fix") {
|
|
2226
|
+
const dir = hud.dir && existsSync(hud.dir) ? hud.dir : process.cwd();
|
|
2227
|
+
const steps = hud.lines ?? [];
|
|
2228
|
+
console.error(`${B("What stopped it")}\n ${hud.why ?? "(not recorded)"}\n`);
|
|
2229
|
+
console.error(`${B("Where")} ${dir}\n`);
|
|
2230
|
+
if (!steps.length) {
|
|
2231
|
+
console.error(D("Nothing to run from here."));
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
console.error(B("Steps"));
|
|
2235
|
+
for (const l of steps)
|
|
2236
|
+
console.error(` ${C(l)}`);
|
|
2237
|
+
if (!process.stdin.isTTY)
|
|
2238
|
+
return;
|
|
2239
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
2240
|
+
try {
|
|
2241
|
+
const answer = (await new Promise((res) => rl.question(`\n${D("run these here now? [y/N] ")}`, res))).trim().toLowerCase();
|
|
2242
|
+
if (answer !== "y" && answer !== "yes") {
|
|
2243
|
+
console.error(D("left for you — the commands are above"));
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
const { spawnSync } = await import("node:child_process");
|
|
2247
|
+
for (const line of steps) {
|
|
2248
|
+
const cmd = line.replace(/\s+#.*$/, "").trim();
|
|
2249
|
+
if (!cmd)
|
|
2250
|
+
continue;
|
|
2251
|
+
console.error(D(`\n$ ${cmd}`));
|
|
2252
|
+
const r = spawnSync("sh", ["-lc", cmd], { cwd: dir, stdio: "inherit" });
|
|
2253
|
+
if (r.status !== 0) {
|
|
2254
|
+
console.error(`\n${B("stopped")} — "${cmd}" exited ${r.status}. The rest is yours.`);
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
console.error(`\n${B("done")} — if that was the blocker, the next run can finish.`);
|
|
2259
|
+
}
|
|
2260
|
+
finally {
|
|
2261
|
+
rl.close();
|
|
2262
|
+
}
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2204
2265
|
if (hud?.action === "done") {
|
|
2205
2266
|
const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
2206
2267
|
await c2.connect().catch((e) => die(e.message));
|
|
@@ -2576,8 +2637,14 @@ async function main() {
|
|
|
2576
2637
|
}
|
|
2577
2638
|
case "doctor": {
|
|
2578
2639
|
const dir = resolve(flag("project", "."));
|
|
2579
|
-
|
|
2640
|
+
const share = argv.includes("--share");
|
|
2641
|
+
if (!share)
|
|
2642
|
+
console.log(`${B("pantheon doctor")} ${D(dir)}\n`);
|
|
2580
2643
|
const checks = await doctor(dir);
|
|
2644
|
+
if (share) {
|
|
2645
|
+
console.log(shareBlock(checks));
|
|
2646
|
+
process.exit(checks.some((c) => c.level === "fail") ? 1 : 0);
|
|
2647
|
+
}
|
|
2581
2648
|
const { text, failed } = render(checks);
|
|
2582
2649
|
console.log(text);
|
|
2583
2650
|
process.exit(failed ? 1 : 0);
|
package/dist/client.js
CHANGED
package/dist/config.js
CHANGED
|
@@ -305,4 +305,32 @@ export function fromInvite(invite) {
|
|
|
305
305
|
token,
|
|
306
306
|
};
|
|
307
307
|
}
|
|
308
|
+
export const INVITE_CODE = /^[A-Za-z0-9_-]{16,64}$/;
|
|
309
|
+
export const looksLikeInviteCode = (s) => INVITE_CODE.test(s.trim().replace(/^['"`]|['"`]$/g, ""));
|
|
310
|
+
export async function resolveInviteCode(code, relays, fetchImpl = fetch) {
|
|
311
|
+
const token = code.trim().replace(/^['"`]|['"`]$/g, "");
|
|
312
|
+
const tried = [...new Set(relays.map((r) => r.replace(/\/+$/, "")).filter(Boolean))];
|
|
313
|
+
let answered = false;
|
|
314
|
+
for (const relay of tried) {
|
|
315
|
+
const http = relay.replace(/^ws/, "http");
|
|
316
|
+
try {
|
|
317
|
+
const res = await fetchImpl(`${http}/join?token=${encodeURIComponent(token)}`, { signal: AbortSignal.timeout(6_000) });
|
|
318
|
+
if (res.status === 404) {
|
|
319
|
+
answered = true;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (!res.ok)
|
|
323
|
+
continue;
|
|
324
|
+
const body = (await res.json());
|
|
325
|
+
if (typeof body.team === "string" && body.team)
|
|
326
|
+
return { relay, space: body.team, token };
|
|
327
|
+
}
|
|
328
|
+
catch { }
|
|
329
|
+
}
|
|
330
|
+
const hosts = tried.map((r) => new URL(r).host).join(", ");
|
|
331
|
+
if (answered) {
|
|
332
|
+
throw new Error(`INVITE_CODE_UNKNOWN: that is an invite code, but no open invite on ${hosts} matches it — it may have been used already or expired (they last 24 hours).\n Ask your teammate for a fresh one: pantheon invite`);
|
|
333
|
+
}
|
|
334
|
+
throw new Error(`INVITE_CODE_UNREACHABLE: that looks like an invite code, but ${hosts} could not be asked which team it opens.\n Paste the whole invite line instead: pantheon join https://www.joinpantheon.network/join/<team>#${token}`);
|
|
335
|
+
}
|
|
308
336
|
//# sourceMappingURL=config.js.map
|
package/dist/decision-ui.js
CHANGED
|
@@ -1,16 +1,38 @@
|
|
|
1
1
|
const oneLine = (value) => value.replace(/\s+/g, " ").trim();
|
|
2
|
+
const LAYERS = [
|
|
3
|
+
/^([a-z0-9._-]+)\s+escalated\s*:\s*/i,
|
|
4
|
+
/^([a-z0-9._-]+)\s+needs? a decision\s*:\s*/i,
|
|
5
|
+
/^([a-z0-9._-]+)\s+(?:asked|asks|said|says|wrote|reported)\s*:\s*/i,
|
|
6
|
+
/^([a-z0-9._-]+)\s+is\s+(?:asking|saying|reporting)(?:\s+that)?\s*:\s*/i,
|
|
7
|
+
/^decision (?:for|required for|needed from)\s+(?:you|[a-z0-9._-]+)\s*(?:\([^)]*\))?\s*[:—-]\s*/i,
|
|
8
|
+
/^(?:action|input|answer|decision)\s+(?:required|needed)\s*(?:\([^)]*\))?\s*(?:from|for)?\s*(?:you|[a-z0-9._-]+)?\s*[:—-]\s*/i,
|
|
9
|
+
/^needs (?:you|your input|a human)[,:]?\s*([a-z0-9._-]*)\s*[:—-]\s*/i,
|
|
10
|
+
/^human (?:action|input) required\s*[—:-]?\s*/i,
|
|
11
|
+
];
|
|
12
|
+
export function stripCeremony(body) {
|
|
13
|
+
let text = oneLine(body);
|
|
14
|
+
let from;
|
|
15
|
+
for (let round = 0; round < 6; round++) {
|
|
16
|
+
const before = text;
|
|
17
|
+
for (const re of LAYERS) {
|
|
18
|
+
const m = re.exec(text);
|
|
19
|
+
if (!m)
|
|
20
|
+
continue;
|
|
21
|
+
const name = (m[1] ?? "").trim();
|
|
22
|
+
if (!from && name && !/^(you|your|a)$/i.test(name))
|
|
23
|
+
from = name.toLowerCase();
|
|
24
|
+
text = text.slice(m[0].length);
|
|
25
|
+
}
|
|
26
|
+
if (text === before)
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
return { text: text.trim(), ...(from ? { from } : {}) };
|
|
30
|
+
}
|
|
2
31
|
export function decisionTitle(body, max = 96) {
|
|
3
|
-
let text =
|
|
4
|
-
.replace(
|
|
5
|
-
.replace(/^[a-z0-9._-]+\s+needs a decision:\s*/i, "")
|
|
6
|
-
.replace(/^human (?:action|input) required\s*[—:-]\s*/i, "")
|
|
7
|
-
.replace(/^human (?:action|input) required\s*/i, "")
|
|
8
|
-
.replace(/^needs (?:you|your input|a human)[,:]?\s*[a-z0-9._-]*\s*[:—-]\s*/i, "")
|
|
9
|
-
.replace(/\(goal\s+g_[a-z0-9]+\s*,\s*/gi, "(")
|
|
32
|
+
let text = stripCeremony(body).text
|
|
33
|
+
.replace(/\(\s*goal\s+g_[a-z0-9]+\s*,\s*/gi, "(")
|
|
10
34
|
.replace(/\s*\b(?:of|in|for|on|from)?\s*\(?\bgoal\s+g_[a-z0-9]+\)?/gi, "")
|
|
11
35
|
.replace(/\s{2,}/g, " ")
|
|
12
|
-
.replace(/[\s,]+(?:of|in|for|on|at|to|from)$/i, "")
|
|
13
|
-
.replace(/[\s,;:]+$/, "")
|
|
14
36
|
.trim();
|
|
15
37
|
const blockedAsk = text.match(/^cannot answer (.+?)\s+ask\s*:/i);
|
|
16
38
|
if (blockedAsk) {
|