/**
 * Skill Loader — prompt skills for the Studiograph agent.
 *
 * Skills come from two sources:
 *
 *   1. Bundled filesystem skills — shipped with the app. Two formats:
 *        Directory skill:   skills/create-deck/skill.md (+ optional sub-files)
 *        Single-file skill: skills/quick-tip.md
 *      Frontmatter fields: name, description, loading ("eager" | "on-demand").
 *
 *   2. Graph skills — authored by users as `skill` entities in any folder
 *      (personal or shared). The entity id IS the skill name; `description`
 *      and `loading` are frontmatter fields; the body markdown is the
 *      instruction content. Graph skills use a `graph://<repo>/<id>`
 *      entrypoint sentinel and are resolved via an optional graphResolver
 *      callback passed to `loadSkillContent` / `loadEagerSkills`.
 */
import { existsSync, readdirSync, readFileSync } from 'fs';
import { join, extname, basename } from 'path';
/**
 * Minimal YAML frontmatter parser — handles the subset used by skills.
 */
function parseFrontmatter(content) {
    const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
    if (!match) {
        return { meta: {}, body: content };
    }
    const meta = {};
    for (const line of match[1].split('\n')) {
        const colon = line.indexOf(':');
        if (colon === -1)
            continue;
        const key = line.slice(0, colon).trim();
        const value = line.slice(colon + 1).trim();
        if (key)
            meta[key] = value;
    }
    return { meta, body: match[2] };
}
/**
 * Scan one or more skill directories and return a de-duplicated skill index.
 * Directories are processed in order — first occurrence of a name wins,
 * so workspace-level skills override app-bundled skills.
 */
export function loadSkillIndex(skillsDirs) {
    const skills = [];
    const seen = new Set();
    for (const dir of skillsDirs) {
        if (!existsSync(dir))
            continue;
        let entries;
        try {
            entries = readdirSync(dir);
        }
        catch {
            continue;
        }
        for (const entry of entries) {
            if (entry.startsWith('.'))
                continue;
            const entryPath = join(dir, entry);
            // Directory skill: subdirectory containing skill.md
            const skillMd = join(entryPath, 'skill.md');
            if (existsSync(skillMd)) {
                try {
                    const content = readFileSync(skillMd, 'utf-8');
                    const { meta } = parseFrontmatter(content);
                    const name = meta.name || entry;
                    if (!seen.has(name)) {
                        seen.add(name);
                        skills.push({
                            name,
                            description: meta.description || '',
                            loading: meta.loading === 'eager' ? 'eager' : 'on-demand',
                            dir: entryPath,
                            entrypoint: skillMd,
                            isDirectory: true,
                        });
                    }
                }
                catch {
                    // skip malformed skill
                }
                continue;
            }
            // Single-file skill: top-level .md file with valid frontmatter
            if (extname(entry) === '.md') {
                try {
                    const content = readFileSync(entryPath, 'utf-8');
                    const { meta } = parseFrontmatter(content);
                    // Must have at least name or description to be a skill
                    if (!meta.name && !meta.description)
                        continue;
                    const name = meta.name || basename(entry, '.md');
                    if (!seen.has(name)) {
                        seen.add(name);
                        skills.push({
                            name,
                            description: meta.description || '',
                            loading: meta.loading === 'eager' ? 'eager' : 'on-demand',
                            dir,
                            entrypoint: entryPath,
                            isDirectory: false,
                        });
                    }
                }
                catch {
                    // skip
                }
            }
        }
    }
    return skills;
}
const GRAPH_PREFIX = 'graph://';
/**
 * Parse a `graph://<repo>/<id>` entrypoint into its components.
 * Returns null for filesystem paths.
 */
export function parseGraphSkillEntrypoint(entrypoint) {
    if (!entrypoint.startsWith(GRAPH_PREFIX))
        return null;
    const rest = entrypoint.slice(GRAPH_PREFIX.length);
    const slash = rest.indexOf('/');
    if (slash === -1)
        return null;
    return { repo: rest.slice(0, slash), id: rest.slice(slash + 1) };
}
/**
 * Load the full content of a skill's entrypoint or a named sub-file.
 * Sanitises the file parameter to prevent path traversal. Graph-authored
 * skills route through the provided resolver; filesystem skills read from
 * disk.
 */
export function loadSkillContent(meta, file, graphResolver) {
    const graph = parseGraphSkillEntrypoint(meta.entrypoint);
    if (graph) {
        if (file) {
            return `Error: skill "${meta.name}" is a graph skill and has no sub-files. Use wikilinks in the skill body to reference other entries.`;
        }
        if (!graphResolver) {
            return `Error: cannot load graph skill "${meta.name}" — no resolver provided.`;
        }
        return graphResolver(graph.repo, graph.id);
    }
    if (file) {
        if (!meta.isDirectory) {
            return `Error: skill "${meta.name}" is a single-file skill with no sub-files.`;
        }
        // Allow only simple filenames — no slashes, no dots at start
        const safeName = file.replace(/[^a-zA-Z0-9._-]/g, '');
        if (!safeName || safeName.startsWith('.')) {
            return `Error: invalid sub-file name "${file}".`;
        }
        const filePath = join(meta.dir, safeName);
        if (!existsSync(filePath)) {
            return `Error: sub-file "${safeName}" not found in skill "${meta.name}".`;
        }
        return readFileSync(filePath, 'utf-8');
    }
    return readFileSync(meta.entrypoint, 'utf-8');
}
/**
 * Build the skill index section to append to the system prompt.
 * Eager skills are inlined separately; this lists only on-demand skills.
 */
export function buildSkillIndexPrompt(skills) {
    const onDemand = skills.filter(s => s.loading === 'on-demand');
    if (onDemand.length === 0)
        return '';
    const formatSource = (dir) => {
        const graph = parseGraphSkillEntrypoint(dir + '/');
        return graph ? ` _(folder: ${graph.repo})_` : '';
    };
    const lines = [
        '## Available Skills',
        '',
        'Call `load_skill` to load domain-specific guidance when it is relevant to the current task.',
        '',
        ...onDemand.map(s => `- **${s.name}**${formatSource(s.dir)} — ${s.description}`),
    ];
    return lines.join('\n');
}
/** Cap on total eager-skill content inlined into the system prompt. */
export const EAGER_SKILL_BYTE_CAP = 20_000;
/**
 * Return the full content of all eager skills, to be injected directly
 * into the system prompt at startup. Respects `EAGER_SKILL_BYTE_CAP` —
 * skills beyond the cap are dropped with a warning to stderr.
 */
export function loadEagerSkills(skills, graphResolver) {
    const pieces = [];
    let total = 0;
    for (const skill of skills.filter(s => s.loading === 'eager')) {
        const content = loadSkillContent(skill, undefined, graphResolver);
        if (total + content.length > EAGER_SKILL_BYTE_CAP) {
            if (process.env.STUDIOGRAPH_VERBOSE === '1') {
                console.warn(`[skill-loader] eager skill "${skill.name}" dropped — cap ${EAGER_SKILL_BYTE_CAP}B reached.`);
            }
            continue;
        }
        pieces.push(content);
        total += content.length;
    }
    return pieces.join('\n\n---\n\n');
}
//# sourceMappingURL=skill-loader.js.map