@crustjs/skills 0.1.1 → 0.2.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/LICENSE +21 -0
- package/README.md +2 -679
- package/dist/index.d.ts +172 -886
- package/dist/index.js +1435 -16
- package/package.json +29 -15
package/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# @crustjs/skills
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Instead of hand-maintaining skill files for AI coding agents, generate them from your Crust command metadata. The output is a portable skill bundle that developers can download and install into their own agent environments (OpenCode, Claude Code, etc.).
|
|
3
|
+
Package and install agent skills for AI coding assistants.
|
|
6
4
|
|
|
7
5
|
## Install
|
|
8
6
|
|
|
@@ -10,681 +8,6 @@ Instead of hand-maintaining skill files for AI coding agents, generate them from
|
|
|
10
8
|
bun add @crustjs/skills
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
## Quick Start
|
|
14
|
-
|
|
15
|
-
### CLI (via `@crustjs/crust`)
|
|
16
|
-
|
|
17
|
-
```sh
|
|
18
|
-
crust skills generate ./src/cli.ts --name my-cli --description "My CLI tool"
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
### Programmatic API
|
|
22
|
-
|
|
23
|
-
```ts
|
|
24
|
-
import { generateSkill } from "@crustjs/skills";
|
|
25
|
-
import { rootCommand } from "./commands.ts";
|
|
26
|
-
|
|
27
|
-
const result = await generateSkill({
|
|
28
|
-
command: rootCommand,
|
|
29
|
-
meta: {
|
|
30
|
-
name: "my-cli",
|
|
31
|
-
description: "CLI tool for managing widgets",
|
|
32
|
-
version: "1.0.0",
|
|
33
|
-
},
|
|
34
|
-
agents: ["opencode", "claude-code"],
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
for (const agent of result.agents) {
|
|
38
|
-
console.log(`${agent.agent}: ${agent.status} -> ${agent.outputDir}`);
|
|
39
|
-
}
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
### Runtime Plugin (`autoUpdate`)
|
|
43
|
-
|
|
44
|
-
Register `skillPlugin()` on your `Crust` builder with `.use()`:
|
|
45
|
-
|
|
46
|
-
```ts
|
|
47
|
-
import { Crust } from "@crustjs/core";
|
|
48
|
-
import { skillPlugin } from "@crustjs/skills";
|
|
49
|
-
|
|
50
|
-
const app = new Crust("my-cli")
|
|
51
|
-
.meta({ description: "My CLI" })
|
|
52
|
-
.use(
|
|
53
|
-
skillPlugin({
|
|
54
|
-
version: "1.0.0",
|
|
55
|
-
instructions: `
|
|
56
|
-
Prefer readonly commands before mutating project state.
|
|
57
|
-
|
|
58
|
-
## Response Policy
|
|
59
|
-
|
|
60
|
-
- Read the relevant command doc before suggesting flags.
|
|
61
|
-
`,
|
|
62
|
-
// autoUpdate: true (default) — silently updates installed skills
|
|
63
|
-
// command: "skill" (default) — registers "my-cli skill" subcommand
|
|
64
|
-
// defaultScope: "global" | "project" — skip scope prompt when set
|
|
65
|
-
// installMode: "auto" | "symlink" | "copy" (default: "auto")
|
|
66
|
-
}),
|
|
67
|
-
)
|
|
68
|
-
.run(() => {
|
|
69
|
-
console.log("hello");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
await app.execute();
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
The plugin automatically updates already-installed skills when the version changes, checking both project and global paths for the current working directory. If the current working directory is the home directory, `project` scope is normalized to `global` so installs, updates, and status checks use the global skill locations. First-time installation is done via the interactive `skill` subcommand (or `skill update` for update-only flows), or programmatically using the exported primitives.
|
|
76
|
-
|
|
77
|
-
Generated bundles are written once to a canonical store (`.crust/skills` for project scope, `~/.crust/skills` for global scope) and then installed into agent paths via symlink or copy depending on `installMode`.
|
|
78
|
-
|
|
79
|
-
#### Hand-authored bundles via `customSkills`
|
|
80
|
-
|
|
81
|
-
The plugin can also manage **hand-authored** skill bundles alongside the
|
|
82
|
-
auto-generated command-reference skill. Pass an array of
|
|
83
|
-
`CustomSkillConfig` entries via `customSkills`; each entry is reconciled
|
|
84
|
-
through the same lifecycle as the main skill.
|
|
85
|
-
|
|
86
|
-
```ts
|
|
87
|
-
import { Crust } from "@crustjs/core";
|
|
88
|
-
import { skillPlugin } from "@crustjs/skills";
|
|
89
|
-
import pkg from "./package.json" with { type: "json" };
|
|
90
|
-
|
|
91
|
-
const app = new Crust("my-cli")
|
|
92
|
-
.meta({ description: "My CLI" })
|
|
93
|
-
.use(
|
|
94
|
-
skillPlugin({
|
|
95
|
-
version: pkg.version,
|
|
96
|
-
customSkills: [
|
|
97
|
-
// Inherits `version: pkg.version` from the plugin — the typical
|
|
98
|
-
// case when the bundle ships in the same package as the CLI.
|
|
99
|
-
{
|
|
100
|
-
name: "funnel-builder",
|
|
101
|
-
// Resolved against the nearest package.json walking up from
|
|
102
|
-
// process.argv[1] — same rules as installSkillBundle().
|
|
103
|
-
sourceDir: "skills/funnel-builder",
|
|
104
|
-
},
|
|
105
|
-
// Explicit override for an independently-versioned bundle.
|
|
106
|
-
{
|
|
107
|
-
name: "vendored-toolkit",
|
|
108
|
-
sourceDir: "skills/vendored-toolkit",
|
|
109
|
-
version: "0.3.0",
|
|
110
|
-
},
|
|
111
|
-
],
|
|
112
|
-
}),
|
|
113
|
-
)
|
|
114
|
-
.run(() => {});
|
|
115
|
-
|
|
116
|
-
await app.execute();
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
- **`name`** must satisfy `isValidSkillName` (1–64 lowercase alphanumeric
|
|
120
|
-
characters and hyphens, no leading/trailing/consecutive hyphens), must
|
|
121
|
-
be unique within the array, and must not collide with the main skill's
|
|
122
|
-
name. The bundle's `SKILL.md` frontmatter must declare a matching
|
|
123
|
-
`name:` field — mismatches are rejected at install time.
|
|
124
|
-
- **`sourceDir`** accepts a `URL` (`file:` protocol), an absolute path, or
|
|
125
|
-
a relative string resolved from the nearest `package.json`. Resolution
|
|
126
|
-
errors surface at install time, not at plugin setup.
|
|
127
|
-
- **`version`** is optional. When omitted, the bundle inherits the
|
|
128
|
-
plugin's top-level `version` — the typical case when the bundle ships
|
|
129
|
-
alongside the CLI. Pass an explicit value when the bundle's release
|
|
130
|
-
cadence is independent of the consuming CLI (for example, vendored
|
|
131
|
-
from another package). Identical-version reinstalls are skipped, so
|
|
132
|
-
bump the effective version whenever bundle contents change. The
|
|
133
|
-
bundle's `SKILL.md` frontmatter `version:` / `metadata.version`, if
|
|
134
|
-
any, is intentionally ignored — see the [`installSkillBundle()` note](#installing-hand-authored-bundles).
|
|
135
|
-
- **`scope`** and **`installMode`** are optional per-entry overrides;
|
|
136
|
-
unset values inherit from the plugin's `defaultScope` / `installMode`.
|
|
137
|
-
|
|
138
|
-
The interactive `skill` command shows one multiselect prompt per skill in
|
|
139
|
-
order: the main auto-generated skill first, then each `customSkills`
|
|
140
|
-
entry with its name in the prompt header (e.g. `"Select agents to install
|
|
141
|
-
skills for [funnel-builder]"`). Each prompt is independent — selecting
|
|
142
|
-
or deselecting agents reconciles only that skill's installs. `skill
|
|
143
|
-
--all` skips every prompt and installs every skill for the full agent
|
|
144
|
-
set; `skill update` updates outdated installs across main + every bundle.
|
|
145
|
-
|
|
146
|
-
Auto-update on plugin startup is per-skill: only outdated installs are
|
|
147
|
-
rewritten, and a single bundle's failure (e.g. missing `sourceDir`) does
|
|
148
|
-
not abort the others. Pass `autoUpdate: false` to disable startup auto-
|
|
149
|
-
update for both the main skill and all bundles.
|
|
150
|
-
|
|
151
|
-
### Programmatic Auto-Install
|
|
152
|
-
|
|
153
|
-
For full control over first-time installation, call `generateSkill()`
|
|
154
|
-
directly from your handler. With `agents` omitted, it installs into every
|
|
155
|
-
universal agent plus every additional agent whose CLI is on `PATH`, and
|
|
156
|
-
returns `up-to-date` for targets that already match the current version —
|
|
157
|
-
so the same call is safe to run on every invocation. Pass `agents: []` to
|
|
158
|
-
opt out, or an explicit array to scope the install.
|
|
159
|
-
|
|
160
|
-
```ts
|
|
161
|
-
import { Crust } from "@crustjs/core";
|
|
162
|
-
import { generateSkill } from "@crustjs/skills";
|
|
163
|
-
|
|
164
|
-
export const app = new Crust("my-cli")
|
|
165
|
-
.meta({ description: "My CLI" })
|
|
166
|
-
.run(async (ctx) => {
|
|
167
|
-
// Defaults to universal + agents detected on PATH. Idempotent: targets
|
|
168
|
-
// that already match the current version are returned as `up-to-date`.
|
|
169
|
-
const result = await generateSkill({
|
|
170
|
-
command: ctx.command,
|
|
171
|
-
meta: {
|
|
172
|
-
name: ctx.command.meta.name,
|
|
173
|
-
description: ctx.command.meta.description ?? "",
|
|
174
|
-
version: "1.0.0",
|
|
175
|
-
},
|
|
176
|
-
scope: "global",
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
const changed = result.agents.filter((a) => a.status !== "up-to-date");
|
|
180
|
-
if (changed.length > 0) {
|
|
181
|
-
console.log(`Installed or updated skills for ${changed.length} target(s).`);
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
if (import.meta.main) {
|
|
186
|
-
await app.execute();
|
|
187
|
-
}
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
`getUniversalAgents()`, `getAdditionalAgents()`, and
|
|
191
|
-
`detectInstalledAgents()` remain exported for callers that want to compose
|
|
192
|
-
their own agent list.
|
|
193
|
-
|
|
194
|
-
#### Troubleshooting
|
|
195
|
-
|
|
196
|
-
If auto-update does not appear to work:
|
|
197
|
-
|
|
198
|
-
- Ensure `skillPlugin(...)` is registered on the `Crust` builder via `.use()`.
|
|
199
|
-
- Ensure at least one supported agent is detected. Auto-update checks both project and global install paths, with home-directory `project` scope treated as `global`.
|
|
200
|
-
- Check for existing conflicting skill directories without `crust.json`.
|
|
201
|
-
|
|
202
|
-
## Recommended Export Pattern
|
|
203
|
-
|
|
204
|
-
To avoid side effects when your command module is imported for generation, guard runtime code with `import.meta.main`:
|
|
205
|
-
|
|
206
|
-
```ts
|
|
207
|
-
import { Crust } from "@crustjs/core";
|
|
208
|
-
|
|
209
|
-
// Export the command — used by skill generation.
|
|
210
|
-
export const rootCommand = new Crust("my-cli")
|
|
211
|
-
.meta({ description: "My CLI tool" })
|
|
212
|
-
.run(({ args }) => {
|
|
213
|
-
console.log("Hello from my-cli!");
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// Only execute when run directly — not when imported for generation.
|
|
217
|
-
if (import.meta.main) {
|
|
218
|
-
await rootCommand.execute();
|
|
219
|
-
}
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
### Custom Instructions
|
|
223
|
-
|
|
224
|
-
Use plugin-level `instructions` to add top-level guidance to the generated
|
|
225
|
-
`SKILL.md`, and `annotate()` to add prompt guidance to specific
|
|
226
|
-
command docs under `commands/`.
|
|
227
|
-
|
|
228
|
-
- `instructions: string` renders as a raw markdown block.
|
|
229
|
-
- `instructions: string[]` renders as bullet list items.
|
|
230
|
-
- Empty or whitespace-only instruction input is ignored.
|
|
231
|
-
- `annotate()` always renders command guidance as bullets.
|
|
232
|
-
|
|
233
|
-
```ts
|
|
234
|
-
import { Crust } from "@crustjs/core";
|
|
235
|
-
import { annotate, skillPlugin } from "@crustjs/skills";
|
|
236
|
-
|
|
237
|
-
const deploy = annotate(
|
|
238
|
-
new Crust("deploy")
|
|
239
|
-
.meta({ description: "Deploy the application" })
|
|
240
|
-
.flags({
|
|
241
|
-
"dry-run": { type: "boolean", description: "Preview changes only" },
|
|
242
|
-
})
|
|
243
|
-
.run(() => {
|
|
244
|
-
// ...
|
|
245
|
-
}),
|
|
246
|
-
[
|
|
247
|
-
"Prefer `--dry-run` before executing deployment changes.",
|
|
248
|
-
"Ask for confirmation before production deployments.",
|
|
249
|
-
],
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
const app = new Crust("my-cli")
|
|
253
|
-
.meta({ description: "My CLI" })
|
|
254
|
-
.use(
|
|
255
|
-
skillPlugin({
|
|
256
|
-
version: "1.0.0",
|
|
257
|
-
instructions: `
|
|
258
|
-
Read command docs before suggesting exact flags.
|
|
259
|
-
|
|
260
|
-
## Answer Style
|
|
261
|
-
|
|
262
|
-
- Prefer exact syntax copied from the relevant command file.
|
|
263
|
-
`,
|
|
264
|
-
}),
|
|
265
|
-
)
|
|
266
|
-
.command(deploy);
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
This pattern lets `crust skills generate` import the command definition without triggering `app.execute()`.
|
|
270
|
-
|
|
271
|
-
## CLI Usage
|
|
272
|
-
|
|
273
|
-
The `crust skills generate` command is provided by `@crustjs/crust`:
|
|
274
|
-
|
|
275
|
-
```sh
|
|
276
|
-
crust skills generate <module> [options]
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
### Arguments
|
|
280
|
-
|
|
281
|
-
| Argument | Description |
|
|
282
|
-
| -------- | ------------------------------------------------ |
|
|
283
|
-
| `module` | Path to the command module (e.g. `./src/cli.ts`) |
|
|
284
|
-
|
|
285
|
-
### Flags
|
|
286
|
-
|
|
287
|
-
| Flag | Alias | Required | Default | Description |
|
|
288
|
-
| --------------- | ----- | -------- | --------- | ---------------------------------------------- |
|
|
289
|
-
| `--name` | `-n` | Yes | - | Skill name (used as directory name) |
|
|
290
|
-
| `--description` | `-d` | Yes | - | Human-readable description |
|
|
291
|
-
| `--version` | `-V` | No | - | Version string |
|
|
292
|
-
| `--out-dir` | `-o` | No | `.` | Output directory |
|
|
293
|
-
| `--clean` | - | No | `true` | Remove existing skill directory before writing |
|
|
294
|
-
| `--export` | `-e` | No | `default` | Named export to use from the module |
|
|
295
|
-
|
|
296
|
-
### Examples
|
|
297
|
-
|
|
298
|
-
```sh
|
|
299
|
-
# Basic generation
|
|
300
|
-
crust skills generate ./src/cli.ts --name my-cli --description "My CLI"
|
|
301
|
-
|
|
302
|
-
# With version and custom output directory
|
|
303
|
-
crust skills generate ./src/cli.ts -n my-cli -d "My CLI" --version 1.0.0 -o ./dist
|
|
304
|
-
|
|
305
|
-
# Using a named export instead of the default export
|
|
306
|
-
crust skills generate ./src/cli.ts -n my-cli -d "My CLI" --export rootCommand
|
|
307
|
-
|
|
308
|
-
# Keep existing files (no clean)
|
|
309
|
-
crust skills generate ./src/cli.ts -n my-cli -d "My CLI" --no-clean
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
## Programmatic API
|
|
313
|
-
|
|
314
|
-
### `generateSkill(options)`
|
|
315
|
-
|
|
316
|
-
High-level API that runs the full pipeline: introspection, rendering, and writing to disk.
|
|
317
|
-
|
|
318
|
-
The `meta.name` must be a valid skill name — lowercase alphanumeric with hyphens, 1–64 characters (validated against the [Agent Skills spec](https://agentskills.io/specification) pattern). Use `isValidSkillName()` to check before calling.
|
|
319
|
-
|
|
320
|
-
```ts
|
|
321
|
-
import { generateSkill } from "@crustjs/skills";
|
|
322
|
-
|
|
323
|
-
const result = await generateSkill({
|
|
324
|
-
command: rootCommand,
|
|
325
|
-
meta: {
|
|
326
|
-
name: "my-cli",
|
|
327
|
-
description: "My CLI tool",
|
|
328
|
-
version: "1.0.0",
|
|
329
|
-
instructions: ["Prefer readonly commands before making changes."],
|
|
330
|
-
},
|
|
331
|
-
agents: ["opencode"],
|
|
332
|
-
scope: "project", // default: "global"
|
|
333
|
-
installMode: "auto", // default: "auto" — symlink first, fallback to copy
|
|
334
|
-
clean: true, // default: true — removes existing skill dir first
|
|
335
|
-
force: false, // default: false — throws SkillConflictError if dir exists without crust.json
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
// result.agents — per-agent install results
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
### `buildManifest(command)`
|
|
342
|
-
|
|
343
|
-
Introspects a command tree and produces a canonical, serializable manifest.
|
|
344
|
-
|
|
345
|
-
```ts
|
|
346
|
-
import { buildManifest } from "@crustjs/skills";
|
|
347
|
-
|
|
348
|
-
const manifest = buildManifest(rootCommand);
|
|
349
|
-
// manifest.name, manifest.path, manifest.args, manifest.flags, manifest.children
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
### `renderSkill(manifest, meta)`
|
|
353
|
-
|
|
354
|
-
Renders markdown files from a manifest tree without writing to disk.
|
|
355
|
-
|
|
356
|
-
```ts
|
|
357
|
-
import { buildManifest, renderSkill } from "@crustjs/skills";
|
|
358
|
-
|
|
359
|
-
const manifest = buildManifest(rootCommand);
|
|
360
|
-
const files = renderSkill(manifest, { name: "my-cli", description: "My CLI" });
|
|
361
|
-
|
|
362
|
-
for (const file of files) {
|
|
363
|
-
console.log(file.path); // e.g. "SKILL.md", "commands/serve.md"
|
|
364
|
-
console.log(file.content); // markdown content
|
|
365
|
-
}
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
### `resolveCanonicalSkillPath(scope, name)`
|
|
369
|
-
|
|
370
|
-
Resolves the canonical store path where Crust writes the single source-of-truth skill bundle. Agent install paths are symlinked (or copied) from this location.
|
|
371
|
-
|
|
372
|
-
```ts
|
|
373
|
-
import { resolveCanonicalSkillPath } from "@crustjs/skills";
|
|
374
|
-
|
|
375
|
-
resolveCanonicalSkillPath("project", "my-cli");
|
|
376
|
-
// → "<cwd>/.crust/skills/my-cli"
|
|
377
|
-
|
|
378
|
-
resolveCanonicalSkillPath("global", "my-cli");
|
|
379
|
-
// → "~/.crust/skills/my-cli"
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
When `process.cwd()` is the home directory, `resolveCanonicalSkillPath("project", ...)` returns the same global path as `resolveCanonicalSkillPath("global", ...)`.
|
|
383
|
-
|
|
384
|
-
### `isValidSkillName(name)`
|
|
385
|
-
|
|
386
|
-
Validates a skill name against the [Agent Skills spec](https://agentskills.io/specification) pattern: 1–64 lowercase alphanumeric characters and hyphens, no leading/trailing/consecutive hyphens.
|
|
387
|
-
|
|
388
|
-
```ts
|
|
389
|
-
import { isValidSkillName } from "@crustjs/skills";
|
|
390
|
-
|
|
391
|
-
isValidSkillName("my-cli"); // true
|
|
392
|
-
isValidSkillName("My_CLI"); // false — uppercase and underscores not allowed
|
|
393
|
-
isValidSkillName("-leading"); // false — leading hyphen
|
|
394
|
-
isValidSkillName("a".repeat(65)); // false — exceeds 64 characters
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
> **Note:** `generateSkill()` automatically validates `meta.name` and throws a descriptive error if the name is invalid.
|
|
398
|
-
|
|
399
|
-
## Skill Metadata
|
|
400
|
-
|
|
401
|
-
The `SkillMeta` object controls the generated `SKILL.md` frontmatter. Beyond the required `name`, `description`, and `version` fields, several optional fields are supported:
|
|
402
|
-
|
|
403
|
-
```ts
|
|
404
|
-
const meta: SkillMeta = {
|
|
405
|
-
name: "my-cli",
|
|
406
|
-
description: "CLI tool for managing widgets",
|
|
407
|
-
version: "1.0.0",
|
|
408
|
-
|
|
409
|
-
// Optional fields — emitted in SKILL.md YAML frontmatter when set
|
|
410
|
-
allowedTools: "Bash(my-cli *) Read Grep", // Pre-approved tools (avoids per-use prompts)
|
|
411
|
-
license: "MIT", // License name or reference
|
|
412
|
-
compatibility: "Requires my-cli on PATH", // Environment requirements (max 500 chars)
|
|
413
|
-
disableModelInvocation: false, // true = agent won't auto-load; user must invoke manually
|
|
414
|
-
};
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
| Field | Frontmatter Key | Description |
|
|
418
|
-
| ------------------------ | -------------------------- | ---------------------------------------------------------------------------- |
|
|
419
|
-
| `allowedTools` | `allowed-tools` | Space-delimited list of pre-approved tools (e.g. `Bash(my-cli *) Read Grep`) |
|
|
420
|
-
| `license` | `license` | License name or file reference |
|
|
421
|
-
| `compatibility` | `compatibility` | Environment requirements or compatibility notes |
|
|
422
|
-
| `disableModelInvocation` | `disable-model-invocation` | When `true`, prevents agents from auto-loading the skill |
|
|
423
|
-
|
|
424
|
-
## Escaping
|
|
425
|
-
|
|
426
|
-
The renderer automatically handles special characters in generated output:
|
|
427
|
-
|
|
428
|
-
- **YAML frontmatter**: Values containing YAML-special characters (`:`, `#`, `*`, `!`, `[`, `{`, `'`, `"`, etc.) are wrapped in double quotes with internal quotes escaped.
|
|
429
|
-
- **Markdown tables**: Literal `|` characters in argument/flag descriptions are escaped as `\|` to prevent broken table rendering.
|
|
430
|
-
|
|
431
|
-
No manual escaping is needed — pass raw values and the renderer handles the rest.
|
|
432
|
-
|
|
433
|
-
## Output Structure
|
|
434
|
-
|
|
435
|
-
Generated output goes to `<outDir>/skills/<name>/`:
|
|
436
|
-
|
|
437
|
-
```
|
|
438
|
-
skills/my-cli/
|
|
439
|
-
SKILL.md # Entrypoint — loaded by the agent
|
|
440
|
-
commands/ # Per-command documentation mirroring the CLI hierarchy
|
|
441
|
-
my-cli.md # Root command
|
|
442
|
-
serve.md # Subcommand
|
|
443
|
-
db/
|
|
444
|
-
migrate.md # Nested subcommand
|
|
445
|
-
seed.md
|
|
446
|
-
crust.json # Machine-readable bundle metadata (Crust ownership marker)
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
### File Details
|
|
450
|
-
|
|
451
|
-
| File | Purpose |
|
|
452
|
-
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
453
|
-
| `SKILL.md` | Agent entrypoint with YAML frontmatter and an embedded command reference table listing every command path, type (runnable/group), and documentation link. |
|
|
454
|
-
| `commands/*.md` | Per-command reference files. Leaf commands include usage, arguments, flags, defaults, and aliases. Group commands list subcommands with links. |
|
|
455
|
-
| `crust.json` | Crust-specific JSON metadata: name, description, and version. Also serves as an ownership marker — its presence indicates the skill was generated by Crust. |
|
|
456
|
-
|
|
457
|
-
## Conflict Detection
|
|
458
|
-
|
|
459
|
-
Each Crust-managed skill directory contains a `crust.json` file that acts
|
|
460
|
-
as an ownership marker. Both `generateSkill()` and `installSkillBundle()`
|
|
461
|
-
refuse to overwrite a target directory in three cases:
|
|
462
|
-
|
|
463
|
-
1. **No `crust.json`** — the directory exists but was not created by Crust
|
|
464
|
-
(e.g. manually authored or installed by another tool).
|
|
465
|
-
2. **Kind mismatch** — `crust.json` records a different
|
|
466
|
-
[`kind`](#kind-field-on-crustjson) than the install attempt (e.g. an
|
|
467
|
-
existing `generated` skill collides with an incoming `bundle`).
|
|
468
|
-
3. **Malformed manifest** — `crust.json` is present but cannot be
|
|
469
|
-
interpreted (invalid JSON, top-level non-object, missing `version`, or
|
|
470
|
-
an unrecognized `kind` value such as a hand-edit typo).
|
|
471
|
-
|
|
472
|
-
All three throw `SkillConflictError`. Pass `force: true` to overwrite, or
|
|
473
|
-
uninstall the existing skill first.
|
|
474
|
-
|
|
475
|
-
`SkillConflictError.details` carries optional discriminators:
|
|
476
|
-
|
|
477
|
-
- `details.kindMismatch?: { existing, attempted }` — set on kind mismatch.
|
|
478
|
-
- `details.manifestMalformed?: { reason, rawKind? }` — set on malformed
|
|
479
|
-
`crust.json`. `reason` is one of `"parse-error"`, `"not-an-object"`,
|
|
480
|
-
`"missing-version"`, or `"unknown-kind"`. `rawKind` is populated only
|
|
481
|
-
when `reason === "unknown-kind"`.
|
|
482
|
-
- Neither field set — the original "directory exists with no `crust.json`" case.
|
|
483
|
-
|
|
484
|
-
```ts
|
|
485
|
-
import { generateSkill, SkillConflictError } from "@crustjs/skills";
|
|
486
|
-
|
|
487
|
-
try {
|
|
488
|
-
await generateSkill({ command, meta, agents });
|
|
489
|
-
} catch (err) {
|
|
490
|
-
if (!(err instanceof SkillConflictError)) throw err;
|
|
491
|
-
|
|
492
|
-
if (err.details.kindMismatch) {
|
|
493
|
-
const { existing, attempted } = err.details.kindMismatch;
|
|
494
|
-
console.error(
|
|
495
|
-
`Cannot install ${attempted} skill at ${err.details.outputDir} — ` +
|
|
496
|
-
`existing skill was installed as ${existing}.`,
|
|
497
|
-
);
|
|
498
|
-
} else if (err.details.manifestMalformed) {
|
|
499
|
-
console.error(
|
|
500
|
-
`crust.json at ${err.details.outputDir} is malformed: ` +
|
|
501
|
-
`${err.details.manifestMalformed.reason}.`,
|
|
502
|
-
);
|
|
503
|
-
} else {
|
|
504
|
-
console.error(
|
|
505
|
-
`${err.details.outputDir} exists but was not created by Crust.`,
|
|
506
|
-
);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
### Uninstall Cleanup
|
|
512
|
-
|
|
513
|
-
When `uninstallSkill()` removes agent install paths, it also checks whether any other agent paths still reference the skill. If no agent installs remain, the canonical store entry (`.crust/skills/<skill>` or `~/.crust/skills/<skill>`) is automatically removed.
|
|
514
|
-
|
|
515
|
-
## Installing Generated Skills
|
|
516
|
-
|
|
517
|
-
After generating a skill bundle, consumers can install it by copying the skill directory.
|
|
518
|
-
|
|
519
|
-
### Universal agents (OpenCode, Codex, Cursor, and others)
|
|
520
|
-
|
|
521
|
-
```sh
|
|
522
|
-
cp -r skills/my-cli/ .agents/skills/my-cli/
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
Global install for universal agents:
|
|
526
|
-
|
|
527
|
-
```sh
|
|
528
|
-
cp -r skills/my-cli/ ~/.agents/skills/my-cli/
|
|
529
|
-
```
|
|
530
|
-
|
|
531
|
-
### Claude Code
|
|
532
|
-
|
|
533
|
-
```sh
|
|
534
|
-
cp -r skills/my-cli/ .claude/skills/my-cli/
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
The agent will discover the skill from `SKILL.md` and load command documentation on demand from the `commands/` directory.
|
|
538
|
-
|
|
539
|
-
## Installing Hand-Authored Bundles
|
|
540
|
-
|
|
541
|
-
`generateSkill()` produces a skill bundle from a Crust command tree.
|
|
542
|
-
`installSkillBundle()` is the dual entrypoint for **hand-authored** bundles —
|
|
543
|
-
use it when you have a directory containing `SKILL.md` and any supporting
|
|
544
|
-
files that you want to install through the same canonical-store + agent
|
|
545
|
-
fan-out pipeline.
|
|
546
|
-
|
|
547
|
-
Use `installSkillBundle()` when:
|
|
548
|
-
|
|
549
|
-
- You ship a published CLI package that bundles authored skill directories
|
|
550
|
-
alongside generated ones.
|
|
551
|
-
- The skill's `SKILL.md` is hand-curated (or produced by your own renderer)
|
|
552
|
-
and Crust just needs to handle install plumbing — canonical storage,
|
|
553
|
-
symlink/copy fan-out, version tracking, and conflict detection.
|
|
554
|
-
|
|
555
|
-
```ts
|
|
556
|
-
import { installSkillBundle } from "@crustjs/skills";
|
|
557
|
-
import pkg from "./package.json" with { type: "json" };
|
|
558
|
-
|
|
559
|
-
await installSkillBundle({
|
|
560
|
-
// Resolved relative to the nearest package.json walking up from
|
|
561
|
-
// process.argv[1]. You can also pass an absolute string or a file: URL.
|
|
562
|
-
sourceDir: "skills/funnel-builder",
|
|
563
|
-
agents: ["claude-code", "opencode"],
|
|
564
|
-
version: pkg.version,
|
|
565
|
-
});
|
|
566
|
-
```
|
|
567
|
-
|
|
568
|
-
### Where `name`, `description`, and `version` come from
|
|
569
|
-
|
|
570
|
-
The bundle's `SKILL.md` frontmatter is the source of truth for `name` and
|
|
571
|
-
`description` — Crust reads them but never rewrites the file. Both fields
|
|
572
|
-
are required:
|
|
573
|
-
|
|
574
|
-
```yaml
|
|
575
|
-
---
|
|
576
|
-
name: funnel-builder
|
|
577
|
-
description: Build a sales funnel
|
|
578
|
-
---
|
|
579
|
-
```
|
|
580
|
-
|
|
581
|
-
`version` is supplied by the caller and recorded in `crust.json`. Wiring
|
|
582
|
-
it to the consuming package's `package.json` `version` (as in the example
|
|
583
|
-
above) is the typical pattern; pass any string explicitly when one package
|
|
584
|
-
publishes multiple bundles with independent versions:
|
|
585
|
-
|
|
586
|
-
```ts
|
|
587
|
-
await installSkillBundle({
|
|
588
|
-
sourceDir: "skills/funnel-builder",
|
|
589
|
-
agents: ["claude-code"],
|
|
590
|
-
version: "2.0.0",
|
|
591
|
-
});
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
> **Note:** `metadata.version` declared inside the bundle's SKILL.md
|
|
595
|
-
> frontmatter is **not** read — the `version` option is the sole source of
|
|
596
|
-
> truth for `crust.json` and update detection. If you keep a
|
|
597
|
-
> `metadata.version` in your SKILL.md for Agent Skills spec compliance,
|
|
598
|
-
> keep it in sync with the value you pass here.
|
|
599
|
-
|
|
600
|
-
### Options
|
|
601
|
-
|
|
602
|
-
| Option | Type | Default | Description |
|
|
603
|
-
| ------------- | ----------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
604
|
-
| `sourceDir` | `string \| URL` | — required | Bundle directory. Absolute path, `file:` URL, or relative path resolved from the nearest `package.json`. |
|
|
605
|
-
| `agents` | `AgentTarget[]` | — required | Agents to install for. `[]` validates the bundle without installing (no auto-detection — unlike `generateSkill()`). |
|
|
606
|
-
| `version` | `string` | — required | Recorded in `crust.json` and compared on subsequent installs. |
|
|
607
|
-
| `scope` | `"global" \| "project"` | `"global"` | Install scope. When `process.cwd()` is the home directory, `"project"` normalizes to `"global"`. |
|
|
608
|
-
| `installMode` | `"auto" \| "symlink" \| "copy"` | `"auto"` | Same semantics as `generateSkill()`. `"auto"` symlinks from the canonical store, falling back to copy. |
|
|
609
|
-
| `clean` | `boolean` | `true` | Remove the existing skill directory before writing. |
|
|
610
|
-
| `force` | `boolean` | `false` | Overwrite a conflicting directory (no `crust.json`, kind mismatch, or malformed manifest) instead of throwing. |
|
|
611
|
-
|
|
612
|
-
### What gets copied
|
|
613
|
-
|
|
614
|
-
The bundle's `SKILL.md` plus every supporting file is copied into the
|
|
615
|
-
canonical Crust store — markdown, configs, scripts, images, fonts, and other
|
|
616
|
-
assets. Bundle files are copied as raw bytes; `SKILL.md` is also parsed as
|
|
617
|
-
UTF-8 to read its required frontmatter.
|
|
618
|
-
|
|
619
|
-
Bundle content changes do not propagate without a `version` bump:
|
|
620
|
-
identical-version reinstalls report `up-to-date` and leave the canonical
|
|
621
|
-
store untouched. Pass a fresh `version` (typically wired to the consuming
|
|
622
|
-
package's `package.json` `version`) whenever the bundle contents change.
|
|
623
|
-
|
|
624
|
-
Bundle contents are copied as authored — no implicit name-based filtering.
|
|
625
|
-
Dotfiles, `node_modules/`, `.DS_Store`, and editor cruft are all copied if
|
|
626
|
-
present. Keep `sourceDir` clean. The only reserved filename is
|
|
627
|
-
`crust.json` at the bundle root (Crust generates this and rejects bundles
|
|
628
|
-
that ship one).
|
|
629
|
-
|
|
630
|
-
### Publishing a bundle to npm
|
|
631
|
-
|
|
632
|
-
Two gotchas trip up bundle authors who publish to npm:
|
|
633
|
-
|
|
634
|
-
1. **Include the bundle directory in the published tarball.** Add the path
|
|
635
|
-
to your `package.json` `files` array and verify with `npm pack --dry-run`
|
|
636
|
-
before publishing. Local installs work even when the directory would be
|
|
637
|
-
excluded from the tarball, but consumers will hit a missing-`SKILL.md`
|
|
638
|
-
error.
|
|
639
|
-
|
|
640
|
-
```json
|
|
641
|
-
{
|
|
642
|
-
"name": "acme-skills",
|
|
643
|
-
"version": "1.0.0",
|
|
644
|
-
"files": ["dist", "skills"]
|
|
645
|
-
}
|
|
646
|
-
```
|
|
647
|
-
|
|
648
|
-
2. **Consumers point at the published path with `import.meta.resolve`.**
|
|
649
|
-
Relative `sourceDir` resolution walks up from the consumer's
|
|
650
|
-
`process.argv[1]`, so it lands in the consumer's package — not yours.
|
|
651
|
-
Consumers should use a `file:` URL via `import.meta.resolve`:
|
|
652
|
-
|
|
653
|
-
```ts
|
|
654
|
-
import skillsPkg from "acme-skills/package.json" with { type: "json" };
|
|
655
|
-
|
|
656
|
-
await installSkillBundle({
|
|
657
|
-
sourceDir: new URL(import.meta.resolve("acme-skills/skills/funnel-builder")),
|
|
658
|
-
agents: ["claude-code"],
|
|
659
|
-
version: skillsPkg.version,
|
|
660
|
-
});
|
|
661
|
-
```
|
|
662
|
-
|
|
663
|
-
For this to work, the bundle directory must be reachable from your
|
|
664
|
-
package's `exports` (or accessible as a subpath of the package root).
|
|
665
|
-
Bundle authors who want explicit subpath access can declare it in
|
|
666
|
-
`package.json` `exports`.
|
|
667
|
-
|
|
668
|
-
### `kind` field on `crust.json`
|
|
669
|
-
|
|
670
|
-
Every installed bundle records its origin in `crust.json` as a `kind`
|
|
671
|
-
field: `"generated"` for `generateSkill()` output, `"bundle"` for
|
|
672
|
-
`installSkillBundle()`. This prevents accidental cross-overwrites:
|
|
673
|
-
|
|
674
|
-
- Trying to install a bundle on top of a generated skill (or vice versa) at
|
|
675
|
-
the same name throws a `SkillConflictError` whose `details.kindMismatch`
|
|
676
|
-
carries `{ existing, attempted }`.
|
|
677
|
-
- To proceed anyway, uninstall the existing skill first or pass
|
|
678
|
-
`force: true`.
|
|
679
|
-
|
|
680
|
-
Legacy `crust.json` files written before this field existed are read as
|
|
681
|
-
`kind: "generated"` for backward compatibility — generated installs continue
|
|
682
|
-
to update cleanly with no migration step.
|
|
683
|
-
|
|
684
11
|
## Documentation
|
|
685
12
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
## License
|
|
689
|
-
|
|
690
|
-
MIT
|
|
13
|
+
Full docs: [crustjs.com/docs/modules/skills](https://crustjs.com/docs/modules/skills)
|