UNPKG

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
"use strict"; /** * 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