@join-pantheon/cli 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/accounts.js +13 -2
- package/dist/agents.js +377 -9
- package/dist/cli.js +377 -192
- package/dist/client.js +73 -12
- package/dist/config.js +15 -0
- package/dist/doctor.js +13 -10
- package/dist/grants.js +16 -1
- package/dist/hooks-install.js +4 -2
- package/dist/host-env.js +51 -6
- package/dist/install.js +216 -16
- package/dist/kiro-files.js +23 -6
- package/dist/ledger.js +8 -2
- package/dist/machine.js +273 -54
- package/dist/mcp-supervisor.js +5 -0
- package/dist/mcp.js +33 -6
- package/dist/network.js +258 -57
- package/dist/pending.js +14 -7
- package/dist/quickstart.js +180 -21
- package/dist/refusal.js +29 -0
- package/dist/relay.js +686 -440
- package/dist/roster-file.js +19 -3
- package/dist/run-failure.js +18 -7
- package/dist/service.js +173 -40
- package/dist/setup-guide.js +21 -4
- package/dist/store.js +9 -3
- package/dist/which.js +66 -4
- package/dist/workdir.js +7 -1
- package/dist/worker.js +522 -101
- package/helper/macos/menubar.swift +11 -3
- package/package.json +1 -1
package/dist/machine.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
-
import { homedir, platform } from "node:os";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, rmSync, rmdirSync, statSync } from "node:fs";
|
|
3
|
+
import { homedir, hostname, networkInterfaces, platform } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { CONFIG_DIR } from "./config.js";
|
|
6
|
-
import { plistPath } from "./service.js";
|
|
6
|
+
import { disableService, plistPath, serviceInstalled, serviceLogFile, unitPath, uninstallService } from "./service.js";
|
|
7
7
|
import { copilotMcpConfigFile, vsCodeUserDir, kiroMcpConfigFile, kiroDir } from "./install.js";
|
|
8
8
|
const sh = (cmd) => {
|
|
9
9
|
try {
|
|
@@ -46,6 +46,35 @@ function fromLaunchd(kind) {
|
|
|
46
46
|
}
|
|
47
47
|
return info;
|
|
48
48
|
}
|
|
49
|
+
export function parseSystemdShow(out) {
|
|
50
|
+
const get = (k) => new RegExp(`^${k}=(.*)$`, "m").exec(out)?.[1]?.trim();
|
|
51
|
+
const pid = Number(get("MainPID"));
|
|
52
|
+
const state = get("ActiveState");
|
|
53
|
+
const status = get("ExecMainStatus");
|
|
54
|
+
return {
|
|
55
|
+
active: state === "active" || state === "activating" || state === "reloading",
|
|
56
|
+
...(Number.isFinite(pid) && pid > 0 ? { pid } : {}),
|
|
57
|
+
...(status && status !== "0" ? { lastExit: status } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function fromSystemd(kind) {
|
|
61
|
+
if (platform() !== "linux" || !existsSync(unitPath(kind)))
|
|
62
|
+
return {};
|
|
63
|
+
const out = sh(`systemctl --user show pantheon-${kind} -p MainPID -p ActiveState -p ExecMainStatus 2>/dev/null`);
|
|
64
|
+
const label = `pantheon-${kind}.service`;
|
|
65
|
+
if (!out)
|
|
66
|
+
return { managedBy: "systemd", label, running: false };
|
|
67
|
+
const s = parseSystemdShow(out);
|
|
68
|
+
return {
|
|
69
|
+
managedBy: "systemd", label,
|
|
70
|
+
...(s.active && s.pid ? { running: true, pid: s.pid, since: uptime(s.pid) } : { running: false }),
|
|
71
|
+
...(s.lastExit ? { lastExit: s.lastExit } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const fromManager = (kind) => {
|
|
75
|
+
const l = fromLaunchd(kind);
|
|
76
|
+
return l.managedBy ? l : fromSystemd(kind);
|
|
77
|
+
};
|
|
49
78
|
function listenerOn(port) {
|
|
50
79
|
const out = sh(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t 2>/dev/null | head -1`);
|
|
51
80
|
const pid = out ? Number(out) : NaN;
|
|
@@ -54,48 +83,29 @@ function listenerOn(port) {
|
|
|
54
83
|
return { pid, ...(uptime(pid) ? { since: uptime(pid) } : {}) };
|
|
55
84
|
}
|
|
56
85
|
export function relayProc(port) {
|
|
57
|
-
const svc =
|
|
86
|
+
const svc = fromManager("relay");
|
|
58
87
|
const held = listenerOn(port);
|
|
59
88
|
if (svc.running && svc.pid)
|
|
60
|
-
return { kind: "relay", running: true
|
|
89
|
+
return { kind: "relay", ...svc, running: true };
|
|
61
90
|
if (held) {
|
|
62
91
|
return { kind: "relay", running: true, managedBy: "foreground", pid: held.pid, ...(held.since ? { since: held.since } : {}) };
|
|
63
92
|
}
|
|
64
|
-
return { kind: "relay", running: false, managedBy: svc.
|
|
65
|
-
}
|
|
66
|
-
export function workerProc() {
|
|
67
|
-
const svc = fromLaunchd("worker");
|
|
68
|
-
if (svc.running && svc.pid)
|
|
69
|
-
return { kind: "worker", running: true, managedBy: "launchd", ...svc };
|
|
70
|
-
const bare = sh(`pgrep -f "pantheon.* worker" | head -1`);
|
|
71
|
-
const pid = bare ? Number(bare) : NaN;
|
|
72
|
-
if (Number.isFinite(pid)) {
|
|
73
|
-
return { kind: "worker", running: true, managedBy: "foreground", pid, ...(uptime(pid) ? { since: uptime(pid) } : {}) };
|
|
74
|
-
}
|
|
75
|
-
return { kind: "worker", running: false, managedBy: svc.label ? "launchd" : "none", ...svc };
|
|
76
|
-
}
|
|
77
|
-
export function notifierProc() {
|
|
78
|
-
const svc = fromLaunchd("notifier");
|
|
79
|
-
if (svc.running && svc.pid)
|
|
80
|
-
return { kind: "notifier", running: true, managedBy: "launchd", ...svc };
|
|
81
|
-
const bare = sh(`pgrep -f "pantheon.* notifier" | head -1`);
|
|
82
|
-
const pid = bare ? Number(bare) : NaN;
|
|
83
|
-
if (Number.isFinite(pid)) {
|
|
84
|
-
return { kind: "notifier", running: true, managedBy: "foreground", pid, ...(uptime(pid) ? { since: uptime(pid) } : {}) };
|
|
85
|
-
}
|
|
86
|
-
return { kind: "notifier", running: false, managedBy: svc.label ? "launchd" : "none", ...svc };
|
|
93
|
+
return { kind: "relay", running: false, managedBy: svc.managedBy ?? "none", ...svc };
|
|
87
94
|
}
|
|
88
|
-
|
|
89
|
-
const svc =
|
|
95
|
+
function bareProc(kind, pattern) {
|
|
96
|
+
const svc = fromManager(kind);
|
|
90
97
|
if (svc.running && svc.pid)
|
|
91
|
-
return { kind
|
|
92
|
-
const bare = sh(`pgrep -f "
|
|
98
|
+
return { kind, ...svc, running: true };
|
|
99
|
+
const bare = sh(`pgrep -f "${pattern}" | head -1`);
|
|
93
100
|
const pid = bare ? Number(bare) : NaN;
|
|
94
101
|
if (Number.isFinite(pid)) {
|
|
95
|
-
return { kind
|
|
102
|
+
return { kind, running: true, managedBy: "foreground", pid, ...(uptime(pid) ? { since: uptime(pid) } : {}) };
|
|
96
103
|
}
|
|
97
|
-
return { kind
|
|
104
|
+
return { kind, running: false, managedBy: svc.managedBy ?? "none", ...svc };
|
|
98
105
|
}
|
|
106
|
+
export function workerProc() { return bareProc("worker", "pantheon.* worker"); }
|
|
107
|
+
export function notifierProc() { return bareProc("notifier", "pantheon.* notifier"); }
|
|
108
|
+
export function menubarProc() { return bareProc("menubar", "pantheon.* menubar|pantheon-tray"); }
|
|
99
109
|
export function wiring() {
|
|
100
110
|
const out = [];
|
|
101
111
|
const claude = sh(`claude mcp list 2>/dev/null | grep -i "^pantheon"`);
|
|
@@ -129,7 +139,7 @@ export function wiring() {
|
|
|
129
139
|
export function logFileFor(kind, proc) {
|
|
130
140
|
if (proc.logPath && existsSync(proc.logPath))
|
|
131
141
|
return proc.logPath;
|
|
132
|
-
for (const c of [join(process.cwd(), ".pantheon", `${kind}.log`), join(CONFIG_DIR, `${kind}.log`)]) {
|
|
142
|
+
for (const c of [serviceLogFile(kind), join(process.cwd(), ".pantheon", `${kind}.log`), join(CONFIG_DIR, `${kind}.log`)]) {
|
|
133
143
|
if (existsSync(c))
|
|
134
144
|
return c;
|
|
135
145
|
}
|
|
@@ -149,32 +159,223 @@ export function dataSummary(dir) {
|
|
|
149
159
|
const spaces = files.split("\n").length;
|
|
150
160
|
return `${acts} acts across ${spaces} space${spaces > 1 ? "s" : ""}`;
|
|
151
161
|
}
|
|
152
|
-
const defaultStopIo = () => ({
|
|
153
|
-
proc: (kind) => kind === "relay" ? relayProc(
|
|
154
|
-
installed: (kind) =>
|
|
162
|
+
const defaultStopIo = (port = 8787) => ({
|
|
163
|
+
proc: (kind) => kind === "relay" ? relayProc(port) : kind === "worker" ? workerProc() : kind === "menubar" ? menubarProc() : notifierProc(),
|
|
164
|
+
installed: (kind) => serviceInstalled(kind),
|
|
155
165
|
sh,
|
|
166
|
+
disable: (kind) => disableService(kind),
|
|
167
|
+
cmdline: (pid) => sh(`ps -o command= -p ${pid}`),
|
|
156
168
|
});
|
|
157
|
-
export
|
|
169
|
+
export const isPantheonCommand = (cmd) => /pantheon/i.test(cmd) || /\bcli\.js\s+(relay|start)\b/.test(cmd);
|
|
170
|
+
export function stopProcResult(kind, io = defaultStopIo()) {
|
|
158
171
|
const p = io.proc(kind);
|
|
172
|
+
const ours = io.installed(kind);
|
|
173
|
+
if (!ours && (p.managedBy === "launchd" || p.managedBy === "systemd") && p.running) {
|
|
174
|
+
return { lines: [`${kind}: running under ${p.managedBy}, but not installed from this home — left alone`], stopped: false };
|
|
175
|
+
}
|
|
176
|
+
const killLive = (q) => {
|
|
177
|
+
if (!q.running || !q.pid)
|
|
178
|
+
return null;
|
|
179
|
+
if (kind === "relay") {
|
|
180
|
+
const cmd = (io.cmdline ?? (() => null))(q.pid);
|
|
181
|
+
if (!cmd || !isPantheonCommand(cmd)) {
|
|
182
|
+
return { line: `relay: the port is held by pid ${q.pid}${cmd ? ` (${cmd.slice(0, 80)})` : ""}, which is not Pantheon's relay — left alone`, killed: false };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
io.sh(`kill ${q.pid}`);
|
|
186
|
+
return { line: `${kind}: stopped pid ${q.pid}${q.managedBy === "foreground" ? " (it was started by hand, not as a service)" : ""}`, killed: true };
|
|
187
|
+
};
|
|
188
|
+
if (ours) {
|
|
189
|
+
const lines = [];
|
|
190
|
+
let disabled = false;
|
|
191
|
+
try {
|
|
192
|
+
io.disable(kind);
|
|
193
|
+
disabled = true;
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
const how = platform() === "darwin"
|
|
197
|
+
? `launchctl disable gui/${process.getuid?.() ?? 0}/dev.pantheon.${kind}`
|
|
198
|
+
: `systemctl --user disable --now pantheon-${kind}`;
|
|
199
|
+
lines.push(`${kind}: could not disable the service (${e.message.split("\n")[0]}) — it may start again at login; try: ${how}`);
|
|
200
|
+
}
|
|
201
|
+
const after = io.proc(kind);
|
|
202
|
+
const k = killLive(after);
|
|
203
|
+
if (k)
|
|
204
|
+
lines.push(k.line);
|
|
205
|
+
const stillUp = !!after.running && !k?.killed;
|
|
206
|
+
if (disabled) {
|
|
207
|
+
lines.unshift(`${kind}: ${stillUp ? "disabled, but a process is still running (see below)" : p.running || k?.killed ? "stopped, and will not start again at login" : "was not running; disabled, and will not start again at login"}`);
|
|
208
|
+
lines.push(` start it again with: pantheon service install ${kind}`);
|
|
209
|
+
}
|
|
210
|
+
return { lines, stopped: (p.running && disabled && !stillUp) || !!k?.killed };
|
|
211
|
+
}
|
|
159
212
|
if (!p.running)
|
|
160
|
-
return [`${kind}: already stopped`];
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
213
|
+
return { lines: [`${kind}: already stopped`], stopped: false };
|
|
214
|
+
const k = killLive(p);
|
|
215
|
+
if (k)
|
|
216
|
+
return { lines: [k.line], stopped: k.killed };
|
|
217
|
+
return { lines: [`${kind}: running but no pid found — nothing stopped`], stopped: false };
|
|
218
|
+
}
|
|
219
|
+
export function stopProc(kind, io = defaultStopIo()) {
|
|
220
|
+
return stopProcResult(kind, io).lines;
|
|
221
|
+
}
|
|
222
|
+
export async function waitStopped(kind, ms = 3000, proc = defaultStopIo().proc) {
|
|
223
|
+
const until = Date.now() + ms;
|
|
224
|
+
for (;;) {
|
|
225
|
+
if (!proc(kind).running)
|
|
226
|
+
return true;
|
|
227
|
+
if (Date.now() >= until)
|
|
228
|
+
return false;
|
|
229
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
export function hostsRelay(url, port, localAddrs = localAddresses(), host = hostname()) {
|
|
233
|
+
if (!url)
|
|
234
|
+
return false;
|
|
235
|
+
let u;
|
|
236
|
+
try {
|
|
237
|
+
u = new URL(url.replace(/^ws/, "http"));
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
const p = Number(u.port || (u.protocol === "https:" ? 443 : 80));
|
|
243
|
+
if (p !== port)
|
|
244
|
+
return false;
|
|
245
|
+
const h = u.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
246
|
+
const names = new Set(["localhost", "0.0.0.0", "::", "::1", host.toLowerCase(), `${host.toLowerCase()}.local`, ...localAddrs.map((a) => a.toLowerCase())]);
|
|
247
|
+
return names.has(h) || /^127\./.test(h);
|
|
248
|
+
}
|
|
249
|
+
export function localAddresses() {
|
|
250
|
+
try {
|
|
251
|
+
return Object.values(networkInterfaces()).flatMap((l) => (l ?? []).map((a) => a.address));
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
export function stopMachine(what, opts, io = defaultStopIo(opts.port), hosts = hostsRelay) {
|
|
258
|
+
const lines = [];
|
|
259
|
+
let relayStopped = false;
|
|
260
|
+
for (const kind of ["relay", "worker", "notifier", "menubar"]) {
|
|
261
|
+
if (what !== kind && what !== "all")
|
|
262
|
+
continue;
|
|
263
|
+
const r = stopProcResult(kind, io);
|
|
264
|
+
lines.push(...r.lines);
|
|
265
|
+
if (kind === "relay")
|
|
266
|
+
relayStopped = r.stopped;
|
|
267
|
+
}
|
|
268
|
+
if (!relayStopped)
|
|
269
|
+
return { lines };
|
|
270
|
+
const teams = opts.teams ?? (opts.teamRelay ? [{ name: "", relay: opts.teamRelay }] : []);
|
|
271
|
+
const hostedHere = teams.filter((t) => hosts(t.relay, opts.port));
|
|
272
|
+
if (hostedHere.length) {
|
|
273
|
+
const named = hostedHere.map((t) => t.name).filter(Boolean);
|
|
274
|
+
return { lines, note: `${named.length ? `Your teammates in ${named.map((n) => `"${n}"`).join(", ")} are` : "Your teammates are"} offline until a relay is running again.` };
|
|
275
|
+
}
|
|
276
|
+
const elsewhere = [...new Set(teams.map((t) => t.relay))];
|
|
277
|
+
return { lines, note: `Your team${teams.length === 1 ? "'s relay is" : "s' relays are"} ${elsewhere.join(", ") || "elsewhere"}, not this one — your teammates are not affected.` };
|
|
278
|
+
}
|
|
279
|
+
const LOG_KINDS = ["worker", "relay", "notifier", "menubar"];
|
|
280
|
+
export function uninstallMachine(opts, io) {
|
|
281
|
+
const lines = [], removed = [], left = [];
|
|
282
|
+
const kinds = ["worker", "notifier", "menubar", ...(opts.stopRelay ? ["relay"] : [])];
|
|
283
|
+
for (const kind of kinds) {
|
|
284
|
+
if (io.installed(kind)) {
|
|
285
|
+
try {
|
|
286
|
+
for (const l of io.uninstall(kind))
|
|
287
|
+
lines.push(`${kind}: ${l.trim()}`);
|
|
288
|
+
removed.push(`the ${kind} service — stopped, and its definition deleted so it cannot return at login`);
|
|
289
|
+
}
|
|
290
|
+
catch (e) {
|
|
291
|
+
left.push(`the ${kind} service (${e.message.split("\n")[0]}) — pantheon service uninstall ${kind}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const p = io.proc(kind);
|
|
295
|
+
if (p.running && p.managedBy === "foreground") {
|
|
296
|
+
if (kind === "relay")
|
|
297
|
+
for (const l of io.stop(kind))
|
|
298
|
+
lines.push(l);
|
|
299
|
+
else
|
|
300
|
+
left.push(`a ${kind} started by hand is still running (pid ${p.pid ?? "?"}) — close its terminal, or: kill ${p.pid ?? "<pid>"}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (!opts.stopRelay && (io.installed("relay") || io.proc("relay").running)) {
|
|
304
|
+
left.push("the relay — others may be using it. Stop and remove it too: pantheon leave --stop-relay");
|
|
305
|
+
}
|
|
306
|
+
if (io.hasIntent()) {
|
|
307
|
+
io.clearIntent();
|
|
308
|
+
removed.push("the hook intent (hook-intent.json) — nothing will put the hooks back");
|
|
309
|
+
}
|
|
310
|
+
const mcp = io.removeMcpEntries();
|
|
311
|
+
removed.push(...mcp.removed);
|
|
312
|
+
left.push(...mcp.left);
|
|
313
|
+
try {
|
|
314
|
+
if (existsSync(io.logDir)) {
|
|
315
|
+
rmSync(io.logDir, { recursive: true, force: true });
|
|
316
|
+
removed.push(`service logs (${io.logDir})`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
catch { }
|
|
320
|
+
const repoDir = join(opts.project, ".pantheon");
|
|
321
|
+
let repoLogs = 0;
|
|
322
|
+
for (const k of LOG_KINDS) {
|
|
323
|
+
const f = join(repoDir, `${k}.log`);
|
|
324
|
+
try {
|
|
325
|
+
if (existsSync(f)) {
|
|
326
|
+
rmSync(f, { force: true });
|
|
327
|
+
repoLogs++;
|
|
328
|
+
}
|
|
170
329
|
}
|
|
171
|
-
|
|
330
|
+
catch { }
|
|
172
331
|
}
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
332
|
+
if (repoLogs) {
|
|
333
|
+
let rest = [];
|
|
334
|
+
try {
|
|
335
|
+
rest = readdirSync(repoDir);
|
|
336
|
+
}
|
|
337
|
+
catch { }
|
|
338
|
+
if (!rest.length) {
|
|
339
|
+
try {
|
|
340
|
+
rmdirSync(repoDir);
|
|
341
|
+
}
|
|
342
|
+
catch { }
|
|
343
|
+
removed.push(`${repoDir} (service logs an older version wrote into your repo)`);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
removed.push(`${repoLogs} service log${repoLogs === 1 ? "" : "s"} from ${repoDir}`);
|
|
347
|
+
left.push(`${repoDir}: ${rest.join(", ")} — your team's files, not logs`);
|
|
348
|
+
}
|
|
176
349
|
}
|
|
177
|
-
return
|
|
350
|
+
return { lines, removed, left };
|
|
351
|
+
}
|
|
352
|
+
export async function panicMachine(io) {
|
|
353
|
+
const lines = io.stopWorker().map((l) => ` ${l}`);
|
|
354
|
+
const workerDown = await io.workerDown();
|
|
355
|
+
if (!workerDown)
|
|
356
|
+
lines.push(` ! the worker is still running. Stop it by hand: ${platform() === "darwin" ? "launchctl bootout gui/$(id -u)/dev.pantheon.worker" : "systemctl --user disable --now pantheon-worker"} or kill its pid (pantheon ps)`);
|
|
357
|
+
let allRevoked = true;
|
|
358
|
+
if (!io.teams.length)
|
|
359
|
+
lines.push(" no team on this machine, so there are no wake grants to revoke");
|
|
360
|
+
for (const t of io.teams) {
|
|
361
|
+
try {
|
|
362
|
+
const who = await io.revoke(t);
|
|
363
|
+
lines.push(who.length ? ` ${t.name}: revoked wake rights from ${who.join(", ")}` : ` ${t.name}: no peer had wake rights to revoke`);
|
|
364
|
+
}
|
|
365
|
+
catch (e) {
|
|
366
|
+
allRevoked = false;
|
|
367
|
+
lines.push(` ${t.name}: could not reach its relay (${e.message.split("\n")[0]}), so its wake grants were not changed — run pantheon panic again when it is reachable`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (workerDown)
|
|
371
|
+
lines.push("", " Nothing can start an agent run on this machine now.");
|
|
372
|
+
else
|
|
373
|
+
lines.push("", " The worker is still running — stop it as above.");
|
|
374
|
+
return { lines, workerDown, ok: workerDown && allRevoked };
|
|
375
|
+
}
|
|
376
|
+
export function leavePlan(all) {
|
|
377
|
+
const teams = Object.values(all?.spaces ?? {}).map((m) => ({ name: m.name, relay: m.relay }));
|
|
378
|
+
return { teams, ...(all?.member ? { member: all.member } : {}) };
|
|
178
379
|
}
|
|
179
380
|
export function statFile(f) {
|
|
180
381
|
try {
|
|
@@ -185,4 +386,22 @@ export function statFile(f) {
|
|
|
185
386
|
return "unreadable";
|
|
186
387
|
}
|
|
187
388
|
}
|
|
389
|
+
export function realUninstallIo(deps) {
|
|
390
|
+
const s = defaultStopIo(deps.port ?? 8787);
|
|
391
|
+
return {
|
|
392
|
+
installed: (k) => serviceInstalled(k),
|
|
393
|
+
uninstall: (k) => uninstallService(k),
|
|
394
|
+
stop: (k) => stopProc(k, s),
|
|
395
|
+
proc: (k) => {
|
|
396
|
+
const p = s.proc(k);
|
|
397
|
+
if (k === "relay" && p.running && p.managedBy === "foreground" && p.pid && !isPantheonCommand(s.cmdline?.(p.pid) ?? ""))
|
|
398
|
+
return { ...p, running: false };
|
|
399
|
+
return p;
|
|
400
|
+
},
|
|
401
|
+
removeMcpEntries: deps.removeMcpEntries,
|
|
402
|
+
hasIntent: deps.hasIntent,
|
|
403
|
+
clearIntent: deps.clearIntent,
|
|
404
|
+
logDir: join(CONFIG_DIR, "logs"),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
188
407
|
//# sourceMappingURL=machine.js.map
|
package/dist/mcp-supervisor.js
CHANGED
|
@@ -261,6 +261,11 @@ export async function superviseMcp() {
|
|
|
261
261
|
await new Promise((r) => setTimeout(r, 250));
|
|
262
262
|
if (!children.has(c))
|
|
263
263
|
return;
|
|
264
|
+
try {
|
|
265
|
+
mkdirSync(supervisorStateDir(), { recursive: true });
|
|
266
|
+
writeFileSync(join(supervisorStateDir(), `retiring-${c.proc.pid}`), String(Date.now()));
|
|
267
|
+
}
|
|
268
|
+
catch { }
|
|
264
269
|
try {
|
|
265
270
|
c.proc.stdin.end();
|
|
266
271
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
6
|
import { z } from "zod";
|
|
@@ -15,6 +18,7 @@ import { loadProfile, organizationProfile, routeDecision } from "./profile.js";
|
|
|
15
18
|
import { renderBrief } from "./brief.js";
|
|
16
19
|
import { protocol, sessionInstructions } from "./mcp-instructions.js";
|
|
17
20
|
import { recentRuns } from "./spend.js";
|
|
21
|
+
import { refusalOf } from "./refusal.js";
|
|
18
22
|
const ACT_HELP = {
|
|
19
23
|
NOTE: "FYI. Creates no obligation.",
|
|
20
24
|
ASK: "A question a named peer owes you an answer to. Requires `to`.",
|
|
@@ -111,15 +115,16 @@ export async function runMcp() {
|
|
|
111
115
|
const mcpActivity = role === "interactive" && !!backend &&
|
|
112
116
|
(backend.id === "claude-code" ? !hooksOn : true);
|
|
113
117
|
let toolsInFlight = 0;
|
|
118
|
+
const activityKey = `${process.pid.toString(36)}-${randomBytes(4).toString("hex")}`;
|
|
114
119
|
const activityIdle = () => {
|
|
115
120
|
if (!mcpActivity)
|
|
116
121
|
return;
|
|
117
|
-
reportMcpIdle(cfg,
|
|
122
|
+
reportMcpIdle(cfg, activityKey, client.readSeq || client.head).catch(() => false);
|
|
118
123
|
};
|
|
119
124
|
const activityBusy = () => {
|
|
120
125
|
if (!mcpActivity)
|
|
121
126
|
return;
|
|
122
|
-
reportMcpBusy(cfg,
|
|
127
|
+
reportMcpBusy(cfg, activityKey).catch(() => false);
|
|
123
128
|
};
|
|
124
129
|
const activityAround = async (fn) => {
|
|
125
130
|
if (mcpActivity) {
|
|
@@ -154,6 +159,11 @@ export async function runMcp() {
|
|
|
154
159
|
return fail(`pantheon: still connecting to the relay (${Math.round((Date.now() - startedAt) / 100) / 10}s since this session started) — ` +
|
|
155
160
|
`try this call again in a moment. Nothing to restart.`);
|
|
156
161
|
}
|
|
162
|
+
const refused = connectError ? refusalOf(connectError) : null;
|
|
163
|
+
if (refused) {
|
|
164
|
+
return fail(`pantheon: the relay refused this machine (${refused.code}): ${refused.message}\n` +
|
|
165
|
+
`${refused.fix ? `The fix: ${refused.fix}.\n` : ""}Waiting will not clear this: it needs your human. Once they fix it, this session connects on its own — nothing to restart. Keep working locally meanwhile.`);
|
|
166
|
+
}
|
|
157
167
|
return fail(`pantheon cannot reach the relay yet: ${connectError ?? "no answer"}. Retrying in the background; try again in a few seconds.\n` +
|
|
158
168
|
`${versionNote() ?? "If this persists the relay may be down — tell your human, and keep working locally meanwhile."}`);
|
|
159
169
|
}
|
|
@@ -474,7 +484,9 @@ export async function runMcp() {
|
|
|
474
484
|
url.searchParams.set("space", cfg.space);
|
|
475
485
|
url.searchParams.set("token", cfg.token);
|
|
476
486
|
url.searchParams.set("goal", goal);
|
|
477
|
-
return fetch(url).then(async (res) => {
|
|
487
|
+
return fetch(url, { signal: AbortSignal.timeout(10_000) }).then(async (res) => {
|
|
488
|
+
if (!res.ok)
|
|
489
|
+
return fail(`the relay answered ${res.status} for this goal's thread — try again in a moment`);
|
|
478
490
|
const { acts } = (await res.json());
|
|
479
491
|
const hits = acts.filter((a) => a.goal === goal).slice(-(limit ?? 60));
|
|
480
492
|
return text(`goal ${g.id} "${g.title}" [${g.state}] ${g.acts}/${g.budget} acts${deadlineLabel(g.deadline)}\nacceptance: ${g.acceptance || "(none)"}\n\n` +
|
|
@@ -490,11 +502,26 @@ export async function runMcp() {
|
|
|
490
502
|
shuttingDown = true;
|
|
491
503
|
if (mcpActivity && client.connected) {
|
|
492
504
|
await Promise.race([
|
|
493
|
-
reportMcpIdle(cfg,
|
|
505
|
+
reportMcpIdle(cfg, activityKey, client.readSeq || client.head).catch(() => false),
|
|
494
506
|
new Promise((r) => setTimeout(r, 1_200)),
|
|
495
507
|
]);
|
|
496
508
|
}
|
|
497
|
-
|
|
509
|
+
let handover = false;
|
|
510
|
+
if (process.env.PANTHEON_MCP_SUPERVISED) {
|
|
511
|
+
try {
|
|
512
|
+
const { supervisorStateDir } = await import("./mcp-supervisor.js");
|
|
513
|
+
const mark = join(supervisorStateDir(), `retiring-${process.pid}`);
|
|
514
|
+
if (existsSync(mark)) {
|
|
515
|
+
handover = true;
|
|
516
|
+
rmSync(mark, { force: true });
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
catch { }
|
|
520
|
+
}
|
|
521
|
+
if (handover)
|
|
522
|
+
client.handOff();
|
|
523
|
+
else
|
|
524
|
+
client.close();
|
|
498
525
|
process.exit(0);
|
|
499
526
|
};
|
|
500
527
|
process.on("SIGINT", shutdown);
|
|
@@ -521,7 +548,7 @@ export async function runMcp() {
|
|
|
521
548
|
connectError = e.message;
|
|
522
549
|
connecting = false;
|
|
523
550
|
firstAttemptDone();
|
|
524
|
-
if (/^PENDING\b/.test(connectError)) {
|
|
551
|
+
if (/^PENDING\b/.test(connectError) || refusalOf(connectError)) {
|
|
525
552
|
await new Promise((r) => setTimeout(r, PENDING_RETRY_MS));
|
|
526
553
|
continue;
|
|
527
554
|
}
|