/**
 * Slash-skill discovery and parsing.
 *
 * Discovers SKILL.md files from Claude Code-compatible locations:
 *   - .claude/skills/<name>/SKILL.md  (project)
 *   - ~/.claude/skills/<name>/SKILL.md (personal)
 *   - .claude/commands/<name>.md       (legacy)
 *
 * Skills provide slash-command autocomplete and can inject instructions
 * into the agent's prompt when invoked via `/skill-name`.
 */
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { fileURLToPath } from "node:url";
import YAML from "yaml";
import { parseCustomDirectories as parseCustomDirsFromConfig } from "../agent/config-directories.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/** Built-in skills shipped with Bobbit in defaults/skills/ (copied to dist/server/defaults/skills/ at build time). */
const BUILTIN_SKILLS_DIR = path.join(__dirname, "..", "defaults", "skills");
/** Normalize the allowed-tools / allowed_tools field into a string[] (or undefined). */
function normalizeAllowedTools(fm) {
    const raw = fm["allowed-tools"] ?? fm.allowed_tools;
    if (raw == null)
        return undefined;
    if (Array.isArray(raw))
        return raw.map((s) => String(s).trim()).filter(Boolean);
    if (typeof raw === "string")
        return raw.split(/,\s*/).map((s) => s.trim()).filter(Boolean);
    return undefined;
}
/** Parse YAML frontmatter from a SKILL.md or command .md file. */
export function parseFrontmatter(raw) {
    const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
    if (!match)
        return { frontmatter: {}, content: raw };
    const yamlBlock = match[1];
    const content = match[2];
    try {
        const parsed = YAML.parse(yamlBlock);
        if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
            return { frontmatter: parsed, content };
        }
        return { frontmatter: {}, content };
    }
    catch (err) {
        console.warn(`[slash-skills] Failed to parse YAML frontmatter:`, err);
        return { frontmatter: {}, content: raw };
    }
}
/** Apply $ARGUMENTS, $ARGUMENTS[N], and $N substitutions. */
export function applySubstitutions(content, args) {
    // Split arguments by whitespace
    const argParts = args.trim() ? args.trim().split(/\s+/) : [];
    // Replace $ARGUMENTS[N] and $N (indexed)
    let result = content.replace(/\$ARGUMENTS\[(\d+)\]/g, (_, idx) => argParts[parseInt(idx)] ?? "");
    result = result.replace(/\$(\d+)/g, (_, idx) => argParts[parseInt(idx)] ?? "");
    // Replace $ARGUMENTS (full string)
    result = result.replace(/\$ARGUMENTS/g, args);
    return result;
}
/** Scan a directory for SKILL.md files (each in a subdirectory). */
function scanSkillDir(dir, source) {
    const skills = [];
    if (!fs.existsSync(dir))
        return skills;
    let entries;
    try {
        entries = fs.readdirSync(dir, { withFileTypes: true });
    }
    catch {
        return skills;
    }
    for (const entry of entries) {
        if (!entry.isDirectory())
            continue;
        const skillFile = path.join(dir, entry.name, "SKILL.md");
        if (!fs.existsSync(skillFile))
            continue;
        try {
            const raw = fs.readFileSync(skillFile, "utf-8");
            const { frontmatter, content: rawContent } = parseFrontmatter(raw);
            // Do NOT auto-inline @path/foo.md references. Claude Code uses Level-3
            // progressive disclosure — the agent reads referenced files on demand,
            // and the activation-header manifest tells it what's available.
            const content = rawContent;
            const name = frontmatter.name || entry.name;
            const description = frontmatter.description ||
                content.split("\n").find((l) => l.trim().length > 0)?.trim() || "";
            skills.push({
                name,
                description,
                argumentHint: frontmatter["argument-hint"],
                disableModelInvocation: frontmatter["disable-model-invocation"],
                userInvocable: frontmatter["user-invocable"],
                content,
                source,
                filePath: skillFile,
                allowedTools: normalizeAllowedTools(frontmatter),
                context: frontmatter.context,
                agent: frontmatter.agent,
            });
        }
        catch (err) {
            console.warn(`[slash-skills] Failed to parse ${skillFile}:`, err);
        }
    }
    return skills;
}
/** Scan legacy .claude/commands/ directory for .md files. */
function scanCommandsDir(dir) {
    const skills = [];
    if (!fs.existsSync(dir))
        return skills;
    let entries;
    try {
        entries = fs.readdirSync(dir, { withFileTypes: true });
    }
    catch {
        return skills;
    }
    for (const entry of entries) {
        if (!entry.isFile() || !entry.name.endsWith(".md"))
            continue;
        const filePath = path.join(dir, entry.name);
        const baseName = entry.name.replace(/\.md$/, "");
        try {
            const raw = fs.readFileSync(filePath, "utf-8");
            const { frontmatter, content: rawContent } = parseFrontmatter(raw);
            // Do NOT auto-inline @path/foo.md references — Level-3 progressive
            // disclosure (agent reads on demand).
            const content = rawContent;
            const name = frontmatter.name || baseName;
            const description = frontmatter.description ||
                content.split("\n").find((l) => l.trim().length > 0)?.trim() || "";
            skills.push({
                name,
                description,
                argumentHint: frontmatter["argument-hint"],
                disableModelInvocation: frontmatter["disable-model-invocation"],
                userInvocable: frontmatter["user-invocable"],
                content,
                source: "legacy",
                filePath,
                allowedTools: normalizeAllowedTools(frontmatter),
                context: frontmatter.context,
                agent: frontmatter.agent,
            });
        }
        catch (err) {
            console.warn(`[slash-skills] Failed to parse command ${filePath}:`, err);
        }
    }
    return skills;
}
/** Built-in slash commands that are always available. */
const BUILTIN_SKILLS = [
    {
        name: "compact",
        description: "Compact conversation context to reduce token usage",
        content: "",
        source: "built-in",
        filePath: "(built-in)",
    },
];
/** Parse custom skill directories from project config store (delegates to shared config-directories module). */
function parseCustomSkillDirectories(projectConfigStore) {
    if (!projectConfigStore)
        return [];
    return parseCustomDirsFromConfig(projectConfigStore)
        .filter(d => d.types.includes("skills"))
        .map(d => ({ path: d.path }));
}
/**
 * Get the complete list of directories scanned for slash skills.
 * Returns both default (built-in) and custom directories.
 */
export function getSkillDirectories(cwd, projectConfigStore) {
    const dirs = [
        { path: path.join(cwd, ".claude", "skills"), source: "project", isCustom: false },
        { path: path.join(cwd, ".bobbit", "skills"), source: "project", isCustom: false },
        { path: path.join(os.homedir(), ".claude", "skills"), source: "personal", isCustom: false },
        { path: path.join(os.homedir(), ".bobbit", "skills"), source: "personal", isCustom: false },
        { path: path.join(cwd, ".claude", "commands"), source: "legacy", isCustom: false },
    ];
    for (const entry of parseCustomSkillDirectories(projectConfigStore)) {
        dirs.push({ path: entry.path, source: "custom", isCustom: true });
    }
    return dirs;
}
// Simple TTL cache
let _cache = null;
const CACHE_TTL_MS = 5_000;
/**
 * Discover all slash skills for a given working directory.
 * Merges project, personal, legacy, custom, and built-in sources.
 * Priority (highest wins): project > bobbit project > personal > bobbit personal > legacy > custom > built-in.
 */
export function discoverSlashSkills(cwd, projectConfigStore) {
    const configVal = (projectConfigStore?.get("skill_directories") ?? "") + "|" + (projectConfigStore?.get("config_directories") ?? "");
    if (_cache && _cache.cwd === cwd && _cache.configVal === configVal && Date.now() - _cache.ts < CACHE_TTL_MS) {
        return _cache.skills;
    }
    const projectSkillsDir = path.join(cwd, ".claude", "skills");
    const personalSkillsDir = path.join(os.homedir(), ".claude", "skills");
    const bobbitProjectSkillsDir = path.join(cwd, ".bobbit", "skills");
    const bobbitPersonalSkillsDir = path.join(os.homedir(), ".bobbit", "skills");
    const legacyCommandsDir = path.join(cwd, ".claude", "commands");
    const projectSkills = scanSkillDir(projectSkillsDir, "project");
    const personalSkills = scanSkillDir(personalSkillsDir, "personal");
    const bobbitProjectSkills = scanSkillDir(bobbitProjectSkillsDir, "project");
    const bobbitPersonalSkills = scanSkillDir(bobbitPersonalSkillsDir, "personal");
    const legacyCommands = scanCommandsDir(legacyCommandsDir);
    // Scan custom directories
    const customSkills = [];
    for (const entry of parseCustomSkillDirectories(projectConfigStore)) {
        customSkills.push(...scanSkillDir(entry.path, "custom"));
    }
    // Scan builtin skills shipped with Bobbit (defaults/skills/)
    const builtinFileSkills = scanSkillDir(BUILTIN_SKILLS_DIR, "built-in");
    // Merge with priority (lowest to highest — later insertions overwrite):
    // built-in → builtin-file → custom → legacy → bobbit personal → claude personal → bobbit project → claude project
    const byName = new Map();
    for (const skill of BUILTIN_SKILLS)
        byName.set(skill.name, skill);
    for (const skill of builtinFileSkills)
        byName.set(skill.name, skill);
    for (const skill of customSkills)
        byName.set(skill.name, skill);
    for (const skill of legacyCommands)
        byName.set(skill.name, skill);
    for (const skill of bobbitPersonalSkills)
        byName.set(skill.name, skill);
    for (const skill of personalSkills)
        byName.set(skill.name, skill);
    for (const skill of bobbitProjectSkills)
        byName.set(skill.name, skill);
    for (const skill of projectSkills)
        byName.set(skill.name, skill);
    // Filter to user-invocable skills only (default is true)
    const skills = Array.from(byName.values()).filter((s) => s.userInvocable !== false);
    // Sort alphabetically
    skills.sort((a, b) => a.name.localeCompare(b.name));
    _cache = { skills, cwd, configVal, ts: Date.now() };
    return skills;
}
/** Look up a single slash skill by name. */
export function getSlashSkill(cwd, name, projectConfigStore) {
    return discoverSlashSkills(cwd, projectConfigStore).find((s) => s.name === name);
}
/**
 * Build the prompt text to inject when a slash skill is invoked.
 * Applies argument substitutions and returns the processed content.
 * If $ARGUMENTS is not present in the content, appends "ARGUMENTS: <args>" at the end.
 */
export function buildSlashSkillPrompt(skill, args) {
    let content = skill.content;
    if (args.trim()) {
        const hasArgsPlaceholder = /\$ARGUMENTS|\$\d+/.test(content);
        content = applySubstitutions(content, args);
        if (!hasArgsPlaceholder) {
            content += `\n\nARGUMENTS: ${args}`;
        }
    }
    return content;
}
//# sourceMappingURL=slash-skills.js.map