@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
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { lstat, mkdir, open, readdir, realpath, rm, rmdir, stat, unlink, utimes, chmod, writeFile } from "node:fs/promises";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { join, resolve, win32 } from "node:path";
|
|
5
|
+
import { SECRET_FILE_GLOBS, SECRET_HOME_PATHS } from "./claude-confine.js";
|
|
6
|
+
export const FOLDER_SNAPSHOT_LIMITS = { files: 5_000, bytes: 200 * 1024 * 1024, dirs: 5_000 };
|
|
7
|
+
export const SNAPSHOT_STALE_MS = 3 * 24 * 60 * 60_000;
|
|
8
|
+
export const SNAPSHOT_LOG_KEEP_MS = 14 * 24 * 60 * 60_000;
|
|
9
|
+
export const DRAFTS_MARKER = ".pantheon-drafts.json";
|
|
10
|
+
const LEFT_OUT_DIRS = new Set([".git", "node_modules"]);
|
|
11
|
+
export function snapshotRoot(scratch, folder) {
|
|
12
|
+
return join(scratch, `folder-${createHash("sha256").update(resolve(folder)).digest("hex").slice(0, 10)}`);
|
|
13
|
+
}
|
|
14
|
+
const globRe = (g) => new RegExp(`^${g.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`, "i");
|
|
15
|
+
const SECRET_FILES = SECRET_FILE_GLOBS.map(globRe);
|
|
16
|
+
export function isSecretPath(rel, dir) {
|
|
17
|
+
const low = rel.toLowerCase();
|
|
18
|
+
const name = low.slice(low.lastIndexOf("/") + 1);
|
|
19
|
+
if (!dir && SECRET_FILES.some((re) => re.test(name)))
|
|
20
|
+
return true;
|
|
21
|
+
return SECRET_HOME_PATHS.some((p) => p.dir === dir && (low === p.rel.toLowerCase() || low.endsWith(`/${p.rel.toLowerCase()}`)));
|
|
22
|
+
}
|
|
23
|
+
const longPath = (p, os) => (os === "win32" ? win32.toNamespacedPath(p) : p);
|
|
24
|
+
export const shown = (s) => s.replace(/\\\\\?\\UNC\\/gi, "\\\\").replace(/\\\\\?\\/g, "");
|
|
25
|
+
const samePath = (a, b, os) => {
|
|
26
|
+
const n = (p) => shown(p).normalize("NFC");
|
|
27
|
+
return os === "win32" || os === "darwin" ? n(a).toLowerCase() === n(b).toLowerCase() : n(a) === n(b);
|
|
28
|
+
};
|
|
29
|
+
export async function takeSnapshot(src, dest, o = {}) {
|
|
30
|
+
const limits = o.limits ?? FOLDER_SNAPSHOT_LIMITS;
|
|
31
|
+
const os = o.os ?? process.platform;
|
|
32
|
+
const L = (p) => longPath(p, os);
|
|
33
|
+
let root;
|
|
34
|
+
const why = (e) => shown(e.message.split("\n")[0] ?? "");
|
|
35
|
+
try {
|
|
36
|
+
root = await realpath(L(src));
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
return { ok: false, error: `the folder could not be read (${why(e)})` };
|
|
40
|
+
}
|
|
41
|
+
root = shown(root);
|
|
42
|
+
const skipped = { links: [], secrets: [], leftOut: [], other: [] };
|
|
43
|
+
const dirs = [];
|
|
44
|
+
const files = [];
|
|
45
|
+
let bytes = 0;
|
|
46
|
+
const stack = [""];
|
|
47
|
+
try {
|
|
48
|
+
while (stack.length) {
|
|
49
|
+
const rel = stack.pop();
|
|
50
|
+
const here = rel ? join(root, ...rel.split("/")) : root;
|
|
51
|
+
let names;
|
|
52
|
+
try {
|
|
53
|
+
names = (await readdir(L(here))).sort();
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
if (!rel)
|
|
57
|
+
throw e;
|
|
58
|
+
skipped.other.push(rel);
|
|
59
|
+
dirs.splice(dirs.indexOf(rel), 1);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
let again = "";
|
|
63
|
+
try {
|
|
64
|
+
again = await realpath(L(here));
|
|
65
|
+
}
|
|
66
|
+
catch { }
|
|
67
|
+
if (!again || !samePath(again, here, os)) {
|
|
68
|
+
if (!rel)
|
|
69
|
+
return { ok: false, error: "the folder moved while it was being copied" };
|
|
70
|
+
skipped.links.push(rel);
|
|
71
|
+
dirs.splice(dirs.indexOf(rel), 1);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
for (const name of names) {
|
|
75
|
+
const r = rel ? `${rel}/${name}` : name;
|
|
76
|
+
const abs = join(here, name);
|
|
77
|
+
let st;
|
|
78
|
+
try {
|
|
79
|
+
st = await lstat(L(abs));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
skipped.other.push(r);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (st.isSymbolicLink()) {
|
|
86
|
+
skipped.links.push(r);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (st.isDirectory()) {
|
|
90
|
+
if (LEFT_OUT_DIRS.has(name.toLowerCase())) {
|
|
91
|
+
skipped.leftOut.push(r);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (isSecretPath(r, true)) {
|
|
95
|
+
skipped.secrets.push(r);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
let real = "";
|
|
99
|
+
try {
|
|
100
|
+
real = await realpath(L(abs));
|
|
101
|
+
}
|
|
102
|
+
catch { }
|
|
103
|
+
if (!real || !samePath(real, abs, os)) {
|
|
104
|
+
skipped.links.push(r);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (dirs.length >= limits.dirs)
|
|
108
|
+
return { ok: false, tooLarge: { what: "dirs", limit: limits.dirs } };
|
|
109
|
+
dirs.push(r);
|
|
110
|
+
stack.push(r);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (!st.isFile()) {
|
|
114
|
+
skipped.other.push(r);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (isSecretPath(r, false)) {
|
|
118
|
+
skipped.secrets.push(r);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (st.nlink > 1) {
|
|
122
|
+
skipped.links.push(r);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (files.length >= limits.files)
|
|
126
|
+
return { ok: false, tooLarge: { what: "files", limit: limits.files } };
|
|
127
|
+
bytes += st.size;
|
|
128
|
+
if (bytes > limits.bytes)
|
|
129
|
+
return { ok: false, tooLarge: { what: "bytes", limit: limits.bytes } };
|
|
130
|
+
files.push({ rel: r, size: st.size, ino: st.ino, mode: st.mode });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
return { ok: false, error: `the folder could not be read (${why(e)})` };
|
|
136
|
+
}
|
|
137
|
+
const copied = new Map();
|
|
138
|
+
const budget = { used: 0, limit: limits.bytes, over: false };
|
|
139
|
+
try {
|
|
140
|
+
await mkdir(L(dest), { recursive: true, mode: 0o700 });
|
|
141
|
+
for (const d of dirs)
|
|
142
|
+
await mkdir(L(join(dest, ...d.split("/"))), { mode: 0o755 });
|
|
143
|
+
const queue = files.slice();
|
|
144
|
+
const worker = async () => {
|
|
145
|
+
for (let f = queue.shift(); f && !budget.over; f = queue.shift()) {
|
|
146
|
+
const got = await copyOne(join(root, ...f.rel.split("/")), join(dest, ...f.rel.split("/")), f, os, budget);
|
|
147
|
+
if (got === "changed") {
|
|
148
|
+
skipped.other.push(f.rel);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (got === "over") {
|
|
152
|
+
budget.over = true;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
copied.set(f.rel, got);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const failed = (await Promise.allSettled(Array.from({ length: 8 }, worker))).find((s) => s.status === "rejected");
|
|
159
|
+
if (failed)
|
|
160
|
+
throw failed.reason;
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
await removeSnapshot(dest, os);
|
|
164
|
+
return { ok: false, error: `the folder could not be copied (${why(e)})` };
|
|
165
|
+
}
|
|
166
|
+
if (budget.over) {
|
|
167
|
+
await removeSnapshot(dest, os);
|
|
168
|
+
return { ok: false, tooLarge: { what: "bytes", limit: limits.bytes } };
|
|
169
|
+
}
|
|
170
|
+
return { ok: true, snap: { dir: dest, src: root, files: copied, bytes: budget.used, skipped } };
|
|
171
|
+
}
|
|
172
|
+
async function copyOne(from, to, want, os, budget) {
|
|
173
|
+
const flags = constants.O_RDONLY | (os === "win32" ? 0 : (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
174
|
+
let fh;
|
|
175
|
+
try {
|
|
176
|
+
fh = await open(longPath(from, os), flags);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return "changed";
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const st = await fh.stat();
|
|
183
|
+
if (!st.isFile() || st.nlink > 1 || (want.ino && st.ino && st.ino !== want.ino))
|
|
184
|
+
return "changed";
|
|
185
|
+
if (budget.over || budget.used + st.size > budget.limit)
|
|
186
|
+
return "over";
|
|
187
|
+
const out = await open(longPath(to, os), "wx", 0o600);
|
|
188
|
+
let size = 0;
|
|
189
|
+
try {
|
|
190
|
+
const buf = Buffer.allocUnsafe(1 << 20);
|
|
191
|
+
for (;;) {
|
|
192
|
+
let bytesRead;
|
|
193
|
+
try {
|
|
194
|
+
({ bytesRead } = await fh.read(buf, 0, buf.length, null));
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
budget.used -= size;
|
|
198
|
+
size = -1;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
if (!bytesRead)
|
|
202
|
+
break;
|
|
203
|
+
if (budget.over || budget.used + bytesRead > budget.limit)
|
|
204
|
+
return "over";
|
|
205
|
+
budget.used += bytesRead;
|
|
206
|
+
size += bytesRead;
|
|
207
|
+
await out.write(buf, 0, bytesRead);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
await out.close();
|
|
212
|
+
}
|
|
213
|
+
if (size < 0) {
|
|
214
|
+
try {
|
|
215
|
+
await unlink(longPath(to, os));
|
|
216
|
+
}
|
|
217
|
+
catch { }
|
|
218
|
+
return "changed";
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
await utimes(longPath(to, os), st.atime, st.mtime);
|
|
222
|
+
}
|
|
223
|
+
catch { }
|
|
224
|
+
if (os !== "win32") {
|
|
225
|
+
try {
|
|
226
|
+
await chmod(to, st.mode & 0o777);
|
|
227
|
+
}
|
|
228
|
+
catch { }
|
|
229
|
+
}
|
|
230
|
+
const after = await stat(longPath(to, os));
|
|
231
|
+
return { size, mtimeMs: after.mtimeMs };
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
await fh.close();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
export async function snapshotDrafts(snap, pantheons, o = {}) {
|
|
238
|
+
const os = o.os ?? process.platform;
|
|
239
|
+
let budget = o.max ?? FOLDER_SNAPSHOT_LIMITS.files * 4;
|
|
240
|
+
const drafts = [];
|
|
241
|
+
const stack = [""];
|
|
242
|
+
while (stack.length) {
|
|
243
|
+
const rel = stack.pop();
|
|
244
|
+
let names;
|
|
245
|
+
try {
|
|
246
|
+
names = (await readdir(longPath(rel ? join(snap.dir, ...rel.split("/")) : snap.dir, os))).sort();
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
for (const name of names) {
|
|
252
|
+
if (--budget < 0)
|
|
253
|
+
return { drafts, complete: false };
|
|
254
|
+
const r = rel ? `${rel}/${name}` : name;
|
|
255
|
+
let st;
|
|
256
|
+
try {
|
|
257
|
+
st = await lstat(longPath(join(snap.dir, ...r.split("/")), os));
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (st.isDirectory()) {
|
|
263
|
+
stack.push(r);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (!st.isFile() || pantheons(r))
|
|
267
|
+
continue;
|
|
268
|
+
const was = snap.files.get(r);
|
|
269
|
+
if (!was || was.size !== st.size || was.mtimeMs !== st.mtimeMs)
|
|
270
|
+
drafts.push(r);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return { drafts: drafts.sort(), complete: true };
|
|
274
|
+
}
|
|
275
|
+
export async function keepOnlyDrafts(dir, drafts, meta, os = process.platform) {
|
|
276
|
+
await writeDraftsMarker(dir, drafts, meta, true, os);
|
|
277
|
+
const keep = new Set([...drafts, DRAFTS_MARKER]);
|
|
278
|
+
const walk = async (rel) => {
|
|
279
|
+
const here = rel ? join(dir, ...rel.split("/")) : dir;
|
|
280
|
+
let names;
|
|
281
|
+
try {
|
|
282
|
+
names = await readdir(longPath(here, os));
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
let left = 0;
|
|
288
|
+
for (const name of names) {
|
|
289
|
+
const r = rel ? `${rel}/${name}` : name;
|
|
290
|
+
const abs = longPath(join(here, name), os);
|
|
291
|
+
let st;
|
|
292
|
+
try {
|
|
293
|
+
st = await lstat(abs);
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (st.isDirectory()) {
|
|
299
|
+
if (await walk(r)) {
|
|
300
|
+
try {
|
|
301
|
+
await rmdir(abs);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
catch { }
|
|
305
|
+
}
|
|
306
|
+
left++;
|
|
307
|
+
}
|
|
308
|
+
else if (keep.has(r))
|
|
309
|
+
left++;
|
|
310
|
+
else {
|
|
311
|
+
try {
|
|
312
|
+
await unlink(abs);
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
left++;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return left === 0;
|
|
320
|
+
};
|
|
321
|
+
await walk("");
|
|
322
|
+
}
|
|
323
|
+
async function writeDraftsMarker(dir, drafts, meta, complete, os) {
|
|
324
|
+
const at = longPath(join(dir, DRAFTS_MARKER), os);
|
|
325
|
+
await rm(at, { recursive: true, force: true });
|
|
326
|
+
await writeFile(at, JSON.stringify({ folder: meta.folder, at: meta.at, drafts, ...(complete ? {} : { complete: false }) }, null, 2), { mode: 0o644, flag: "wx" });
|
|
327
|
+
}
|
|
328
|
+
export async function keepDrafts(dir, found, meta, o = {}) {
|
|
329
|
+
const os = o.os ?? process.platform;
|
|
330
|
+
const why = (e) => shown(e.message.split("\n")[0] ?? "");
|
|
331
|
+
try {
|
|
332
|
+
await writeDraftsMarker(dir, found.drafts, meta, found.complete, os);
|
|
333
|
+
}
|
|
334
|
+
catch (e) {
|
|
335
|
+
return { cut: false, error: `could not mark ${shown(dir)} as drafts (${why(e)}) — the whole copy is kept, but it is not protected from the sweep at the next start` };
|
|
336
|
+
}
|
|
337
|
+
if (!found.complete)
|
|
338
|
+
return { cut: false };
|
|
339
|
+
try {
|
|
340
|
+
await (o.cut ?? keepOnlyDrafts)(dir, found.drafts, meta, os);
|
|
341
|
+
return { cut: true };
|
|
342
|
+
}
|
|
343
|
+
catch (e) {
|
|
344
|
+
try {
|
|
345
|
+
await lstat(longPath(join(dir, DRAFTS_MARKER), os));
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
try {
|
|
349
|
+
await writeDraftsMarker(dir, found.drafts, meta, found.complete, os);
|
|
350
|
+
}
|
|
351
|
+
catch { }
|
|
352
|
+
}
|
|
353
|
+
return { cut: false, error: `could not clear the unchanged files out of ${shown(dir)} (${why(e)}) — the whole copy is kept` };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
export async function removeSnapshot(dir, os = process.platform) {
|
|
357
|
+
try {
|
|
358
|
+
await rm(longPath(dir, os), { recursive: true, force: true, maxRetries: 3 });
|
|
359
|
+
}
|
|
360
|
+
catch { }
|
|
361
|
+
}
|
|
362
|
+
export async function sweepSnapshots(scratch, o = {}) {
|
|
363
|
+
const now = o.now ?? Date.now();
|
|
364
|
+
const os = o.os ?? process.platform;
|
|
365
|
+
const staleMs = o.staleMs ?? SNAPSHOT_STALE_MS, logMs = o.logMs ?? SNAPSHOT_LOG_KEEP_MS;
|
|
366
|
+
const gone = [];
|
|
367
|
+
let roots;
|
|
368
|
+
try {
|
|
369
|
+
roots = (await readdir(longPath(scratch, os))).filter((n) => n.startsWith("folder-"));
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return gone;
|
|
373
|
+
}
|
|
374
|
+
for (const r of roots) {
|
|
375
|
+
const root = join(scratch, r);
|
|
376
|
+
let st;
|
|
377
|
+
try {
|
|
378
|
+
st = await lstat(longPath(root, os));
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (!st.isDirectory())
|
|
384
|
+
continue;
|
|
385
|
+
let names;
|
|
386
|
+
try {
|
|
387
|
+
names = await readdir(longPath(root, os));
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
let left = names.length;
|
|
393
|
+
for (const n of names) {
|
|
394
|
+
const p = join(root, n);
|
|
395
|
+
let s;
|
|
396
|
+
try {
|
|
397
|
+
s = await lstat(longPath(p, os));
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (s.isDirectory()) {
|
|
403
|
+
if (now - s.mtimeMs < staleMs)
|
|
404
|
+
continue;
|
|
405
|
+
try {
|
|
406
|
+
await lstat(longPath(join(p, DRAFTS_MARKER), os));
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
catch { }
|
|
410
|
+
await removeSnapshot(p, os);
|
|
411
|
+
gone.push(p);
|
|
412
|
+
left--;
|
|
413
|
+
}
|
|
414
|
+
else if (n.endsWith(".run.log") && now - s.mtimeMs >= logMs) {
|
|
415
|
+
try {
|
|
416
|
+
await unlink(longPath(p, os));
|
|
417
|
+
gone.push(p);
|
|
418
|
+
left--;
|
|
419
|
+
}
|
|
420
|
+
catch { }
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (left === 0) {
|
|
424
|
+
try {
|
|
425
|
+
await rmdir(longPath(root, os));
|
|
426
|
+
}
|
|
427
|
+
catch { }
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return gone;
|
|
431
|
+
}
|
|
432
|
+
//# sourceMappingURL=folder-snapshot.js.map
|
package/dist/install.js
CHANGED
|
@@ -258,6 +258,20 @@ export function claudeAddFailed(e, command) {
|
|
|
258
258
|
return `failed: claude mcp add did not work (${why}) — fix: run \`${command}\` yourself to see why, then pantheon install --user`;
|
|
259
259
|
}
|
|
260
260
|
export const CLAUDE_DIRECT_NOTE = "written to ~/.claude.json directly (no claude CLI here) — the Claude desktop app and Claude Code read it";
|
|
261
|
+
export function claudeUserEntryState(file, entry) {
|
|
262
|
+
let there;
|
|
263
|
+
try {
|
|
264
|
+
there = JSON.parse(stripBom(readFileSync(file, "utf8")))?.mcpServers?.["pantheon"];
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return "absent";
|
|
268
|
+
}
|
|
269
|
+
if (there === undefined)
|
|
270
|
+
return "absent";
|
|
271
|
+
if (JSON.stringify(there) === JSON.stringify(claudeUserEntryShape(entry)))
|
|
272
|
+
return "matches";
|
|
273
|
+
return isOurMcpEntry(there) ? "other" : "not ours";
|
|
274
|
+
}
|
|
261
275
|
export function claudeUserScope(entry, present = true, claudeBin = which("claude"), home = homedir(), direct = false) {
|
|
262
276
|
const tool = "claude code";
|
|
263
277
|
if (!claudeBin) {
|
|
@@ -268,14 +282,7 @@ export function claudeUserScope(entry, present = true, claudeBin = which("claude
|
|
|
268
282
|
if (conflict)
|
|
269
283
|
return { tool, where: "-", status: `skipped (claude not on PATH) — ${conflict}` };
|
|
270
284
|
if (!direct || inWokenRun()) {
|
|
271
|
-
|
|
272
|
-
const d = JSON.parse(stripBom(readFileSync(file, "utf8")));
|
|
273
|
-
return d?.mcpServers?.["pantheon"];
|
|
274
|
-
}
|
|
275
|
-
catch {
|
|
276
|
-
return undefined;
|
|
277
|
-
} })();
|
|
278
|
-
return there !== undefined && JSON.stringify(there) === JSON.stringify(claudeUserEntryShape(entry))
|
|
285
|
+
return claudeUserEntryState(file, entry) === "matches"
|
|
279
286
|
? { tool, where: file, status: `unchanged — ${CLAUDE_DIRECT_NOTE}` }
|
|
280
287
|
: { tool, where: "-", status: "skipped (claude not on PATH here) — run pantheon install --user yourself to write ~/.claude.json directly" };
|
|
281
288
|
}
|
package/dist/machine.js
CHANGED
|
@@ -5,7 +5,9 @@ import { dirname, join, resolve } from "node:path";
|
|
|
5
5
|
import { CONFIG_DIR } from "./config.js";
|
|
6
6
|
import { borrowedHome, disableService, existingUnitPath, plistPath, serviceInstalled, serviceLogFile, stopWindowsHost, unitPath, unitPaths, uninstallService } from "./service.js";
|
|
7
7
|
import { LEGACY_TASK_NAME, hostAlive, hostChildRunning, hostFiles, isHostKind, readHostState, recordedTaskName } from "./win-host.js";
|
|
8
|
-
import { copilotMcpConfigFile, vsCodeUserDir, kiroMcpConfigFile, kiroDir } from "./install.js";
|
|
8
|
+
import { copilotMcpConfigFile, vsCodeUserDir, kiroMcpConfigFile, kiroDir, claudeUserEntryState, userEntry } from "./install.js";
|
|
9
|
+
import { claudeJsonFile } from "./claude-config.js";
|
|
10
|
+
import { agentSpawn, which } from "./which.js";
|
|
9
11
|
import { HOME_MARKER, homeEntryFor, isPantheonHomeName, pantheonHomeRefusal, refusedHomeLine } from "./home-safety.js";
|
|
10
12
|
const sh = (cmd) => {
|
|
11
13
|
try {
|
|
@@ -364,10 +366,48 @@ export function recordWorkerPid(file = workerPidFile()) {
|
|
|
364
366
|
}
|
|
365
367
|
catch { }
|
|
366
368
|
}
|
|
367
|
-
|
|
369
|
+
const realClaudeWiringDeps = () => ({
|
|
370
|
+
claudeBin: which("claude"),
|
|
371
|
+
list: (bin) => {
|
|
372
|
+
try {
|
|
373
|
+
const s = agentSpawn(bin, ["mcp", "list"]);
|
|
374
|
+
return execFileSync(s.command, s.args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 30_000, ...s.options });
|
|
375
|
+
}
|
|
376
|
+
catch (e) {
|
|
377
|
+
const out = e.stdout;
|
|
378
|
+
return typeof out === "string" && out.trim() ? out : null;
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
home: homedir(),
|
|
382
|
+
entry: () => userEntry(),
|
|
383
|
+
});
|
|
384
|
+
export function claudeWiring(d = realClaudeWiringDeps()) {
|
|
385
|
+
if (d.claudeBin) {
|
|
386
|
+
const listed = d.list(d.claudeBin);
|
|
387
|
+
if (listed !== null) {
|
|
388
|
+
const line = listed.split(/\r?\n/).find((l) => /^\s*pantheon\b/i.test(l));
|
|
389
|
+
if (!line)
|
|
390
|
+
return "not wired";
|
|
391
|
+
return /✓|✔|\bConnected/i.test(line) && !/fail/i.test(line) ? "wired, connects" : "wired, not connecting";
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
let entry = null;
|
|
395
|
+
try {
|
|
396
|
+
entry = d.entry();
|
|
397
|
+
}
|
|
398
|
+
catch { }
|
|
399
|
+
const state = claudeUserEntryState(claudeJsonFile(d.home), entry ?? { command: "", args: [] });
|
|
400
|
+
const where = d.claudeBin ? "in ~/.claude.json" : "in ~/.claude.json, which the Claude app reads (no claude CLI here)";
|
|
401
|
+
switch (state) {
|
|
402
|
+
case "matches": return `wired — ${where}`;
|
|
403
|
+
case "other": return `wired to another pantheon install — ${where}; pantheon install --user points it at this one`;
|
|
404
|
+
case "not ours": return "not wired — ~/.claude.json has a server called pantheon that Pantheon did not write";
|
|
405
|
+
default: return "not wired";
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
export function wiring(claude = realClaudeWiringDeps()) {
|
|
368
409
|
const out = [];
|
|
369
|
-
|
|
370
|
-
out.push({ tool: "claude code", state: claude ? (/✔|Connected/i.test(claude) ? "wired, connects" : "wired, not connecting") : "not wired" });
|
|
410
|
+
out.push({ tool: "claude code", state: claudeWiring(claude) });
|
|
371
411
|
const cursor = join(homedir(), ".cursor", "mcp.json");
|
|
372
412
|
out.push({
|
|
373
413
|
tool: "cursor",
|
|
@@ -583,6 +623,46 @@ export function hostsRelay(url, port, localAddrs = localAddresses(), host = host
|
|
|
583
623
|
const names = new Set(["localhost", "0.0.0.0", "::", "::1", host.toLowerCase(), `${host.toLowerCase()}.local`, ...localAddrs.map((a) => a.toLowerCase())]);
|
|
584
624
|
return names.has(h) || /^127\./.test(h);
|
|
585
625
|
}
|
|
626
|
+
export function relayRowsWhenNoneHere(relays, port, reach = {}, hosts = (u, p) => hostsRelay(u, p)) {
|
|
627
|
+
const urls = [...new Set(relays.filter(Boolean))];
|
|
628
|
+
const local = (u) => hosts(u, port) || loopbackRelay(u);
|
|
629
|
+
const here = urls.filter(local);
|
|
630
|
+
if (!urls.length || here.some((u) => reach[u] !== true)) {
|
|
631
|
+
return [{ line: "not running", dim: here.length ? `start it with: pantheon start (your team${here.length === 1 ? "" : "s"} ${here.length === 1 ? "uses" : "use"} the relay on this machine)` : "start it with: pantheon start" }];
|
|
632
|
+
}
|
|
633
|
+
return urls.map((u) => {
|
|
634
|
+
let host = u;
|
|
635
|
+
try {
|
|
636
|
+
host = new URL(u.replace(/^ws/, "http")).host;
|
|
637
|
+
}
|
|
638
|
+
catch { }
|
|
639
|
+
const r = reach[u];
|
|
640
|
+
if (local(u))
|
|
641
|
+
return { line: `local ${host}`, dim: "connected — a relay on this machine" };
|
|
642
|
+
const kind = /(?:^|\.)joinpantheon\.network$/i.test(host.replace(/:\d+$/, "")) ? "hosted " : "remote ";
|
|
643
|
+
return { line: `${kind}${host}`, dim: r === true ? "connected — no relay needs to run on this machine" : r === false ? "not reachable from this machine right now — pantheon doctor says why" : "no relay needs to run on this machine" };
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
export function loopbackRelay(url) {
|
|
647
|
+
let h;
|
|
648
|
+
try {
|
|
649
|
+
h = new URL(url.replace(/^ws/, "http")).hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
return h === "localhost" || h === "::1" || /^127\./.test(h);
|
|
655
|
+
}
|
|
656
|
+
export async function relayReachable(url, timeoutMs = 5_000) {
|
|
657
|
+
try {
|
|
658
|
+
const res = await fetch(`${url.replace(/^ws/i, "http").replace(/\/+$/, "")}/health`, { signal: AbortSignal.timeout(timeoutMs), redirect: "error" });
|
|
659
|
+
await res.body?.cancel().catch(() => { });
|
|
660
|
+
return res.ok;
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
586
666
|
export function localAddresses() {
|
|
587
667
|
try {
|
|
588
668
|
return Object.values(networkInterfaces()).flatMap((l) => (l ?? []).map((a) => a.address));
|
package/dist/managed.js
CHANGED
|
@@ -255,8 +255,8 @@ export function joinInstructions(invite, relay, installers = true) {
|
|
|
255
255
|
intro: "You're invited to my Pantheon team. Paste the line for your computer into a terminal:",
|
|
256
256
|
steps,
|
|
257
257
|
outro: sh || win
|
|
258
|
-
? "It installs Pantheon (with its own Node, no admin rights), asks for your email, sends you a 6-digit code, and sets up the coding agents on your computer."
|
|
259
|
-
: "`pantheon join` asks for your email, sends you a 6-digit code, and sets up the coding agents on your computer.",
|
|
258
|
+
? "It installs Pantheon (with its own Node, no admin rights), asks for your email, sends you a 6-digit code (check spam if it does not arrive), and sets up the coding agents on your computer."
|
|
259
|
+
: "`pantheon join` asks for your email, sends you a 6-digit code (check spam if it does not arrive), and sets up the coding agents on your computer.",
|
|
260
260
|
};
|
|
261
261
|
}
|
|
262
262
|
export async function relayServesInstaller(relay, timeoutMs = 4000) {
|
package/dist/mcp.js
CHANGED
|
@@ -9,7 +9,7 @@ import { fenceBlock, fenceRubric, assess, fenceReminder } from "./untrusted.js";
|
|
|
9
9
|
import { trusted, trustLabel } from "./verify-act.js";
|
|
10
10
|
import { bindRepo, loadAll, resolveFor } from "./config.js";
|
|
11
11
|
import { refuseSecrets, secretGuard } from "./secret-guard.js";
|
|
12
|
-
import { LIMITS, SENDABLE } from "./protocol.js";
|
|
12
|
+
import { LIMITS, SENDABLE, siblingSessions } from "./protocol.js";
|
|
13
13
|
import { goalKindLabel, openedText } from "./worklist.js";
|
|
14
14
|
import { VERSION } from "./version.js";
|
|
15
15
|
import { installedVersion } from "./mcp-supervisor.js";
|
|
@@ -298,8 +298,7 @@ export async function runMcp() {
|
|
|
298
298
|
const mine = client.myWork();
|
|
299
299
|
const taken = client.takenBySibling();
|
|
300
300
|
const peers = client.roster.filter((m) => m.id !== cfg.member);
|
|
301
|
-
const siblings = (client.roster
|
|
302
|
-
.filter((s) => s.id !== client.session);
|
|
301
|
+
const siblings = siblingSessions(client.roster, cfg.member, client.session);
|
|
303
302
|
const warnSiblings = siblings.length && !siblingsWarned;
|
|
304
303
|
if (siblings.length)
|
|
305
304
|
siblingsWarned = true;
|
|
@@ -320,6 +319,9 @@ export async function runMcp() {
|
|
|
320
319
|
taken.length
|
|
321
320
|
? `taken by another of your sessions (leave these alone):\n${taken.map((o) => ` ${o.id} ${o.kind} — ${o.body}`).join("\n")}`
|
|
322
321
|
: ``,
|
|
322
|
+
...client.members.filter((e) => e.recovery).map((e) => e.member === cfg.member
|
|
323
|
+
? `SOMEONE ASKED TO MOVE YOUR SEAT in ${cfg.space} to another device ("${e.recovery.label}", key ${e.recovery.fp}); it moves ${new Date(e.recovery.due_at).toISOString()} unless cancelled. Tell your human now. If they did not ask for it, they run pantheon recover cancel ${e.member} themselves, in a terminal, and secure their email account.`
|
|
324
|
+
: `seat move waiting: ${e.member} asked to move their seat to a new device ("${e.recovery.label}", key ${e.recovery.fp}). Your human may confirm it only after checking the key with ${e.member} themselves, by running pantheon recover confirm ${e.member} in a terminal (it asks them to type the key), or cancel it (pantheon recover cancel ${e.member}) — theirs to run, not yours.`),
|
|
323
325
|
].filter(Boolean).join("\n");
|
|
324
326
|
};
|
|
325
327
|
server.registerTool("pantheon_status", {
|