Crust logoCrust

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/skills

Quick 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 skill presents a multiselect prompt for toggling installations. It includes a single Universal option plus additional agents. A skill update subcommand 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:

  1. If --scope flag is passed, that scope is used.
  2. If defaultScope is set, that scope is used (no prompt).
  3. Otherwise, an interactive prompt asks the user to choose project or 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:

GroupGlobal PathProject Path
Universal~/.agents/skills/<name>/.agents/skills/<name>/
AdditionalAgent-specific convention per CLIAgent-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 skill

Conflict 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

ContextConflict behavior
Auto-update middlewareLogs a warning and skips — the CLI continues running normally
Interactive skill commandPrompts the user to confirm overwriting the conflicting skill
skill update commandLogs a warning and skips the conflicting agent
generateSkill() APIThrows 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,
};
FieldFrontmatter KeyDescription
allowedToolsallowed-toolsSpace-delimited list of pre-approved tools (e.g. Bash(my-cli *) Read Grep)
licenselicenseLicense name or file reference
compatibilitycompatibilityEnvironment requirements or compatibility notes
disableModelInvocationdisable-model-invocationWhen 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: string renders as a raw markdown block in SKILL.md.
  • instructions: string[] renders as bullet list items in SKILL.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"); // false

Low-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,
});

On this page