Skip to content

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.

ts
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 ​

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

ts
/** 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 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.

ts
/**
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 ​

ts
@alwaysUnder(dir)
effect std::skills::commandsDir {
  dir: string
}

(source)

std::skills::save ​

ts
@alwaysUnder(dir)
effect std::skills::save {
  dir: string;
  name: string;
  content: string
}

(source)

std::skills::review ​

ts
effect std::skills::review {
  dir: string;
  name: string;
  description: string;
  body: string
}

(source)

Constants ​

MAX_TOOL_NAME_LEN ​

ts
export static const MAX_TOOL_NAME_LEN = 64

(source)

Functions ​

skillsToolFromEntries ​

ts
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:

NameTypeDefault
dirstring
entriesSkillEntry[]
namestring""
briefbooleanfalse

(source)

skillsDir ​

ts
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:

NameTypeDefault
dirstring
layout"flat" | "standard""standard"
namestring""

Throws: std::skills::skillsDir

(source)

scanSkillsSubdirs ​

ts
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:

NameTypeDefault
rootstring
subdirsstring[]

Returns: Result<Record<string, SkillGroup>>

Throws: std::skills::skillsDir

(source)

writeSkill ​

ts
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:

NameTypeDefault
dirstring
namestring
descriptionstring
bodystring

Returns: Result<SkillEntry>

Throws: std::skills::save, std::mkdir, std::write

(source)

designSkill ​

ts
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:

NameTypeDefault
dirstring
namestring
descriptionstring
bodystring
maxRoundsnumber3
modelstring""
providerstring""

Returns: Result<SkillEntry>

Throws: std::skills::review, std::skills::save, std::mkdir, std::write

(source)

docsEntries ​

ts
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:

NameTypeDefault
sectionDocsSection

Returns: SkillEntry[]

(source)

docsToolFromEntries ​

ts
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:

NameTypeDefault
sectionDocsSection
entriesSkillEntry[]
briefbooleanfalse

(source)

docsSkill ​

ts
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:

NameTypeDefault
sectionDocsSection
briefbooleanfalse

(source)

bundledDocsDir ​

ts
bundledDocsDir(): string

The 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 ​

ts
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:

NameTypeDefault
agentstring

(source)

agentPrompt ​

ts
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:

NameTypeDefault
filenamestring

Returns: Result<string>

(source)

commandsDir ​

ts
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:

ts
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:

NameTypeDefault
dirstring

Returns: any[]

Throws: std::skills::commandsDir

(source)

expandSlash ​

ts
expandSlash(msg: string, commands: any[]): string

Expand 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 $ARGUMENTS substituted.
  • If the body has no $ARGUMENTS token and args were passed, appends \n\nARGUMENTS: <raw> so the LLM still sees the input (matches Claude Code).
  • Otherwise returns msg unchanged — unknown /foo inputs 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:

NameTypeDefault
msgstring
commandsany[]

Returns: string

(source)