| |
| |
| |
| |
|
|
| import { SkillFrontmatter } from './types'; |
|
|
| const FRONTMATTER_REGEX = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/; |
|
|
| |
| |
| |
| export function parseSkillFile(content: string): { |
| frontmatter: SkillFrontmatter; |
| markdown: string; |
| } { |
| const match = content.match(FRONTMATTER_REGEX); |
|
|
| if (!match) { |
| throw new Error('Invalid SKILL.md format: Missing YAML frontmatter'); |
| } |
|
|
| const [, yamlContent, markdown] = match; |
|
|
| try { |
| const frontmatter = parseYAML(yamlContent); |
| validateFrontmatter(frontmatter); |
| return { |
| frontmatter, |
| markdown: markdown.trim() |
| }; |
| } catch (error) { |
| throw new Error(`Failed to parse SKILL.md: ${error instanceof Error ? error.message : 'Unknown error'}`); |
| } |
| } |
|
|
| |
| |
| |
| |
| function parseYAML(yamlContent: string): SkillFrontmatter { |
| const lines = yamlContent.split('\n'); |
| const result: Record<string, unknown> = {}; |
|
|
| for (const line of lines) { |
| const trimmed = line.trim(); |
| if (!trimmed || trimmed.startsWith('#')) continue; |
|
|
| const colonIndex = trimmed.indexOf(':'); |
| if (colonIndex === -1) continue; |
|
|
| const key = trimmed.slice(0, colonIndex).trim(); |
| let value: string | boolean | number = trimmed.slice(colonIndex + 1).trim(); |
|
|
| |
| if ((value.startsWith('"') && value.endsWith('"')) || |
| (value.startsWith("'") && value.endsWith("'"))) { |
| value = value.slice(1, -1); |
| } |
|
|
| |
| if (value === 'true') value = true; |
| else if (value === 'false') value = false; |
| else if (!isNaN(Number(value)) && value !== '') value = Number(value); |
|
|
| result[key] = value; |
| } |
|
|
| return result as SkillFrontmatter; |
| } |
|
|
| |
| |
| |
| function validateFrontmatter(frontmatter: SkillFrontmatter): void { |
| if (!frontmatter.name || typeof frontmatter.name !== 'string') { |
| throw new Error('Missing or invalid "name" field in frontmatter'); |
| } |
|
|
| if (!frontmatter.description || typeof frontmatter.description !== 'string') { |
| throw new Error('Missing or invalid "description" field in frontmatter'); |
| } |
|
|
| |
| if (!/^[a-z0-9-]+$/.test(frontmatter.name)) { |
| throw new Error('Skill name must be lowercase with hyphens only (e.g., "my-skill-name")'); |
| } |
|
|
| |
| if (frontmatter.description.length > 500) { |
| throw new Error('Description must be 500 characters or less'); |
| } |
| } |
|
|
| |
| |
| |
| export function generateSkillFile(frontmatter: SkillFrontmatter, markdown: string): string { |
| const yamlLines: string[] = []; |
|
|
| |
| for (const [key, value] of Object.entries(frontmatter)) { |
| if (value === undefined || value === null) continue; |
|
|
| if (typeof value === 'string' && (value.includes(':') || value.includes('\n'))) { |
| yamlLines.push(`${key}: "${value}"`); |
| } else { |
| yamlLines.push(`${key}: ${value}`); |
| } |
| } |
|
|
| return `---\n${yamlLines.join('\n')}\n---\n\n${markdown.trim()}\n`; |
| } |
|
|
| |
| |
| |
| export function createSkillTemplate(name: string, description: string): string { |
| const frontmatter: SkillFrontmatter = { |
| name: name.toLowerCase().replace(/\s+/g, '-'), |
| description |
| }; |
|
|
| const markdown = `# ${name} |
| |
| ## Purpose |
| [Describe what this skill helps with] |
| |
| ## Guidelines |
| - Guideline 1 |
| - Guideline 2 |
| - Guideline 3 |
| |
| ## Examples |
| [Provide code examples or usage patterns] |
| |
| ## Best Practices |
| [List best practices and recommendations] |
| `; |
|
|
| return generateSkillFile(frontmatter, markdown); |
| } |
|
|