Crust logoCrust

Skills

Generate and install agent skills for AI coding assistants from Crust commands

@crustjs/skills renders agent skills from a Command Snapshot. Build output is shipped with the CLI, then linked into agent directories when a user installs it.

Install

npm install @crustjs/skills

Packaged skills workflow

import { Crust as WriteCrust, defineCommand as defineWriteCommand } from "@crustjs/core";
import { skill as writeSkill, writeSkills } from "@crustjs/skills";

const writeApp = new WriteCrust("my-cli", {
	description: "Manage deployments",
	version: "1.2.3",
})
	.add(
		defineWriteCommand("deploy", { description: "Deploy the app" }, (command) =>
			command.action(() => {}),
		),
	)
	.extend(writeSkill({}));
const files = await writeSkills({
	app: writeApp,
	outDir: ".crust/artifacts/skills",
	version: "1.2.3",
});
console.log(files);
// ["my-cli/SKILL.md", "my-cli/commands/my-cli.md", "my-cli/commands/deploy.md",
//  "my-cli/commands/skills.md", "my-cli/commands/skills/install.md",
//  "my-cli/commands/skills/repair.md", "my-cli/commands/skills/uninstall.md"]

writeSkills() generates one skill directory from app (a SKILL.md plus one file per command under commands/) and copies any authored extras. It replaces a dedicated output directory named skills and returns paths relative to it. An authored skill with the generated skill's name replaces the generated one.

Command file names are trimmed and lowercased: each command renders to <name>.md, and each nested command group also gets a <name> directory. Before replacing output, generation rejects command names containing path separators or equal to . or .., and generated file or directory names with control characters, Windows-reserved characters, device names (such as con.md or nul.txt.md), or a trailing dot (so a trailing. group fails, while a trailing. command renders to trailing..md). It also rejects file/file and file/directory collisions after lowercasing and canonical Unicode normalization, including composed/decomposed spellings that alias on macOS. Unicode normalization is only used for collision checks; generated paths and command invocations retain their original Unicode spelling.

Prop

Type

Use writeSkillsFromSnapshot() when a prepared Command Snapshot is available. Omit app only when at least one authored extra is provided.

Enable discovery and installation

import { Crust, defineCommand } from "@crustjs/core";
import { skill } from "@crustjs/skills";

const deploy = defineCommand("deploy", { description: "Deploy the app" }, (command) =>
	command.action(() => {}),
);

export const app = new Crust("my-cli", { description: "Manage deployments", version: "1.2.3" })
	.add(deploy)
	.extend(skill({}));

The Extension registers my-cli skills (alias skill) with install, uninstall, and repair subcommands; bare my-cli skills is shorthand for skills install. A custom command name replaces the default command without adding the alias. It also contributes an Agent skills root section, excluded from the generated skills with except: [skill], that lists each packaged skill and its source directory.

During crust build, the Extension's build hook renders generated and authored skills under skills/, recorded as artifacts owned by crust:skills. At runtime the commands read them from resolveArtifactDir("skills"), so run crust build before using them; until then help shows a note instead of failing.

The section's source paths are resolved when help or man output is generated. Build from the project root that contains the packaged skills directory so man pages do not embed absolute build-machine paths.

my-cli skills install
my-cli skills install --all --scope project
my-cli skills            # shorthand for `skills install`
my-cli skills uninstall
my-cli skills repair

--scope accepts project or global. With --all, scope comes from --scope, then defaultScope, then global; without --all, an interactive run prompts when neither setting is supplied. install --all links every packaged skill for the Universal group, every agent detected on PATH, and every agent that already holds one of the packaged skills.

  • Install is additive. It links or repairs the selected skills for the selected agents and never removes links. Interactively, it asks which skills (when more than one is packaged) and which agents, and confirms before overwriting each directory it does not own.
  • Uninstall removes selected links. It lists the skills linked at the chosen scope, with nothing preselected, and removes the selected ones from every agent directory. --all is the only non-interactive form.
  • Repair relinks stale or dangling links without prompting.

Prop

Type

project: .agents/skills/my-cli -> ../../node_modules/my-cli/skills/my-cli
global:  ~/.agents/skills/my-cli -> /usr/lib/node_modules/my-cli/skills/my-cli

Project links are relative and global links are absolute. The pre-run hook of an installed CLI repairs stale or dangling Crust-owned links unless autoUpdate: false; it does not create links that were never installed. Running from source (bun run, node, deno run) skips it, so trying a checkout never moves an installed CLI's links to that checkout's build. Run skills repair there to relink deliberately.

A link is Crust-owned when its textual target ends in skills/<name>. Real directories and foreign links conflict unless force: true is passed to low-level installation.

Supported agents

project: .agents/skills/<name>
global:  ~/.agents/skills/<name>

Universal targets share those directories: amp, cline, codex, cursor, gemini-cli, github-copilot, kimi-cli, opencode, pi, replit, warp, and zed. Additional targets use agent-specific locations and are found through a non-executing PATH lookup.

When the working directory is the home directory, project scope becomes global scope. Result objects include the effective scope.

await installSkill({ sourceDir, agents: ["opencode"], scope: "global" });
const status = await getSkillStatus({ name: "my-cli", sourceDir });
await uninstallSkill({ name: "my-cli" });

getSkillStatus() reports linked, dangling, conflict, or absent per agent directory. uninstallSkill() removes only owned links, including dangling ones. Only missing paths (ENOENT) count as absent; permission and other filesystem failures reject. Each result lists one entry per agent with its effective scope and status.

Prop

Type

Prop

Type

Prop

Type

detectInstalledAgents(), getUniversalAgents(), getAdditionalAgents(), and isUniversalAgent() expose the agent catalog. The package entry point lists every exported type.

Command sections

import { defineCommand as defineSectionCommand } from "@crustjs/core";
import { skill as sectionSkill } from "@crustjs/skills";

export const documentedDeploy = defineSectionCommand(
	"deploy",
	{
		sections: [
			{ title: "Safety", body: "Run preview first." },
			{
				title: "Agent procedure",
				body: "Inspect preview output.",
				only: [sectionSkill],
			},
		],
	},
	(command) => command.action(() => {}),
);

Command metadata sections render as ## Title blocks in each command's file under commands/. Passing the skill factory in only limits a section to the skills renderer.

Sections with the same exact title share one heading and join their bodies. See CommandSectionInput for audience rules.

Name validation

isValidSkillName("deploy-guide"); // true
isValidSkillName("Deploy Guide"); // false

Names contain 1 to 64 lowercase letters, digits, and hyphens. They cannot start or end with a hyphen or contain consecutive hyphens, matching the Agent Skills specification.

Source helpers and errors

import { resolveArtifactDir } from "@crustjs/core";
import { loadPackagedSkills } from "@crustjs/skills";

const root = resolveArtifactDir("skills");
const skills = loadPackagedSkills(root);

loadPackagedSkills() reads an absolute packaged skills root, validates each SKILL.md name and non-empty description, and skips directories without a SKILL.md.

SkillConflictError covers install conflicts, SkillSourceConflictError covers duplicate authored names, and SkillSourceUnavailableError means the root is missing or empty: the CLI has not been built yet.

On this page