portable-agent-layer 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/package.json +1 -1
- package/src/cli/index.ts +152 -7
- package/src/hooks/StopOrchestrator.ts +12 -0
- package/src/hooks/handlers/readme-sync.ts +61 -0
- package/src/hooks/lib/claude-md.ts +2 -1
- package/src/hooks/lib/readme-sync.ts +129 -0
- package/src/targets/lib.ts +1 -0
package/README.md
CHANGED
|
@@ -30,7 +30,10 @@ With PAL, you can:
|
|
|
30
30
|
|
|
31
31
|
### Prerequisites
|
|
32
32
|
|
|
33
|
+
> **Bun is required.** PAL is built on [Bun](https://bun.sh) and will not work with Node.js or other runtimes. Install it with `curl -fsSL https://bun.sh/install | bash`.
|
|
34
|
+
|
|
33
35
|
- [Bun](https://bun.sh) >= 1.3.0
|
|
36
|
+
- At least one of: [Claude Code](https://claude.ai/code) or [opencode](https://opencode.ai)
|
|
34
37
|
|
|
35
38
|
### Package mode (recommended)
|
|
36
39
|
|
|
@@ -77,6 +80,7 @@ pal cli status # check your setup
|
|
|
77
80
|
| `pal cli export` | Export user state (telos, memory) to a zip |
|
|
78
81
|
| `pal cli import` | Import user state from a zip |
|
|
79
82
|
| `pal cli status` | Show current PAL configuration |
|
|
83
|
+
| `pal cli doctor` | Check prerequisites and system health |
|
|
80
84
|
|
|
81
85
|
### Target flags
|
|
82
86
|
|
|
@@ -111,6 +115,27 @@ pal cli install # both (default)
|
|
|
111
115
|
|
|
112
116
|
---
|
|
113
117
|
|
|
118
|
+
## Skills
|
|
119
|
+
|
|
120
|
+
PAL ships with built-in skills that extend your agent's capabilities:
|
|
121
|
+
|
|
122
|
+
| Skill | Description |
|
|
123
|
+
|-------|-------------|
|
|
124
|
+
| `analyze-pdf` | Download and analyze PDF files |
|
|
125
|
+
| `analyze-youtube` | Analyze YouTube videos using Gemini |
|
|
126
|
+
| `council` | Multi-perspective parallel debate on decisions |
|
|
127
|
+
| `create-skill` | Scaffold a new skill from a description |
|
|
128
|
+
| `extract-entities` | Extract people and companies from content |
|
|
129
|
+
| `extract-wisdom` | Extract structured insights from content |
|
|
130
|
+
| `first-principles` | Break down problems to fundamentals |
|
|
131
|
+
| `fyzz-chat-api` | Query Fyzz Chat conversations via API |
|
|
132
|
+
| `reflect` | Diagnose why a PAL behavior didn't trigger |
|
|
133
|
+
| `research` | Multi-agent parallel research |
|
|
134
|
+
| `review` | Security-focused code review |
|
|
135
|
+
| `summarize` | Structured summarization |
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
114
139
|
## Core idea
|
|
115
140
|
|
|
116
141
|
PAL stands for **Portable Agent Layer**.
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* export [path] [--dry-run] Export user state to zip
|
|
14
14
|
* import [path] [--dry-run] Import user state from zip
|
|
15
15
|
* status Show current PAL configuration
|
|
16
|
+
* doctor Check prerequisites and system health
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import { spawnSync } from "node:child_process";
|
|
@@ -35,18 +36,53 @@ if (allArgs[0] === "cli") {
|
|
|
35
36
|
await session(allArgs);
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
// ── Session: pal [
|
|
39
|
+
// ── Session: pal [args] ──
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
interface ToolCheck {
|
|
42
|
+
name: string;
|
|
43
|
+
available: boolean;
|
|
44
|
+
version?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function checkTool(cmd: string, versionArgs: string[] = ["--version"]): ToolCheck {
|
|
48
|
+
try {
|
|
49
|
+
const result = spawnSync(cmd, versionArgs, {
|
|
50
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
51
|
+
shell: true,
|
|
52
|
+
timeout: 5000,
|
|
53
|
+
});
|
|
54
|
+
if (result.status === 0) {
|
|
55
|
+
const version = (result.stdout?.toString() || "").trim().split("\n")[0];
|
|
56
|
+
return { name: cmd, available: true, version };
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// not found
|
|
60
|
+
}
|
|
61
|
+
return { name: cmd, available: false };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function detectAgent(): string | null {
|
|
65
|
+
if (checkTool("claude").available) return "claude";
|
|
66
|
+
if (checkTool("opencode").available) return "opencode";
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function session(sessionArgs: string[]) {
|
|
71
|
+
const agent = detectAgent();
|
|
72
|
+
if (!agent) {
|
|
73
|
+
log.error("No supported agent found. Install Claude Code or opencode.");
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = spawnSync(agent, sessionArgs, {
|
|
43
78
|
stdio: "inherit",
|
|
44
79
|
shell: true,
|
|
45
80
|
});
|
|
46
81
|
|
|
47
82
|
const exitCode = result.status ?? 1;
|
|
48
83
|
|
|
49
|
-
//
|
|
84
|
+
// Session summary (Claude only)
|
|
85
|
+
if (agent !== "claude") process.exit(exitCode);
|
|
50
86
|
try {
|
|
51
87
|
const projectsDir = resolve(homedir(), ".claude", "projects");
|
|
52
88
|
if (!existsSync(projectsDir)) process.exit(exitCode);
|
|
@@ -98,7 +134,7 @@ async function runCli(command: string | undefined, args: string[]) {
|
|
|
98
134
|
break;
|
|
99
135
|
case "install":
|
|
100
136
|
banner();
|
|
101
|
-
await install(
|
|
137
|
+
await install(resolveTargets(args));
|
|
102
138
|
break;
|
|
103
139
|
case "uninstall":
|
|
104
140
|
await uninstall(args);
|
|
@@ -112,6 +148,9 @@ async function runCli(command: string | undefined, args: string[]) {
|
|
|
112
148
|
case "status":
|
|
113
149
|
await status();
|
|
114
150
|
break;
|
|
151
|
+
case "doctor":
|
|
152
|
+
doctor();
|
|
153
|
+
break;
|
|
115
154
|
case "--help":
|
|
116
155
|
case "-h":
|
|
117
156
|
case "help":
|
|
@@ -148,6 +187,7 @@ function showHelp() {
|
|
|
148
187
|
pal cli export [path] [--dry-run] Export state to zip
|
|
149
188
|
pal cli import [path] [--dry-run] Import state from zip
|
|
150
189
|
pal cli status Show PAL configuration
|
|
190
|
+
pal cli doctor Check prerequisites and health
|
|
151
191
|
|
|
152
192
|
Environment:
|
|
153
193
|
PAL_HOME Override user state directory (default: ~/.pal or repo root)
|
|
@@ -176,6 +216,103 @@ function parseTargets(args: string[]): {
|
|
|
176
216
|
return { claude, opencode };
|
|
177
217
|
}
|
|
178
218
|
|
|
219
|
+
/** Resolve targets against available agents. Errors if explicitly requested but missing. */
|
|
220
|
+
function resolveTargets(
|
|
221
|
+
args: string[],
|
|
222
|
+
health?: DoctorResult
|
|
223
|
+
): { claude: boolean; opencode: boolean } {
|
|
224
|
+
const requested = parseTargets(args);
|
|
225
|
+
const h = health || doctor(true);
|
|
226
|
+
const explicit = args.some(
|
|
227
|
+
(a) => a === "--claude" || a === "--opencode" || a === "--all"
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (explicit) {
|
|
231
|
+
// User explicitly requested — error if not available
|
|
232
|
+
if (requested.claude && !h.claude.available) {
|
|
233
|
+
log.error("Claude Code is not installed. Run 'pal cli doctor' for details.");
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
if (requested.opencode && !h.opencode.available) {
|
|
237
|
+
log.error("opencode is not installed. Run 'pal cli doctor' for details.");
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
return requested;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Default (no flags) — install for available agents only
|
|
244
|
+
const targets = {
|
|
245
|
+
claude: h.claude.available,
|
|
246
|
+
opencode: h.opencode.available,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
if (!targets.claude) log.info("Skipping Claude Code (not installed)");
|
|
250
|
+
if (!targets.opencode) log.info("Skipping opencode (not installed)");
|
|
251
|
+
|
|
252
|
+
return targets;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── Doctor ──
|
|
256
|
+
|
|
257
|
+
interface DoctorResult {
|
|
258
|
+
bun: ToolCheck;
|
|
259
|
+
claude: ToolCheck;
|
|
260
|
+
opencode: ToolCheck;
|
|
261
|
+
hasAgent: boolean;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function doctor(silent = false): DoctorResult {
|
|
265
|
+
// Allow CI/tests to skip agent detection
|
|
266
|
+
if (process.env.PAL_SKIP_DOCTOR === "1") {
|
|
267
|
+
return {
|
|
268
|
+
bun: { name: "bun", available: true, version: Bun.version },
|
|
269
|
+
claude: { name: "claude", available: true },
|
|
270
|
+
opencode: { name: "opencode", available: true },
|
|
271
|
+
hasAgent: true,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const bun = { name: "bun", available: true, version: Bun.version };
|
|
276
|
+
const claude = checkTool("claude");
|
|
277
|
+
const opencode = checkTool("opencode");
|
|
278
|
+
const hasAgent = claude.available || opencode.available;
|
|
279
|
+
|
|
280
|
+
const home = palHome();
|
|
281
|
+
const isRepo = existsSync(resolve(palPkg(), ".palroot"));
|
|
282
|
+
const telosCount = (() => {
|
|
283
|
+
try {
|
|
284
|
+
return readdirSync(resolve(home, "telos")).filter((f) => f.endsWith(".md")).length;
|
|
285
|
+
} catch {
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
})();
|
|
289
|
+
|
|
290
|
+
if (!silent) {
|
|
291
|
+
const ok = (msg: string) => log.info(` \u2713 ${msg}`);
|
|
292
|
+
const fail = (msg: string) => log.warn(` \u2717 ${msg}`);
|
|
293
|
+
|
|
294
|
+
console.log("");
|
|
295
|
+
log.info("Doctor");
|
|
296
|
+
ok(`Bun ${bun.version}`);
|
|
297
|
+
claude.available
|
|
298
|
+
? ok(`Claude Code ${claude.version || ""}`.trim())
|
|
299
|
+
: fail("Claude Code — not found");
|
|
300
|
+
opencode.available
|
|
301
|
+
? ok(`opencode ${opencode.version || ""}`.trim())
|
|
302
|
+
: fail("opencode — not found");
|
|
303
|
+
ok(`PAL home: ${home} (${isRepo ? "repo" : "package"} mode)`);
|
|
304
|
+
telosCount > 0 ? ok(`TELOS: ${telosCount} files`) : fail("TELOS: not scaffolded");
|
|
305
|
+
|
|
306
|
+
if (!hasAgent) {
|
|
307
|
+
console.log("");
|
|
308
|
+
log.error("No supported agent found. Install Claude Code or opencode.");
|
|
309
|
+
}
|
|
310
|
+
console.log("");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return { bun, claude, opencode, hasAgent };
|
|
314
|
+
}
|
|
315
|
+
|
|
179
316
|
// ── Commands ──
|
|
180
317
|
|
|
181
318
|
async function init(args: string[]) {
|
|
@@ -184,6 +321,12 @@ async function init(args: string[]) {
|
|
|
184
321
|
|
|
185
322
|
banner();
|
|
186
323
|
|
|
324
|
+
// Run doctor first — abort if no agents available
|
|
325
|
+
const health = doctor(false);
|
|
326
|
+
if (!health.hasAgent) {
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
|
|
187
330
|
const home = palHome();
|
|
188
331
|
const isRepo = existsSync(resolve(palPkg(), ".palroot"));
|
|
189
332
|
|
|
@@ -196,7 +339,9 @@ async function init(args: string[]) {
|
|
|
196
339
|
scaffoldTelos();
|
|
197
340
|
ensureSetupState();
|
|
198
341
|
|
|
199
|
-
|
|
342
|
+
// Auto-detect available targets
|
|
343
|
+
const targets = resolveTargets(args, health);
|
|
344
|
+
await install(targets);
|
|
200
345
|
|
|
201
346
|
console.log("");
|
|
202
347
|
const state = ensureSetupState();
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Transcript is read from the file at transcript_path, NOT from stdin.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { checkReadmeSync } from "./handlers/readme-sync";
|
|
9
10
|
import { logError } from "./lib/log";
|
|
10
11
|
import { readStdinJSON } from "./lib/stdin";
|
|
11
12
|
import { runStopHandlers } from "./lib/stop";
|
|
@@ -17,6 +18,17 @@ interface StopHookInput {
|
|
|
17
18
|
last_assistant_message?: string;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
// Check README sync before anything else — may block the session
|
|
22
|
+
try {
|
|
23
|
+
const decision = checkReadmeSync();
|
|
24
|
+
if (decision.decision === "block") {
|
|
25
|
+
console.log(JSON.stringify(decision));
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
} catch (err) {
|
|
29
|
+
logError("StopOrchestrator:readme-sync", err);
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
const input = await readStdinJSON<StopHookInput>();
|
|
21
33
|
if (!input?.transcript_path) {
|
|
22
34
|
logError("StopOrchestrator", "No transcript_path in hook input");
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stop handler: check if README.md is out of sync with code.
|
|
3
|
+
*
|
|
4
|
+
* Runs git diff to see if documentable files changed in this session.
|
|
5
|
+
* If they did and README is stale, returns a block decision.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
9
|
+
import { logDebug } from "../lib/log";
|
|
10
|
+
import { palPkg } from "../lib/paths";
|
|
11
|
+
import { validateReadmeSync, WATCHED_PATHS } from "../lib/readme-sync";
|
|
12
|
+
|
|
13
|
+
/** Check if any watched files have uncommitted changes. */
|
|
14
|
+
function hasDocumentableChanges(): boolean {
|
|
15
|
+
try {
|
|
16
|
+
const diff = execSync("git diff --name-only HEAD", {
|
|
17
|
+
cwd: palPkg(),
|
|
18
|
+
encoding: "utf-8",
|
|
19
|
+
}).trim();
|
|
20
|
+
|
|
21
|
+
const staged = execSync("git diff --name-only --cached", {
|
|
22
|
+
cwd: palPkg(),
|
|
23
|
+
encoding: "utf-8",
|
|
24
|
+
}).trim();
|
|
25
|
+
|
|
26
|
+
const changed = `${diff}\n${staged}`.split("\n").filter((f) => f.length > 0);
|
|
27
|
+
|
|
28
|
+
return changed.some((file) =>
|
|
29
|
+
WATCHED_PATHS.some((watched) => file === watched || file.startsWith(`${watched}/`))
|
|
30
|
+
);
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ReadmeSyncDecision {
|
|
37
|
+
decision?: "block";
|
|
38
|
+
reason?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Returns a block decision if README is stale, or empty object to allow stop. */
|
|
42
|
+
export function checkReadmeSync(): ReadmeSyncDecision {
|
|
43
|
+
if (!hasDocumentableChanges()) {
|
|
44
|
+
logDebug("readme-sync", "No documentable changes detected");
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
logDebug("readme-sync", "Documentable files changed — validating README");
|
|
49
|
+
const result = validateReadmeSync();
|
|
50
|
+
|
|
51
|
+
if (!result.ok) {
|
|
52
|
+
logDebug("readme-sync", `README out of sync: ${result.issues.join("; ")}`);
|
|
53
|
+
return {
|
|
54
|
+
decision: "block",
|
|
55
|
+
reason: `README.md is out of date. Please update it before finishing:\n${result.issues.map((i) => `- ${i}`).join("\n")}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
logDebug("readme-sync", "README is in sync");
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from "node:fs";
|
|
19
19
|
import { dirname, relative, resolve } from "node:path";
|
|
20
20
|
import { loadTelos } from "./context";
|
|
21
|
-
import { assets, palHome, paths, platform } from "./paths";
|
|
21
|
+
import { assets, ensureDir, palHome, paths, platform } from "./paths";
|
|
22
22
|
import { buildSetupPrompt, readSetupState } from "./setup";
|
|
23
23
|
|
|
24
24
|
const TEMPLATE_PATH = assets.agentsMdTemplate();
|
|
@@ -116,6 +116,7 @@ export function regenerateIfNeeded(): boolean {
|
|
|
116
116
|
const { outputPath } = getOutputPaths();
|
|
117
117
|
ensureSymlink();
|
|
118
118
|
if (!needsRebuild()) return false;
|
|
119
|
+
ensureDir(dirname(outputPath));
|
|
119
120
|
writeFileSync(outputPath, buildClaudeMd(), "utf-8");
|
|
120
121
|
return true;
|
|
121
122
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* README sync validation — ensures README.md reflects current code surfaces.
|
|
3
|
+
*
|
|
4
|
+
* Checks CLI commands, environment variables, and skills against README content.
|
|
5
|
+
* Used by tests (CI/pre-commit) and the Stop hook (blocks session if stale).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import { palPkg } from "./paths";
|
|
11
|
+
|
|
12
|
+
export interface SyncResult {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
issues: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Files that, when changed, should trigger a README check. */
|
|
18
|
+
export const WATCHED_PATHS = [
|
|
19
|
+
"src/cli/index.ts",
|
|
20
|
+
"src/hooks/lib/paths.ts",
|
|
21
|
+
"src/hooks/lib/inference.ts",
|
|
22
|
+
"src/tools/youtube-analyze.ts",
|
|
23
|
+
"assets/skills",
|
|
24
|
+
"assets/agents",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/** Extract CLI command names from the switch statement in index.ts */
|
|
28
|
+
function extractCliCommands(): string[] {
|
|
29
|
+
const pkg = palPkg();
|
|
30
|
+
const cliPath = resolve(pkg, "src", "cli", "index.ts");
|
|
31
|
+
if (!existsSync(cliPath)) return [];
|
|
32
|
+
|
|
33
|
+
const content = readFileSync(cliPath, "utf-8");
|
|
34
|
+
const matches = content.matchAll(/case\s+"([^"]+)":/g);
|
|
35
|
+
const commands: string[] = [];
|
|
36
|
+
|
|
37
|
+
for (const match of matches) {
|
|
38
|
+
const cmd = match[1];
|
|
39
|
+
// Skip help aliases and internal routing
|
|
40
|
+
if (["--help", "-h", "help", "cli"].includes(cmd)) continue;
|
|
41
|
+
commands.push(cmd);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return [...new Set(commands)];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Extract PAL_* env var names from paths.ts + API keys from source */
|
|
48
|
+
function extractEnvVars(): string[] {
|
|
49
|
+
const pkg = palPkg();
|
|
50
|
+
const vars: Set<string> = new Set();
|
|
51
|
+
|
|
52
|
+
// PAL_* from paths.ts
|
|
53
|
+
const pathsFile = resolve(pkg, "src", "hooks", "lib", "paths.ts");
|
|
54
|
+
if (existsSync(pathsFile)) {
|
|
55
|
+
const content = readFileSync(pathsFile, "utf-8");
|
|
56
|
+
for (const match of content.matchAll(/process\.env\.(PAL_\w+)/g)) {
|
|
57
|
+
vars.add(match[1]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ANTHROPIC_API_KEY from inference.ts
|
|
62
|
+
const inferenceFile = resolve(pkg, "src", "hooks", "lib", "inference.ts");
|
|
63
|
+
if (existsSync(inferenceFile)) {
|
|
64
|
+
const content = readFileSync(inferenceFile, "utf-8");
|
|
65
|
+
if (content.includes("ANTHROPIC_API_KEY")) {
|
|
66
|
+
vars.add("ANTHROPIC_API_KEY");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// GEMINI_API_KEY from youtube-analyze.ts
|
|
71
|
+
const youtubeFile = resolve(pkg, "src", "tools", "youtube-analyze.ts");
|
|
72
|
+
if (existsSync(youtubeFile)) {
|
|
73
|
+
const content = readFileSync(youtubeFile, "utf-8");
|
|
74
|
+
if (content.includes("GEMINI_API_KEY")) {
|
|
75
|
+
vars.add("GEMINI_API_KEY");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return [...vars];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Extract skill names from assets/skills/ */
|
|
83
|
+
function extractSkillNames(): string[] {
|
|
84
|
+
const pkg = palPkg();
|
|
85
|
+
const skillsDir = resolve(pkg, "assets", "skills");
|
|
86
|
+
if (!existsSync(skillsDir)) return [];
|
|
87
|
+
|
|
88
|
+
return readdirSync(skillsDir)
|
|
89
|
+
.filter((f) => f.endsWith(".md"))
|
|
90
|
+
.map((f) => f.replace(/\.md$/, ""));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Validate that README.md documents all code surfaces. */
|
|
94
|
+
export function validateReadmeSync(): SyncResult {
|
|
95
|
+
const pkg = palPkg();
|
|
96
|
+
const readmePath = resolve(pkg, "README.md");
|
|
97
|
+
|
|
98
|
+
if (!existsSync(readmePath)) {
|
|
99
|
+
return { ok: false, issues: ["README.md not found"] };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const readme = readFileSync(readmePath, "utf-8");
|
|
103
|
+
const issues: string[] = [];
|
|
104
|
+
|
|
105
|
+
// Check CLI commands
|
|
106
|
+
for (const cmd of extractCliCommands()) {
|
|
107
|
+
if (!readme.includes(`pal cli ${cmd}`)) {
|
|
108
|
+
issues.push(`CLI command "${cmd}" exists in code but not documented in README`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check environment variables
|
|
113
|
+
for (const envVar of extractEnvVars()) {
|
|
114
|
+
if (!readme.includes(envVar)) {
|
|
115
|
+
issues.push(
|
|
116
|
+
`Environment variable "${envVar}" used in code but not documented in README`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check skills — just verify the count is mentioned or each name appears
|
|
122
|
+
const skills = extractSkillNames();
|
|
123
|
+
const undocumentedSkills = skills.filter((name) => !readme.includes(name));
|
|
124
|
+
if (undocumentedSkills.length > 0) {
|
|
125
|
+
issues.push(`Skills not documented in README: ${undocumentedSkills.join(", ")}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { ok: issues.length === 0, issues };
|
|
129
|
+
}
|
package/src/targets/lib.ts
CHANGED
|
@@ -48,6 +48,7 @@ export function scaffoldTelos(): void {
|
|
|
48
48
|
const templatesDir = assets.telosTemplates();
|
|
49
49
|
const telosDir = resolve(palHome(), "telos");
|
|
50
50
|
if (!existsSync(templatesDir)) return;
|
|
51
|
+
mkdirSync(telosDir, { recursive: true });
|
|
51
52
|
|
|
52
53
|
for (const file of readdirSync(templatesDir).filter((f) => f.endsWith(".md"))) {
|
|
53
54
|
const src = resolve(templatesDir, file);
|