roocommander
Version:
Bridge Claude Code skills to Roo Code with intelligent orchestration. CLI tool + Custom Mode + 60+ production-tested skills for Cloudflare, AI, Frontend development.
259 lines • 9.71 kB
JavaScript
;
/**
* Claude Code Skill Parser
*
* Core parsing library for reading skills from ~/.claude/skills/ directory.
* Provides functions to parse individual skills and scan for all available skills.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_SKILLS_DIR = void 0;
exports.parseSkill = parseSkill;
exports.validateSkill = validateSkill;
exports.findAllSkills = findAllSkills;
exports.getSkillByName = getSkillByName;
const path_1 = require("path");
const os_1 = require("os");
const fs_extra_1 = require("fs-extra");
const types_js_1 = require("./types.js");
const yaml_parser_js_1 = require("./yaml-parser.js");
/**
* Default skills directory path
*/
exports.DEFAULT_SKILLS_DIR = (0, path_1.join)((0, os_1.homedir)(), '.claude', 'skills');
/**
* Parse a single skill from its directory
*
* Reads SKILL.md, extracts metadata, discovers templates and reference files.
*
* @param skillPath - Absolute path to skill directory
* @param options - Parse options
* @returns Parsed ClaudeSkill object
* @throws SkillParseError if SKILL.md missing or malformed
* @throws SkillValidationError if validation enabled and fails
*/
async function parseSkill(skillPath, options = {}) {
const { validate = true } = options;
// Construct path to SKILL.md
const skillFilePath = (0, path_1.join)(skillPath, 'SKILL.md');
// Check if SKILL.md exists
if (!(await (0, fs_extra_1.pathExists)(skillFilePath))) {
throw new types_js_1.SkillParseError(`SKILL.md not found in ${skillPath}`, skillPath);
}
// Parse frontmatter and content
const parsed = await (0, yaml_parser_js_1.parseFrontmatter)(skillFilePath);
// Extract required fields
const name = parsed.data.name;
const description = parsed.data.description;
// Validate required fields
if (!name || typeof name !== 'string' || name.trim().length === 0) {
throw new types_js_1.SkillParseError(`Missing or invalid 'name' field in ${skillFilePath}`, skillPath);
}
if (!description ||
typeof description !== 'string' ||
description.trim().length === 0) {
throw new types_js_1.SkillParseError(`Missing or invalid 'description' field in ${skillFilePath}`, skillPath);
}
// Extract keywords and "use when" from description
const keywords = (0, yaml_parser_js_1.extractKeywords)(description);
const useWhen = (0, yaml_parser_js_1.extractUseWhen)(description);
// Build metadata object
const metadata = {
name: name.trim(),
description: description.trim(),
keywords,
useWhen,
// Include any additional frontmatter fields
...parsed.data,
};
// Discover template files
const templatesDir = (0, path_1.join)(skillPath, 'templates');
const templates = (await (0, fs_extra_1.pathExists)(templatesDir))
? await listFilesRecursive(templatesDir, skillPath)
: undefined;
// Discover reference files
const referenceDir = (0, path_1.join)(skillPath, 'reference');
const referenceFiles = (await (0, fs_extra_1.pathExists)(referenceDir))
? await listFilesRecursive(referenceDir, skillPath)
: undefined;
// Check for README.md
const readmePath = (0, path_1.join)(skillPath, 'README.md');
const readmeExists = await (0, fs_extra_1.pathExists)(readmePath);
// Build skill object
const skill = {
metadata,
content: parsed.content,
path: skillPath,
skillFilePath,
templates,
referenceFiles,
readmeExists,
};
// Validate if requested
if (validate) {
const validationResult = await validateSkill(skill);
if (!validationResult.valid) {
throw new types_js_1.SkillValidationError(`Skill validation failed for ${skillPath}`, skillPath, validationResult.errors);
}
}
return skill;
}
/**
* Validate skill structure and content
*
* Checks required fields, file existence, and reports warnings for missing optional elements.
*
* @param skill - Parsed skill object
* @returns Validation result with errors and warnings
*/
async function validateSkill(skill) {
const errors = [];
const warnings = [];
// Check required fields
if (!skill.metadata.name || skill.metadata.name.trim().length === 0) {
errors.push('Missing or empty name field');
}
if (!skill.metadata.description ||
skill.metadata.description.trim().length === 0) {
errors.push('Missing or empty description field');
}
// Check SKILL.md file exists
if (!(await (0, fs_extra_1.pathExists)(skill.skillFilePath))) {
errors.push(`SKILL.md file not found at ${skill.skillFilePath}`);
}
// Validate template file paths
if (skill.templates) {
for (const templatePath of skill.templates) {
const fullPath = (0, path_1.join)(skill.path, templatePath);
if (!(await (0, fs_extra_1.pathExists)(fullPath))) {
errors.push(`Template file not found: ${templatePath}`);
}
}
}
// Validate reference file paths
if (skill.referenceFiles) {
for (const refPath of skill.referenceFiles) {
const fullPath = (0, path_1.join)(skill.path, refPath);
if (!(await (0, fs_extra_1.pathExists)(fullPath))) {
errors.push(`Reference file not found: ${refPath}`);
}
}
}
// Warnings for missing optional elements
if (!skill.metadata.keywords || skill.metadata.keywords.length === 0) {
warnings.push('No keywords found in description');
}
if (!skill.metadata.useWhen) {
warnings.push('No "Use when:" section found in description');
}
if (!skill.templates || skill.templates.length === 0) {
warnings.push('No template files found');
}
if (!skill.readmeExists) {
warnings.push('No README.md found');
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Find and parse all skills in a directory
*
* Scans skillsDir for subdirectories containing SKILL.md files.
* Skips invalid skills and continues processing.
*
* @param skillsDir - Path to skills directory (default: ~/.claude/skills/)
* @param options - Parse options
* @returns Array of successfully parsed skills
*/
async function findAllSkills(skillsDir = exports.DEFAULT_SKILLS_DIR, options = {}) {
const skills = [];
// Check if skills directory exists
if (!(await (0, fs_extra_1.pathExists)(skillsDir))) {
console.warn(`Skills directory not found: ${skillsDir}`);
return [];
}
// List all subdirectories
const entries = await (0, fs_extra_1.readdir)(skillsDir);
for (const entry of entries) {
const entryPath = (0, path_1.join)(skillsDir, entry);
// Check if it's a directory (skip if stat fails - broken symlinks, etc.)
try {
const stats = await (0, fs_extra_1.stat)(entryPath);
if (!stats.isDirectory()) {
continue;
}
}
catch (error) {
console.warn(`Skipping ${entry}: Unable to stat (${error.message})`);
continue;
}
// Check for SKILL.md
const skillFilePath = (0, path_1.join)(entryPath, 'SKILL.md');
if (!(await (0, fs_extra_1.pathExists)(skillFilePath))) {
console.warn(`Skipping ${entry}: No SKILL.md found`);
continue;
}
// Try to parse the skill
try {
const skill = await parseSkill(entryPath, options);
skills.push(skill);
}
catch (error) {
// Log error but continue processing other skills
if (error instanceof types_js_1.SkillParseError) {
console.error(`Failed to parse skill ${entry}: ${error.message}`);
}
else if (error instanceof types_js_1.SkillValidationError) {
console.error(`Validation failed for skill ${entry}: ${error.errors.join(', ')}`);
}
else {
console.error(`Unexpected error parsing skill ${entry}:`, error);
}
}
}
return skills;
}
/**
* List all files in a directory recursively
*
* Returns paths relative to the baseDir.
*
* @param dir - Directory to scan
* @param baseDir - Base directory for relative paths
* @returns Array of relative file paths
*/
async function listFilesRecursive(dir, baseDir) {
const files = [];
const entries = await (0, fs_extra_1.readdir)(dir);
for (const entry of entries) {
const entryPath = (0, path_1.join)(dir, entry);
const stats = await (0, fs_extra_1.stat)(entryPath);
if (stats.isDirectory()) {
// Recurse into subdirectories
const subFiles = await listFilesRecursive(entryPath, baseDir);
files.push(...subFiles);
}
else {
// Add file with relative path
const relativePath = (0, path_1.relative)(baseDir, entryPath);
files.push(relativePath);
}
}
return files;
}
/**
* Get skill by name
*
* Convenience function to find a skill by its name.
*
* @param skillName - Name of skill to find
* @param skillsDir - Skills directory (default: ~/.claude/skills/)
* @returns Parsed skill or undefined if not found
*/
async function getSkillByName(skillName, skillsDir = exports.DEFAULT_SKILLS_DIR) {
const skills = await findAllSkills(skillsDir, { validate: false });
return skills.find((skill) => skill.metadata.name.toLowerCase() === skillName.toLowerCase());
}
//# sourceMappingURL=skill-parser.js.map