import { readdirSync, readFileSync, existsSync } from 'node:fs'; import { join } from 'node:path'; import { parse as parseYaml } from 'yaml'; import { RECOMMENDED_SKILLS } from '../constants.js'; export interface SkillEntry { name: string; description: string; version?: string; recommended: boolean; source: 'canonical' | 'local'; } export function loadSkillsCatalog(mosaicHome: string): SkillEntry[] { const skills: SkillEntry[] = []; // Load canonical skills const canonicalDir = join(mosaicHome, 'skills'); if (existsSync(canonicalDir)) { skills.push(...loadSkillsFromDir(canonicalDir, 'canonical')); } // Fallback to source repo const sourceDir = join(mosaicHome, 'sources', 'agent-skills', 'skills'); if (skills.length === 0 && existsSync(sourceDir)) { skills.push(...loadSkillsFromDir(sourceDir, 'canonical')); } // Load local skills const localDir = join(mosaicHome, 'skills-local'); if (existsSync(localDir)) { skills.push(...loadSkillsFromDir(localDir, 'local')); } return skills.sort((a, b) => a.name.localeCompare(b.name)); } function loadSkillsFromDir(dir: string, source: 'canonical' | 'local'): SkillEntry[] { const entries: SkillEntry[] = []; let dirEntries; try { dirEntries = readdirSync(dir, { withFileTypes: true }); } catch { return entries; } for (const entry of dirEntries) { if (!entry.isDirectory() || entry.name.startsWith('.')) continue; const skillMdPath = join(dir, entry.name, 'SKILL.md'); if (!existsSync(skillMdPath)) continue; try { const content = readFileSync(skillMdPath, 'utf-8'); const frontmatter = parseFrontmatter(content); entries.push({ name: (frontmatter['name'] as string | undefined) ?? entry.name, description: (frontmatter['description'] as string | undefined) ?? '', version: frontmatter['version'] as string | undefined, recommended: RECOMMENDED_SKILLS.has(entry.name), source, }); } catch { // Skip malformed skills entries.push({ name: entry.name, description: '', recommended: RECOMMENDED_SKILLS.has(entry.name), source, }); } } return entries; } function parseFrontmatter(content: string): Record { const match = content.match(/^---\n([\s\S]*?)\n---/); if (!match?.[1]) return {}; try { return (parseYaml(match[1]) as Record) ?? {}; } catch { // Fallback: simple key-value parsing const result: Record = {}; for (const line of match[1].split('\n')) { const kv = line.match(/^(\w[\w-]*)\s*:\s*(.+)/); if (kv?.[1] !== undefined && kv[2] !== undefined) { result[kv[1]] = kv[2].replace(/^['"]|['"]$/g, ''); } } return result; } }