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/README.md
CHANGED
|
@@ -81,7 +81,7 @@ pal cli status # check your setup
|
|
|
81
81
|
| `pal cli export` | Export user state (telos, memory) to a zip |
|
|
82
82
|
| `pal cli import` | Import user state from a zip |
|
|
83
83
|
| `pal cli status` | Show current PAL configuration |
|
|
84
|
-
| `pal cli doctor` |
|
|
84
|
+
| `pal cli doctor` | Find what is wrong and the command that fixes each problem (`--verbose` lists every check, `--json` for agents); exits 1 on any failure |
|
|
85
85
|
| `pal cli migrate` | Run pending data migrations (non-destructive) |
|
|
86
86
|
| `pal cli analyze [--actionable]` | Learning analysis: rating trends, failure patterns, graduation candidates |
|
|
87
87
|
| `pal cli usage` | Summarize token usage and estimated cost |
|
|
@@ -87,7 +87,7 @@ Wraps `bun run dev` in the report directory. Open the URL printed by Next, edit
|
|
|
87
87
|
### 4. Render the PDF
|
|
88
88
|
|
|
89
89
|
```bash
|
|
90
|
-
pal cli skill run consulting-report generate-pdf
|
|
90
|
+
pal cli skill run consulting-report generate-pdf <report-dir>
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
Runs `next build` (which produces a static export at `out/`), then Playwright loads it via a tiny in-process HTTP server and prints the PDF with page-numbered header/footer. Output:
|
|
@@ -98,8 +98,6 @@ Runs `next build` (which produces a static export at `out/`), then Playwright lo
|
|
|
98
98
|
|
|
99
99
|
Override with `--pdf <path>`. Pass `--skip-build` to re-render the PDF from the existing `out/` without re-building.
|
|
100
100
|
|
|
101
|
-
Run with **Node**, not Bun — Playwright's `chromium.launch()` hangs under Bun on Windows.
|
|
102
|
-
|
|
103
101
|
## Directory Layout
|
|
104
102
|
|
|
105
103
|
```
|
|
@@ -149,14 +147,13 @@ Run with **Node**, not Bun — Playwright's `chromium.launch()` hangs under Bun
|
|
|
149
147
|
## Demo
|
|
150
148
|
|
|
151
149
|
```bash
|
|
152
|
-
pal cli skill run consulting-report generate-pdf
|
|
150
|
+
pal cli skill run consulting-report generate-pdf ~/.pal/skills/consulting-report/demo
|
|
153
151
|
```
|
|
154
152
|
|
|
155
153
|
Renders the bundled Acme Industries example end-to-end. Inspect the resulting PDF to see the full layout before authoring your own.
|
|
156
154
|
|
|
157
155
|
## Important
|
|
158
156
|
|
|
159
|
-
- Runs on Node (Playwright); the tool ships as a compiled `.mjs`, and `pal cli skill run` picks Node for that extension so no `--experimental-strip-types` is needed
|
|
160
157
|
- Bundled fonts come from Google Fonts via `next/font/google` — no licensing surface, no CDN at runtime, glyphs embedded at build time
|
|
161
158
|
- Reports are disposable artifacts of `lib/report-data.ts` + `app/page.tsx`; commit the source, not the PDF
|
|
162
159
|
- The scaffolder runs `bun install` inside the target by default — pass `--no-install` to skip
|
|
@@ -1,25 +1,18 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
// consulting-report skill tool: build the Next.js report and render it to PDF.
|
|
4
4
|
// Pipeline: `next build` produces a static export at out/ — Playwright loads
|
|
5
5
|
// out/index.html and prints a PDF with branded header/footer/page numbers.
|
|
6
6
|
//
|
|
7
|
-
// Run with Node (not Bun) — Playwright's chromium.launch() hangs under Bun on
|
|
8
|
-
// Windows because it uses --remote-debugging-pipe over stdio and Bun's Windows
|
|
9
|
-
// child-process pipe handling doesn't complete the CDP handshake.
|
|
10
|
-
//
|
|
11
|
-
// pal-build:mjs — ships as a compiled generate-pdf.mjs sibling (scripts/build-skill-tools.ts)
|
|
12
|
-
// and is invoked as that .mjs: a .ts under node_modules can't be type-stripped by Node.
|
|
13
|
-
//
|
|
14
7
|
// Usage:
|
|
15
|
-
// pal cli skill run consulting-report generate-pdf
|
|
8
|
+
// pal cli skill run consulting-report generate-pdf <report-dir> [--pdf <out>] [--html <out>] [--skip-build]
|
|
16
9
|
|
|
17
10
|
import { spawnSync } from "node:child_process";
|
|
18
|
-
import { createReadStream, constants as fsConstants
|
|
11
|
+
import { createReadStream, constants as fsConstants } from "node:fs";
|
|
19
12
|
import { access, readFile, stat, unlink, writeFile } from "node:fs/promises";
|
|
20
13
|
import { createServer, type Server } from "node:http";
|
|
21
14
|
import { extname, join, resolve } from "node:path";
|
|
22
|
-
import {
|
|
15
|
+
import { pathToFileURL } from "node:url";
|
|
23
16
|
import { PDFDocument } from "pdf-lib";
|
|
24
17
|
import { chromium } from "playwright";
|
|
25
18
|
|
|
@@ -312,22 +305,4 @@ async function run(argv: string[] = process.argv.slice(2)): Promise<void> {
|
|
|
312
305
|
console.log(`PDF: ${pdfPath} (${(pdfStat.size / 1024).toFixed(1)} KB)`);
|
|
313
306
|
}
|
|
314
307
|
|
|
315
|
-
|
|
316
|
-
// Use realpathSync on both sides so symlinked skill paths (e.g. ~/.pal/skills →
|
|
317
|
-
// PAL repo) match the resolved import.meta.url.
|
|
318
|
-
// fileURLToPath, never new URL().pathname: on Windows the latter yields
|
|
319
|
-
// "/C:/Users/..." — leading slash, forward slashes — which can never equal argv[1]'s
|
|
320
|
-
// "C:\Users\...", so the gate silently failed and run() never executed there.
|
|
321
|
-
function realResolve(p: string): string {
|
|
322
|
-
try {
|
|
323
|
-
return realpathSync(resolve(p));
|
|
324
|
-
} catch {
|
|
325
|
-
return resolve(p);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
const isMain =
|
|
329
|
-
process.argv[1] &&
|
|
330
|
-
realResolve(process.argv[1]) === realResolve(fileURLToPath(import.meta.url));
|
|
331
|
-
if (isMain) {
|
|
332
|
-
await run();
|
|
333
|
-
}
|
|
308
|
+
if (import.meta.main) await run();
|
|
@@ -100,7 +100,7 @@ async function run(argv: string[] = process.argv.slice(2)): Promise<void> {
|
|
|
100
100
|
console.log(` 2. Edit lib/report-data.ts (metadata) and app/page.tsx (layout)`);
|
|
101
101
|
console.log(` 3. Live preview: bun run dev`);
|
|
102
102
|
console.log(
|
|
103
|
-
` 4. Generate PDF: pal cli skill run consulting-report generate-pdf
|
|
103
|
+
` 4. Generate PDF: pal cli skill run consulting-report generate-pdf ${opts.targetDir}`
|
|
104
104
|
);
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -68,13 +68,13 @@ Invoke the skill tool. Flags:
|
|
|
68
68
|
Single-file example:
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
|
-
pal cli skill run create-pdf md-to-html-pdf
|
|
71
|
+
pal cli skill run create-pdf md-to-html-pdf /path/to/report.md --pdf /path/to/report.pdf
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
Multi-file example (after Step 2):
|
|
75
75
|
|
|
76
76
|
```bash
|
|
77
|
-
pal cli skill run create-pdf md-to-html-pdf
|
|
77
|
+
pal cli skill run create-pdf md-to-html-pdf /tmp/combined.md --pdf /path/to/report.pdf --html /path/to/report.html
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
The tool writes the self-contained HTML (inline CSS, UTF-8) and the PDF, and prints both paths + sizes on stdout.
|
|
@@ -95,13 +95,13 @@ Default styling (A4, 25mm margins, GitHub-ish look, table-friendly, page-break-a
|
|
|
95
95
|
- `--header <html|file>` / `--footer <html|file>` — running header/footer on every page. The value is either an inline HTML string or a path to an HTML file. Templates may use Playwright's injected classes: `pageNumber`, `totalPages`, `date`, `title`, `url`.
|
|
96
96
|
|
|
97
97
|
```bash
|
|
98
|
-
pal cli skill run create-pdf md-to-html-pdf
|
|
98
|
+
pal cli skill run create-pdf md-to-html-pdf report.md --pdf report.pdf \
|
|
99
99
|
--margin 18mm \
|
|
100
100
|
--header '<div style="font-size:9px;width:100%;text-align:center;color:#888">CONFIDENTIAL</div>' \
|
|
101
101
|
--footer '<div style="font-size:9px;width:100%;text-align:right;padding-right:12mm;color:#888"><span class="pageNumber"></span>/<span class="totalPages"></span></div>'
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
Header/footer templates **need an explicit `font-size`** (Playwright defaults them to 0) and render *inside* the page margin — widen `--margin` so they have room. Do NOT also add CSS `@page` margin-box rules; they duplicate. For deeper changes (fonts, base CSS), edit the `css` string in `tools/md-to-html-pdf.ts
|
|
104
|
+
Header/footer templates **need an explicit `font-size`** (Playwright defaults them to 0) and render *inside* the page margin — widen `--margin` so they have room. Do NOT also add CSS `@page` margin-box rules; they duplicate. For deeper changes (fonts, base CSS), edit the `css` string in `tools/md-to-html-pdf.ts`.
|
|
105
105
|
|
|
106
106
|
## Translation Variant
|
|
107
107
|
|
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
2
|
// create-pdf skill tool: Markdown -> HTML (marked, GFM) -> PDF (Playwright).
|
|
3
3
|
// Self-contained HTML: all CSS inlined, no CDN at render time.
|
|
4
4
|
//
|
|
5
|
-
// Run with Node (not Bun) — Playwright's chromium.launch hangs under Bun on Windows
|
|
6
|
-
// because it uses --remote-debugging-pipe over stdio and Bun's Windows child-process
|
|
7
|
-
// pipe handling doesn't complete the CDP handshake.
|
|
8
|
-
//
|
|
9
|
-
// pal-build:mjs — ships as a compiled md-to-html-pdf.mjs sibling (scripts/build-skill-tools.ts)
|
|
10
|
-
// and is invoked as that .mjs: a .ts under node_modules can't be type-stripped by Node.
|
|
11
|
-
//
|
|
12
5
|
// Usage:
|
|
13
|
-
// pal cli skill run create-pdf md-to-html-pdf
|
|
6
|
+
// pal cli skill run create-pdf md-to-html-pdf <input.md> \
|
|
14
7
|
// [--html <out.html>] [--pdf <out.pdf>] [--margin <css>] [--header <html|file>] [--footer <html|file>]
|
|
15
8
|
// --margin defaults to 25mm (all sides). --header/--footer accept inline HTML or a file path.
|
|
16
9
|
|
|
@@ -50,18 +50,6 @@ prefix_rule(
|
|
|
50
50
|
],
|
|
51
51
|
)
|
|
52
52
|
|
|
53
|
-
prefix_rule(
|
|
54
|
-
pattern = ["node", "--experimental-strip-types", "~/.pal/skills/consulting-report/tools/generate-pdf.ts"],
|
|
55
|
-
decision = "allow",
|
|
56
|
-
justification = "PAL consulting-report PDF generation requires Node for Playwright compatibility",
|
|
57
|
-
match = [
|
|
58
|
-
"node --experimental-strip-types ~/.pal/skills/consulting-report/tools/generate-pdf.ts ./report",
|
|
59
|
-
],
|
|
60
|
-
not_match = [
|
|
61
|
-
"node ~/.pal/skills/consulting-report/tools/generate-pdf.ts ./report",
|
|
62
|
-
],
|
|
63
|
-
)
|
|
64
|
-
|
|
65
53
|
prefix_rule(
|
|
66
54
|
pattern = ["pal", "cli", "project"],
|
|
67
55
|
decision = "allow",
|
|
@@ -120,7 +108,7 @@ prefix_rule(
|
|
|
120
108
|
justification = "A skill's own tools ship with PAL and are safe to run without repeated approval",
|
|
121
109
|
match = [
|
|
122
110
|
"pal cli skill run presentation build ./deck",
|
|
123
|
-
"pal cli skill run consulting-report generate-pdf
|
|
111
|
+
"pal cli skill run consulting-report generate-pdf ./report",
|
|
124
112
|
],
|
|
125
113
|
not_match = [
|
|
126
114
|
"pal cli skill link my-skill",
|
|
@@ -34,7 +34,6 @@
|
|
|
34
34
|
"Bash(rtk gain *)",
|
|
35
35
|
"Bash(bun ~/.pal/skills/*/tools/*.ts *)",
|
|
36
36
|
"Bash(bun ~/.pal/tools/*.ts *)",
|
|
37
|
-
"Bash(node --experimental-strip-types ~/.pal/skills/consulting-report/tools/generate-pdf.ts *)",
|
|
38
37
|
"Bash(pal cli skill run *)",
|
|
39
38
|
"Bash(pal cli algorithm-reflect *)",
|
|
40
39
|
"Bash(pal cli algorithm-synthesize *)",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portable-agent-layer",
|
|
3
|
-
"version": "0.85.
|
|
3
|
+
"version": "0.85.11",
|
|
4
4
|
"description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -55,10 +55,9 @@
|
|
|
55
55
|
"secretlint": "secretlint \"**/*\"",
|
|
56
56
|
"lint-staged": "lint-staged",
|
|
57
57
|
"prepare": "bun .husky/install.mjs",
|
|
58
|
-
"build:skill-tools": "bun run scripts/build-skill-tools.ts",
|
|
59
58
|
"build:ui": "bun run --filter @pal/control-room-ui build",
|
|
60
59
|
"dev:ui": "bun run --filter @pal/control-room-ui dev",
|
|
61
|
-
"prepack": "bun run build:
|
|
60
|
+
"prepack": "bun run build:ui",
|
|
62
61
|
"install:all": "bun run src/cli/index.ts cli install",
|
|
63
62
|
"uninstall": "bun run src/cli/index.ts cli uninstall",
|
|
64
63
|
"tool:synthesize": "bun run src/tools/agent/synthesize.ts",
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, resolve } from "node:path";
|
|
4
|
+
import { palPkg, platform } from "../../hooks/lib/paths";
|
|
5
|
+
import { NO_SESSION_AGENT_MESSAGE } from "../session-agent";
|
|
6
|
+
import { type Finding, failing, optional, passed, warning } from "./finding";
|
|
7
|
+
|
|
8
|
+
export type AgentName = "claude" | "opencode" | "cursor" | "copilot" | "codex";
|
|
9
|
+
|
|
10
|
+
const AGENT_NAMES: AgentName[] = ["claude", "codex", "copilot", "cursor", "opencode"];
|
|
11
|
+
|
|
12
|
+
export interface ToolCheck {
|
|
13
|
+
name: string;
|
|
14
|
+
available: boolean;
|
|
15
|
+
version?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DoctorResult {
|
|
19
|
+
bun: ToolCheck;
|
|
20
|
+
claude: ToolCheck;
|
|
21
|
+
opencode: ToolCheck;
|
|
22
|
+
cursor: ToolCheck;
|
|
23
|
+
copilot: ToolCheck;
|
|
24
|
+
codex: ToolCheck;
|
|
25
|
+
rtk: ToolCheck;
|
|
26
|
+
hasAgent: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function checkTool(cmd: string, versionArgs: string[] = ["--version"]): ToolCheck {
|
|
30
|
+
try {
|
|
31
|
+
const result = spawnSync(cmd, versionArgs, {
|
|
32
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
33
|
+
shell: true,
|
|
34
|
+
timeout: 5000,
|
|
35
|
+
});
|
|
36
|
+
if (result.status === 0) {
|
|
37
|
+
const version = (result.stdout?.toString() || "").trim().split("\n")[0];
|
|
38
|
+
return { name: cmd, available: true, version };
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// not found
|
|
42
|
+
}
|
|
43
|
+
return { name: cmd, available: false };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Copilot ships as a VS Code extension as well as a CLI, and the extension puts
|
|
48
|
+
* no `copilot` binary on PATH — but both read hooks, skills and agents out of
|
|
49
|
+
* ~/.copilot. Fall back to that directory so PAL's copilot checks still run.
|
|
50
|
+
*/
|
|
51
|
+
function checkCopilot(): ToolCheck {
|
|
52
|
+
const cli = checkTool("copilot", ["version"]);
|
|
53
|
+
if (cli.available) return cli;
|
|
54
|
+
if (existsSync(platform.copilotDir())) {
|
|
55
|
+
return { name: "copilot", available: true, version: "~/.copilot (no CLI on PATH)" };
|
|
56
|
+
}
|
|
57
|
+
return cli;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The Cursor CLI installs `cursor-agent` (and `agent`) and no `cursor` command;
|
|
62
|
+
* `cursor` is the editor's shell command. Both read ~/.cursor, so either counts.
|
|
63
|
+
*/
|
|
64
|
+
function checkCursor(): ToolCheck {
|
|
65
|
+
const cli = checkTool("cursor-agent");
|
|
66
|
+
if (cli.available) return cli;
|
|
67
|
+
return checkTool("cursor");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function detectAgents(): DoctorResult {
|
|
71
|
+
if (process.env.PAL_SKIP_DOCTOR === "1") {
|
|
72
|
+
return {
|
|
73
|
+
bun: { name: "bun", available: true, version: Bun.version },
|
|
74
|
+
claude: { name: "claude", available: true },
|
|
75
|
+
opencode: { name: "opencode", available: true },
|
|
76
|
+
cursor: { name: "cursor", available: true },
|
|
77
|
+
copilot: { name: "copilot", available: true },
|
|
78
|
+
codex: { name: "codex", available: true },
|
|
79
|
+
rtk: { name: "rtk", available: true },
|
|
80
|
+
hasAgent: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const claude = checkTool("claude");
|
|
84
|
+
const opencode = checkTool("opencode");
|
|
85
|
+
const cursor = checkCursor();
|
|
86
|
+
const copilot = checkCopilot();
|
|
87
|
+
const codex = checkTool("codex");
|
|
88
|
+
return {
|
|
89
|
+
bun: { name: "bun", available: true, version: Bun.version },
|
|
90
|
+
claude,
|
|
91
|
+
opencode,
|
|
92
|
+
cursor,
|
|
93
|
+
copilot,
|
|
94
|
+
codex,
|
|
95
|
+
rtk: checkTool("rtk"),
|
|
96
|
+
hasAgent: [claude, opencode, cursor, copilot, codex].some((tool) => tool.available),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function installedAgents(result: DoctorResult): AgentName[] {
|
|
101
|
+
return AGENT_NAMES.filter((name) => result[name].available);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface AgentLayout {
|
|
105
|
+
label: string;
|
|
106
|
+
skillsDir: () => string;
|
|
107
|
+
hookFile?: () => string;
|
|
108
|
+
instructions?: { file: () => string; name: string };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const LAYOUT: Record<AgentName, AgentLayout> = {
|
|
112
|
+
claude: {
|
|
113
|
+
label: "Claude Code",
|
|
114
|
+
skillsDir: () => resolve(platform.claudeDir(), "skills"),
|
|
115
|
+
hookFile: () => resolve(platform.claudeDir(), "settings.json"),
|
|
116
|
+
instructions: {
|
|
117
|
+
file: () => resolve(platform.claudeDir(), "CLAUDE.md"),
|
|
118
|
+
name: "CLAUDE.md",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
codex: {
|
|
122
|
+
label: "Codex",
|
|
123
|
+
skillsDir: () => resolve(platform.codexDir(), "skills"),
|
|
124
|
+
hookFile: () => resolve(platform.codexDir(), "hooks.json"),
|
|
125
|
+
},
|
|
126
|
+
copilot: {
|
|
127
|
+
label: "Copilot",
|
|
128
|
+
skillsDir: () => resolve(platform.copilotDir(), "skills"),
|
|
129
|
+
hookFile: () => resolve(platform.copilotDir(), "hooks", "pal-hooks.json"),
|
|
130
|
+
},
|
|
131
|
+
cursor: {
|
|
132
|
+
label: "Cursor",
|
|
133
|
+
skillsDir: () => resolve(platform.cursorDir(), "skills"),
|
|
134
|
+
hookFile: () => resolve(platform.cursorDir(), "hooks.json"),
|
|
135
|
+
},
|
|
136
|
+
opencode: {
|
|
137
|
+
label: "opencode",
|
|
138
|
+
skillsDir: () => resolve(platform.agentsDir(), "skills"),
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const reinstall = (agent: AgentName) => ({
|
|
143
|
+
say: `Reinstall PAL for ${LAYOUT[agent].label}`,
|
|
144
|
+
command: `pal cli install --${agent}`,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
/** Hook-config keys that carry a shell command: cross-platform and per-shell variants. */
|
|
148
|
+
function isHookCommandField(key: string): boolean {
|
|
149
|
+
return key === "command" || key === "bash" || key === "powershell";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function extractAllHookCommands(obj: unknown, out: string[] = []): string[] {
|
|
153
|
+
if (Array.isArray(obj)) {
|
|
154
|
+
for (const item of obj) extractAllHookCommands(item, out);
|
|
155
|
+
} else if (obj && typeof obj === "object") {
|
|
156
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
157
|
+
if (isHookCommandField(k) && typeof v === "string") {
|
|
158
|
+
out.push(v);
|
|
159
|
+
} else {
|
|
160
|
+
extractAllHookCommands(v, out);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* True when a hook command names its agent, by env prefix or by argv flag.
|
|
169
|
+
*
|
|
170
|
+
* An env prefix only parses in one shell family, so hook configs whose host
|
|
171
|
+
* shell is unknown declare the agent with a shell-agnostic `--agent=` flag.
|
|
172
|
+
*/
|
|
173
|
+
function declaresAgent(cmd: string, agentName: string): boolean {
|
|
174
|
+
return (
|
|
175
|
+
cmd.startsWith(`PAL_AGENT=${agentName} `) ||
|
|
176
|
+
cmd.startsWith(`$env:PAL_AGENT='${agentName}'; `) ||
|
|
177
|
+
cmd.includes(`--agent=${agentName}`)
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const SCRIPT =
|
|
182
|
+
/"([^"]+\.(?:ts|js|mjs|sh|ps1))"|'([^']+\.(?:ts|js|mjs|sh|ps1))'|(\S+\.(?:ts|js|mjs|sh|ps1))(?=\s|$)/g;
|
|
183
|
+
|
|
184
|
+
function scriptPaths(cmd: string): string[] {
|
|
185
|
+
return Array.from(cmd.matchAll(SCRIPT), (m) => m[1] ?? m[2] ?? m[3] ?? "").filter(
|
|
186
|
+
isAbsolute
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isPalHook(cmd: string, agent: AgentName): boolean {
|
|
191
|
+
return (
|
|
192
|
+
declaresAgent(cmd, agent) ||
|
|
193
|
+
scriptPaths(cmd).some((path) => /[\\/]src[\\/]hooks[\\/][^\\/]+$/.test(path))
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function readHookCommands(file: string): string[] | "missing" | "unreadable" {
|
|
198
|
+
if (!existsSync(file)) return "missing";
|
|
199
|
+
try {
|
|
200
|
+
const data = JSON.parse(readFileSync(file, "utf-8"));
|
|
201
|
+
return extractAllHookCommands(data.hooks ?? data);
|
|
202
|
+
} catch {
|
|
203
|
+
return "unreadable";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function hookFindings(agent: AgentName, file: string): Finding[] {
|
|
208
|
+
const label = LAYOUT[agent].label;
|
|
209
|
+
const commands = readHookCommands(file);
|
|
210
|
+
if (commands === "unreadable")
|
|
211
|
+
return [
|
|
212
|
+
failing(
|
|
213
|
+
`${agent}.hooks.unreadable`,
|
|
214
|
+
`${label} hook config is not valid JSON (${file})`,
|
|
215
|
+
{
|
|
216
|
+
say: `Fix the JSON in ${file}, then reinstall`,
|
|
217
|
+
command: `pal cli install --${agent}`,
|
|
218
|
+
}
|
|
219
|
+
),
|
|
220
|
+
];
|
|
221
|
+
const pal = commands === "missing" ? [] : commands.filter((c) => isPalHook(c, agent));
|
|
222
|
+
if (!pal.some((c) => c.includes("LoadContext")))
|
|
223
|
+
return [
|
|
224
|
+
failing(
|
|
225
|
+
`${agent}.hooks.missing`,
|
|
226
|
+
`${label} hooks are not registered`,
|
|
227
|
+
reinstall(agent)
|
|
228
|
+
),
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
const findings = [passed(`${agent}.hooks`, `${label}: ${pal.length} hooks registered`)];
|
|
232
|
+
const undeclared = pal.filter((c) => !declaresAgent(c, agent));
|
|
233
|
+
if (undeclared.length > 0)
|
|
234
|
+
findings.push(
|
|
235
|
+
failing(
|
|
236
|
+
`${agent}.hooks.undeclared`,
|
|
237
|
+
`${label}: ${undeclared.length} of ${pal.length} hooks do not name ${agent} as their agent`,
|
|
238
|
+
reinstall(agent)
|
|
239
|
+
)
|
|
240
|
+
);
|
|
241
|
+
const broken = pal.filter((c) => scriptPaths(c).some((path) => !existsSync(path)));
|
|
242
|
+
if (broken.length > 0)
|
|
243
|
+
findings.push(
|
|
244
|
+
failing(
|
|
245
|
+
`${agent}.hooks.scripts`,
|
|
246
|
+
`${label}: ${broken.length} of ${pal.length} hooks run a script that no longer exists (${scriptPaths(broken[0]).find((p) => !existsSync(p))})`,
|
|
247
|
+
reinstall(agent)
|
|
248
|
+
)
|
|
249
|
+
);
|
|
250
|
+
return findings;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function opencodePluginFindings(): Finding[] {
|
|
254
|
+
const installed = resolve(platform.opencodeDir(), "plugins", "pal-plugin.ts");
|
|
255
|
+
const source = resolve(palPkg(), "src", "targets", "opencode", "plugin.ts");
|
|
256
|
+
if (!existsSync(installed))
|
|
257
|
+
return [
|
|
258
|
+
failing(
|
|
259
|
+
"opencode.hooks.missing",
|
|
260
|
+
"opencode plugin is not installed",
|
|
261
|
+
reinstall("opencode")
|
|
262
|
+
),
|
|
263
|
+
];
|
|
264
|
+
if (existsSync(source) && statSync(installed).mtimeMs < statSync(source).mtimeMs)
|
|
265
|
+
return [
|
|
266
|
+
warning(
|
|
267
|
+
"opencode.plugin.stale",
|
|
268
|
+
"opencode plugin is older than PAL's — recent fixes have not reached opencode",
|
|
269
|
+
reinstall("opencode")
|
|
270
|
+
),
|
|
271
|
+
];
|
|
272
|
+
return [passed("opencode.hooks", "opencode plugin installed and current")];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function countSkills(dir: string): number {
|
|
276
|
+
if (!existsSync(dir)) return 0;
|
|
277
|
+
return readdirSync(dir).filter((f) => existsSync(resolve(dir, f, "SKILL.md"))).length;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function oneAgentFindings(agent: AgentName): Finding[] {
|
|
281
|
+
const layout = LAYOUT[agent];
|
|
282
|
+
const skills = countSkills(layout.skillsDir());
|
|
283
|
+
const findings = [
|
|
284
|
+
skills > 0
|
|
285
|
+
? passed(`${agent}.skills`, `${layout.label}: ${skills} skills`)
|
|
286
|
+
: warning(`${agent}.skills`, `${layout.label} has no PAL skills`, reinstall(agent)),
|
|
287
|
+
];
|
|
288
|
+
findings.push(
|
|
289
|
+
...(layout.hookFile
|
|
290
|
+
? hookFindings(agent, layout.hookFile())
|
|
291
|
+
: opencodePluginFindings())
|
|
292
|
+
);
|
|
293
|
+
if (layout.instructions) {
|
|
294
|
+
const { file, name } = layout.instructions;
|
|
295
|
+
findings.push(
|
|
296
|
+
existsSync(file())
|
|
297
|
+
? passed(`${agent}.instructions`, `${name} present`)
|
|
298
|
+
: failing(`${agent}.instructions`, `${name} is missing`, reinstall(agent))
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
return findings;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function agentFindings(agents: AgentName[]): Finding[] {
|
|
305
|
+
if (agents.length === 0) return [];
|
|
306
|
+
const agentsMd = resolve(platform.opencodeDir(), "AGENTS.md");
|
|
307
|
+
return [
|
|
308
|
+
existsSync(agentsMd)
|
|
309
|
+
? passed("agents-md", "AGENTS.md present")
|
|
310
|
+
: failing("agents-md", "AGENTS.md is missing", {
|
|
311
|
+
say: "Reinstall PAL",
|
|
312
|
+
command: "pal cli install",
|
|
313
|
+
}),
|
|
314
|
+
...agents.flatMap(oneAgentFindings),
|
|
315
|
+
];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function rosterFindings(agents: AgentName[]): Finding[] {
|
|
319
|
+
if (agents.length === 0)
|
|
320
|
+
return [
|
|
321
|
+
failing("agents.none", "No supported agent is installed", {
|
|
322
|
+
say: NO_SESSION_AGENT_MESSAGE,
|
|
323
|
+
}),
|
|
324
|
+
];
|
|
325
|
+
const others = AGENT_NAMES.filter((name) => !agents.includes(name));
|
|
326
|
+
if (others.length === 0) return [];
|
|
327
|
+
return [
|
|
328
|
+
optional(
|
|
329
|
+
"agents.others",
|
|
330
|
+
`Other agents PAL supports — ${others.map((name) => LAYOUT[name].label).join(", ")}`,
|
|
331
|
+
{
|
|
332
|
+
say: `install it, then run pal cli install --${others.length === 1 ? others[0] : "<agent>"}`,
|
|
333
|
+
}
|
|
334
|
+
),
|
|
335
|
+
];
|
|
336
|
+
}
|