portable-agent-layer 0.45.2 → 0.47.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 CHANGED
@@ -85,6 +85,7 @@ pal cli status # check your setup
85
85
  | `pal cli migrate` | Run pending data migrations (non-destructive) |
86
86
  | `pal cli usage` | Summarize token usage and estimated cost |
87
87
  | `pal cli knowledge` | Query & manage the knowledge store (search, graph, stats, hubs, find, show, add, ls, ingest) |
88
+ | `pal cli skill link <name>` | Link a personal `~/.pal/skills/<name>/` into every installed agent so it is discoverable |
88
89
 
89
90
  ### Target flags
90
91
 
@@ -1,69 +1,72 @@
1
1
  ---
2
2
  name: create-skill
3
- description: Scaffold a new PAL skill from a description. Use when creating a new skill, adding a capability, or building a custom command. Enforces general, assistant-facing, personally-clean skill writing.
3
+ description: Create a new personal skill for this user, scaffolded into their own ~/.pal/skills/ and linked into every installed agent. Use when the user asks to create a skill, add a capability, build a custom command, or "make a skill that…".
4
4
  argument-hint: <skill name> <skill description>
5
5
  ---
6
6
 
7
- ## What makes a good PAL skill
7
+ # Create a personal skill
8
8
 
9
- Before writing any skill, internalize these three rules. A skill that violates them gets re-written, not committed.
9
+ This scaffolds a personal skill into the user's own `~/.pal/skills/<name>/` and links it into every installed agent so it is immediately discoverable.
10
10
 
11
- 1. **Pointed at the assistant, not the user.** A skill is *instructions you (the assistant) follow*. Write in second person addressing yourself ("read X", "extract Y", "output Z"). Prescriptive verbs, deterministic flow. It is **not** a user tutorial, marketing blurb, or README. The user reads `README.md`; you read `SKILL.md`.
11
+ ## What makes a good skill
12
12
 
13
- 2. **Zero personal information.** No usernames, real names, employer names, project codenames, dataset names, hostnames, absolute home paths (`C:\Users\someone\…`), email addresses, or anecdotes about the author's own work. A skill must be reusable by any user. If the rule is "don't expand `~` on Windows cmd," that is the rule — don't add "because $username's primary shell is cmd." Personal context belongs in the user's private memory, never in the skill body.
13
+ 1. **Pointed at the assistant, not the user.** A skill is *instructions you (the assistant) follow*. Write in second person addressing yourself ("read X", "run Y", "output Z"). Prescriptive verbs, deterministic flow not a tutorial or marketing blurb.
14
14
 
15
- 3. **General, not user-specific.** A skill describes a *workflow class* (e.g. "build a deck," "summarize a PDF," "lint a python file"), not a single user's habits. Use placeholders (`<deck-dir>`, `<input.pdf>`), not hardcoded paths. If the workflow only makes sense for one person, it is a memory entry or a personal hook, not a skill.
15
+ 2. **One skill, one job.** A skill describes a single workflow. If it needs branches like "for case A do X, for case B do Y," it is two skills.
16
+
17
+ 3. **Concise and concrete.** Assume the model is already smart — only add what it doesn't already know. Every step has a verb and an object; no "as needed" or "appropriately." Keep the SKILL.md body well under 500 lines; push long reference material into sibling files linked one level deep.
18
+
19
+ A personal skill **may** contain this user's own context — their paths, project names, preferences, conventions. That is the point of a personal skill.
16
20
 
17
21
  ## Skill anatomy
18
22
 
19
23
  ```markdown
20
24
  ---
21
25
  name: <slug> # the slash-command name; lowercase-kebab
22
- description: <one sentence trigger> # WHEN to invoke (used by the dispatcher), not WHAT it does
26
+ description: <what it does + WHEN to invoke> # the dispatcher matches on this
23
27
  argument-hint: <args> # optional; how the user passes input
24
28
  ---
25
29
 
26
30
  ## Overview / Workflow
27
31
 
28
- Numbered steps the assistant follows on invocation. Each step is concrete:
29
- read this file, run this command, ask this question, output this format.
32
+ Numbered, concrete steps the assistant follows on invocation.
30
33
 
31
34
  ## Output format
32
35
 
33
- Exactly what the assistant returns to the user. Specify structure if the
34
- caller will parse it; specify tone if the caller is a human.
36
+ Exactly what the assistant returns structure if it will be parsed, tone if a human reads it.
35
37
 
36
38
  ## When to use / Do NOT use
37
39
 
38
- Two short lists. The "do not" list disambiguates this skill from neighbours
39
- that would otherwise also match the user's request.
40
+ Two short lists; the "do not" list disambiguates this skill from neighbours.
40
41
  ```
41
42
 
42
- ## Workflow when the user invokes `/create-skill <name> <description>`
43
-
44
- 1. Sanity-check the name and description against the three rules above. If the description leaks personal info ("a skill for me to clean my Notion db"), rewrite it to the general form ("clean a Notion database via the API") before scaffolding.
45
- 2. Create `assets/skills/<name>/SKILL.md` in the repo (the canonical source). The installed copy at `~/.pal/skills/<name>/SKILL.md` may be a junction to this path — verify the user's setup before assuming.
46
- 3. Populate the SKILL.md from the anatomy above. Required fields: `name`, `description`, body sections (Workflow, Output format, When to use). Add `argument-hint` if the skill takes arguments.
47
- 4. If the skill needs runtime tooling (TypeScript, scripts, vendored assets), scaffold a `tools/` subdir alongside SKILL.md. Otherwise leave the skill markdown-only.
48
- 5. Validate before declaring done:
49
- - **Trigger clarity** — could a model decide *not* to invoke this skill from the description alone? If yes, tighten the description.
50
- - **Step concreteness** — every step has a verb and an object; no "as needed" or "appropriately."
51
- - **Output specification** — caller knows what they get back.
52
- - **Scope discipline** one skill, one job. If the skill needs section headers like "for case A do X, for case B do Y," it is two skills.
53
- - **Personal-info scan** — grep the new SKILL.md for usernames, real names, absolute home paths, employer or project codenames; remove any hits.
54
- - **Generality test** — could a stranger with the same workflow need use this unchanged? If no, factor the user-specific bits into memory.
55
-
56
- ## Anti-patterns to refuse
57
-
58
- - A SKILL.md whose description starts with "I want…" or "My …" that's a journal entry, not a skill.
59
- - Hardcoded paths under `C:\Users\<name>\…` or `/Users/<name>/…` — use `~` (and document the cmd.exe `%USERPROFILE%` alternative if Windows is in scope).
60
- - Brand- or company-specific defaults baked into the skill body. Default brand colors, footer strings, etc. belong in *templates* (user data) or *config*, never in the skill.
61
- - A description longer than ~30 words, or one that explains the implementation rather than the trigger.
62
- - Skills that duplicate an existing skill's trigger surface. Read the existing skills index before scaffolding.
43
+ The `description` should state **both what the skill does and when to invoke it**, in third person, with the trigger terms a model would match on. A vague description ("helps with documents") will not trigger reliably.
44
+
45
+ ## Workflow when invoked with `<name> <description>`
46
+
47
+ 1. Validate the name: lowercase-kebab, no spaces, not colliding with an existing skill (check `~/.pal/skills/` and the active skill list).
48
+ 2. Confirm the trigger with the user if the description is ambiguous about *when* the skill should fire.
49
+ 3. Create the directory and SKILL.md at the user's PAL home:
50
+ ```bash
51
+ mkdir -p ~/.pal/skills/<name>
52
+ ```
53
+ Write `~/.pal/skills/<name>/SKILL.md` populated from the anatomy above (frontmatter + Workflow + Output format + When to use).
54
+ 4. If the skill needs runtime tooling, scaffold a `tools/` subdir alongside SKILL.md and write the scripts there.
55
+ 5. Link the new skill into every installed agent so it is discoverable:
56
+ ```bash
57
+ pal cli skill link <name>
58
+ ```
59
+ This creates the per-skill discovery symlink in each installed agent's skills directory (Claude Code, Cursor, Copilot, Codex); opencode discovers it automatically via `~/.pal/skills/`.
60
+ 6. Validate before declaring done:
61
+ - **Trigger clarity** could a model decide *not* to invoke this from the description alone? If so, tighten it.
62
+ - **Step concreteness** every step has a verb and an object.
63
+ - **Output specification** the caller knows what they get back.
64
+ - **Scope discipline** — one skill, one job.
63
65
 
64
66
  ## Output format
65
67
 
66
68
  After scaffolding, return:
67
- - The path of the created `SKILL.md`.
69
+ - The path of the created `SKILL.md` (under `~/.pal/skills/<name>/`).
70
+ - The agents it was linked into (from the `pal cli skill link` output).
68
71
  - A 2-3 sentence summary of the trigger and workflow.
69
- - Any anti-pattern violations you caught and corrected during scaffolding (so the user learns the rule).
72
+ - How to invoke it (its `/<name>` slash command or trigger phrase).
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: playwright
3
+ description: Capture a screenshot of a URL or local page and load it into context for a visual check. Use when asked to check visually, use playwright, screenshot a page, see or look at the design yourself, or verify a layout on desktop and mobile widths.
4
+ argument-hint: <url> [--viewport WxH] [--full-page] [--selector <css>]
5
+ ---
6
+
7
+ ## Overview
8
+
9
+ Take a screenshot of a running page and `Read` it into context so you can see the rendered result yourself — for design review, responsive checks (desktop + mobile widths), and confirming a layout fix actually looks right. This skill returns an **image**, not page data.
10
+
11
+ ## Engine selection (handled by the tool)
12
+
13
+ The bundled tool picks the best available local engine automatically:
14
+
15
+ 1. **System `playwright-cli`** (Microsoft's stateful agent CLI) — used when it is on `PATH` and no exact viewport/full-page is requested (its `screenshot` command can't set those).
16
+ 2. **PAL-installed Playwright, launched via Node** — the cross-platform-safe path (Playwright's Chromium hangs under Bun on Windows). Honors `--viewport`, `--full-page`, and `--selector` precisely.
17
+
18
+ If neither engine is usable, the tool prints `NO_PLAYWRIGHT_CLI` and exits non-zero — only then use the **Playwright MCP** (tier 3) in step 4.
19
+
20
+ ## Workflow
21
+
22
+ 1. Resolve the URL. If it's a local app, make sure the dev server is already running first.
23
+ 2. Run the tool (it prints the absolute PNG path as its last stdout line):
24
+
25
+ ```bash
26
+ node --experimental-strip-types ~/.pal/skills/playwright/tools/shot.ts <url> \
27
+ [--viewport 1440x900] [--full-page] [--selector "<css>"] [-o <out.png>]
28
+ ```
29
+
30
+ 3. `Read` the printed path so the screenshot enters your context. For a responsive check, run once per width (e.g. `--viewport 1440x900` then `--viewport 390x844`) and Read each.
31
+ 4. If the tool exits with `NO_PLAYWRIGHT_CLI`:
32
+ - If Playwright MCP tools (`mcp__playwright__browser_*`) are available: `browser_navigate` to the URL, `browser_take_screenshot` to a file, then `Read` it.
33
+ - Otherwise, tell the user Playwright isn't installed and suggest running `pal cli install`.
34
+
35
+ ## Output format
36
+
37
+ The screenshot image (via `Read`) plus one line stating the URL and viewport captured. Add a review only if the caller asked you to assess the design.
38
+
39
+ ## When to use
40
+
41
+ - "check visually", "use playwright", "screenshot this", "see/look at the design yourself", "does this look right", desktop/mobile layout verification.
42
+
43
+ ## Do NOT use
44
+
45
+ - To scrape page text or DOM data — fetch the page directly instead; this returns an image.
46
+ - To measure computed styles, element sizes, or horizontal overflow — use the Playwright MCP `browser_evaluate` for metrics; this skill does not return numbers.
@@ -0,0 +1,61 @@
1
+ // Pure, side-effect-free helpers for the playwright skill tool.
2
+ // Kept separate from shot.ts so they can be unit-tested without launching a browser.
3
+
4
+ export type ShotOptions = {
5
+ url: string;
6
+ out: string;
7
+ viewport?: { width: number; height: number };
8
+ fullPage: boolean;
9
+ selector?: string;
10
+ waitMs?: number;
11
+ };
12
+
13
+ const USAGE =
14
+ "usage: shot.ts <url> [-o <file>] [--viewport WxH] [--full-page] [--selector <css>] [--wait <ms>]";
15
+
16
+ export function parseArgs(argv: string[]): ShotOptions {
17
+ let url = "";
18
+ let out = "";
19
+ let viewport: ShotOptions["viewport"];
20
+ let fullPage = false;
21
+ let selector: string | undefined;
22
+ let waitMs: number | undefined;
23
+
24
+ for (let i = 0; i < argv.length; i++) {
25
+ const a = argv[i];
26
+ if (a === "-o" || a === "--out") out = argv[++i] ?? "";
27
+ else if (a === "--viewport") {
28
+ const m = /^(\d+)[x,](\d+)$/.exec(argv[++i] ?? "");
29
+ if (!m) throw new Error("--viewport expects WxH, e.g. 1440x900");
30
+ viewport = { width: Number(m[1]), height: Number(m[2]) };
31
+ } else if (a === "--full-page") fullPage = true;
32
+ else if (a === "--selector") selector = argv[++i];
33
+ else if (a === "--wait") {
34
+ const n = Number(argv[++i]);
35
+ if (!Number.isFinite(n)) throw new Error("--wait expects a number of milliseconds");
36
+ waitMs = n;
37
+ } else if (!a.startsWith("-") && !url) url = a;
38
+ else throw new Error(`unknown argument: ${a}\n${USAGE}`);
39
+ }
40
+
41
+ if (!url) throw new Error(`a URL is required\n${USAGE}`);
42
+ return { url, out, viewport, fullPage, selector, waitMs };
43
+ }
44
+
45
+ export type Tier = "cli" | "node";
46
+
47
+ /**
48
+ * Decide which local engine to use. `playwright-cli` (Microsoft's stateful agent CLI)
49
+ * is preferred when present, but its `screenshot` command cannot set a viewport or
50
+ * capture full-page — so any request that needs exact size/full-page falls to the
51
+ * Node-launched PAL Playwright, which honors them precisely.
52
+ */
53
+ export function chooseTier(opts: {
54
+ cliAvailable: boolean;
55
+ viewport?: unknown;
56
+ fullPage?: boolean;
57
+ }): Tier {
58
+ if (!opts.cliAvailable) return "node";
59
+ if (opts.viewport || opts.fullPage) return "node";
60
+ return "cli";
61
+ }
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ // playwright skill tool: capture a screenshot of a URL and print its absolute path.
3
+ //
4
+ // Engine selection (see chooseTier):
5
+ // Tier 1 — system `playwright-cli` binary (Microsoft's stateful agent CLI), when on
6
+ // PATH and no exact viewport/full-page is requested.
7
+ // Tier 2 — PAL-installed Playwright, launched via Node. (Playwright's chromium.launch
8
+ // hangs under Bun on Windows — the same Node exception create-pdf relies on.)
9
+ // If neither engine works, prints NO_PLAYWRIGHT_CLI on stderr and exits non-zero so the
10
+ // caller (SKILL.md) can fall back to the Playwright MCP.
11
+ //
12
+ // Run with Node:
13
+ // node --experimental-strip-types ~/.pal/skills/playwright/tools/shot.ts <url> [opts]
14
+
15
+ import { spawnSync } from "node:child_process";
16
+ import { existsSync, mkdtempSync, rmSync } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join, resolve } from "node:path";
19
+ import { chooseTier, parseArgs, type ShotOptions } from "./shot-lib.ts";
20
+
21
+ function playwrightCliAvailable(): boolean {
22
+ try {
23
+ return spawnSync("playwright-cli", ["--version"], { stdio: "ignore" }).status === 0;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ function runViaCli(opts: ShotOptions, out: string): boolean {
30
+ // playwright-cli writes a `.playwright-cli/` session dir (logs + page snapshots)
31
+ // into its cwd. Run it inside a throwaway temp dir so nothing lands in the user's
32
+ // repo, then remove it. `out` is already absolute, so the screenshot is unaffected.
33
+ // Cross-platform: os.tmpdir + fs.mkdtemp/rm — no shell, no platform-specific paths.
34
+ const work = mkdtempSync(join(tmpdir(), "pal-pwcli-"));
35
+ const run = (args: string[], quiet = false) =>
36
+ spawnSync("playwright-cli", args, { stdio: quiet ? "ignore" : "inherit", cwd: work });
37
+ try {
38
+ if (run(["open", opts.url]).status !== 0) return false;
39
+ const args = ["screenshot", `--filename=${out}`];
40
+ if (opts.selector) args.push(opts.selector);
41
+ const shot = run(args);
42
+ run(["close"], true);
43
+ return shot.status === 0 && existsSync(out);
44
+ } finally {
45
+ rmSync(work, { recursive: true, force: true });
46
+ }
47
+ }
48
+
49
+ // "unavailable" = the engine itself can't run (package or Chromium missing) → MCP fallback.
50
+ // "error" = the engine ran but the page/screenshot failed → a real error, not an MCP case.
51
+ type NodeResult = "ok" | "unavailable" | "error";
52
+
53
+ async function runViaNode(opts: ShotOptions, out: string): Promise<NodeResult> {
54
+ let chromium: typeof import("playwright").chromium;
55
+ try {
56
+ ({ chromium } = await import("playwright"));
57
+ } catch {
58
+ return "unavailable"; // package not resolvable
59
+ }
60
+
61
+ let browser: Awaited<ReturnType<typeof chromium.launch>>;
62
+ try {
63
+ browser = await chromium.launch();
64
+ } catch (e) {
65
+ console.error(`chromium launch failed: ${(e as Error).message}`);
66
+ return "unavailable"; // browser not installed / can't launch
67
+ }
68
+
69
+ try {
70
+ const page = await browser.newPage(opts.viewport ? { viewport: opts.viewport } : {});
71
+ await page.goto(opts.url, { waitUntil: "networkidle" });
72
+ if (opts.waitMs) await page.waitForTimeout(opts.waitMs);
73
+ if (opts.selector) await page.locator(opts.selector).screenshot({ path: out });
74
+ else await page.screenshot({ path: out, fullPage: opts.fullPage });
75
+ return existsSync(out) ? "ok" : "error";
76
+ } catch (e) {
77
+ console.error(`screenshot failed: ${(e as Error).message}`);
78
+ return "error";
79
+ } finally {
80
+ await browser.close();
81
+ }
82
+ }
83
+
84
+ async function main(): Promise<void> {
85
+ let opts: ShotOptions;
86
+ try {
87
+ opts = parseArgs(process.argv.slice(2));
88
+ } catch (e) {
89
+ console.error((e as Error).message);
90
+ process.exit(2);
91
+ }
92
+
93
+ const out = opts.out ? resolve(opts.out) : join(tmpdir(), `pal-shot-${Date.now()}.png`);
94
+ const tier = chooseTier({
95
+ cliAvailable: playwrightCliAvailable(),
96
+ viewport: opts.viewport,
97
+ fullPage: opts.fullPage,
98
+ });
99
+
100
+ if (tier === "cli" && runViaCli(opts, out)) {
101
+ console.log(out);
102
+ return;
103
+ }
104
+
105
+ const result = await runViaNode(opts, out);
106
+ if (result === "ok") {
107
+ console.log(out);
108
+ return;
109
+ }
110
+ if (result === "unavailable") {
111
+ console.error("NO_PLAYWRIGHT_CLI");
112
+ process.exit(3); // no local engine → caller falls back to Playwright MCP
113
+ }
114
+ process.exit(1); // engine ran but the capture failed — a real error, not an MCP case
115
+ }
116
+
117
+ await main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.45.2",
3
+ "version": "0.47.0",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/index.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  * status Show current PAL configuration
17
17
  * doctor Check prerequisites and system health
18
18
  * usage Summarize token usage and cost
19
+ * skill link <name> Link a personal ~/.pal/skills/<name>/ into installed agents
19
20
  */
20
21
 
21
22
  import { spawnSync } from "node:child_process";
@@ -192,6 +193,12 @@ async function runCli(command: string | undefined, args: string[]) {
192
193
  if (code !== 0) process.exit(code);
193
194
  break;
194
195
  }
196
+ case "skill": {
197
+ const { runSkill } = await import("./skill");
198
+ const code = await runSkill(args);
199
+ if (code !== 0) process.exit(code);
200
+ break;
201
+ }
195
202
  case "--help":
196
203
  case "-h":
197
204
  case "help":
@@ -234,6 +241,7 @@ function showHelp() {
234
241
  pal cli usage Summarize token usage and cost
235
242
  pal cli knowledge <sub> [args] Query & manage the knowledge store
236
243
  (search · graph · stats · hubs · find · show · add · ls)
244
+ pal cli skill link <name> Link a personal ~/.pal/skills/<name>/ into installed agents
237
245
 
238
246
  Environment:
239
247
  PAL_HOME Override user state directory (default: ~/.pal or repo root)
@@ -0,0 +1,40 @@
1
+ /**
2
+ * pal cli skill — manage personal skills under ~/.pal/skills/.
3
+ *
4
+ * pal cli skill link <name> Link an existing ~/.pal/skills/<name>/ into
5
+ * every installed agent so it is discoverable.
6
+ */
7
+
8
+ import { linkPersonalSkill, log } from "../targets/lib";
9
+
10
+ export async function runSkill(args: string[]): Promise<number> {
11
+ const [sub, name] = args;
12
+
13
+ if (sub === "link") {
14
+ if (!name) {
15
+ log.error("Usage: pal cli skill link <name>");
16
+ return 1;
17
+ }
18
+ try {
19
+ const linked = linkPersonalSkill(name);
20
+ if (linked.length === 0) {
21
+ log.warn(
22
+ `'${name}' linked to no per-skill agents (none installed). ` +
23
+ "It is still discoverable by opencode via ~/.pal/skills/."
24
+ );
25
+ } else {
26
+ log.success(
27
+ `Linked '${name}' into: ${linked.join(", ")} ` +
28
+ "(opencode: auto via ~/.pal/skills/)"
29
+ );
30
+ }
31
+ return 0;
32
+ } catch (e) {
33
+ log.error(e instanceof Error ? e.message : String(e));
34
+ return 1;
35
+ }
36
+ }
37
+
38
+ log.error("Usage: pal cli skill link <name>");
39
+ return 1;
40
+ }
@@ -506,6 +506,46 @@ export function copySkills(claudeSkillsDir: string): number {
506
506
  return count;
507
507
  }
508
508
 
509
+ /**
510
+ * Agent skills directories that need a per-skill discovery link.
511
+ *
512
+ * opencode is intentionally absent: it discovers the whole ~/.pal/skills/ tree
513
+ * via the ~/.agents/skills → ~/.pal/skills symlink, so any personal skill is
514
+ * picked up without a per-skill link.
515
+ */
516
+ function perSkillAgentDirs(): { agent: string; dir: string }[] {
517
+ return [
518
+ { agent: "claude", dir: resolve(platform.claudeDir(), "skills") },
519
+ { agent: "cursor", dir: resolve(platform.cursorDir(), "skills") },
520
+ { agent: "copilot", dir: resolve(platform.copilotDir(), "skills") },
521
+ { agent: "codex", dir: resolve(platform.codexDir(), "skills") },
522
+ ];
523
+ }
524
+
525
+ /**
526
+ * Link a personal skill that already lives at ~/.pal/skills/<name>/ into every
527
+ * installed agent's skills directory so the agent discovers it. Mirrors the
528
+ * per-skill discovery link copySkills() creates for shipped skills.
529
+ *
530
+ * An agent counts as installed when its skills directory already exists.
531
+ * Returns the agents a discovery link was created for (opencode excluded — it
532
+ * is covered by the whole-dir ~/.agents/skills link).
533
+ */
534
+ export function linkPersonalSkill(name: string): string[] {
535
+ const palLink = resolve(PAL_SKILLS_DIR, name);
536
+ if (!existsSync(resolve(palLink, "SKILL.md"))) {
537
+ throw new Error(`No skill found at ${palLink}/SKILL.md`);
538
+ }
539
+ const linkType = process.platform === "win32" ? "junction" : "dir";
540
+ const linked: string[] = [];
541
+ for (const { agent, dir } of perSkillAgentDirs()) {
542
+ if (!existsSync(dir)) continue; // agent not installed
543
+ ensureSymlink(resolve(dir, name), palLink, linkType);
544
+ linked.push(agent);
545
+ }
546
+ return linked;
547
+ }
548
+
509
549
  /** Create or update a symlink/junction, replacing any non-symlink entry. */
510
550
  function ensureSymlink(link: string, target: string, type: "dir" | "junction"): void {
511
551
  try {