/**
* skill_loader.ts
* Purpose: resolve skill id -> SKILL.md path, read file, parse Boundary+Kernel, expose governance fields.
*/
import path from 'node:path';
import { readFile } from 'node:fs/promises';
import { parseSkillMarkdown, type SkillDocumentParts } from './skill_parser';
export interface LoadedSkill extends SkillDocumentParts {
sourcePath: string;
name: string;
allowedTools: string[];
}
function asStringArray(x: unknown): string[] {
if (!Array.isArray(x)) return [];
return x.map((v) => String(v)).map((s) => s.trim()).filter((s) => s.length > 0);
}
export function skillIdToDir(skillId: string): string {
// Accept ids like "fpf-skill:hello-world" -> directory "hello-world"
const parts = skillId.split(':');
return parts[parts.length - 1];
}
export async function loadSkillFromFile(registryRoot: string, skillId: string): Promise<LoadedSkill> {
const dir = skillIdToDir(skillId);
const sourcePath = path.join(registryRoot, dir, 'SKILL.md');
const raw = await readFile(sourcePath, 'utf-8');
const parsed = parseSkillMarkdown(raw);
const name = String(parsed.boundary['name'] ?? '').trim();
if (!name) throw new Error(`Skill Boundary missing required field: name (${sourcePath})`);
const allowedTools = asStringArray(parsed.boundary['allowed_tools'] ?? parsed.boundary['allowedTools'] ?? []);
return { ...parsed, sourcePath, name, allowedTools };
}