// yaml-frontmatter.mjs — Regex-based YAML frontmatter parser // Handles Claude Code plugin command/agent/skill frontmatter. // Zero dependencies. /** * Parse YAML frontmatter from a markdown file. * Returns null if no frontmatter found. * * @param {string} content - File content * @returns {{ name?: string, description?: string, model?: string, color?: string, * tools?: string[], allowed_tools?: string[] } | null} */ export function parseFrontmatter(content) { // Match --- delimited block at start of file const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (!match) return null; const block = match[1]; const result = {}; // Parse simple key: value pairs for (const line of block.split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; // Handle key: value const kvMatch = trimmed.match(/^(\w[\w-]*)\s*:\s*(.*)$/); if (!kvMatch) continue; const [, key, rawValue] = kvMatch; let value = rawValue.trim(); // Strip quotes if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } // Handle inline arrays: [Read, Write, Bash] if (value.startsWith('[') && value.endsWith(']')) { value = value.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, '')); } // Handle multi-line description with | if (value === '|' || value === '>') { const descLines = []; const lines = block.split('\n'); const lineIdx = lines.indexOf(line); for (let i = lineIdx + 1; i < lines.length; i++) { const dLine = lines[i]; if (/^\S/.test(dLine) && !dLine.startsWith(' ') && !dLine.startsWith('\t')) break; descLines.push(dLine.replace(/^ /, '')); } value = descLines.join('\n').trim(); } // Normalize key names const normalizedKey = key.replace(/-/g, '_'); result[normalizedKey] = value; } // Parse tools from allowed-tools (comma-separated string) or tools (array) if (typeof result.allowed_tools === 'string') { result.allowed_tools = result.allowed_tools.split(',').map(s => s.trim()); } if (typeof result.tools === 'string') { result.tools = result.tools.split(',').map(s => s.trim()); } return Object.keys(result).length > 0 ? result : null; } /** * Classify a plugin file by its path and frontmatter. * @param {string} relPath - Relative path within plugin * @param {object|null} frontmatter - Parsed frontmatter * @returns {'command' | 'agent' | 'skill' | 'hook-config' | 'knowledge' | 'template' | 'unknown'} */ export function classifyPluginFile(relPath, frontmatter) { const lower = relPath.toLowerCase(); if (lower.includes('/commands/') || lower.startsWith('commands/')) return 'command'; if (lower.includes('/agents/') || lower.startsWith('agents/')) return 'agent'; if (lower.includes('/skills/') || lower.startsWith('skills/') || lower.endsWith('skill.md')) return 'skill'; if (lower.endsWith('hooks.json') || lower.includes('/hooks/')) return 'hook-config'; if (lower.includes('/knowledge/') || lower.startsWith('knowledge/')) return 'knowledge'; if (lower.includes('/templates/') || lower.startsWith('templates/')) return 'template'; if (frontmatter?.name && frontmatter?.allowed_tools) return 'command'; if (frontmatter?.name && frontmatter?.tools) return 'agent'; return 'unknown'; }