portable-agent-layer 0.85.9 → 0.85.11
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 +1 -1
- package/assets/skills/consulting-report/SKILL.md +2 -5
- package/assets/skills/consulting-report/tools/generate-pdf.ts +5 -30
- package/assets/skills/consulting-report/tools/scaffold.ts +1 -1
- package/assets/skills/create-pdf/SKILL.md +4 -4
- package/assets/skills/create-pdf/tools/md-to-html-pdf.ts +2 -9
- package/assets/templates/rules.codex.rules +1 -13
- package/assets/templates/settings.claude.json +0 -1
- package/package.json +2 -3
- package/src/cli/doctor/agents.ts +336 -0
- package/src/cli/doctor/environment.ts +129 -0
- package/src/cli/doctor/finding.ts +35 -0
- package/src/cli/doctor/health.ts +54 -0
- package/src/cli/doctor/inference.ts +91 -0
- package/src/cli/doctor/os-release.ts +30 -0
- package/src/cli/doctor/render.ts +82 -0
- package/src/cli/doctor/run.ts +46 -0
- package/src/cli/doctor/state.ts +229 -0
- package/src/cli/doctor/version-control.ts +108 -0
- package/src/cli/index.ts +10 -683
- package/src/cli/skill.ts +1 -6
- package/src/hooks/lib/log.ts +24 -11
- package/src/hooks/lib/projects.ts +0 -16
- package/assets/skills/consulting-report/tools/generate-pdf.mjs +0 -226
- package/assets/skills/create-pdf/tools/md-to-html-pdf.mjs +0 -103
- package/src/cli/doctor-tools.ts +0 -133
package/src/cli/index.ts
CHANGED
|
@@ -47,17 +47,14 @@ import {
|
|
|
47
47
|
summarize,
|
|
48
48
|
} from "../hooks/lib/import-merge";
|
|
49
49
|
import { inference, previewInferenceRoute } from "../hooks/lib/inference";
|
|
50
|
-
import { logDebug
|
|
50
|
+
import { logDebug } from "../hooks/lib/log";
|
|
51
51
|
import { ensureRegistered, writeRegistryEntry } from "../hooks/lib/machine";
|
|
52
|
-
import { opencodeBackgroundModel } from "../hooks/lib/opencode-config";
|
|
53
52
|
import { palHome, palPkg, paths, platform, toPath } from "../hooks/lib/paths";
|
|
54
|
-
import { auditBindings, describeBindingIssue } from "../hooks/lib/projects";
|
|
55
53
|
import { telosStatus } from "../hooks/lib/telos-topics";
|
|
56
|
-
import { findBinaryOnPath } from "../hooks/lib/which";
|
|
57
54
|
import { log } from "../targets/lib";
|
|
58
55
|
import { builtinToolVerbs, runBuiltinTool } from "./builtin-tools";
|
|
59
|
-
import {
|
|
60
|
-
import {
|
|
56
|
+
import { type DoctorResult, detectAgents } from "./doctor/agents";
|
|
57
|
+
import { runDoctor } from "./doctor/run";
|
|
61
58
|
import { findSessionAgent, NO_SESSION_AGENT_MESSAGE } from "./session-agent";
|
|
62
59
|
|
|
63
60
|
const allArgs = process.argv.slice(2);
|
|
@@ -75,53 +72,6 @@ if (allArgs[0] === "cli") {
|
|
|
75
72
|
|
|
76
73
|
// ── Session: pal [args] ──
|
|
77
74
|
|
|
78
|
-
interface ToolCheck {
|
|
79
|
-
name: string;
|
|
80
|
-
available: boolean;
|
|
81
|
-
version?: string;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function checkTool(cmd: string, versionArgs: string[] = ["--version"]): ToolCheck {
|
|
85
|
-
try {
|
|
86
|
-
const result = spawnSync(cmd, versionArgs, {
|
|
87
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
88
|
-
shell: true,
|
|
89
|
-
timeout: 5000,
|
|
90
|
-
});
|
|
91
|
-
if (result.status === 0) {
|
|
92
|
-
const version = (result.stdout?.toString() || "").trim().split("\n")[0];
|
|
93
|
-
return { name: cmd, available: true, version };
|
|
94
|
-
}
|
|
95
|
-
} catch {
|
|
96
|
-
// not found
|
|
97
|
-
}
|
|
98
|
-
return { name: cmd, available: false };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Copilot ships as a VS Code extension as well as a CLI, and the extension puts
|
|
103
|
-
* no `copilot` binary on PATH — but both read hooks, skills and agents out of
|
|
104
|
-
* ~/.copilot. Fall back to that directory so PAL's copilot checks still run.
|
|
105
|
-
*/
|
|
106
|
-
function checkCopilot(): ToolCheck {
|
|
107
|
-
const cli = checkTool("copilot", ["version"]);
|
|
108
|
-
if (cli.available) return cli;
|
|
109
|
-
if (existsSync(platform.copilotDir())) {
|
|
110
|
-
return { name: "copilot", available: true, version: "~/.copilot (no CLI on PATH)" };
|
|
111
|
-
}
|
|
112
|
-
return cli;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* The Cursor CLI installs `cursor-agent` (and `agent`) and no `cursor` command;
|
|
117
|
-
* `cursor` is the editor's shell command. Both read ~/.cursor, so either counts.
|
|
118
|
-
*/
|
|
119
|
-
function checkCursor(): ToolCheck {
|
|
120
|
-
const cli = checkTool("cursor-agent");
|
|
121
|
-
if (cli.available) return cli;
|
|
122
|
-
return checkTool("cursor");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
75
|
async function session(sessionArgs: string[]) {
|
|
126
76
|
const agent = findSessionAgent();
|
|
127
77
|
if (!agent) {
|
|
@@ -223,10 +173,11 @@ async function runCli(command: string | undefined, args: string[]) {
|
|
|
223
173
|
await status();
|
|
224
174
|
break;
|
|
225
175
|
case "doctor": {
|
|
226
|
-
|
|
176
|
+
const exitCode = runDoctor(args);
|
|
227
177
|
if (args.includes("--probe-inference") || args.includes("--probe")) {
|
|
228
178
|
await probeInference();
|
|
229
179
|
}
|
|
180
|
+
process.exit(exitCode);
|
|
230
181
|
break;
|
|
231
182
|
}
|
|
232
183
|
case "migrate": {
|
|
@@ -346,7 +297,8 @@ function showHelp() {
|
|
|
346
297
|
pal cli export [path] [--dry-run] Export state to zip
|
|
347
298
|
pal cli import [path] [--dry-run] Merge state from zip (--overwrite to replace)
|
|
348
299
|
pal cli status Show PAL configuration
|
|
349
|
-
pal cli doctor [--
|
|
300
|
+
pal cli doctor [--verbose] [--json] Find what is wrong and how to fix it; exits 1 on any failure
|
|
301
|
+
[--probe-inference] Also fire real inference per route
|
|
350
302
|
pal cli migrate [--list] [--dry-run] Run pending data migrations
|
|
351
303
|
pal cli <tool> [args] Run a built-in agent tool ('<tool> --help' for its flags):
|
|
352
304
|
${builtinToolVerbs.join(" · ")}
|
|
@@ -419,7 +371,7 @@ function parseTargets(args: string[]): Targets {
|
|
|
419
371
|
/** Resolve targets against available agents. Errors if explicitly requested but missing. */
|
|
420
372
|
function resolveTargets(args: string[], health?: DoctorResult): Targets {
|
|
421
373
|
const requested = parseTargets(args);
|
|
422
|
-
const h = health ||
|
|
374
|
+
const h = health || detectAgents();
|
|
423
375
|
const explicit = args.some(
|
|
424
376
|
(a) =>
|
|
425
377
|
a === "--claude" ||
|
|
@@ -472,140 +424,6 @@ function resolveTargets(args: string[], health?: DoctorResult): Targets {
|
|
|
472
424
|
return targets;
|
|
473
425
|
}
|
|
474
426
|
|
|
475
|
-
// ── Hook health ──
|
|
476
|
-
|
|
477
|
-
function checkClaudeHooksRegistered(): boolean {
|
|
478
|
-
const settingsPath = resolve(platform.claudeDir(), "settings.json");
|
|
479
|
-
if (!existsSync(settingsPath)) return false;
|
|
480
|
-
try {
|
|
481
|
-
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
482
|
-
const groups = settings?.hooks?.SessionStart;
|
|
483
|
-
if (!Array.isArray(groups)) return false;
|
|
484
|
-
return groups.some((g: { hooks?: { command?: string }[] }) =>
|
|
485
|
-
g?.hooks?.some((h) => h?.command?.includes("LoadContext"))
|
|
486
|
-
);
|
|
487
|
-
} catch {
|
|
488
|
-
return false;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function checkCursorHooksRegistered(): boolean {
|
|
493
|
-
const hooksPath = resolve(platform.cursorDir(), "hooks.json");
|
|
494
|
-
if (!existsSync(hooksPath)) return false;
|
|
495
|
-
try {
|
|
496
|
-
const data = JSON.parse(readFileSync(hooksPath, "utf-8"));
|
|
497
|
-
const hooks = data?.hooks?.sessionStart;
|
|
498
|
-
if (!Array.isArray(hooks)) return false;
|
|
499
|
-
return hooks.some((h: { command?: string }) => h?.command?.includes("LoadContext"));
|
|
500
|
-
} catch {
|
|
501
|
-
return false;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function checkOpencodePluginInstalled(): boolean {
|
|
506
|
-
return existsSync(resolve(platform.opencodeDir(), "plugins", "pal-plugin.ts"));
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function checkCopilotHooksRegistered(): boolean {
|
|
510
|
-
return existsSync(resolve(platform.copilotDir(), "hooks", "pal-hooks.json"));
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function checkCodexHooksRegistered(): boolean {
|
|
514
|
-
const hooksPath = resolve(platform.codexDir(), "hooks.json");
|
|
515
|
-
if (!existsSync(hooksPath)) return false;
|
|
516
|
-
try {
|
|
517
|
-
const data = JSON.parse(readFileSync(hooksPath, "utf-8"));
|
|
518
|
-
const entries = data?.hooks?.SessionStart;
|
|
519
|
-
if (!Array.isArray(entries)) return false;
|
|
520
|
-
return entries.some((entry: { command?: string; hooks?: { command?: string }[] }) => {
|
|
521
|
-
if (entry?.command?.includes("LoadContext")) return true;
|
|
522
|
-
return entry?.hooks?.some((h) => h?.command?.includes("LoadContext")) ?? false;
|
|
523
|
-
});
|
|
524
|
-
} catch {
|
|
525
|
-
return false;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
function checkCopilotInstructionsPresent(): boolean {
|
|
530
|
-
const instructionsDir = resolve(platform.copilotDir(), "instructions");
|
|
531
|
-
if (!existsSync(instructionsDir)) return false;
|
|
532
|
-
return readdirSync(instructionsDir).some(
|
|
533
|
-
(f) => f.startsWith("pal-") && f.endsWith(".instructions.md")
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// ── Install integrity (Tier 2 doctor checks) ──
|
|
538
|
-
|
|
539
|
-
/** Hook-config keys that carry a shell command: cross-platform and per-shell variants. */
|
|
540
|
-
function isHookCommandField(key: string): boolean {
|
|
541
|
-
return key === "command" || key === "bash" || key === "powershell";
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
/** Recursively collect every command-carrying field value in a hook-config JSON. */
|
|
545
|
-
function extractAllHookCommands(obj: unknown, out: string[] = []): string[] {
|
|
546
|
-
if (Array.isArray(obj)) {
|
|
547
|
-
for (const item of obj) extractAllHookCommands(item, out);
|
|
548
|
-
} else if (obj && typeof obj === "object") {
|
|
549
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
550
|
-
if (isHookCommandField(k) && typeof v === "string") {
|
|
551
|
-
out.push(v);
|
|
552
|
-
} else {
|
|
553
|
-
extractAllHookCommands(v, out);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
return out;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
interface HookPrefixCheck {
|
|
561
|
-
ok: boolean;
|
|
562
|
-
total: number;
|
|
563
|
-
missing: number;
|
|
564
|
-
firstMissing?: string;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
/**
|
|
568
|
-
* True when a hook command names its agent, by env prefix or by argv flag.
|
|
569
|
-
*
|
|
570
|
-
* An env prefix only parses in one shell family, so hook configs whose host
|
|
571
|
-
* shell is unknown declare the agent with a shell-agnostic `--agent=` flag.
|
|
572
|
-
*/
|
|
573
|
-
function declaresAgent(cmd: string, agentName: string): boolean {
|
|
574
|
-
return (
|
|
575
|
-
cmd.startsWith(`PAL_AGENT=${agentName} `) ||
|
|
576
|
-
cmd.startsWith(`$env:PAL_AGENT='${agentName}'; `) ||
|
|
577
|
-
cmd.includes(`--agent=${agentName}`)
|
|
578
|
-
);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
/** Verify every command in an installed hook file names `<agent>` as its agent. */
|
|
582
|
-
function checkAgentHookPrefix(filePath: string, agentName: string): HookPrefixCheck {
|
|
583
|
-
if (!existsSync(filePath)) return { ok: false, total: 0, missing: 0 };
|
|
584
|
-
try {
|
|
585
|
-
const data = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
586
|
-
const commands = extractAllHookCommands(data.hooks ?? data);
|
|
587
|
-
const missing = commands.filter((c) => !declaresAgent(c, agentName));
|
|
588
|
-
return {
|
|
589
|
-
ok: commands.length > 0 && missing.length === 0,
|
|
590
|
-
total: commands.length,
|
|
591
|
-
missing: missing.length,
|
|
592
|
-
firstMissing: missing[0]?.slice(0, 80),
|
|
593
|
-
};
|
|
594
|
-
} catch {
|
|
595
|
-
return { ok: false, total: 0, missing: 0 };
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
interface FreshnessCheck {
|
|
600
|
-
ok: boolean;
|
|
601
|
-
reason?: string;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* Verify the installed opencode plugin is at least as new as the source. Catches
|
|
606
|
-
* the "stale install" failure mode we hit live — plugin file is a copy, not a
|
|
607
|
-
* symlink, so source edits don't reach the running opencode until reinstall.
|
|
608
|
-
*/
|
|
609
427
|
/**
|
|
610
428
|
* Probe every supported agent route with a tiny real inference call.
|
|
611
429
|
* Sequential (concurrent multi-CLI spawns trigger the empty-abort race we
|
|
@@ -662,483 +480,6 @@ async function probeInference(): Promise<void> {
|
|
|
662
480
|
}
|
|
663
481
|
}
|
|
664
482
|
|
|
665
|
-
function checkOpencodePluginFresh(): FreshnessCheck {
|
|
666
|
-
const installedPath = resolve(platform.opencodeDir(), "plugins", "pal-plugin.ts");
|
|
667
|
-
const sourcePath = resolve(palPkg(), "src", "targets", "opencode", "plugin.ts");
|
|
668
|
-
if (!existsSync(installedPath))
|
|
669
|
-
return { ok: false, reason: "installed plugin missing" };
|
|
670
|
-
if (!existsSync(sourcePath)) return { ok: true }; // can't compare; assume installed is fine
|
|
671
|
-
try {
|
|
672
|
-
const installedMtime = statSync(installedPath).mtimeMs;
|
|
673
|
-
const sourceMtime = statSync(sourcePath).mtimeMs;
|
|
674
|
-
if (installedMtime >= sourceMtime) return { ok: true };
|
|
675
|
-
const ageMin = Math.round((sourceMtime - installedMtime) / 60000);
|
|
676
|
-
return {
|
|
677
|
-
ok: false,
|
|
678
|
-
reason: `source is ${ageMin}m newer than installed — run 'pal cli install --opencode'`,
|
|
679
|
-
};
|
|
680
|
-
} catch {
|
|
681
|
-
return { ok: true };
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
function playwrightBrowsersPath(): string {
|
|
686
|
-
if (process.env.PLAYWRIGHT_BROWSERS_PATH) return process.env.PLAYWRIGHT_BROWSERS_PATH;
|
|
687
|
-
const home = homedir();
|
|
688
|
-
if (process.platform === "darwin") return resolve(home, "Library/Caches/ms-playwright");
|
|
689
|
-
if (process.platform === "win32") return resolve(home, "AppData/Local/ms-playwright");
|
|
690
|
-
return resolve(home, ".cache/ms-playwright");
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
function checkPlaywrightChromium(): boolean {
|
|
694
|
-
const base = playwrightBrowsersPath();
|
|
695
|
-
if (!existsSync(base)) return false;
|
|
696
|
-
try {
|
|
697
|
-
return readdirSync(base).some((f) => f.startsWith("chromium-"));
|
|
698
|
-
} catch {
|
|
699
|
-
return false;
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
interface NodeCheck {
|
|
704
|
-
available: boolean;
|
|
705
|
-
version?: string;
|
|
706
|
-
meetsMinimum?: boolean;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// Minimum Node version with `--experimental-strip-types` is 22.6.0 — required
|
|
710
|
-
// by the consulting-report skill, which runs under Node on Windows because
|
|
711
|
-
// Playwright's chromium.launch() hangs under Bun.
|
|
712
|
-
function checkNode(): NodeCheck {
|
|
713
|
-
const minMajor = 22;
|
|
714
|
-
const minMinor = 6;
|
|
715
|
-
const result = checkTool("node");
|
|
716
|
-
if (!result.available) return { available: false };
|
|
717
|
-
const raw = (result.version || "").replace(/^v/, "");
|
|
718
|
-
const [majorStr = "", minorStr = ""] = raw.split(".");
|
|
719
|
-
const major = Number(majorStr);
|
|
720
|
-
const minor = Number(minorStr);
|
|
721
|
-
const meetsMinimum =
|
|
722
|
-
Number.isFinite(major) &&
|
|
723
|
-
Number.isFinite(minor) &&
|
|
724
|
-
(major > minMajor || (major === minMajor && minor >= minMinor));
|
|
725
|
-
return { available: true, version: raw, meetsMinimum };
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
function nodeInstallHint(): string {
|
|
729
|
-
if (process.platform === "win32")
|
|
730
|
-
return "install Node ≥ 22.6 (`winget install OpenJS.NodeJS.LTS` or https://nodejs.org)";
|
|
731
|
-
if (process.platform === "darwin")
|
|
732
|
-
return "install Node ≥ 22.6 (`brew install node` or https://nodejs.org)";
|
|
733
|
-
return "install Node ≥ 22.6 (see https://nodejs.org or your package manager)";
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
function rtkInstallHint(): string {
|
|
737
|
-
if (process.platform === "win32")
|
|
738
|
-
return "download rtk.exe from https://github.com/rtk-ai/rtk/releases and add it to PATH";
|
|
739
|
-
if (process.platform === "darwin") return "`brew install rtk`";
|
|
740
|
-
return "`curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh`";
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
// ── Doctor ──
|
|
744
|
-
|
|
745
|
-
interface DoctorResult {
|
|
746
|
-
bun: ToolCheck;
|
|
747
|
-
claude: ToolCheck;
|
|
748
|
-
opencode: ToolCheck;
|
|
749
|
-
cursor: ToolCheck;
|
|
750
|
-
copilot: ToolCheck;
|
|
751
|
-
codex: ToolCheck;
|
|
752
|
-
rtk: ToolCheck;
|
|
753
|
-
hasAgent: boolean;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
function doctor(silent = false): DoctorResult {
|
|
757
|
-
// Allow CI/tests to skip agent detection
|
|
758
|
-
if (process.env.PAL_SKIP_DOCTOR === "1") {
|
|
759
|
-
return {
|
|
760
|
-
bun: { name: "bun", available: true, version: Bun.version },
|
|
761
|
-
claude: { name: "claude", available: true },
|
|
762
|
-
opencode: { name: "opencode", available: true },
|
|
763
|
-
cursor: { name: "cursor", available: true },
|
|
764
|
-
copilot: { name: "copilot", available: true },
|
|
765
|
-
codex: { name: "codex", available: true },
|
|
766
|
-
rtk: { name: "rtk", available: true },
|
|
767
|
-
hasAgent: true,
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
const bun = { name: "bun", available: true, version: Bun.version };
|
|
772
|
-
const claude = checkTool("claude");
|
|
773
|
-
const opencode = checkTool("opencode");
|
|
774
|
-
const cursor = checkCursor();
|
|
775
|
-
const copilot = checkCopilot();
|
|
776
|
-
const codex = checkTool("codex");
|
|
777
|
-
const rtk = checkTool("rtk");
|
|
778
|
-
const hasAgent =
|
|
779
|
-
claude.available ||
|
|
780
|
-
opencode.available ||
|
|
781
|
-
cursor.available ||
|
|
782
|
-
copilot.available ||
|
|
783
|
-
codex.available;
|
|
784
|
-
|
|
785
|
-
const home = palHome();
|
|
786
|
-
const telosCount = (() => {
|
|
787
|
-
try {
|
|
788
|
-
return readdirSync(resolve(home, "telos")).filter((f) => f.endsWith(".md")).length;
|
|
789
|
-
} catch {
|
|
790
|
-
return 0;
|
|
791
|
-
}
|
|
792
|
-
})();
|
|
793
|
-
|
|
794
|
-
if (!silent) {
|
|
795
|
-
const ok = (msg: string) => console.log(` \x1b[32m\u2713\x1b[0m ${msg}`);
|
|
796
|
-
const warn = (msg: string) => console.log(` \x1b[33m\u26A0\x1b[0m ${msg}`);
|
|
797
|
-
const fail = (msg: string) => console.log(` \x1b[31m\u2717\x1b[0m ${msg}`);
|
|
798
|
-
const info = (msg: string) => console.log(` \x1b[90m\u00B7\x1b[0m ${msg}`);
|
|
799
|
-
|
|
800
|
-
console.log("");
|
|
801
|
-
log.info("Prerequisites");
|
|
802
|
-
ok(`Bun ${bun.version}`);
|
|
803
|
-
const palBin = findBinaryOnPath("pal");
|
|
804
|
-
palBin
|
|
805
|
-
? ok(`pal on PATH — ${palBin}`)
|
|
806
|
-
: fail(
|
|
807
|
-
"pal — not on PATH; skills and docs invoke tools as 'pal cli ...', which will not resolve. Install globally: bun add -g portable-agent-layer"
|
|
808
|
-
);
|
|
809
|
-
const node = checkNode();
|
|
810
|
-
if (!node.available) {
|
|
811
|
-
warn(
|
|
812
|
-
`Node — not found; consulting-report PDF skill will not work. ${nodeInstallHint()}`
|
|
813
|
-
);
|
|
814
|
-
} else if (!node.meetsMinimum) {
|
|
815
|
-
warn(
|
|
816
|
-
`Node ${node.version} — too old for consulting-report PDF skill (needs ≥ 22.6 for --experimental-strip-types). ${nodeInstallHint()}`
|
|
817
|
-
);
|
|
818
|
-
} else {
|
|
819
|
-
ok(`Node ${node.version}`);
|
|
820
|
-
}
|
|
821
|
-
claude.available
|
|
822
|
-
? ok(`Claude Code ${claude.version || ""}`.trim())
|
|
823
|
-
: fail("Claude Code — not found");
|
|
824
|
-
opencode.available
|
|
825
|
-
? ok(`opencode ${opencode.version || ""}`.trim())
|
|
826
|
-
: fail("opencode — not found");
|
|
827
|
-
cursor.available
|
|
828
|
-
? ok(`Cursor ${cursor.version || ""}`.trim())
|
|
829
|
-
: fail("Cursor — not found");
|
|
830
|
-
copilot.available
|
|
831
|
-
? ok(`Copilot ${copilot.version || ""}`.trim())
|
|
832
|
-
: fail("Copilot — not found");
|
|
833
|
-
codex.available
|
|
834
|
-
? ok(`Codex ${codex.version || ""}`.trim())
|
|
835
|
-
: fail("Codex — not found");
|
|
836
|
-
checkPlaywrightChromium()
|
|
837
|
-
? ok("Playwright Chromium installed")
|
|
838
|
-
: fail(
|
|
839
|
-
"Playwright Chromium — not found (run 'pal cli install' or 'bunx playwright install chromium')"
|
|
840
|
-
);
|
|
841
|
-
rtk.available
|
|
842
|
-
? ok(rtk.version || "rtk")
|
|
843
|
-
: info(
|
|
844
|
-
`rtk — not installed (optional; enables Bash output compression — ${rtkInstallHint()})`
|
|
845
|
-
);
|
|
846
|
-
const print = { ok, warn, info };
|
|
847
|
-
for (const line of versionControlLines()) print[line.level](line.text);
|
|
848
|
-
|
|
849
|
-
console.log("");
|
|
850
|
-
log.info("PAL state");
|
|
851
|
-
ok(`PAL home: ${home}`);
|
|
852
|
-
telosCount > 0 ? ok(`TELOS: ${telosCount} files`) : fail("TELOS: not scaffolded");
|
|
853
|
-
|
|
854
|
-
// Identity
|
|
855
|
-
const palSettingsPath = resolve(home, "memory", "pal-settings.json");
|
|
856
|
-
if (existsSync(palSettingsPath)) {
|
|
857
|
-
try {
|
|
858
|
-
const s = JSON.parse(readFileSync(palSettingsPath, "utf-8"));
|
|
859
|
-
const hasIdentity = s?.identity?.principal?.name && s?.identity?.ai?.name;
|
|
860
|
-
hasIdentity
|
|
861
|
-
? ok("Identity configured")
|
|
862
|
-
: warn("Identity — incomplete (run 'pal cli install')");
|
|
863
|
-
} catch {
|
|
864
|
-
warn("Identity — could not read pal-settings.json");
|
|
865
|
-
}
|
|
866
|
-
} else {
|
|
867
|
-
warn("Identity — pal-settings.json missing (run 'pal cli install')");
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// AGENTS.md
|
|
871
|
-
const agentsMdPath = resolve(platform.opencodeDir(), "AGENTS.md");
|
|
872
|
-
existsSync(agentsMdPath)
|
|
873
|
-
? ok("AGENTS.md present")
|
|
874
|
-
: fail("AGENTS.md — missing (run 'pal cli install')");
|
|
875
|
-
|
|
876
|
-
if (claude.available) {
|
|
877
|
-
const claudeMdPath = resolve(platform.claudeDir(), "CLAUDE.md");
|
|
878
|
-
existsSync(claudeMdPath)
|
|
879
|
-
? ok("CLAUDE.md present")
|
|
880
|
-
: fail("CLAUDE.md — missing (run 'pal cli install --claude')");
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
// An empty TELOS is a normal state, not a broken install: the onboarding
|
|
884
|
-
// skill fills it whenever the user is ready, which may be months from now.
|
|
885
|
-
{
|
|
886
|
-
const unanswered = telosStatus(home)
|
|
887
|
-
.filter((topic) => topic.priority && !topic.answered)
|
|
888
|
-
.map((topic) => topic.key);
|
|
889
|
-
unanswered.length === 0
|
|
890
|
-
? ok("TELOS answered")
|
|
891
|
-
: warn(
|
|
892
|
-
`TELOS unanswered — ${unanswered.join(", ")} (ask your agent to onboard you)`
|
|
893
|
-
);
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
// Project bindings — where each project lives on THIS machine
|
|
897
|
-
{
|
|
898
|
-
const issues = auditBindings();
|
|
899
|
-
if (issues.length === 0) {
|
|
900
|
-
ok("Project bindings healthy");
|
|
901
|
-
} else {
|
|
902
|
-
for (const issue of issues) warn(`Binding: ${describeBindingIssue(issue)}`);
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
// Dependencies (PAL's own npm packages)
|
|
907
|
-
const nodeModulesPath = resolve(palPkg(), "node_modules");
|
|
908
|
-
existsSync(nodeModulesPath)
|
|
909
|
-
? ok("Dependencies installed")
|
|
910
|
-
: fail("Dependencies missing — run 'pal cli install'");
|
|
911
|
-
|
|
912
|
-
// Skills (per installed agent)
|
|
913
|
-
console.log("");
|
|
914
|
-
log.info("Skills");
|
|
915
|
-
const countSkillsIn = (dir: string) =>
|
|
916
|
-
existsSync(dir)
|
|
917
|
-
? readdirSync(dir).filter((f) => existsSync(resolve(dir, f, "SKILL.md"))).length
|
|
918
|
-
: 0;
|
|
919
|
-
if (claude.available) {
|
|
920
|
-
const n = countSkillsIn(resolve(platform.claudeDir(), "skills"));
|
|
921
|
-
n > 0
|
|
922
|
-
? ok(`Claude Code skills: ${n}`)
|
|
923
|
-
: warn("Claude Code skills — none found (run 'pal cli install --claude')");
|
|
924
|
-
}
|
|
925
|
-
if (opencode.available) {
|
|
926
|
-
const n = countSkillsIn(resolve(platform.agentsDir(), "skills"));
|
|
927
|
-
n > 0
|
|
928
|
-
? ok(`opencode skills: ${n}`)
|
|
929
|
-
: warn("opencode skills — none found (run 'pal cli install --opencode')");
|
|
930
|
-
}
|
|
931
|
-
if (cursor.available) {
|
|
932
|
-
const n = countSkillsIn(resolve(platform.cursorDir(), "skills"));
|
|
933
|
-
n > 0
|
|
934
|
-
? ok(`Cursor skills: ${n}`)
|
|
935
|
-
: warn("Cursor skills — none found (run 'pal cli install --cursor')");
|
|
936
|
-
}
|
|
937
|
-
if (copilot.available) {
|
|
938
|
-
const n = countSkillsIn(resolve(platform.copilotDir(), "skills"));
|
|
939
|
-
n > 0
|
|
940
|
-
? ok(`Copilot skills: ${n}`)
|
|
941
|
-
: warn("Copilot skills — none found (run 'pal cli install --copilot')");
|
|
942
|
-
}
|
|
943
|
-
if (codex.available) {
|
|
944
|
-
const n = countSkillsIn(resolve(platform.codexDir(), "skills"));
|
|
945
|
-
n > 0
|
|
946
|
-
? ok(`Codex skills: ${n}`)
|
|
947
|
-
: warn("Codex skills — none found (run 'pal cli install --codex')");
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
// Hook registration (per installed agent)
|
|
951
|
-
console.log("");
|
|
952
|
-
log.info("Hooks");
|
|
953
|
-
if (claude.available) {
|
|
954
|
-
checkClaudeHooksRegistered()
|
|
955
|
-
? ok("Claude Code hooks registered")
|
|
956
|
-
: fail("Claude Code hooks — not registered (run 'pal cli install --claude')");
|
|
957
|
-
}
|
|
958
|
-
if (opencode.available) {
|
|
959
|
-
checkOpencodePluginInstalled()
|
|
960
|
-
? ok("opencode plugin installed")
|
|
961
|
-
: fail("opencode plugin — not installed (run 'pal cli install --opencode')");
|
|
962
|
-
}
|
|
963
|
-
if (cursor.available) {
|
|
964
|
-
checkCursorHooksRegistered()
|
|
965
|
-
? ok("Cursor hooks registered")
|
|
966
|
-
: fail("Cursor hooks — not registered (run 'pal cli install --cursor')");
|
|
967
|
-
}
|
|
968
|
-
if (copilot.available) {
|
|
969
|
-
checkCopilotHooksRegistered()
|
|
970
|
-
? ok("Copilot hooks registered")
|
|
971
|
-
: fail("Copilot hooks — not registered (run 'pal cli install --copilot')");
|
|
972
|
-
checkCopilotInstructionsPresent()
|
|
973
|
-
? ok("Copilot instructions present")
|
|
974
|
-
: warn("Copilot instructions missing (written at first session stop)");
|
|
975
|
-
}
|
|
976
|
-
if (codex.available) {
|
|
977
|
-
checkCodexHooksRegistered()
|
|
978
|
-
? ok("Codex hooks registered")
|
|
979
|
-
: fail("Codex hooks — not registered (run 'pal cli install --codex')");
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
// Install integrity — verify PAL_AGENT prefix on every command in installed
|
|
983
|
-
// hook files. Catches stale installs after template changes (the exact bug
|
|
984
|
-
// we hit live for opencode + copilot). opencode uses a plugin file instead
|
|
985
|
-
// of a hooks.json, so it gets a separate freshness check.
|
|
986
|
-
console.log("");
|
|
987
|
-
log.info("Install integrity");
|
|
988
|
-
const prefixCheck = (
|
|
989
|
-
filePath: string,
|
|
990
|
-
agentName: string,
|
|
991
|
-
installCmd: string
|
|
992
|
-
): void => {
|
|
993
|
-
const r = checkAgentHookPrefix(filePath, agentName);
|
|
994
|
-
if (r.ok) {
|
|
995
|
-
ok(`${agentName}: declared on all ${r.total} hook commands`);
|
|
996
|
-
} else if (r.total === 0) {
|
|
997
|
-
fail(`${agentName}: hook file missing or unreadable at ${filePath}`);
|
|
998
|
-
} else {
|
|
999
|
-
fail(
|
|
1000
|
-
`${agentName}: ${r.missing}/${r.total} hook commands do not declare ${agentName} (run '${installCmd}')`
|
|
1001
|
-
);
|
|
1002
|
-
if (r.firstMissing) {
|
|
1003
|
-
log.warn(` First offender: ${r.firstMissing}…`);
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
};
|
|
1007
|
-
if (claude.available) {
|
|
1008
|
-
prefixCheck(
|
|
1009
|
-
resolve(platform.claudeDir(), "settings.json"),
|
|
1010
|
-
"claude",
|
|
1011
|
-
"pal cli install --claude"
|
|
1012
|
-
);
|
|
1013
|
-
}
|
|
1014
|
-
if (cursor.available) {
|
|
1015
|
-
prefixCheck(
|
|
1016
|
-
resolve(platform.cursorDir(), "hooks.json"),
|
|
1017
|
-
"cursor",
|
|
1018
|
-
"pal cli install --cursor"
|
|
1019
|
-
);
|
|
1020
|
-
}
|
|
1021
|
-
if (copilot.available) {
|
|
1022
|
-
prefixCheck(
|
|
1023
|
-
resolve(platform.copilotDir(), "hooks", "pal-hooks.json"),
|
|
1024
|
-
"copilot",
|
|
1025
|
-
"pal cli install --copilot"
|
|
1026
|
-
);
|
|
1027
|
-
}
|
|
1028
|
-
if (codex.available) {
|
|
1029
|
-
prefixCheck(
|
|
1030
|
-
resolve(platform.codexDir(), "hooks.json"),
|
|
1031
|
-
"codex",
|
|
1032
|
-
"pal cli install --codex"
|
|
1033
|
-
);
|
|
1034
|
-
}
|
|
1035
|
-
if (opencode.available) {
|
|
1036
|
-
const fresh = checkOpencodePluginFresh();
|
|
1037
|
-
fresh.ok
|
|
1038
|
-
? ok("opencode plugin: source-and-installed in sync")
|
|
1039
|
-
: fail(`opencode plugin: ${fresh.reason}`);
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
// Inference routing preview — what `inference()` would do RIGHT NOW
|
|
1043
|
-
console.log("");
|
|
1044
|
-
log.info("Inference");
|
|
1045
|
-
{
|
|
1046
|
-
const preview = previewInferenceRoute();
|
|
1047
|
-
console.log(` → Active agent: ${preview.agent}`);
|
|
1048
|
-
if (preview.route === "none") {
|
|
1049
|
-
fail(`Would route to: NONE — ${preview.reason}`);
|
|
1050
|
-
} else if (preview.route === "disabled") {
|
|
1051
|
-
warn(`Would route to: DISABLED — ${preview.reason}`);
|
|
1052
|
-
} else if (preview.route.endsWith("-api")) {
|
|
1053
|
-
warn(`Would route to: ${preview.route} (${preview.reason})`);
|
|
1054
|
-
} else {
|
|
1055
|
-
ok(`Would route to: ${preview.route} (${preview.reason})`);
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
if (opencode.available) {
|
|
1059
|
-
const model = opencodeBackgroundModel();
|
|
1060
|
-
model
|
|
1061
|
-
? ok(`opencode background model: ${model}`)
|
|
1062
|
-
: warn(
|
|
1063
|
-
`opencode background model: not pinned — background inference uses the model last picked in the TUI. Pin one with "model" in ${resolve(platform.opencodeDir(), "config.json")}`
|
|
1064
|
-
);
|
|
1065
|
-
}
|
|
1066
|
-
if (process.env.PAL_INFERENCE_DISABLED === "1") {
|
|
1067
|
-
warn(
|
|
1068
|
-
"PAL_INFERENCE_DISABLED=1 — test kill-switch leaked into prod env; every inference call will return failure"
|
|
1069
|
-
);
|
|
1070
|
-
} else {
|
|
1071
|
-
ok("PAL_INFERENCE_DISABLED is not set (production-safe)");
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
// Spawn-guard env vars should never appear in the user's shell — they're
|
|
1075
|
-
// set ONLY in PAL-spawned subprocesses. Leaks into the user shell cause
|
|
1076
|
-
// every inference call to short-circuit silently.
|
|
1077
|
-
if (process.env.PAL_SPAWNED_INFERENCE) {
|
|
1078
|
-
fail(
|
|
1079
|
-
`PAL_SPAWNED_INFERENCE=${process.env.PAL_SPAWNED_INFERENCE} leaked into shell — every inference call will refuse. Unset it.`
|
|
1080
|
-
);
|
|
1081
|
-
} else {
|
|
1082
|
-
ok("PAL_SPAWNED_INFERENCE not leaked (recursion guard clean)");
|
|
1083
|
-
}
|
|
1084
|
-
if (process.env.PAL_INFERENCE_DEPTH) {
|
|
1085
|
-
fail(
|
|
1086
|
-
`PAL_INFERENCE_DEPTH=${process.env.PAL_INFERENCE_DEPTH} leaked into shell — depth circuit-breaker will fire. Unset it.`
|
|
1087
|
-
);
|
|
1088
|
-
} else {
|
|
1089
|
-
ok("PAL_INFERENCE_DEPTH not leaked (depth counter clean)");
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
// API key checks — both are optional safety-net fallbacks. Unset is normal
|
|
1093
|
-
// for CLI-only setups; only matters if your native CLI breaks.
|
|
1094
|
-
process.env.PAL_ANTHROPIC_API_KEY
|
|
1095
|
-
? ok("PAL_ANTHROPIC_API_KEY set (anthropic-api fallback available)")
|
|
1096
|
-
: info("PAL_ANTHROPIC_API_KEY unset (optional — anthropic-api fallback off)");
|
|
1097
|
-
process.env.PAL_OPENAI_API_KEY
|
|
1098
|
-
? ok("PAL_OPENAI_API_KEY set (openai-api fallback for codex available)")
|
|
1099
|
-
: info("PAL_OPENAI_API_KEY unset (optional — openai-api fallback off)");
|
|
1100
|
-
process.env.PAL_GEMINI_API_KEY
|
|
1101
|
-
? ok("PAL_GEMINI_API_KEY is set")
|
|
1102
|
-
: warn("PAL_GEMINI_API_KEY — not set (optional, for YouTube analysis)");
|
|
1103
|
-
process.env.PAL_XAI_API_KEY
|
|
1104
|
-
? ok("PAL_XAI_API_KEY is set")
|
|
1105
|
-
: warn("PAL_XAI_API_KEY — not set (optional, for Grok researcher)");
|
|
1106
|
-
process.env.PAL_PERPLEXITY_API_KEY
|
|
1107
|
-
? ok("PAL_PERPLEXITY_API_KEY is set")
|
|
1108
|
-
: warn("PAL_PERPLEXITY_API_KEY — not set (optional, for Perplexity researcher)");
|
|
1109
|
-
|
|
1110
|
-
// Hook health from debug.log
|
|
1111
|
-
const hookHealth = recentHookErrors();
|
|
1112
|
-
if (hookHealth.totalErrors === 0) {
|
|
1113
|
-
ok("Hooks: no recent errors");
|
|
1114
|
-
} else {
|
|
1115
|
-
fail(`Hooks: ${hookHealth.totalErrors} error(s) in last 24h`);
|
|
1116
|
-
if (hookHealth.lastError) {
|
|
1117
|
-
log.warn(` Last: ${hookHealth.lastError}`);
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
// Pending migrations
|
|
1122
|
-
const pendingMigrations = checkPendingMigrations();
|
|
1123
|
-
if (pendingMigrations.length > 0) {
|
|
1124
|
-
for (const m of pendingMigrations) {
|
|
1125
|
-
const detail = m.detail ? ` (${m.detail})` : "";
|
|
1126
|
-
warn(
|
|
1127
|
-
`Migration pending: ${m.id} — ${m.description}${detail} → run 'pal cli migrate'`
|
|
1128
|
-
);
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
if (!hasAgent) {
|
|
1133
|
-
console.log("");
|
|
1134
|
-
log.error(NO_SESSION_AGENT_MESSAGE);
|
|
1135
|
-
}
|
|
1136
|
-
console.log("");
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
return { bun, claude, opencode, cursor, copilot, codex, rtk, hasAgent };
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
483
|
// ── Commands ──
|
|
1143
484
|
|
|
1144
485
|
async function init(args: string[]) {
|
|
@@ -1146,8 +487,8 @@ async function init(args: string[]) {
|
|
|
1146
487
|
|
|
1147
488
|
banner();
|
|
1148
489
|
|
|
1149
|
-
|
|
1150
|
-
|
|
490
|
+
const health = detectAgents();
|
|
491
|
+
runDoctor([], health);
|
|
1151
492
|
if (!health.hasAgent) {
|
|
1152
493
|
process.exit(1);
|
|
1153
494
|
}
|
|
@@ -1201,20 +542,6 @@ async function install(targets: Targets) {
|
|
|
1201
542
|
}
|
|
1202
543
|
}
|
|
1203
544
|
|
|
1204
|
-
// Node check — the consulting-report skill runs under `node --experimental-strip-types`
|
|
1205
|
-
// because Playwright's chromium.launch() hangs under Bun on Windows (CDP handshake over
|
|
1206
|
-
// stdio pipes). Node ≥ 22.6 is required for --experimental-strip-types.
|
|
1207
|
-
const node = checkNode();
|
|
1208
|
-
if (!node.available) {
|
|
1209
|
-
log.warn(
|
|
1210
|
-
`Node not found — consulting-report PDF skill will not work. ${nodeInstallHint()}`
|
|
1211
|
-
);
|
|
1212
|
-
} else if (!node.meetsMinimum) {
|
|
1213
|
-
log.warn(
|
|
1214
|
-
`Node ${node.version} is older than 22.6 — consulting-report PDF skill will not work (needs --experimental-strip-types). ${nodeInstallHint()}`
|
|
1215
|
-
);
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
545
|
// Scaffold TELOS + PAL settings, then prompt for missing identity
|
|
1219
546
|
const { scaffoldTelos, scaffoldPalSettings, copyPalDocs, generateSkillIndex } =
|
|
1220
547
|
await import("../targets/lib");
|