Crust logoCrust

Skills

Package and install agent skills for AI coding assistants.

@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/update.md"]

writeSkills() generates one skill from app and copies any authored extras. It replaces a dedicated output directory named skills and returns paths relative to that directory. An authored skill with the generated skill's name replaces the generated skill.

Each skill has its own directory:

.crust/artifacts/skills/
└── my-cli/
    ├── SKILL.md
    └── commands/
        ├── my-cli.md
        ├── deploy.md
        ├── skills.md
        └── skills/
            └── update.md

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 and my-cli skills update, with skill retained as an alias. Setting command to a custom name replaces the default command without adding the alias. It also contributes an Agent skills root section with except: [skill] (the Extension factory), so it appears in help, man pages, and any other consumer, but not in the skills it generates.

The help section includes this escaped excerpt:

Agent skills:
  my-cli \u2014 Manage deployments
    Source: .crust/root/skills/my-cli

Packaged skills are read at runtime from resolveArtifactDir("skills"); before the first build, help shows a note asking you to run crust build instead of failing. The build hook renders generated and authored skills under skills/; crust build writes them to .crust/artifacts/skills and records those files as artifacts owned by crust:skills.

Generated man pages can embed source paths resolved on the build machine. Run builds from the project root that contains the packaged skills directory to avoid absolute build-machine paths.

my-cli skills
my-cli skills --all --scope project
my-cli skills update

--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.

In that prompt, deselecting an agent with an installed link removes its link. It does not merely skip a new installation.

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 repairs stale or dangling Crust-owned links unless autoUpdate: false; it does not create links that were never installed.

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 links. Only missing paths (ENOENT) count as absent or non-resolving; permission and other filesystem failures reject instead of being reported as missing. This applies to link inspection during installation, status checks, and uninstallation.

Prop

Type

Public workflow types

type Scope = "global" | "project";
type SkillLinkStatus = "linked" | "dangling" | "conflict" | "absent";

The root exports the option, result, status, source, scope, agent, and packaged-skill types used by these functions.

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 Command Snapshot sections for shared snapshot behavior.

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