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.
When the current working directory is the home directory, project scope is normalized to global. In that case, installs, updates, and status checks all use the global skill locations and update output reports (global).
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 |
If process.cwd() is the home directory, project paths collapse to the same global paths shown above.
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,
});