Skip to content

createAgentSkills

The ergonomic entry point. It resolves a config once, runs discovery once, and returns an AgentSkills object with listSkills(), loadSkill(), resourcePath(), and refresh(). This is the high-level API; the lower-level surface exposes the same behavior unwrapped.

Signature

ts
function createAgentSkills(options: AgentSkillsOptions): AgentSkills;

options is the object accepted by resolveConfigroots is required, every other field is optional. Discovery runs once, synchronously, inside createAgentSkills; the configured roots are not re-scanned until you call refresh(). An empty roots list, or an explicit workspace that is not an existing directory, throws a StartupError synchronously (see Options).

ts
import { createAgentSkills, clarvisSkillRoots } from "@clarvis/agent-skills";

const skills = createAgentSkills({ roots: clarvisSkillRoots({ workspace: process.cwd() }) });

AgentSkills

ts
interface AgentSkills {
  readonly config: SkillConfig;
  listSkills(): SkillInfo[];
  loadSkill(name: string): SkillContent | undefined;
  resourcePath(name: string, rel: string): string;
  refresh(): void;
}
MemberTypeDescription
configSkillConfigThe fully-resolved config produced at construction (see Configuration).
listSkills() => SkillInfo[]The discovered catalog across the configured roots — tier 1, cheap, computed at discovery.
loadSkill(name) => SkillContent | undefinedThe body and resources for one skill by name, or undefined when unknown — tiers 2 and 3.
resourcePath(name, rel) => stringA confined absolute path to a bundled resource; throws on an unknown skill, an escape, or a missing/non-file target.
refresh() => voidRe-scan the configured roots, reflecting added, body-modified, and removed skills.

The object is a thin wrapper: listSkills(), loadSkill(), and resourcePath() delegate to the registry built by discoverSkills; refresh() rebuilds that registry from the same config.

Identity is the frontmatter name, not the directory name. When the two differ the frontmatter name wins and a non-fatal warning fires; name is also the key used to override across roots and to look skills up here. See Discovery & precedence.

Options

ts
interface AgentSkillsOptions {
  roots: SkillRootInput[];
  workspace?: string;
  cwd?: string;
  home?: string;
  strict?: boolean;
  followSymlinks?: boolean;
}
OptionTypeDefaultMeaning
rootsSkillRootInput[]— (required)The roots to scan, in ascending precedence (last wins). Each is a { path, scope?, source? }; an empty list throws StartupError. See SkillRootInput.
workspacestringcwdThe base for relative root paths — set via this option, else the cwd. Never read from an environment variable.
cwdstringprocess.cwd()Base for resolving a relative workspace, and the fallback base for relative roots when workspace is omitted.
homestringos.homedir()Base for ~/~/… expansion in a root path.
strictbooleanfalseWhen true, a malformed skill and a within-root duplicate name throw a SkillError instead of being skipped with a warning.
followSymlinksbooleantrueWhen true, follow symlinked skill directories, symlinked SKILL.md manifests, and symlinked resources; false ignores all three.

An empty roots list throws StartupError, as does an explicit workspace that is not an existing directory — the workspace is never inferred from the environment, so it never silently redirects. A cross-root override (a higher-precedence root shadowing a lower one of the same name) is normal precedence and never throws, even under strict; only a duplicate name within one root is a duplicate_skill.

SkillRootInput

Each entry in roots is a SkillRootInput:

ts
interface SkillRootInput {
  path: string;        // "~"/"~/x" expands against home; a relative path resolves against the
                       // workspace (else cwd); an absolute path is used as-is
  scope?: SkillScope;  // "user" | "workspace" — provenance label only; default "workspace"
  source?: string;     // free-form provenance label; default ""
}

path follows the exported resolveAgainst rule. scope and source are pure provenance labels — they are surfaced on each SkillInfo (scope, source) but do not affect resolution or precedence; precedence is only the array order, last wins. For the clarvis four-root layout, use the clarvisSkillRoots preset rather than writing the roots by hand:

ts
import { createAgentSkills, clarvisSkillRoots } from "@clarvis/agent-skills";

const skills = createAgentSkills({ roots: clarvisSkillRoots({ workspace: process.cwd() }) });

SkillInfo

The catalog record listSkills() returns for each skill — tier 1 of progressive disclosure.

ts
interface SkillInfo {
  name: string;
  description: string;
  metadata: SkillFrontmatter;
  allowedTools?: string[];
  userInvocable: boolean;
  scope: "user" | "workspace";
  source: string;
  root: string;
  dir: string;
  path: string;
}
FieldTypeDescription
namestringThe frontmatter name — the identity and merge key.
descriptionstringThe frontmatter description — a non-empty trimmed string.
metadataSkillFrontmatterThe raw validated frontmatter, with unknown keys preserved. Not normalized — normalization lands on allowedTools.
allowedToolsstring[] | absentThe declared tool allow-list, from allowed-tools (falling back to tools). Absent when neither is declared.
userInvocablebooleanThe frontmatter user-invocable, default true.
scope"user" | "workspace"The provenance scope label of the root this skill came from.
sourcestringThe free-form source label of the root this skill came from (the clarvis preset uses "agents" / "clarvis").
rootstringThe absolute path of the winning root.
dirstringThe absolute path of the skill's own directory.
pathstringThe absolute path of the skill's SKILL.md manifest.

allowedTools is the one normalized field: both allowed-tools and tools accept a YAML flow array ([Read, Bash]) or a comma-separated string ("Read, Bash"), trimmed and blank-dropping identically; allowed-tools wins when both are present. See Configuration for the full frontmatter schema.

SkillContent

What loadSkill(name) returns — SkillInfo plus the body (tier 2) and enumerated resources (tier 3), or undefined when no skill has that name.

ts
interface SkillContent extends SkillInfo {
  body: string;
  resources: SkillResource[];
}
FieldTypeDescription
bodystringThe trimmed markdown body of the SKILL.md, below the frontmatter.
resourcesSkillResource[]The bundled resource files under the skill directory (the SKILL.md is excluded).

SkillResource

ts
interface SkillResource {
  kind: "scripts" | "references" | "assets" | "examples" | "other";
  rel: string;
  path: string;
}
FieldTypeDescription
kind"scripts" | "references" | "assets" | "examples" | "other"Derived from the top path segment; anything else is "other".
relstringPOSIX-relative path from the skill directory.
pathstringThe absolute path on disk.

Enumeration walks the whole skill directory, guarded against symlink loops by a visited-real-path set. A dangling symlink, or a resource symlink whose real target escapes the skill directory, is skipped with a warning — so enumeration agrees with resourcePath's confinement. To read one resource by hand, pass its rel to resourcePath; the returned path is guaranteed inside the skill directory. See Resource confinement.

Example

ts
import { createAgentSkills, clarvisSkillRoots } from "@clarvis/agent-skills";

const skills = createAgentSkills({ roots: clarvisSkillRoots({ workspace: process.cwd() }) });

for (const skill of skills.listSkills()) {
  console.log(skill.name, "→", skill.description, `(${skill.source}/${skill.scope})`);
}

const pdf = skills.loadSkill("pdf");
if (pdf) {
  console.log(pdf.body);
  for (const res of pdf.resources) {
    console.log(res.kind, res.rel);
  }
  const script = skills.resourcePath("pdf", "scripts/extract.py"); // confined absolute path
  console.log(script);
}

skills.refresh(); // re-scan the configured roots after skills change on disk

See also

Released under the MIT License.