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 +1 -0
- package/assets/skills/create-skill/SKILL.md +39 -36
- package/assets/skills/playwright/SKILL.md +46 -0
- package/assets/skills/playwright/tools/shot-lib.ts +61 -0
- package/assets/skills/playwright/tools/shot.ts +117 -0
- package/package.json +1 -1
- package/src/cli/index.ts +8 -0
- package/src/cli/skill.ts +40 -0
- package/src/targets/lib.ts +40 -0
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:
|
|
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
|
-
|
|
7
|
+
# Create a personal skill
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
11
|
+
## What makes a good skill
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
|
|
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: <
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
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
|
-
-
|
|
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
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)
|
package/src/cli/skill.ts
ADDED
|
@@ -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
|
+
}
|
package/src/targets/lib.ts
CHANGED
|
@@ -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 {
|