'use strict'; const fs = require('node:fs'); const path = require('node:path'); const os = require('node:os'); const SKILLS_DIR = path.join( os.homedir(), 'Library', 'Application Support', 'RA-H', 'skills' ); const LEGACY_GUIDES_DIR = path.join( os.homedir(), 'Library', 'Application Support', 'RA-H', 'guides' ); const BUNDLED_SKILLS_DIR = path.join(__dirname, '..', 'skills'); const SEED_MIGRATION_FLAG = path.join(SKILLS_DIR, '.seed-migrated-2026-03-07-skills-overhaul'); const SEEDED_SKILL_IDS = new Set([ 'db-operations', 'create-skill', 'audit', 'traverse', 'onboarding', 'persona', 'calibration', 'connect', 'node-context-enrichment', ]); const DEPRECATED_SKILL_IDS = new Set([ 'start-here', 'schema', 'creating-nodes', 'edges', 'dimensions', 'extract', 'troubleshooting', 'integrate', 'test-guide', 'ghostwriting-brad', 'write-the-debrief', 'prep', 'preferences', 'research', 'survey', 'traverse-graph', ]); function parseFrontmatter(raw) { const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); if (!match) return { data: {}, content: raw.trim() }; const yamlBlock = match[1]; const content = match[2]; const data = {}; for (const line of yamlBlock.split('\n')) { const colonIdx = line.indexOf(':'); if (colonIdx === -1) continue; const key = line.slice(0, colonIdx).trim(); let value = line.slice(colonIdx + 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; data[key] = value; } return { data, content: content.trim() }; } function stripMdExtension(value) { return value.replace(/\.md$/i, ''); } function normalizeSkillId(value) { return stripMdExtension(value) .trim() .toLowerCase() .replace(/[\s_]+/g, '-') .replace(/[^a-z0-9-]/g, '') .replace(/-+/g, '-') .replace(/^-|-$/g, ''); } function listMarkdownFiles(dir) { if (!fs.existsSync(dir)) return []; return fs.readdirSync(dir).filter((f) => f.endsWith('.md')); } function ensureSkillsDir() { if (!fs.existsSync(SKILLS_DIR)) { fs.mkdirSync(SKILLS_DIR, { recursive: true }); } } function migrateLegacyGuides() { const files = listMarkdownFiles(LEGACY_GUIDES_DIR); for (const file of files) { const dest = path.join(SKILLS_DIR, file); if (!fs.existsSync(dest)) { fs.copyFileSync(path.join(LEGACY_GUIDES_DIR, file), dest); } } } function seedSkills() { ensureSkillsDir(); const bundledFiles = listMarkdownFiles(BUNDLED_SKILLS_DIR); const bundledById = new Map(); for (const file of bundledFiles) { bundledById.set(normalizeSkillId(file), path.join(BUNDLED_SKILLS_DIR, file)); } for (const skillId of SEEDED_SKILL_IDS) { const source = bundledById.get(skillId); if (!source) continue; const dest = path.join(SKILLS_DIR, `${skillId}.md`); if (!fs.existsSync(dest)) { fs.copyFileSync(source, dest); } } } function migrateSeededBaseline() { if (fs.existsSync(SEED_MIGRATION_FLAG)) { return; } const bundledFiles = listMarkdownFiles(BUNDLED_SKILLS_DIR); const bundledById = new Map(); for (const file of bundledFiles) { bundledById.set(normalizeSkillId(file), path.join(BUNDLED_SKILLS_DIR, file)); } for (const skillId of SEEDED_SKILL_IDS) { const source = bundledById.get(skillId); if (!source) continue; const dest = path.join(SKILLS_DIR, `${skillId}.md`); fs.copyFileSync(source, dest); } fs.writeFileSync(SEED_MIGRATION_FLAG, 'ok', 'utf-8'); } function pruneDeprecatedSkills() { const files = listMarkdownFiles(SKILLS_DIR); for (const file of files) { const normalized = normalizeSkillId(file); if (DEPRECATED_SKILL_IDS.has(normalized)) { fs.unlinkSync(path.join(SKILLS_DIR, file)); } } } function resolveSkillFilename(name) { if (!fs.existsSync(SKILLS_DIR)) { return null; } const files = fs.readdirSync(SKILLS_DIR).filter((f) => f.endsWith('.md')); const normalizedInput = normalizeSkillId(name); const directCandidates = [ `${name}.md`, `${name.toLowerCase()}.md`, normalizedInput ? `${normalizedInput}.md` : '', ].filter(Boolean); for (const candidate of directCandidates) { if (files.includes(candidate)) { return candidate; } } for (const file of files) { if (normalizeSkillId(file) === normalizedInput) { return file; } const raw = fs.readFileSync(path.join(SKILLS_DIR, file), 'utf-8'); const { data } = parseFrontmatter(raw); if (typeof data.name === 'string' && normalizeSkillId(data.name) === normalizedInput) { return file; } } return null; } let initialized = false; function init() { if (initialized) return; ensureSkillsDir(); migrateLegacyGuides(); migrateSeededBaseline(); seedSkills(); pruneDeprecatedSkills(); initialized = true; } function listSkills() { init(); if (!fs.existsSync(SKILLS_DIR)) return []; const files = fs.readdirSync(SKILLS_DIR).filter((f) => f.endsWith('.md')); const skills = files.map((file) => { const raw = fs.readFileSync(path.join(SKILLS_DIR, file), 'utf-8'); const { data } = parseFrontmatter(raw); return { name: data.name || file.replace('.md', ''), description: data.description || '', immutable: false, }; }); return skills.sort((a, b) => a.name.localeCompare(b.name)); } function readSkill(name) { init(); const filename = resolveSkillFilename(name); if (!filename) { return null; } const filepath = path.join(SKILLS_DIR, filename); const raw = fs.readFileSync(filepath, 'utf-8'); const { data, content } = parseFrontmatter(raw); return { name: data.name || stripMdExtension(filename), description: data.description || '', immutable: false, content, }; } function writeSkill(name, content) { init(); const normalizedName = normalizeSkillId(name); const existingFilename = resolveSkillFilename(name); const filename = existingFilename || `${normalizedName || name.toLowerCase()}.md`; const filepath = path.join(SKILLS_DIR, filename); ensureSkillsDir(); fs.writeFileSync(filepath, content, 'utf-8'); return { success: true }; } function deleteSkill(name) { init(); const filename = resolveSkillFilename(name); if (!filename) { return { success: false, error: `Skill "${name}" not found.` }; } const filepath = path.join(SKILLS_DIR, filename); fs.unlinkSync(filepath); return { success: true }; } module.exports = { listSkills, readSkill, writeSkill, deleteSkill, };