@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/client.js
CHANGED
|
@@ -7,8 +7,8 @@ import { repoName } from "./repo-route.js";
|
|
|
7
7
|
import { signChallenge, signBytes, actPayload, goalPayload, newActId, newActNonce, canonicalJson } from "./identity.js";
|
|
8
8
|
import { expectsSigs, joinedSigning, loadPins, markMemberSigns, markSigs, memberSigns, observeMembers, pinFor, pinScope, teamPinId, updatePins } from "./pins.js";
|
|
9
9
|
export { joinedSigning } from "./pins.js";
|
|
10
|
-
import { trusted, verifyAct, verifyGoal } from "./verify-act.js";
|
|
11
|
-
import { newId } from "./protocol.js";
|
|
10
|
+
import { trusted, verifyAct, verifyGoal, vouchedMove } from "./verify-act.js";
|
|
11
|
+
import { newId, UPKEEP_AGENT } from "./protocol.js";
|
|
12
12
|
import { VERSION } from "./version.js";
|
|
13
13
|
import { describeNetError, explainNetError, noteRelayAnswered, settleSystemFallback, systemFallbackPending, trySystemFallback, wsOptions } from "./network.js";
|
|
14
14
|
export class PantheonClient {
|
|
@@ -71,7 +71,9 @@ export class PantheonClient {
|
|
|
71
71
|
this.enroll = opts?.enroll ?? null;
|
|
72
72
|
this.session = opts?.session ?? newId("s");
|
|
73
73
|
this.role = opts?.role ?? "interactive";
|
|
74
|
+
this.upkeep = opts?.upkeep === true && this.role === "cli";
|
|
74
75
|
}
|
|
76
|
+
upkeep;
|
|
75
77
|
keepRepos() {
|
|
76
78
|
try {
|
|
77
79
|
rememberTeamRepos({ spaceId: this.cfg.spaceId ?? this.spaceId, space: this.cfg.space, relay: this.cfg.relay }, this.repos);
|
|
@@ -227,7 +229,8 @@ export class PantheonClient {
|
|
|
227
229
|
session: this.session,
|
|
228
230
|
role: this.role,
|
|
229
231
|
display: this.cfg.display ?? this.cfg.member,
|
|
230
|
-
agent: this.cfg.agent ?? "unknown",
|
|
232
|
+
agent: this.upkeep ? UPKEEP_AGENT : (this.cfg.agent ?? "unknown"),
|
|
233
|
+
...(this.upkeep ? { upkeep: true } : {}),
|
|
231
234
|
pkg: VERSION,
|
|
232
235
|
...(this.seq > 0 ? { from_seq: this.seq } : {}),
|
|
233
236
|
...(this.cfg.repo ? { repo: this.cfg.repo, ...(this.cfg.repoOrigin ? { repo_origin: repoName(this.cfg.repoOrigin) } : {}) } : {}),
|
|
@@ -608,7 +611,8 @@ export class PantheonClient {
|
|
|
608
611
|
if (p.corrupt)
|
|
609
612
|
return false;
|
|
610
613
|
const before = JSON.stringify(p);
|
|
611
|
-
|
|
614
|
+
const teams = [this.cfg.spaceId, this.spaceId, this.cfg.space];
|
|
615
|
+
changes = observeMembers(p, team, this.members, { member: this.cfg.member, publicKey: this.cfg.publicKey }, Date.now(), (m, fp, pins) => vouchedMove(m, fp, teams, (who) => pinFor(pins, team, who), (who) => !!this.members.find((x) => x.member === who)?.admitted));
|
|
612
616
|
if (this.features.includes("act-sig"))
|
|
613
617
|
markSigs(p, team);
|
|
614
618
|
return JSON.stringify(p) !== before;
|
package/dist/config.js
CHANGED
|
@@ -631,18 +631,32 @@ export async function resolveInviteCode(code, relays, fetchImpl = fetch) {
|
|
|
631
631
|
const token = code.trim().replace(/^['"`]|['"`]$/g, "");
|
|
632
632
|
const tried = [...new Set(relays.map((r) => r.replace(/\/+$/, "")).filter(Boolean))];
|
|
633
633
|
let answered = false;
|
|
634
|
+
let nonJsonFrom;
|
|
635
|
+
const json = async (res, relay) => {
|
|
636
|
+
const text = await res.text();
|
|
637
|
+
try {
|
|
638
|
+
return JSON.parse(text);
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
nonJsonFrom ??= relay;
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
};
|
|
634
645
|
for (const relay of tried) {
|
|
635
646
|
const http = relay.replace(/^ws/, "http");
|
|
636
647
|
try {
|
|
637
648
|
const res = await fetchImpl(`${http}/join?token=${encodeURIComponent(token)}`, { signal: AbortSignal.timeout(6_000) });
|
|
638
649
|
if (res.status === 404) {
|
|
639
|
-
|
|
650
|
+
const j = await json(res, relay);
|
|
651
|
+
answered = (!!j && typeof j === "object") || answered;
|
|
640
652
|
continue;
|
|
641
653
|
}
|
|
654
|
+
if (res.status >= 502 && res.status <= 504)
|
|
655
|
+
continue;
|
|
656
|
+
const body = await json(res, relay);
|
|
642
657
|
if (!res.ok)
|
|
643
658
|
continue;
|
|
644
|
-
|
|
645
|
-
if (typeof body.team === "string" && body.team)
|
|
659
|
+
if (typeof body?.team === "string" && body.team)
|
|
646
660
|
return { relay, space: body.team, token };
|
|
647
661
|
}
|
|
648
662
|
catch { }
|
|
@@ -651,6 +665,6 @@ export async function resolveInviteCode(code, relays, fetchImpl = fetch) {
|
|
|
651
665
|
if (answered) {
|
|
652
666
|
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`);
|
|
653
667
|
}
|
|
654
|
-
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}`);
|
|
668
|
+
throw Object.assign(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}`), nonJsonFrom ? { nonJsonFrom } : {});
|
|
655
669
|
}
|
|
656
670
|
//# sourceMappingURL=config.js.map
|
package/dist/continued.js
CHANGED
|
@@ -25,6 +25,11 @@ export function loadContinued(space, home) {
|
|
|
25
25
|
export function handledIn(h) {
|
|
26
26
|
return [...new Set(Object.values(readAll(h)).flatMap((l) => (Array.isArray(l) ? l : [])).filter((x) => typeof x === "string" && x.startsWith("handled:")))];
|
|
27
27
|
}
|
|
28
|
+
export const wakeAlertedKey = (team, member, askId) => `wake-alerted:${team}|${member}|${askId}`;
|
|
29
|
+
export function wakeAlerted(team, member, askId, home) {
|
|
30
|
+
const k = wakeAlertedKey(team, member, askId);
|
|
31
|
+
return Object.values(readAll(home)).some((l) => Array.isArray(l) && l.includes(k));
|
|
32
|
+
}
|
|
28
33
|
export function markContinued(space, id, home) {
|
|
29
34
|
try {
|
|
30
35
|
const p = continuedPath(home);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join, win32 } from "node:path";
|
|
5
|
+
export const CLAUDE_BUNDLE = "com.anthropic.claudefordesktop";
|
|
6
|
+
export const CLAUDE_APP_ID_ASSUMED = "Claude";
|
|
7
|
+
const AUMID_RE = /^[A-Za-z0-9.-]{1,64}_[a-z0-9]{13}![A-Za-z0-9._-]{1,64}$/;
|
|
8
|
+
export const AUMID_SCRIPT = "$p = Get-AppxPackage -ErrorAction SilentlyContinue | Where-Object { $_.PackageFamilyName -like 'Claude_*' } | Select-Object -First 1; " +
|
|
9
|
+
"if ($p) { $a = @((Get-AppxPackageManifest $p).Package.Applications.Application)[0]; if ($a) { \"$($p.PackageFamilyName)!$($a.Id)\" } }";
|
|
10
|
+
function runPowershell(script) {
|
|
11
|
+
try {
|
|
12
|
+
const r = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { encoding: "utf8", timeout: 8_000, windowsHide: true });
|
|
13
|
+
return r.status === 0 ? (r.stdout ?? "").trim() : null;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function findClaudeApp(d = {}) {
|
|
20
|
+
const os = d.os ?? process.platform;
|
|
21
|
+
const exists = d.exists ?? existsSync;
|
|
22
|
+
const home = d.home ?? homedir();
|
|
23
|
+
if (os === "darwin") {
|
|
24
|
+
return ["/Applications/Claude.app", join(home, "Applications", "Claude.app")].some((p) => exists(p)) ? { how: "bundle", bundle: CLAUDE_BUNDLE } : null;
|
|
25
|
+
}
|
|
26
|
+
if (os !== "win32")
|
|
27
|
+
return null;
|
|
28
|
+
const env = d.env ?? process.env;
|
|
29
|
+
const local = env.LOCALAPPDATA;
|
|
30
|
+
if (!local)
|
|
31
|
+
return null;
|
|
32
|
+
let families = [];
|
|
33
|
+
try {
|
|
34
|
+
families = (d.list ?? ((p) => readdirSync(p)))(win32.join(local, "Packages")).filter((f) => /^Claude_[a-z0-9]{13}$/.test(f));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
families = [];
|
|
38
|
+
}
|
|
39
|
+
if (families.length) {
|
|
40
|
+
const asked = (d.powershell ?? runPowershell)(AUMID_SCRIPT)?.split(/\r?\n/).map((l) => l.trim()).find((l) => AUMID_RE.test(l));
|
|
41
|
+
if (asked)
|
|
42
|
+
return { how: "appsfolder", aumid: asked, assumed: false };
|
|
43
|
+
const guess = `${families[0]}!${CLAUDE_APP_ID_ASSUMED}`;
|
|
44
|
+
if (AUMID_RE.test(guess))
|
|
45
|
+
return { how: "appsfolder", aumid: guess, assumed: true };
|
|
46
|
+
}
|
|
47
|
+
const exe = win32.join(local, "AnthropicClaude", "claude.exe");
|
|
48
|
+
return exists(exe) ? { how: "exe", path: exe } : null;
|
|
49
|
+
}
|
|
50
|
+
export function openAppCommand(app) {
|
|
51
|
+
if (app.how === "appsfolder")
|
|
52
|
+
return { cmd: "explorer.exe", args: [`shell:AppsFolder\\${app.aumid}`] };
|
|
53
|
+
if (app.how === "exe")
|
|
54
|
+
return { cmd: app.path, args: [] };
|
|
55
|
+
return { cmd: "open", args: ["-b", app.bundle] };
|
|
56
|
+
}
|
|
57
|
+
export function clipboardCommand(os = process.platform, find = () => true) {
|
|
58
|
+
if (os === "darwin")
|
|
59
|
+
return { cmd: "pbcopy", args: [] };
|
|
60
|
+
if (os === "win32")
|
|
61
|
+
return { cmd: "powershell.exe", args: ["-NoProfile", "-NonInteractive", "-Command", "[Console]::InputEncoding = [Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())"] };
|
|
62
|
+
if (find("wl-copy"))
|
|
63
|
+
return { cmd: "wl-copy", args: [] };
|
|
64
|
+
if (find("xclip"))
|
|
65
|
+
return { cmd: "xclip", args: ["-selection", "clipboard"] };
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function copyReal(text) {
|
|
69
|
+
const c = clipboardCommand();
|
|
70
|
+
if (!c)
|
|
71
|
+
return false;
|
|
72
|
+
try {
|
|
73
|
+
return spawnSync(c.cmd, c.args, { input: text, timeout: 8_000, windowsHide: true }).status === 0;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function startReal(cmd, args) {
|
|
80
|
+
try {
|
|
81
|
+
const ch = spawn(cmd, args, { detached: true, stdio: "ignore", windowsHide: false });
|
|
82
|
+
ch.on("error", () => { });
|
|
83
|
+
ch.unref();
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function explainInClaudeApp(prompt, app, d = {}) {
|
|
91
|
+
const copied = (d.copy ?? copyReal)(prompt);
|
|
92
|
+
const o = openAppCommand(app);
|
|
93
|
+
if (!(d.start ?? startReal)(o.cmd, o.args))
|
|
94
|
+
return { ok: false, says: "the Claude app could not be opened" };
|
|
95
|
+
const paste = (d.os ?? process.platform) === "darwin" ? "Cmd+V" : "Ctrl+V";
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
says: copied
|
|
99
|
+
? `opened the Claude app — the question is copied: paste it into a new chat (${paste})`
|
|
100
|
+
: "opened the Claude app — the question could not be copied here; it is printed below to copy",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=desktop-app.js.map
|
package/dist/doctor.js
CHANGED
|
@@ -18,10 +18,10 @@ import { PantheonClient } from "./client.js";
|
|
|
18
18
|
import { load, resolveFor, loadAll, CONFIG_PATH } from "./config.js";
|
|
19
19
|
import { fingerprint, signChallenge, verifyChallenge } from "./identity.js";
|
|
20
20
|
import { approvalSummary, chosenApproval, copilotMcpConfigFile, kiroMcpConfigFile, onPath } from "./install.js";
|
|
21
|
-
import { hookState, readIntent, agentHookStates } from "./notify.js";
|
|
21
|
+
import { hookState, readIntent, agentHookStates, repairNote } from "./notify.js";
|
|
22
22
|
import { loadPersonas } from "./persona.js";
|
|
23
23
|
import { workerProc, menubarProc, notifierProc } from "./machine.js";
|
|
24
|
-
import { agentById, chooseAgent, describeChoice, unattendedRefusal, skippedSummary, SANDBOXED_FOR_UNATTENDED } from "./agents.js";
|
|
24
|
+
import { agentById, chooseAgent, describeChoice, detectAvailable, folderReaderLine, unattendedRefusal, skippedSummary, SANDBOXED_FOR_UNATTENDED } from "./agents.js";
|
|
25
25
|
import { loginShellLookup, npmSpawn, onPathNamed, which, whichWindows } from "./which.js";
|
|
26
26
|
import { VERSION } from "./version.js";
|
|
27
27
|
import { realSys, serviceInstalled, servicesUnavailable } from "./service.js";
|
|
@@ -30,7 +30,7 @@ import { hasPrebuilt } from "./helpers-build.js";
|
|
|
30
30
|
import { traySourcePath, TRAY_PREBUILT } from "./tray.js";
|
|
31
31
|
import { hudSourcePath, HUD_PREBUILT } from "./notifier.js";
|
|
32
32
|
import { hostEnv, platformName } from "./host-env.js";
|
|
33
|
-
import { currentPlan, explainNetError, proxyEnvFor, routeFor, redactProxy } from "./network.js";
|
|
33
|
+
import { checkRelayCertificate, currentPlan, explainNetError, interceptionExplanation, proxyEnvFor, relayJson, routeFor, redactProxy, worthCertificateCheck } from "./network.js";
|
|
34
34
|
import { refusalOf } from "./refusal.js";
|
|
35
35
|
const HOSTED_RELAY = "relay.joinpantheon.network";
|
|
36
36
|
const git = (args, cwd) => {
|
|
@@ -329,9 +329,11 @@ export async function doctor(projectDir) {
|
|
|
329
329
|
else if (fellBack)
|
|
330
330
|
add("warn", "worker agent", line, prof.agent.primary_surface === "kiro"
|
|
331
331
|
? `you chose Kiro at setup; Kiro answers in your live sessions, and runs while you're away use ${choice.backend.label}`
|
|
332
|
-
: surface && !
|
|
333
|
-
? `you
|
|
334
|
-
:
|
|
332
|
+
: surface && !detectAvailable().some((a) => a.backend.id === surface.id)
|
|
333
|
+
? `you use ${surface.label}${surface.id === "claude-code" ? " (the Claude app puts no claude CLI on PATH)" : ""}, and its CLI is not installed here, so runs while you're away use ${choice.backend.label}`
|
|
334
|
+
: surface && !SANDBOXED_FOR_UNATTENDED.has(surface.id) && !unsandboxedOk
|
|
335
|
+
? `you chose ${surface.label} at setup; runs while you're away use ${choice.backend.label} in its sandbox (to have ${surface.label} answer them: pantheon policy grant unsandboxed-runs)`
|
|
336
|
+
: `you chose ${prof.agent.primary_surface} at setup; runs use ${choice.backend.label} until it is signed in again`, shareLine);
|
|
335
337
|
else
|
|
336
338
|
add("pass", "worker agent", line, undefined, shareLine);
|
|
337
339
|
}
|
|
@@ -395,9 +397,32 @@ export async function doctor(projectDir) {
|
|
|
395
397
|
}
|
|
396
398
|
const health = `${cfg.relay.replace(/^ws/, "http")}/health`;
|
|
397
399
|
const where = hosted ? "hosted" : "self-hosted";
|
|
400
|
+
const certAsk = url.protocol === "wss:" ? checkRelayCertificate(cfg.relay).catch(() => null) : Promise.resolve(null);
|
|
401
|
+
let cert = null;
|
|
402
|
+
let certSaid = false;
|
|
403
|
+
const certRow = async (reached) => {
|
|
404
|
+
cert = await certAsk;
|
|
405
|
+
if (!cert || certSaid)
|
|
406
|
+
return cert;
|
|
407
|
+
certSaid = true;
|
|
408
|
+
if (cert.publicCa === true)
|
|
409
|
+
add("pass", "certificate", `issued by ${cert.issuer}, a public certificate authority`, undefined, "from a public CA");
|
|
410
|
+
else if (cert.publicCa === false && reached) {
|
|
411
|
+
add("pass", "certificate", `re-signed by ${cert.issuer}, not a public certificate authority: an antivirus or company proxy inspects this connection, and the relay still answers through it`, undefined, "re-signed by an inspecting proxy; relay answers");
|
|
412
|
+
}
|
|
413
|
+
else if (cert.publicCa === false) {
|
|
414
|
+
const x = interceptionExplanation(cert);
|
|
415
|
+
add("fail", "certificate", x.why, x.fix, x.share);
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
add("warn", "certificate", `could not be verified here (${cert.error ?? "unknown error"})`, "check this machine's date and time; if it is right, try another network", "not verifiable");
|
|
419
|
+
}
|
|
420
|
+
return cert;
|
|
421
|
+
};
|
|
398
422
|
try {
|
|
399
423
|
const res = await fetch(health, { signal: AbortSignal.timeout(5000) });
|
|
400
|
-
const body = (await res
|
|
424
|
+
const body = (await relayJson(res, health));
|
|
425
|
+
await certRow(true);
|
|
401
426
|
if (!body.ok && body.storage) {
|
|
402
427
|
const since = body.storage.at ? ` (since ${new Date(body.storage.at).toLocaleString()})` : "";
|
|
403
428
|
add("fail", "relay", `the relay cannot write to its disk: ${body.storage.error ?? "unknown error"}${since}`, hosted ? `this is on the hosted relay's side — nothing on this machine can fix it; tell us at ${FEEDBACK_ADDRESS}` : "free space or fix permissions on the relay host's data folder, then restart the relay", `cannot write to its disk (${where})`);
|
|
@@ -429,7 +454,8 @@ export async function doctor(projectDir) {
|
|
|
429
454
|
}
|
|
430
455
|
catch (e) {
|
|
431
456
|
const msg = e.message;
|
|
432
|
-
const
|
|
457
|
+
const c = await certRow(false);
|
|
458
|
+
const nx = c?.publicCa === false && worthCertificateCheck(e) ? interceptionExplanation(c) : explainNetError(e, health);
|
|
433
459
|
if (nx) {
|
|
434
460
|
add("fail", "relay", `cannot reach ${health} — ${nx.why}`, nx.fix, `unreachable (${where}): ${nx.share}`);
|
|
435
461
|
return out;
|
|
@@ -441,7 +467,10 @@ export async function doctor(projectDir) {
|
|
|
441
467
|
let joined = false;
|
|
442
468
|
await client.connect().then(() => { joined = true; }).catch(() => { });
|
|
443
469
|
if (!joined) {
|
|
444
|
-
const
|
|
470
|
+
const lastErr = new Error(client.lastError ?? "");
|
|
471
|
+
const seen = cert;
|
|
472
|
+
const nx = client.pending ? null
|
|
473
|
+
: seen?.publicCa === false && worthCertificateCheck(lastErr) ? interceptionExplanation(seen) : explainNetError(lastErr, cfg.relay);
|
|
445
474
|
if (nx) {
|
|
446
475
|
add("fail", "live link", `the health check got through but the live connection did not — ${nx.why}`, nx.fix, `live connection failed: ${nx.share}`);
|
|
447
476
|
client.close();
|
|
@@ -538,13 +567,22 @@ export async function doctor(projectDir) {
|
|
|
538
567
|
const f = loadAll();
|
|
539
568
|
const m = f && Object.values(f.spaces ?? {}).find((x) => x.id === (client.spaceId ?? cfg.spaceId) || x.name === cfg.space);
|
|
540
569
|
if (f && m) {
|
|
541
|
-
const { teamWorkspaces } = await import("./worker-fleet.js");
|
|
570
|
+
const { teamWorkspaces, unservedFolders } = await import("./worker-fleet.js");
|
|
542
571
|
const ws = teamWorkspaces(f, m.id);
|
|
572
|
+
for (const u of unservedFolders(f, m.id, ws))
|
|
573
|
+
add("warn", "worker serves", `${u.path} is bound but not served: ${u.why}`, undefined, "a bound folder is not served");
|
|
543
574
|
const fps = new Set(ws.map((w) => w.fp).filter(Boolean));
|
|
544
575
|
const missing = (m.repos ?? []).filter((r) => !fps.has(r.fp)).map((r) => r.name);
|
|
576
|
+
if (!ws.length && !unservedFolders(f, m.id, ws).length) {
|
|
577
|
+
add("warn", "worker serves", `nothing on this machine is bound to "${cfg.space}" — teammates' requests to you wait for your live session`, "from inside the repo or docs folder: pantheon use <team> --here (the worker serves it at once, no restart)", "nothing bound to this team");
|
|
578
|
+
}
|
|
545
579
|
if (ws.length) {
|
|
546
580
|
add("pass", "worker serves", `${ws.map((w) => `${w.name}${w.kind === "folder" ? " (folder)" : ""}`).join(", ")}${missing.length ? ` — not on this machine: ${missing.join(", ")}` : ""}`, missing.length ? "requests about those are told they wait for you; clone them, then: pantheon repo find" : undefined, `${ws.length} workspace${ws.length === 1 ? "" : "s"}${missing.length ? `, ${missing.length} team repo${missing.length === 1 ? "" : "s"} not here` : ""}`);
|
|
547
581
|
}
|
|
582
|
+
if (ws.some((w) => w.kind === "folder")) {
|
|
583
|
+
const r = folderReaderLine(detectAvailable());
|
|
584
|
+
add(r.ok ? "pass" : "warn", "folders", r.line, undefined, r.ok ? "any agent here answers folder requests, in a copy" : "no agent here can answer folder requests");
|
|
585
|
+
}
|
|
548
586
|
}
|
|
549
587
|
}
|
|
550
588
|
{
|
|
@@ -613,25 +651,8 @@ export async function doctor(projectDir) {
|
|
|
613
651
|
add("pass", "alerts", "installed and running — a desktop notification when a teammate needs you", undefined, "installed and running");
|
|
614
652
|
}
|
|
615
653
|
}
|
|
616
|
-
|
|
617
|
-
add(
|
|
618
|
-
}
|
|
619
|
-
else if (!reportsActivity) {
|
|
620
|
-
add("warn", "wake-up hook", `installed (${hook.events?.join(", ")}) but predates the activity hook, so a worker cannot tell your idle editor from one you are using`, "re-run `pantheon hook install --user` to add the UserPromptSubmit half", `installed (${hook.events?.join(", ")}), predates the activity hook`);
|
|
621
|
-
}
|
|
622
|
-
else {
|
|
623
|
-
const intent = readIntent();
|
|
624
|
-
intent?.repairs
|
|
625
|
-
? add("warn", "wake-up hook", `${hook.events?.join(", ")} (${hook.scope} scope) — but it has been removed and put back ${intent.repairs} time${intent.repairs === 1 ? "" : "s"}`, "something on this machine rewrites ~/.claude/settings.json and drops the hooks key. The worker repairs it, but only while the worker is running", `${hook.events?.join(", ")} (${hook.scope} scope), repaired ${intent.repairs} time${intent.repairs === 1 ? "" : "s"}`)
|
|
626
|
-
: add("pass", "wake-up hook", `${hook.events?.join(", ")} (${hook.scope} scope)`, undefined, `${hook.events?.join(", ")} (${hook.scope} scope)`);
|
|
627
|
-
}
|
|
628
|
-
if (hookStates.some((s) => s.agent === "codex")) {
|
|
629
|
-
add("warn", "codex hooks", "installed — but codex skips new or changed hooks until they are approved", "run /hooks inside codex once to approve them (needs codex ≥ 0.115; sessions still report presence over MCP either way)", "installed, not yet approved inside codex");
|
|
630
|
-
}
|
|
631
|
-
if (hookStates.some((s) => s.agent === "kiro")) {
|
|
632
|
-
kiroDefaultIsPantheon()
|
|
633
|
-
? add("pass", "kiro hooks", "in the pantheon agent, which is your Kiro default — every kiro-cli chat keeps working while a teammate waits", undefined, "in the pantheon agent, the default")
|
|
634
|
-
: add("warn", "kiro hooks", "in the pantheon agent — used only when you run kiro-cli chat --agent pantheon", "kiro-cli agent set-default pantheon makes it every session; without it, Kiro sessions still report presence but end their turn with a teammate waiting", "in the pantheon agent, not the default");
|
|
654
|
+
for (const r of hookChecks({ states: hookStates, hook, reportsActivity, repairs: readIntent()?.repairs, kiroDefault: kiroDefaultIsPantheon })) {
|
|
655
|
+
add(r.level, r.name, r.detail, r.fix, r.share);
|
|
635
656
|
}
|
|
636
657
|
const personas = loadPersonas(projectDir);
|
|
637
658
|
if (personas.length) {
|
|
@@ -651,8 +672,38 @@ export async function doctor(projectDir) {
|
|
|
651
672
|
client.close();
|
|
652
673
|
return out;
|
|
653
674
|
}
|
|
675
|
+
export function hookChecks(f) {
|
|
676
|
+
const out = [];
|
|
677
|
+
const add = (level, name, detail, fix, share) => out.push({ level, name, detail, ...(fix ? { fix } : {}), ...(share ? { share } : {}) });
|
|
678
|
+
const { hook } = f;
|
|
679
|
+
if (!f.states.length && !hook.installed) {
|
|
680
|
+
add("warn", "wake-up hook", "not installed — your agent will not notice a peer's message until you ask it to", "pantheon hook install --user (auto-detects claude / codex / cursor / copilot / kiro on PATH)", "not installed");
|
|
681
|
+
}
|
|
682
|
+
else if (!f.reportsActivity) {
|
|
683
|
+
add("warn", "wake-up hook", `installed (${hook.events?.join(", ")}) but predates the activity hook, so a worker cannot tell your idle editor from one you are using`, "re-run `pantheon hook install --user` to add the UserPromptSubmit half", `installed (${hook.events?.join(", ")}), predates the activity hook`);
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
const where = `${hook.events?.join(", ")} (${hook.scope} scope)`;
|
|
687
|
+
const repaired = repairNote(f.repairs);
|
|
688
|
+
if (!repaired)
|
|
689
|
+
add("pass", "wake-up hook", where, undefined, where);
|
|
690
|
+
else if (!repaired.recurring)
|
|
691
|
+
add("info", "wake-up hook", `${where} — ${repaired.said}`, undefined, `${where}, repaired once`);
|
|
692
|
+
else
|
|
693
|
+
add("warn", "wake-up hook", `${where} — but it has been removed and put back ${f.repairs} times`, `${repaired.why}. The worker repairs it, but only while the worker is running`, `${where}, repaired ${f.repairs} times`);
|
|
694
|
+
}
|
|
695
|
+
if (f.states.some((s) => s.agent === "codex")) {
|
|
696
|
+
add("info", "codex hooks", "installed — codex runs them once they are approved inside codex, a one-time step (and again after an upgrade changes them)", "run /hooks inside codex once to approve them (needs codex ≥ 0.115; sessions still report presence over MCP either way)", "installed, approval inside codex is a one-time step");
|
|
697
|
+
}
|
|
698
|
+
if (f.states.some((s) => s.agent === "kiro")) {
|
|
699
|
+
f.kiroDefault()
|
|
700
|
+
? add("pass", "kiro hooks", "in the pantheon agent, which is your Kiro default — every kiro-cli chat keeps working while a teammate waits", undefined, "in the pantheon agent, the default")
|
|
701
|
+
: add("info", "kiro hooks", "in the pantheon agent — every kiro-cli chat uses it once it is Kiro's default, a one-time step", "kiro-cli agent set-default pantheon makes it every session; without it, Kiro sessions still report presence but end their turn with a teammate waiting", "in the pantheon agent, not the default (a one-time step)");
|
|
702
|
+
}
|
|
703
|
+
return out;
|
|
704
|
+
}
|
|
654
705
|
export function render(checks) {
|
|
655
|
-
const mark = { pass: "\x1b[36m ok \x1b[0m", warn: "\x1b[33mwarn\x1b[0m", fail: "\x1b[31mFAIL\x1b[0m" };
|
|
706
|
+
const mark = { pass: "\x1b[36m ok \x1b[0m", info: "\x1b[2minfo\x1b[0m", warn: "\x1b[33mwarn\x1b[0m", fail: "\x1b[31mFAIL\x1b[0m" };
|
|
656
707
|
const lines = [];
|
|
657
708
|
for (const c of checks) {
|
|
658
709
|
lines.push(` ${mark[c.level]} ${c.name.padEnd(12)} ${c.detail}`);
|
|
@@ -661,16 +712,17 @@ export function render(checks) {
|
|
|
661
712
|
}
|
|
662
713
|
const failed = checks.filter((c) => c.level === "fail").length;
|
|
663
714
|
const warned = checks.filter((c) => c.level === "warn").length;
|
|
715
|
+
const noted = checks.filter((c) => c.level === "info").length;
|
|
664
716
|
lines.push("");
|
|
665
717
|
lines.push(failed
|
|
666
718
|
? `\x1b[31m${failed} blocking problem${failed > 1 ? "s" : ""}\x1b[0m${warned ? `, ${warned} warning${warned > 1 ? "s" : ""}` : ""} — fix the FAILs before you start`
|
|
667
719
|
: warned ? `\x1b[36mno blockers\x1b[0m, ${warned} warning${warned > 1 ? "s" : ""} — readable above`
|
|
668
|
-
: `\x1b[36mall clear\x1b[0m — both machines are ready`);
|
|
720
|
+
: `\x1b[36mall clear\x1b[0m — both machines are ready${noted ? ` (${noted} note${noted > 1 ? "s" : ""} above, marked info)` : ""}`);
|
|
669
721
|
lines.push(`\x1b[2mpantheon doctor --share prints this without names or paths, to paste to ${FEEDBACK_ADDRESS}\x1b[0m`);
|
|
670
722
|
return { text: lines.join("\n"), failed };
|
|
671
723
|
}
|
|
672
724
|
export function shareBlock(checks, now = new Date()) {
|
|
673
|
-
const mark = { pass: "ok ", warn: "warn", fail: "FAIL" };
|
|
725
|
+
const mark = { pass: "ok ", info: "info", warn: "warn", fail: "FAIL" };
|
|
674
726
|
return [
|
|
675
727
|
`pantheon ${VERSION} — doctor, ${now.toISOString().slice(0, 16).replace("T", " ")} UTC`,
|
|
676
728
|
"no names, teams, paths or addresses are in this block; it is safe to paste",
|
package/dist/folder-guard.js
CHANGED
|
@@ -86,4 +86,16 @@ export function folderRefusalHere(dir) {
|
|
|
86
86
|
return folderRefusal(real(dir), folderFacts());
|
|
87
87
|
}
|
|
88
88
|
export const isTemporaryRefusal = (r) => r?.kind === "temporary" || r?.kind === "app-scratch";
|
|
89
|
+
export function projectFolderAnswer(answer, home, cwd, p = path) {
|
|
90
|
+
let a = answer.trim();
|
|
91
|
+
if (a.length >= 2 && ((a.startsWith('"') && a.endsWith('"')) || (a.startsWith("'") && a.endsWith("'"))))
|
|
92
|
+
a = a.slice(1, -1);
|
|
93
|
+
if (p.sep === "/")
|
|
94
|
+
a = a.replace(/\\([ ()&'])/g, "$1");
|
|
95
|
+
if (a === "~")
|
|
96
|
+
a = home;
|
|
97
|
+
else if (/^~[\\/]/.test(a))
|
|
98
|
+
a = p.join(home, a.slice(2));
|
|
99
|
+
return p.resolve(cwd, a);
|
|
100
|
+
}
|
|
89
101
|
//# sourceMappingURL=folder-guard.js.map
|