skills
Give an LLM access to a directory of skills, and support Claude-Code-style slash commands. skillsDir builds a tool that lets the model read skill files on demand. writeSkill saves a new skill after approval. designSkill shows a draft to the user, revises it on their feedback, and saves it through writeSkill. commandsDir and expandSlash load prompt-template commands and expand a user's /command into its body.
import { skillsDir, commandsDir, expandSlash } from "std::skills"
static const commands = commandsDir("${cwd()}/.claude/commands") with approve
node main(msg: string) {
const prompt = expandSlash(msg, commands)
let reply: string = llm(prompt, { tools: [skillsDir("${cwd()}/skills")] })
}Types
SkillEntry
export type SkillEntry = {
name: string;
description: string;
location: string
}(source)
SkillGroup
One subdirectory's scan result: the directory that was scanned (the dir to build a skills tool over) and the entries found in it.
/** One subdirectory's scan result: the directory that was scanned (the
`dir` to build a skills tool over) and the entries found in it. */
export type SkillGroup = {
dir: string;
entries: SkillEntry[]
}(source)
Effects
std::skills::skillsDir
Build a tool that lets an LLM read skill files in dir. Supports two layouts:
- "standard" (default): each subdirectory of
diris one skill with aSKILL.mdentrypoint. Frontmattername/descriptionare read;namedefaults to the subdirectory name. - "flat": each
.md/.markdownfile directly underdiris one skill. Frontmattername(ortitle) anddescriptionare read.
The returned tool is read partially applied with dir: dir. Its description lists every available skill so the LLM knows which location to pass back as filename.
/**
Build a tool that lets an LLM read skill files in `dir`. Supports two
layouts:
- "standard" (default): each subdirectory of `dir` is one skill with
a `SKILL.md` entrypoint. Frontmatter `name` / `description` are
read; `name` defaults to the subdirectory name.
- "flat": each `.md` / `.markdown` file directly under `dir` is one
skill. Frontmatter `name` (or `title`) and `description` are read.
The returned tool is `read` partially applied with `dir: dir`. Its
description lists every available skill so the LLM knows which
`location` to pass back as `filename`.
*/
@alwaysUnder(dir)
effect std::skills::skillsDir {
dir: string;
layout: "flat" | "standard"
}(source)
std::skills::commandsDir
@alwaysUnder(dir)
effect std::skills::commandsDir {
dir: string
}(source)
std::skills::save
@alwaysUnder(dir)
effect std::skills::save {
dir: string;
name: string;
content: string
}(source)
std::skills::review
effect std::skills::review {
dir: string;
name: string;
description: string;
body: string
}(source)
Constants
MAX_TOOL_NAME_LEN
export static const MAX_TOOL_NAME_LEN = 64(source)
Functions
skillsToolFromEntries
skillsToolFromEntries(
dir: string,
entries: SkillEntry[],
name: string = "",
brief: boolean = false,
)Build the skills tool for dir from already-scanned entries.
@param dir - The directory the entries were scanned from; the tool reads each entry's location relative to it. For scanSkillsSubdirs output, pass the group's own dir, not the root. @param entries - The skills to list in the tool description @param name - Optional explicit tool name. Sanitized to alphanumerics/underscores and capped at 64 characters; defaults to a name derived from dir. @param brief - List each file as one path - description line, with no XML and no directory path. For a directory with so many files that the full listing is too long to send on every model call.
The pure build half of a skills tool: no reads, no interrupts. A caller that already holds a directory's entries (a cached catalog, say) can rebuild the tool without rescanning.
Parameters:
| Name | Type | Default |
|---|---|---|
| dir | string | |
| entries | SkillEntry[] | |
| name | string | "" |
| brief | boolean | false |
(source)
skillsDir
skillsDir(
dir: string,
layout: "flat" | "standard" = "standard",
name: string = "",
)Build a skills tool for an LLM over a directory of skills.
@param dir - Directory containing the skills. @param layout - "standard" (default) for subdirectory-per-skill with SKILL.md, "flat" for a directory of loose Markdown files. @param name - Optional explicit tool name. Sanitized to alphanumerics/underscores and capped at 64 characters. Defaults to a name derived from dir; set this to override it — e.g. for a stable, readable name, or to disambiguate two tools whose derived names would collide.
Parameters:
| Name | Type | Default |
|---|---|---|
| dir | string | |
| layout | "flat" | "standard" | "standard" |
| name | string | "" |
Throws: std::skills::skillsDir
(source)
scanSkillsSubdirs
scanSkillsSubdirs(
root: string,
subdirs: string[],
): Result<Record<string, SkillGroup>> raises <std::skills::skillsDir>Scan named subdirectories of a root. Each subdirectory holds one agent's flat-layout skills; the result maps every requested name to its directory and entries (empty when the subdirectory is empty or missing).
@param root - The directory holding the subdirectories @param subdirs - The subdirectory names to scan
For an application with several subagents, each owning one skills subdirectory under a common root: read every subdirectory's skill files in one pass and group the entries by subdirectory name. Each group carries the scanned directory, which is what skillsToolFromEntries needs as its dir.
Parameters:
| Name | Type | Default |
|---|---|---|
| root | string | |
| subdirs | string[] |
Returns: Result<Record<string, SkillGroup>>
Throws: std::skills::skillsDir
(source)
writeSkill
writeSkill(
dir: string,
name: string,
description: string,
body: string,
): Result<SkillEntry> raises <std::skills::save, std::mkdir, std::write>Save one skill file into a skills directory, after approval. Composes the flat-layout markdown (frontmatter with name and description, then the body); a rejection writes nothing and fails the call. To draft a skill with the user and revise it on their feedback, use designSkill.
@param dir - The skills directory to write into @param name - The skill's name; also its filename. Lowercase letters, digits, and hyphens. @param description - One line telling the reading agent when to use the skill @param body - The skill's markdown body
The plain primitive for saving a skill that is already final: show the complete file in a std::skills::save interrupt, then write it. There is deliberately no feedback shape on that interrupt — any approval saves, and data carried on the approval is not consulted. The draft-revise-accept loop is designSkill, which ends by calling this.
Parameters:
| Name | Type | Default |
|---|---|---|
| dir | string | |
| name | string | |
| description | string | |
| body | string |
Returns: Result<SkillEntry>
Throws: std::skills::save, std::mkdir, std::write
(source)
designSkill
designSkill(
dir: string,
name: string,
description: string,
body: string,
maxRounds: number = 3,
model: string = "",
provider: string = "",
): Result<SkillEntry> raises <std::skills::review, std::skills::save, std::mkdir, std::write>Show a draft skill to the user, revise it on their feedback, and save it once they accept.
@param dir - The skills directory to write into @param name - The skill's name, also its filename. Lowercase letters, digits, and hyphens. @param description - One line telling the reading agent when to use the skill @param body - The skill's markdown body, as a first draft @param maxRounds - Reviews before giving up @param model - Model override for the redraft, or "" @param provider - Provider for the model override
The draft-show-revise-accept loop over writeSkill. The caller's draft is shown first, in a std::skills::review interrupt. The answer is text: accept (or a bare approval) accepts, and anything else is feedback, which goes to a model call that rewrites the description and body. The result is shown again, up to maxRounds reviews. Accept calls writeSkill, whose own std::skills::save interrupt still gates the write. This is the function to hand to a model as a tool.
Parameters:
| Name | Type | Default |
|---|---|---|
| dir | string | |
| name | string | |
| description | string | |
| body | string | |
| maxRounds | number | 3 |
| model | string | "" |
| provider | string | "" |
Returns: Result<SkillEntry>
Throws: std::skills::review, std::skills::save, std::mkdir, std::write
(source)
docsEntries
docsEntries(section: DocsSection): SkillEntry[]Scan one section of the packaged Agency documentation. Pass the result to docsToolFromEntries to build the full and the brief tool from one scan.
@param section - Which documentation set to scan
Parameters:
| Name | Type | Default |
|---|---|---|
| section | DocsSection |
Returns: SkillEntry[]
(source)
docsToolFromEntries
docsToolFromEntries(
section: DocsSection,
entries: SkillEntry[],
brief: boolean = false,
)Build a docs tool for an LLM from entries docsEntries returned.
@param section - The documentation set the entries were scanned from @param entries - The pages to list in the tool description @param brief - List each page as one path - description line
Parameters:
| Name | Type | Default |
|---|---|---|
| section | DocsSection | |
| entries | SkillEntry[] | |
| brief | boolean | false |
(source)
docsSkill
docsSkill(section: DocsSection, brief: boolean = false)Build a docs tool for an LLM over the packaged Agency documentation.
@param section - Which documentation set to serve @param brief - List each page as one path - description line
Parameters:
| Name | Type | Default |
|---|---|---|
| section | DocsSection | |
| brief | boolean | false |
(source)
bundledDocsDir
bundledDocsDir(): stringThe directory holding the Agency docs that ship inside the package. A handler can approve reads under it and still reject everything else.
Returns: string
(source)
agentSkill
agentSkill(agent: string)Build a skills tool over the skills shipped for one agent. The returned tool lists every skill in its description and lets the model read any one on demand.
@param agent - Which agent's skills to serve, as a path under the shipped skills directory
Parameters:
| Name | Type | Default |
|---|---|---|
| agent | string |
(source)
agentPrompt
agentPrompt(filename: string): Result<string>Read one of the prompt files shipped for the stdlib agents. A failure means the file did not ship or cannot be read.
@param filename - The file to read, as a path under the shipped prompts directory
Parameters:
| Name | Type | Default |
|---|---|---|
| filename | string |
Returns: Result<string>
(source)
commandsDir
commandsDir(dir: string): any[]Discover .md files under dir and parse each as a slash-command template. Returns [] if dir is missing or empty.
@param dir - Directory containing command markdown files.
Discover Claude-Code-format slash commands under dir. Each .md file becomes one command record { name, description, argHint, body }.
Pair with expandSlash(msg, commands) in your agent's per-turn handler:
static const commands = commandsDir("${cwd()}/.claude/commands") with approve
def _runTurn(msg: string) {
const prompt = expandSlash(msg, commands)
const reply = llm(prompt, { tools })
}commandsDir reads only the description and argument-hint frontmatter fields. It silently ignores all other CC fields (allowed-tools, model, effort, context: fork, disable-model-invocation, user-invocable, hooks, paths, shell, ...). commandsDir is a pure prompt-template loader, not an executor.
Files with no frontmatter still dispatch. description and argHint default to "" (never null/undefined). Missing or empty dir returns [].
Relative dir resolves against the current working directory; pass __dirname for a directory relative to the current Agency file. For project-level commands (e.g. .claude/commands at the project root), pass an absolute path: "${cwd()}/.claude/commands".
Parameters:
| Name | Type | Default |
|---|---|---|
| dir | string |
Returns: any[]
Throws: std::skills::commandsDir
(source)
expandSlash
expandSlash(msg: string, commands: any[]): stringExpand a /command in msg into its command body. Returns the rendered body with $ARGUMENTS substituted, or msg unchanged if no command matches.
@param msg - The raw input line (may have leading/trailing whitespace or newlines). @param commands - Array of command records to match against.
Expand a user-typed slash command against a commandsDir result.
- If
msg(after trimming) matches/<name>with optional whitespace + args, returns the rendered command body with$ARGUMENTSsubstituted. - If the body has no
$ARGUMENTStoken and args were passed, appends\n\nARGUMENTS: <raw>so the LLM still sees the input (matches Claude Code). - Otherwise returns
msgunchanged — unknown/fooinputs fall through to the LLM as plain text, again matching CC.
Args are split off at the first whitespace (space, tab, or newline) after /<name>. expandSlash tolerates leading and trailing whitespace on msg, so piped invocations (echo /foo | agency agent, yielding "/foo\n") dispatch correctly.
Parameters:
| Name | Type | Default |
|---|---|---|
| msg | string | |
| commands | any[] |
Returns: string
(source)