Skills
Agent skill generation for AI coding assistants.
The @crustjs/skills package generates and installs agent skills from your CLI's command definitions. Skills provide structured documentation that AI coding assistants use to understand your CLI.
Install
bun add @crustjs/skillsQuick Start
Add skillPlugin() to your CLI. It reads name and description from your root command's meta — you only need to supply version:
import { Crust } from "@crustjs/core";
import { skillPlugin } from "@crustjs/skills";
const app = new Crust("my-cli").meta({ description: "My CLI tool" }).use(
skillPlugin({
version: "1.0.0",
instructions: `
Read command docs before suggesting exact flags.
## Answer Style
- Prefer exact syntax copied from the relevant command file.
`,
// command defaults to "skill" and registers "my-cli skill"
}),
);
await app.execute();This gives you:
- Auto-detection: additional (non-universal) agents are detected by probing their CLI commands (for example
<agent> --version). - Auto-update: when a user runs any command, the plugin silently updates installed skills if the version has changed.
- Interactive command:
my-cli skillpresents a multiselect prompt for toggling installations. It includes a single Universal option plus additional agents. Askill updatesubcommand is also available for update-only flows.
Plugin Options
interface SkillPluginOptions {
/** Skill version — compared against the installed crust.json */
version: string;
/** Default installation scope for interactive commands. When omitted, prompts for scope. */
defaultScope?: "global" | "project";
/** Auto-update skills when version is outdated. @default true */
autoUpdate?: boolean;
/** Register interactive skill subcommand with a custom name. @default "skill" */
command?: string;
/** Additional top-level instructions rendered into SKILL.md. */
instructions?: string | string[];
}Scope resolution for the interactive command and skill update:
- If
--scopeflag is passed, that scope is used. - If
defaultScopeis set, that scope is used (no prompt). - Otherwise, an interactive prompt asks the user to choose
projectorglobal.
Agent Auto-Detection
detectInstalledAgents() probes CLI binaries for additional agents (for example claude --version, windsurf --version).
- Universal agents are not gated by detection and are always available through the Universal option.
- Additional agents are detected by command availability.
- In the interactive command, an additional agent is also shown if the skill is already installed at that agent path, even when command detection fails.
Detection is used for additional-agent discovery only.
Supported Agents
@crustjs/skills supports the same broad ecosystem as Vercel Skills.
Universal group (single prompt option, shared path):
amp,cline,codex,cursor,gemini-cli,github-copilot,kimi-cli,opencode,replit
Additional agents:
adal,antigravity,augment,claude-code,codebuddy,command-code,continue,cortex,crush,droid,goose,iflow-cli,junie,kilo,kiro-cli,kode,mcpjam,mistral-vibe,mux,neovate,openclaw,openhands,pi,pochi,qoder,qwen-code,roo,trae,trae-cn,windsurf,zencoder
Path model:
| Group | Global Path | Project Path |
|---|---|---|
| Universal | ~/.agents/skills/<name>/ | .agents/skills/<name>/ |
| Additional | Agent-specific convention per CLI | Agent-specific path |
Interactive Command
By default, the plugin registers a skill subcommand that presents a single multiselect prompt. A skill update subcommand is also registered for update-only flows (updates installed skills without prompting for agent selection).
The prompt behavior is:
- A single Universal option is shown (for all universal agents).
- Additional agents are shown when detected, or when the skill is already installed for that agent.
- Choice subtext shows the target installation directory.
- Already-installed targets are pre-selected.
The user toggles options on or off and the system reconciles the desired state:
- Newly selected agents are installed
- Deselected agents are uninstalled
- Already-correct agents are skipped
my-cli skillConflict Detection
Each generated skill directory contains a crust.json file that serves as an ownership marker. When generateSkill() encounters a target directory that already exists but has no crust.json, it assumes the skill was not created by Crust (e.g. manually created or installed by another tool) and throws a SkillConflictError to prevent silent overwrites.
Behavior by context
| Context | Conflict behavior |
|---|---|
| Auto-update middleware | Logs a warning and skips — the CLI continues running normally |
Interactive skill command | Prompts the user to confirm overwriting the conflicting skill |
skill update command | Logs a warning and skips the conflicting agent |
generateSkill() API | Throws SkillConflictError. Pass force: true to overwrite |
Handling conflicts programmatically
import { generateSkill, SkillConflictError } from "@crustjs/skills";
try {
await generateSkill({ command, meta, agents });
} catch (err) {
if (err instanceof SkillConflictError) {
console.error(`Conflict at: ${err.details.outputDir}`);
// Retry with force to overwrite
await generateSkill({ command, meta, agents, force: true });
}
}Skill Metadata
The SkillMeta object controls the generated SKILL.md frontmatter. Beyond the required name, description, and version fields, several optional fields are supported:
const meta: SkillMeta = {
name: "my-cli",
description: "CLI tool for managing widgets",
version: "1.0.0",
allowedTools: "Bash(my-cli *) Read Grep",
license: "MIT",
compatibility: "Requires my-cli on PATH",
disableModelInvocation: false,
};| Field | Frontmatter Key | Description |
|---|---|---|
allowedTools | allowed-tools | Space-delimited list of pre-approved tools (e.g. Bash(my-cli *) Read Grep) |
license | license | License name or file reference |
compatibility | compatibility | Environment requirements or compatibility notes |
disableModelInvocation | disable-model-invocation | When true, prevents agents from auto-loading the skill |
Command-Level Prompt Guidance
Use annotate() to attach agent-facing instructions to a specific
command without extending the public @crustjs/core builder API.
instructions: stringrenders as a raw markdown block inSKILL.md.instructions: string[]renders as bullet list items inSKILL.md.- Empty or whitespace-only instruction input is ignored.
annotate()renders command guidance as bullets in the command doc.
import { Crust } from "@crustjs/core";
import { annotate } from "@crustjs/skills";
const deploy = annotate(
new Crust("deploy")
.meta({ description: "Deploy the application" })
.run(() => {
// ...
}),
[
"Prefer preview modes before running destructive operations.",
"Ask for confirmation before production changes.",
],
);Name Validation
Skill names must conform to the Agent Skills spec: 1–64 lowercase alphanumeric characters and hyphens, no leading/trailing/consecutive hyphens. generateSkill() validates the resolved name (with use- prefix) and throws a descriptive error if invalid.
Use isValidSkillName() to check names before calling generateSkill():
import { isValidSkillName } from "@crustjs/skills";
isValidSkillName("my-cli"); // true
isValidSkillName("My_CLI"); // falseLow-Level Primitives
For custom workflows, the package also exports lower-level functions:
import {
detectInstalledAgents,
generateSkill,
uninstallSkill,
skillStatus,
isValidSkillName,
} from "@crustjs/skills";
// Validate a skill name
isValidSkillName("my-cli"); // true
// Detect which agents are installed
const agents = await detectInstalledAgents();
// Additional detected agents only (universal is handled separately)
const universalAgents = [
"amp",
"cline",
"codex",
"cursor",
"gemini-cli",
"github-copilot",
"kimi-cli",
"opencode",
"replit",
] as const;
// Install skills
const result = await generateSkill({
command: rootCommand,
meta: { name: "my-cli", description: "My CLI", version: "1.0.0" },
agents: [...universalAgents, ...agents],
scope: "global", // or "project"
});
// Check status
const status = await skillStatus({
name: "my-cli",
agents,
});
// Uninstall
const removed = await uninstallSkill({
name: "my-cli",
agents,
});