Primary Role
Discover and parse bundle commands from Markdown files with frontmatter metadata.
Markdown-based command discovery with frontmatter parsing
Bundle commands turn plain Markdown files into interactive slash commands. Each .md file contains YAML frontmatter with metadata (name, description, enabled state) and a body that becomes the prompt template. The discovery engine scans configured directories, parses every Markdown file, and produces typed ClaudeBundleCommandSpec objects for registration.
Bundle Commands is the mechanism that lets plugin authors define new slash commands using nothing more than Markdown files. Instead of writing TypeScript registration code, a plugin author creates a .md file with a YAML frontmatter header and a prompt template body. The discovery engine handles scanning, parsing, and registration automatically.
Think of a restaurant menu binder. Each laminated page (Markdown file) has a header strip at the top with the dish name, price, and dietary flags (frontmatter), and the rest of the page is the recipe (prompt template). A new waiter (the command discovery engine) can fan through the binder, read each header strip, and add every dish to the POS system — without needing to understand how to cook anything.
Discover and parse bundle commands from Markdown files with frontmatter metadata.
Plugin root directory, Claude bundle manifest, config for enable state.
Which directories to scan, how to parse frontmatter (name, description, enabled), how to strip frontmatter from prompt template.
ClaudeBundleCommandSpec[] — array of command specs with pluginId, name, description, promptTemplate.
flowchart LR Root["Plugin Root"] --> Manifest["Read Bundle Manifest"] Manifest --> Roots["resolveClaudeCommandRootDirs()"] Roots --> Scan["listMarkdownFilesRecursive()"] Scan --> Parse["parseFrontmatterBlock()"] Parse --> Strip["stripFrontmatter()"] Strip --> Spec["ClaudeBundleCommandSpec"]
The pipeline flows left-to-right: read the bundle manifest to find command root directories, recursively discover .md files, parse YAML frontmatter from each, strip the frontmatter to isolate the prompt template, and assemble typed command specs.
The Claude bundle manifest may declare explicit command root directories. If none are declared, the engine falls back to a conventional commands/ directory relative to the plugin root.
function resolveClaudeCommandRootDirs(
rootDir: string,
manifest: ClaudeBundleManifest
): string[] {
if (manifest.commandRoots?.length) {
return manifest.commandRoots.map(r => resolve(rootDir, r));
}
return [resolve(rootDir, "commands")];
}
listMarkdownFilesRecursive() walks each command root directory, collecting every file ending in .md. Non-markdown files and hidden directories (starting with .) are skipped.
Each Markdown file may begin with a YAML frontmatter block delimited by --- lines. The parser extracts name, description, and enabled fields. The enabled field uses parseFrontmatterBool() which accepts true, "true", "yes", 1 as truthy values.
stripFrontmatter() removes the YAML block (including delimiters) to produce the clean prompt template body that will be sent to the AI model at command invocation time.
Each parsed file becomes a ClaudeBundleCommandSpec with the pluginId, rawName (derived from filename or frontmatter), description, promptTemplate, and sourceFilePath for debugging.
sequenceDiagram
participant Disc as Command Discovery
participant FS as File System
participant FM as Frontmatter Parser
Disc->>FS: readClaudeBundleManifest(rootDir)
FS-->>Disc: raw manifest
Disc->>Disc: resolveClaudeCommandRootDirs()
loop Each command root dir
Disc->>FS: listMarkdownFilesRecursive(dir)
FS-->>Disc: .md file paths
loop Each .md file
Disc->>FS: readFileSync(path)
FS-->>Disc: content
Disc->>FM: parseFrontmatterBlock(content)
FM-->>Disc: name, description, enabled
Disc->>Disc: stripFrontmatter(content)
Disc->>Disc: Build ClaudeBundleCommandSpec
end
end
The sequence diagram shows the two nested loops: an outer loop over command root directories, and an inner loop over each discovered Markdown file. The frontmatter parser is invoked for each file, and the stripped body becomes the prompt template.
Step through the key utility functions that power frontmatter handling:
stripFrontmatter() removes the YAML block between the opening and closing --- delimiters, returning only the prompt template body:
export function stripFrontmatter(content: string): string {
const lines = content.split("\n");
if (lines[0]?.trim() !== "---") {
return content; // No frontmatter — entire content is the template
}
let closingIdx = -1;
for (let i = 1; i < lines.length; i++) {
if (lines[i].trim() === "---") {
closingIdx = i;
break;
}
}
if (closingIdx === -1) {
return content; // Unclosed frontmatter — treat whole file as template
}
return lines.slice(closingIdx + 1).join("\n").trimStart();
}
--- but never has a closing delimiter, the entire content is returned as the template rather than throwing an error. This ensures a broken frontmatter block never silently swallows the prompt.
parseFrontmatterBool() normalizes various truthy/falsy representations into a strict boolean, used for the enabled field:
export function parseFrontmatterBool(
value: unknown,
defaultValue: boolean = true
): boolean {
if (value === undefined || value === null) return defaultValue;
if (typeof value === "boolean") return value;
if (typeof value === "number") return value !== 0;
if (typeof value === "string") {
const lower = value.toLowerCase().trim();
if (["true", "yes", "1", "on"].includes(lower)) return true;
if (["false", "no", "0", "off"].includes(lower)) return false;
}
return defaultValue;
}
enabled field is omitted entirely, commands default to enabled. Plugin authors only need to explicitly set enabled: false to disable a command.
parseFrontmatterBlock() ties it all together — extracts YAML lines, parses key-value pairs, and returns a typed metadata object:
export function parseFrontmatterBlock(content: string): {
name?: string;
description?: string;
enabled: boolean;
} {
const lines = content.split("\n");
if (lines[0]?.trim() !== "---") {
return { enabled: true }; // No frontmatter
}
const yamlLines: string[] = [];
for (let i = 1; i < lines.length; i++) {
if (lines[i].trim() === "---") break;
yamlLines.push(lines[i]);
}
const fields = parseSimpleYaml(yamlLines.join("\n"));
return {
name: fields.name as string | undefined,
description: fields.description as string | undefined,
enabled: parseFrontmatterBool(fields.enabled),
};
}
The main discovery function assembles specs from all files, filtering out disabled commands:
export function discoverBundleCommands(
rootDir: string,
manifest: ClaudeBundleManifest,
pluginId: string
): ClaudeBundleCommandSpec[] {
const roots = resolveClaudeCommandRootDirs(rootDir, manifest);
const specs: ClaudeBundleCommandSpec[] = [];
for (const dir of roots) {
const mdFiles = listMarkdownFilesRecursive(dir);
for (const filePath of mdFiles) {
const content = readFileSync(filePath, "utf-8");
const meta = parseFrontmatterBlock(content);
if (!meta.enabled) continue; // Skip disabled commands
specs.push({
pluginId,
rawName: meta.name ?? basename(filePath, ".md"),
description: meta.description,
promptTemplate: stripFrontmatter(content),
sourceFilePath: filePath,
});
}
}
return specs;
}
| Scenario | Behavior | Location |
|---|---|---|
No frontmatter (file starts without ---) | Entire file content becomes the prompt template; name derived from filename | parseFrontmatterBlock() |
Frontmatter without closing --- | Treated as no frontmatter — whole file is the template (graceful fallback) | stripFrontmatter() |
enabled: false commands | Skipped during discovery; not included in the returned spec array | discoverBundleCommands() |
| Empty command directories | No error; listMarkdownFilesRecursive() returns an empty array, loop body never executes | discoverBundleCommands() |
| Non-markdown files in command dirs | Ignored by the recursive file lister; only .md extensions are collected | listMarkdownFilesRecursive() |
See also:
src/plugins/
├── bundle-commands.ts # Main entry: discoverBundleCommands()
├── bundle-commands-types.ts # ClaudeBundleCommandSpec type definition
├── bundle-commands-frontmatter.ts # parseFrontmatterBlock(), stripFrontmatter()
├── bundle-commands-bool.ts # parseFrontmatterBool() utility
└── bundle-commands-fs.ts # listMarkdownFilesRecursive(), resolveClaudeCommandRootDirs()