/**
 * Skill discovery, validation, and loading.
 *
 * Skills are markdown files (SKILL.md) with YAML frontmatter (name, description).
 * Discovered from global, project-local, and explicit paths. Validates names
 * (lowercase, hyphens, matching parent dir) and descriptions per the AgentController Skills spec.
 * Also provides `parseSkillBlock` for extracting skill invocations from user messages.
 */
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'fs';
import ignore from 'ignore';
import { homedir } from 'os';
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'path';
import { CONFIG_DIR_NAME, getAgentDir } from '../../config.js';
import { parseFrontmatter } from '../../shared/content/frontmatter.js';
import { parseSkillBlock } from './skill-block.js';
/** Max name length per spec */
const MAX_NAME_LENGTH = 64;
/** Max description length per spec */
const MAX_DESCRIPTION_LENGTH = 1024;
const IGNORE_FILE_NAMES = ['.gitignore', '.ignore', '.fdignore'];
/** Convert OS-native separators to forward slashes for posix-style matching. */
function toPosixPath(p) {
    return p.split(sep).join('/');
}
/** Apply a directory prefix to a single .gitignore-style pattern, preserving negation. */
function prefixIgnorePattern(line, prefix) {
    const trimmed = line.trim();
    if (!trimmed)
        return null;
    if (trimmed.startsWith('#') && !trimmed.startsWith('\\#'))
        return null;
    let pattern = line;
    let negated = false;
    if (pattern.startsWith('!')) {
        negated = true;
        pattern = pattern.slice(1);
    }
    else if (pattern.startsWith('\\!')) {
        pattern = pattern.slice(1);
    }
    if (pattern.startsWith('/')) {
        pattern = pattern.slice(1);
    }
    const prefixed = prefix ? `${prefix}${pattern}` : pattern;
    return negated ? `!${prefixed}` : prefixed;
}
/** Read .gitignore/.ignore/.fdignore files in `dir` and add their patterns to the matcher. */
function addIgnoreRules(ig, dir, rootDir) {
    const relativeDir = relative(rootDir, dir);
    const prefix = relativeDir ? `${toPosixPath(relativeDir)}/` : '';
    for (const filename of IGNORE_FILE_NAMES) {
        const ignorePath = join(dir, filename);
        if (!existsSync(ignorePath))
            continue;
        try {
            const content = readFileSync(ignorePath, 'utf-8');
            const patterns = content
                .split(/\r?\n/)
                .map((line) => prefixIgnorePattern(line, prefix))
                .filter((line) => Boolean(line));
            if (patterns.length > 0) {
                ig.add(patterns);
            }
        }
        catch { }
    }
}
/** Normalize CRLF and CR sequences in `value` to LF. */
function normalizeNewlines(value) {
    return value.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
}
/** Split a markdown document into a YAML frontmatter string and the remaining body. */
function extractFrontmatterBlock(content) {
    const normalized = normalizeNewlines(content);
    if (!normalized.startsWith('---')) {
        return { yamlString: null, body: normalized };
    }
    const endIndex = normalized.indexOf('\n---', 3);
    if (endIndex === -1) {
        return { yamlString: null, body: normalized };
    }
    return {
        yamlString: normalized.slice(4, endIndex),
        body: normalized.slice(endIndex + 4).trim()
    };
}
export { parseSkillBlock };
/** Fill in `location` and `content` on a parsed skill block by looking up the skill on disk. */
export function hydrateParsedSkillBlock(skillBlock, skills) {
    if (skillBlock.content) {
        return skillBlock;
    }
    const skill = skills.find((candidate) => candidate.name === skillBlock.name);
    if (!skill) {
        return skillBlock;
    }
    try {
        const rawContent = readFileSync(skill.filePath, 'utf-8');
        const { body } = parseSkillFrontmatter(rawContent, skill.filePath);
        return {
            ...skillBlock,
            location: skill.filePath,
            content: body
        };
    }
    catch {
        return {
            ...skillBlock,
            location: skill.filePath
        };
    }
}
/**
 * Validate skill name per AgentController Skills spec.
 * Returns array of validation error messages (empty if valid).
 */
function validateName(name, parentDirName) {
    const errors = [];
    if (name !== parentDirName) {
        errors.push(`name "${name}" does not match parent directory "${parentDirName}"`);
    }
    if (name.length > MAX_NAME_LENGTH) {
        errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);
    }
    if (!/^[a-z0-9-]+(?:_(?:[2-9]|[1-9][0-9]+))?$/.test(name)) {
        errors.push(`name contains invalid characters (must be lowercase a-z, 0-9, hyphens, with optional _N version suffix)`);
    }
    if (name.startsWith('-') || name.endsWith('-')) {
        errors.push(`name must not start or end with a hyphen`);
    }
    if (name.includes('--')) {
        errors.push(`name must not contain consecutive hyphens`);
    }
    return errors;
}
/**
 * Validate description per AgentController Skills spec.
 */
function validateDescription(description) {
    const errors = [];
    if (!description || description.trim() === '') {
        errors.push('description is required');
    }
    else if (description.length > MAX_DESCRIPTION_LENGTH) {
        errors.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`);
    }
    return errors;
}
/** Choose strict or external skill-parsing rules based on which agent's skills directory the file lives in. */
function getSkillCompatibilityMode(filePath) {
    const normalized = resolve(filePath).split(sep).join('/');
    if (normalized.includes('/.claude/skills/') ||
        normalized.includes('/.codex/skills/') ||
        normalized.includes('/.agents/skills/')) {
        return 'external';
    }
    return 'strict';
}
/** Parse a single YAML scalar (booleans, quoted strings, bare values) without a real YAML parser. */
function parseLooseFrontmatterScalar(value) {
    const trimmed = value.trim();
    if (trimmed === 'true')
        return true;
    if (trimmed === 'false')
        return false;
    const quoted = trimmed.match(/^(['"])([\s\S]*)\1$/);
    if (quoted) {
        return quoted[2];
    }
    return trimmed;
}
/** Best-effort line-by-line frontmatter parse used as a fallback for external-tool skills. */
function parseLooseSkillFrontmatter(yamlString) {
    const frontmatter = {};
    for (const line of normalizeNewlines(yamlString).split('\n')) {
        const trimmed = line.trim();
        if (!trimmed || trimmed.startsWith('#')) {
            continue;
        }
        const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
        if (!match) {
            continue;
        }
        frontmatter[match[1]] = parseLooseFrontmatterScalar(match[2]);
    }
    return frontmatter;
}
/** Parse SKILL.md frontmatter strictly, falling back to loose parsing for external-tool skills. */
function parseSkillFrontmatter(rawContent, filePath) {
    const compatibilityMode = getSkillCompatibilityMode(filePath);
    const extracted = extractFrontmatterBlock(rawContent);
    try {
        return parseFrontmatter(rawContent);
    }
    catch (error) {
        if (compatibilityMode !== 'external' || !extracted.yamlString) {
            throw error;
        }
        return {
            frontmatter: parseLooseSkillFrontmatter(extracted.yamlString),
            body: extracted.body
        };
    }
}
/** Pull the first paragraph from a skill body to use as a fallback description. */
function extractDescriptionFromBody(body) {
    const paragraph = [];
    for (const line of normalizeNewlines(body).split('\n')) {
        const trimmed = line.trim();
        if (!trimmed) {
            if (paragraph.length > 0) {
                break;
            }
            continue;
        }
        if (paragraph.length === 0 && (trimmed.startsWith('#') || trimmed.startsWith('```'))) {
            continue;
        }
        paragraph.push(trimmed.replace(/^>\s?/, ''));
    }
    if (paragraph.length === 0) {
        return undefined;
    }
    const description = paragraph.join(' ').trim();
    if (!description) {
        return undefined;
    }
    return description.slice(0, MAX_DESCRIPTION_LENGTH);
}
/** Choose the skill description from frontmatter, falling back to body text for external skills. */
function resolveSkillDescription(frontmatter, body, filePath) {
    if (typeof frontmatter.description === 'string' && frontmatter.description.trim()) {
        return frontmatter.description.trim();
    }
    if (getSkillCompatibilityMode(filePath) === 'external') {
        return extractDescriptionFromBody(body);
    }
    return undefined;
}
/**
 * Load skills from a directory.
 *
 * Discovery rules:
 * - direct .md children in the root
 * - recursive SKILL.md under subdirectories
 */
export function loadSkillsFromDir(options) {
    const { dir, source } = options;
    return loadSkillsFromDirInternal(dir, source, true);
}
/** Recursive worker for skill discovery; walks `dir` honoring ignore rules and accumulates results. */
function loadSkillsFromDirInternal(dir, source, includeRootFiles, ignoreMatcher, rootDir) {
    const skills = [];
    const diagnostics = [];
    if (!existsSync(dir)) {
        return { skills, diagnostics };
    }
    const root = rootDir ?? dir;
    const ig = ignoreMatcher ?? ignore();
    addIgnoreRules(ig, dir, root);
    try {
        const entries = readdirSync(dir, { withFileTypes: true });
        for (const entry of entries) {
            if (entry.name.startsWith('.')) {
                continue;
            }
            // Skip node_modules to avoid scanning dependencies
            if (entry.name === 'node_modules') {
                continue;
            }
            const fullPath = join(dir, entry.name);
            // For symlinks, check if they point to a directory and follow them
            let isDirectory = entry.isDirectory();
            let isFile = entry.isFile();
            if (entry.isSymbolicLink()) {
                try {
                    const stats = statSync(fullPath);
                    isDirectory = stats.isDirectory();
                    isFile = stats.isFile();
                }
                catch {
                    // Broken symlink, skip it
                    continue;
                }
            }
            const relPath = toPosixPath(relative(root, fullPath));
            const ignorePath = isDirectory ? `${relPath}/` : relPath;
            if (ig.ignores(ignorePath)) {
                continue;
            }
            if (isDirectory) {
                const subResult = loadSkillsFromDirInternal(fullPath, source, false, ig, root);
                skills.push(...subResult.skills);
                diagnostics.push(...subResult.diagnostics);
                continue;
            }
            if (!isFile) {
                continue;
            }
            const isRootMd = includeRootFiles && entry.name.endsWith('.md');
            const isSkillMd = !includeRootFiles && entry.name === 'SKILL.md';
            if (!isRootMd && !isSkillMd) {
                continue;
            }
            const result = loadSkillFromFile(fullPath, source);
            if (result.skill) {
                skills.push(result.skill);
            }
            diagnostics.push(...result.diagnostics);
        }
    }
    catch { }
    return { skills, diagnostics };
}
/** Parse and validate a single SKILL.md/markdown file into a Skill record plus diagnostics. */
function loadSkillFromFile(filePath, source) {
    const diagnostics = [];
    try {
        const rawContent = readFileSync(filePath, 'utf-8');
        const { frontmatter, body } = parseSkillFrontmatter(rawContent, filePath);
        const skillDir = dirname(filePath);
        const parentDirName = basename(skillDir);
        const description = resolveSkillDescription(frontmatter, body, filePath);
        // Validate description
        const descErrors = validateDescription(description);
        for (const error of descErrors) {
            diagnostics.push({ type: 'warning', message: error, path: filePath });
        }
        const declaredName = typeof frontmatter.name === 'string' && frontmatter.name.trim()
            ? frontmatter.name.trim()
            : undefined;
        // The folder name is the canonical runtime skill id. Frontmatter name is
        // treated as metadata/validation only and must not override the stable slug
        // used for prompt exposure, slash commands, collisions, or path routing.
        const name = parentDirName;
        // Validate the canonical slug and any explicit frontmatter declaration.
        const nameErrors = validateName(name, parentDirName);
        if (declaredName && declaredName !== name) {
            nameErrors.push(`name "${declaredName}" does not match parent directory "${parentDirName}"`);
        }
        if (declaredName) {
            if (declaredName.length > MAX_NAME_LENGTH) {
                nameErrors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${declaredName.length})`);
            }
            if (!/^[a-z0-9-]+(?:_(?:[2-9]|[1-9][0-9]+))?$/.test(declaredName)) {
                nameErrors.push('name contains invalid characters (must be lowercase a-z, 0-9, hyphens, with optional _N version suffix)');
            }
            if (declaredName.startsWith('-') || declaredName.endsWith('-')) {
                nameErrors.push('name must not start or end with a hyphen');
            }
            if (declaredName.includes('--')) {
                nameErrors.push('name must not contain consecutive hyphens');
            }
        }
        for (const error of nameErrors) {
            diagnostics.push({ type: 'warning', message: error, path: filePath });
        }
        // Still load the skill even with warnings (unless description is completely missing)
        if (!description || description.trim() === '') {
            return { skill: null, diagnostics };
        }
        return {
            skill: {
                name,
                description,
                filePath,
                baseDir: skillDir,
                source,
                disableModelInvocation: frontmatter['disable-model-invocation'] === true
            },
            diagnostics
        };
    }
    catch (error) {
        const message = error instanceof Error ? error.message : 'failed to parse skill file';
        diagnostics.push({ type: 'warning', message, path: filePath });
        return { skill: null, diagnostics };
    }
}
/** Expand a leading `~` to the user's home directory. */
function normalizePath(input) {
    const trimmed = input.trim();
    if (trimmed === '~')
        return homedir();
    if (trimmed.startsWith('~/'))
        return join(homedir(), trimmed.slice(2));
    if (trimmed.startsWith('~'))
        return join(homedir(), trimmed.slice(1));
    return trimmed;
}
/** Resolve a skill path against `cwd`, expanding `~` and respecting absolute paths. */
function resolveSkillPath(p, cwd) {
    const normalized = normalizePath(p);
    return isAbsolute(normalized) ? normalized : resolve(cwd, normalized);
}
/**
 * Load skills from all configured locations.
 * Returns skills and any validation diagnostics.
 */
export function loadSkills(options = {}) {
    const { cwd = process.cwd(), agentDir, skillPaths = [], includeDefaults = true } = options;
    // Resolve agentDir - if not provided, use default from config
    const resolvedAgentDir = agentDir ?? getAgentDir();
    const skillMap = new Map();
    const realPathSet = new Set();
    const allDiagnostics = [];
    const collisionDiagnostics = [];
    function addSkills(result) {
        allDiagnostics.push(...result.diagnostics);
        for (const skill of result.skills) {
            // Resolve symlinks to detect duplicate files
            let realPath;
            try {
                realPath = realpathSync(skill.filePath);
            }
            catch {
                realPath = skill.filePath;
            }
            // Skip silently if we've already loaded this exact file (via symlink)
            if (realPathSet.has(realPath)) {
                continue;
            }
            const existing = skillMap.get(skill.name);
            if (existing) {
                collisionDiagnostics.push({
                    type: 'collision',
                    message: `name "${skill.name}" collision`,
                    path: skill.filePath,
                    collision: {
                        resourceType: 'skill',
                        name: skill.name,
                        winnerPath: existing.filePath,
                        loserPath: skill.filePath
                    }
                });
            }
            else {
                skillMap.set(skill.name, skill);
                realPathSet.add(realPath);
            }
        }
    }
    if (includeDefaults) {
        addSkills(loadSkillsFromDirInternal(join(resolvedAgentDir, 'skills'), 'user', true));
        addSkills(loadSkillsFromDirInternal(resolve(cwd, CONFIG_DIR_NAME, 'skills'), 'project', true));
    }
    const userSkillsDir = join(resolvedAgentDir, 'skills');
    const projectSkillsDir = resolve(cwd, CONFIG_DIR_NAME, 'skills');
    const isUnderPath = (target, root) => {
        const normalizedRoot = resolve(root);
        if (target === normalizedRoot) {
            return true;
        }
        const prefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`;
        return target.startsWith(prefix);
    };
    const getSource = (resolvedPath) => {
        if (!includeDefaults) {
            if (isUnderPath(resolvedPath, userSkillsDir))
                return 'user';
            if (isUnderPath(resolvedPath, projectSkillsDir))
                return 'project';
        }
        return 'path';
    };
    for (const rawPath of skillPaths) {
        const resolvedPath = resolveSkillPath(rawPath, cwd);
        if (!existsSync(resolvedPath)) {
            allDiagnostics.push({
                type: 'warning',
                message: 'skill path does not exist',
                path: resolvedPath
            });
            continue;
        }
        try {
            const stats = statSync(resolvedPath);
            const source = getSource(resolvedPath);
            if (stats.isDirectory()) {
                addSkills(loadSkillsFromDirInternal(resolvedPath, source, true));
            }
            else if (stats.isFile() && resolvedPath.endsWith('.md')) {
                const result = loadSkillFromFile(resolvedPath, source);
                if (result.skill) {
                    addSkills({ skills: [result.skill], diagnostics: result.diagnostics });
                }
                else {
                    allDiagnostics.push(...result.diagnostics);
                }
            }
            else {
                allDiagnostics.push({
                    type: 'warning',
                    message: 'skill path is not a markdown file',
                    path: resolvedPath
                });
            }
        }
        catch (error) {
            const message = error instanceof Error ? error.message : 'failed to read skill path';
            allDiagnostics.push({ type: 'warning', message, path: resolvedPath });
        }
    }
    return {
        skills: Array.from(skillMap.values()),
        diagnostics: [...allDiagnostics, ...collisionDiagnostics]
    };
}
//# sourceMappingURL=skills.js.map