import { readdir, readFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { createSeedMcpDefaults } from "../../mcp/index.js";
import { nowIso, sanitizeText } from "../../shared/index.js";
export function describeSkillLoading(enabled) {
    return enabled ? "SEED skill loading is enabled for reporting and reusable workflows." : "SEED skill loading is disabled.";
}
export function skillRoots(directory, config) {
    const roots = [];
    if (config.skills.allow_project_skills) {
        roots.push({ path: path.join(directory, ".seed-agent", "skills"), source: "project" }, { path: path.join(directory, ".opencode", "skills"), source: "project" }, { path: path.join(directory, ".agents", "skills"), source: "project" });
    }
    if (config.skills.allow_user_skills) {
        const home = os.homedir();
        roots.push({ path: path.join(home, ".seed-agent", "skills"), source: "user" }, { path: path.join(home, ".opencode", "skills"), source: "user" }, { path: path.join(home, ".agents", "skills"), source: "user" }, { path: path.join(home, ".codex", "skills"), source: "user" }, { path: path.join(home, ".codex-remote", "skills"), source: "user" });
    }
    return roots;
}
export async function listSeedSkills(roots) {
    const descriptors = new Map();
    for (const root of roots) {
        try {
            for (const entry of await readdir(root.path, { withFileTypes: true })) {
                if (!entry.isDirectory())
                    continue;
                if (descriptors.has(entry.name))
                    continue;
                const file = path.join(root.path, entry.name, "SKILL.md");
                const parsed = await readSkillFile(file, entry.name).catch(() => undefined);
                descriptors.set(entry.name, {
                    name: parsed?.metadata.name ?? entry.name,
                    source: root.source,
                    path: file,
                    description: parsed?.metadata.description,
                    version: parsed?.metadata.version,
                    tags: parsed?.metadata.tags,
                    keywords: parsed?.metadata.keywords,
                    triggers: parsed?.metadata.triggers,
                    agents: parsed?.metadata.agents,
                    categories: parsed?.metadata.categories,
                });
            }
        }
        catch (error) {
            if (!isMissingFile(error))
                throw error;
        }
    }
    return [...descriptors.values()].sort((left, right) => left.name.localeCompare(right.name));
}
export async function loadSeedSkill(name, roots, inventory) {
    const safeName = safeSkillName(name);
    if (!safeName)
        return undefined;
    for (const root of roots) {
        const file = path.join(root.path, safeName, "SKILL.md");
        try {
            const parsed = await readSkillFile(file, safeName);
            const requirements = skillMcpRequirements(parsed.metadata);
            return {
                name: parsed.metadata.name ?? safeName,
                source: root.source,
                path: file,
                body: parsed.body,
                metadata: parsed.metadata,
                requirements,
                mcp_diagnostics: diagnoseSkillMcps(requirements, inventory),
            };
        }
        catch (error) {
            if (!isMissingFile(error))
                throw error;
        }
    }
    return undefined;
}
export async function validateSeedSkills(roots, inventory, input = {}) {
    const includeUser = input.includeUser ?? false;
    const filteredRoots = roots.filter((root) => includeUser || root.source === "project");
    const rootStatuses = await Promise.all(filteredRoots.map(rootStatus));
    const records = [];
    const findings = [];
    const requestedName = input.name ? safeSkillName(input.name) : undefined;
    if (input.name && !requestedName) {
        findings.push({
            severity: "error",
            code: "invalid_requested_skill_name",
            message: `Skill name is not safe to resolve: ${input.name}`,
            skill: input.name,
            action: "Use a relative skill directory name without path traversal.",
        });
        return {
            schema_version: 1,
            checked_at: nowIso(),
            status: "failed",
            scope: "skill",
            include_user: includeUser,
            roots: rootStatuses,
            skills: [],
            findings,
        };
    }
    for (const root of filteredRoots) {
        let entries;
        try {
            entries = await readdir(root.path, { withFileTypes: true });
        }
        catch (error) {
            if (isMissingFile(error))
                continue;
            throw error;
        }
        for (const entry of entries) {
            if (entry.name.startsWith("."))
                continue;
            if (!entry.isDirectory() && !entry.isSymbolicLink())
                continue;
            if (requestedName && entry.name !== requestedName)
                continue;
            records.push(await validateSkillEntry(root, entry.name, inventory));
        }
    }
    if (requestedName && records.length === 0 && findings.length === 0) {
        findings.push({
            severity: "error",
            code: "skill_not_found",
            message: `Skill was not found in the selected skill roots: ${requestedName}`,
            skill: requestedName,
            action: "Create SKILL.md under a project skill directory or enable user skill validation.",
        });
    }
    for (const finding of duplicateSkillFindings(records)) {
        for (const record of records.filter((skill) => normalizeSkillIdentity(skill.name) === finding.skill)) {
            record.findings.push(finding);
        }
    }
    const allFindings = [...findings, ...records.flatMap((record) => record.findings)];
    const status = allFindings.some((finding) => finding.severity === "error") ? "failed" : allFindings.length > 0 ? "warning" : "passed";
    return {
        schema_version: 1,
        checked_at: nowIso(),
        status,
        scope: requestedName ? "skill" : "all",
        include_user: includeUser,
        roots: rootStatuses,
        skills: records.sort((left, right) => left.path.localeCompare(right.path)),
        findings: allFindings,
    };
}
export async function recommendSeedSkills(roots, inventory, input) {
    const queryTokens = tokenize([input.query, input.targetAgent, input.category].filter(Boolean).join(" "));
    const queryText = normalizeForSearch(input.query);
    if (queryTokens.length === 0 && !input.targetAgent && !input.category)
        return [];
    const limit = Math.max(1, Math.min(input.limit ?? 5, 20));
    const skills = await loadSkillsForRecommendation(roots, inventory, input.includeUser ?? true);
    const recommendations = skills.map((skill) => {
        const scored = scoreSkill(skill, queryTokens, queryText, input);
        const requiredBlocked = skill.mcp_diagnostics.some((diagnostic) => diagnostic.required && diagnostic.status !== "available");
        if (requiredBlocked) {
            scored.score = Math.max(0, scored.score - 3);
            scored.reasons.push("required MCP unavailable");
        }
        return {
            name: skill.name,
            source: skill.source,
            path: skill.path,
            description: skill.metadata.description,
            version: skill.metadata.version,
            tags: skill.metadata.tags,
            keywords: skill.metadata.keywords,
            triggers: skill.metadata.triggers,
            agents: skill.metadata.agents,
            categories: skill.metadata.categories,
            score: scored.score,
            status: requiredBlocked ? "blocked" : "ready",
            reasons: unique(scored.reasons).slice(0, 8),
            requirements: skill.requirements,
            mcp_diagnostics: skill.mcp_diagnostics,
        };
    });
    return recommendations
        .filter((recommendation) => recommendation.score > 0)
        .sort((left, right) => right.score - left.score ||
        statusRank(left.status) - statusRank(right.status) ||
        left.name.localeCompare(right.name))
        .slice(0, limit);
}
export function createMcpInventory(config, cwd) {
    const inventory = {};
    for (const disabled of config.disabled_mcps) {
        inventory[disabled] = {
            status: "disabled",
            detail: `${disabled} is disabled by disabled_mcps.`,
            action: `Remove ${disabled} from disabled_mcps or choose a skill that does not require it.`,
        };
    }
    for (const [name, mcp] of Object.entries(createSeedMcpDefaults(config, cwd))) {
        if (inventory[name]?.status === "disabled")
            continue;
        inventory[name] = mcpInventoryEntry(name, mcp);
    }
    return inventory;
}
export function diagnoseSkillMcps(requirements, inventory) {
    const required = requirements.required.map((name) => mcpDiagnostic(name, true, inventory[name]));
    const optional = requirements.optional.map((name) => mcpDiagnostic(name, false, inventory[name]));
    return [...required, ...optional];
}
function mcpInventoryEntry(name, mcp) {
    if ("enabled" in mcp && mcp.enabled === false) {
        const diagnostic = "environment" in mcp ? mcp.environment?.SEED_MCP_DIAGNOSTIC : undefined;
        return {
            status: "unavailable",
            detail: diagnostic ?? `${name} is configured but not enabled.`,
            action: mcpAction(name),
        };
    }
    return {
        status: "available",
        detail: `${name} is configured and enabled.`,
    };
}
function mcpDiagnostic(name, required, entry) {
    if (!entry) {
        return {
            name,
            required,
            status: "missing",
            detail: `${name} is not registered in SEED MCP defaults for this workspace.`,
            action: `Add or enable MCP ${name} in .opencode config, or remove it from the skill metadata.`,
        };
    }
    return {
        name,
        required,
        ...entry,
    };
}
function mcpAction(name) {
    if (name === "websearch")
        return "Set TAVILY_API_KEY or EXA_API_KEY, or disable the skill requirement.";
    if (name === "grep_app")
        return "Set SEED_ENABLE_GREP_APP=1, or keep grep_app as optional.";
    if (name === "lsp")
        return "Build or install packages/lsp-tools-mcp so the local MCP CLI can be resolved.";
    if (name === "ast_grep")
        return "Build or install packages/ast-grep-mcp so the local MCP CLI can be resolved.";
    return `Enable MCP ${name} before using this skill.`;
}
async function readSkillFile(file, fallbackName) {
    const text = await readFile(file, "utf8");
    const { frontmatter, body } = splitFrontmatter(text);
    const metadata = frontmatter ? parseSkillMetadata(frontmatter) : {};
    if (!metadata.name)
        metadata.name = fallbackName;
    return { metadata, body: sanitizeText(body.trim()) };
}
async function validateSkillEntry(root, directoryName, inventory) {
    const file = path.join(root.path, directoryName, "SKILL.md");
    const findings = [];
    let metadata = { name: directoryName };
    let body = "";
    let hasFrontmatter = false;
    try {
        const text = await readFile(file, "utf8");
        const parsed = splitFrontmatter(text);
        hasFrontmatter = Boolean(parsed.frontmatter);
        metadata = parsed.frontmatter ? parseSkillMetadata(parsed.frontmatter) : {};
        body = sanitizeText(parsed.body.trim());
        if (!metadata.name)
            metadata.name = directoryName;
    }
    catch (error) {
        if (!isMissingFile(error))
            throw error;
        findings.push({
            severity: "error",
            code: "missing_skill_file",
            message: `Skill directory does not contain SKILL.md: ${directoryName}`,
            path: file,
            skill: directoryName,
            action: "Add SKILL.md or remove the empty skill directory.",
        });
    }
    const requirements = skillMcpRequirements(metadata);
    const mcpDiagnostics = diagnoseSkillMcps(requirements, inventory);
    const name = metadata.name ?? directoryName;
    if (!hasFrontmatter) {
        findings.push({
            severity: "warning",
            code: "missing_frontmatter",
            message: `Skill ${name} does not declare YAML frontmatter.`,
            path: file,
            skill: name,
            action: "Add frontmatter with at least name and description.",
        });
    }
    if (!isStableSkillName(name)) {
        findings.push({
            severity: "error",
            code: "invalid_skill_name",
            message: `Skill name is not a stable OpenCode skill identifier: ${name}`,
            path: file,
            skill: name,
            action: "Use lowercase letters, numbers, dots, underscores, and dashes.",
        });
    }
    if (name !== directoryName) {
        findings.push({
            severity: "warning",
            code: "name_directory_mismatch",
            message: `Skill metadata name ${name} differs from directory ${directoryName}.`,
            path: file,
            skill: name,
            action: "Keep the frontmatter name and directory name aligned unless this is intentional.",
        });
    }
    if (!metadata.description) {
        findings.push({
            severity: "warning",
            code: "missing_description",
            message: `Skill ${name} does not declare a description.`,
            path: file,
            skill: name,
            action: "Add a concise description so agents can choose the skill safely.",
        });
    }
    if (!body) {
        findings.push({
            severity: "error",
            code: "empty_skill_body",
            message: `Skill ${name} has an empty body.`,
            path: file,
            skill: name,
            action: "Add executable instructions or remove the skill.",
        });
    }
    for (const diagnostic of mcpDiagnostics) {
        if (diagnostic.status === "available")
            continue;
        findings.push({
            severity: diagnostic.required ? "error" : "warning",
            code: diagnostic.required ? "required_mcp_unavailable" : "optional_mcp_unavailable",
            message: `${diagnostic.required ? "Required" : "Optional"} MCP ${diagnostic.name} is ${diagnostic.status}: ${diagnostic.detail}`,
            path: file,
            skill: name,
            action: diagnostic.action,
        });
    }
    return {
        name,
        directory_name: directoryName,
        source: root.source,
        path: file,
        metadata,
        requirements,
        mcp_diagnostics: mcpDiagnostics,
        findings,
    };
}
async function rootStatus(root) {
    try {
        await readdir(root.path);
        return { ...root, exists: true };
    }
    catch (error) {
        if (isMissingFile(error))
            return { ...root, exists: false };
        throw error;
    }
}
function duplicateSkillFindings(records) {
    const byName = new Map();
    for (const record of records) {
        const key = normalizeSkillIdentity(record.name);
        byName.set(key, [...(byName.get(key) ?? []), record]);
    }
    const findings = [];
    for (const [name, matches] of byName) {
        if (matches.length < 2)
            continue;
        findings.push({
            severity: "error",
            code: "duplicate_skill_name",
            message: `Multiple skills declare the same name ${name}: ${matches.map((match) => match.path).join(", ")}`,
            skill: name,
            action: "Rename or remove duplicate skills so skill routing is unambiguous.",
        });
    }
    return findings;
}
function splitFrontmatter(text) {
    const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
    if (!match)
        return { body: text };
    return {
        frontmatter: match[1],
        body: text.slice(match[0].length),
    };
}
function parseSkillMetadata(frontmatter) {
    const metadata = {};
    const lines = frontmatter.split(/\r?\n/);
    for (let index = 0; index < lines.length; index += 1) {
        const line = lines[index] ?? "";
        if (!line.trim() || line.trim().startsWith("#"))
            continue;
        const match = line.match(/^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
        if (!match)
            continue;
        const key = match[1];
        const raw = match[2].trim();
        if (key === "mcp") {
            const nested = parseNestedMcp(lines, index + 1);
            metadata.mcp = nested;
            continue;
        }
        const value = parseScalarOrList(raw);
        if (key === "name" && typeof value === "string")
            metadata.name = value;
        if (key === "description" && typeof value === "string")
            metadata.description = value;
        if (key === "version" && typeof value === "string")
            metadata.version = value;
        if (key === "license" && typeof value === "string")
            metadata.license = value;
        if (key === "compatibility" && typeof value === "string")
            metadata.compatibility = value;
        if (key === "tags")
            metadata.tags = parseListValue(raw ? value : parseIndentedList(lines, index + 1, lineIndent(line)));
        if (key === "keywords")
            metadata.keywords = parseListValue(raw ? value : parseIndentedList(lines, index + 1, lineIndent(line)));
        if (key === "triggers")
            metadata.triggers = parseListValue(raw ? value : parseIndentedList(lines, index + 1, lineIndent(line)));
        if (key === "categories")
            metadata.categories = parseListValue(raw ? value : parseIndentedList(lines, index + 1, lineIndent(line)));
        if (key === "agents" || key === "target_agents") {
            metadata.agents = unique([...(metadata.agents ?? []), ...(parseListValue(raw ? value : parseIndentedList(lines, index + 1, lineIndent(line))) ?? [])]);
        }
        if (key === "required_mcps" || key === "mcp_required") {
            metadata.required_mcps = parseListValue(raw ? value : parseIndentedList(lines, index + 1, lineIndent(line)));
        }
        if (key === "optional_mcps" || key === "mcp_optional") {
            metadata.optional_mcps = parseListValue(raw ? value : parseIndentedList(lines, index + 1, lineIndent(line)));
        }
    }
    return metadata;
}
function parseNestedMcp(lines, start) {
    const result = {};
    for (let index = start; index < lines.length; index += 1) {
        const line = lines[index] ?? "";
        if (!line.startsWith(" ") && !line.startsWith("\t"))
            break;
        const match = line.trim().match(/^(required|optional)\s*:\s*(.*)$/);
        if (!match)
            continue;
        const raw = match[2].trim();
        const value = raw ? parseScalarOrList(raw) : parseIndentedList(lines, index + 1, lineIndent(line));
        result[match[1]] = parseListValue(value);
    }
    return result;
}
function skillMcpRequirements(metadata) {
    return {
        required: unique([...(metadata.required_mcps ?? []), ...(metadata.mcp?.required ?? [])]),
        optional: unique([...(metadata.optional_mcps ?? []), ...(metadata.mcp?.optional ?? [])]),
    };
}
function parseScalarOrList(raw) {
    const trimmed = unquote(raw.trim());
    if (!trimmed)
        return "";
    if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
        return trimmed
            .slice(1, -1)
            .split(",")
            .map((item) => unquote(item.trim()))
            .filter(Boolean);
    }
    return trimmed;
}
function parseListValue(value) {
    if (!value)
        return undefined;
    const values = Array.isArray(value) ? value : value.split(",");
    const cleaned = values.map((item) => unquote(item.trim())).filter(Boolean);
    return cleaned.length > 0 ? cleaned : undefined;
}
function parseIndentedList(lines, start, parentIndent) {
    const values = [];
    for (let index = start; index < lines.length; index += 1) {
        const line = lines[index] ?? "";
        const trimmed = line.trim();
        if (!trimmed || trimmed.startsWith("#"))
            continue;
        if (lineIndent(line) <= parentIndent)
            break;
        const item = trimmed.match(/^-\s*(.+)$/);
        if (item)
            values.push(item[1]);
    }
    return values;
}
function lineIndent(line) {
    return line.match(/^[\t ]*/)?.[0].length ?? 0;
}
function safeSkillName(name) {
    const safeName = name.replace(/^\//, "").trim();
    if (!safeName || safeName.includes("..") || path.isAbsolute(safeName))
        return undefined;
    return safeName;
}
function isStableSkillName(name) {
    return /^[a-z0-9][a-z0-9_.-]*$/.test(name);
}
function normalizeSkillIdentity(name) {
    return name.trim().toLowerCase();
}
async function loadSkillsForRecommendation(roots, inventory, includeUser) {
    const skills = new Map();
    for (const root of roots.filter((candidate) => includeUser || candidate.source === "project")) {
        let entries;
        try {
            entries = await readdir(root.path, { withFileTypes: true });
        }
        catch (error) {
            if (isMissingFile(error))
                continue;
            throw error;
        }
        for (const entry of entries) {
            if (entry.name.startsWith("."))
                continue;
            if (!entry.isDirectory() && !entry.isSymbolicLink())
                continue;
            const file = path.join(root.path, entry.name, "SKILL.md");
            try {
                const parsed = await readSkillFile(file, entry.name);
                const requirements = skillMcpRequirements(parsed.metadata);
                const skill = {
                    name: parsed.metadata.name ?? entry.name,
                    source: root.source,
                    path: file,
                    body: parsed.body,
                    metadata: parsed.metadata,
                    requirements,
                    mcp_diagnostics: diagnoseSkillMcps(requirements, inventory),
                };
                const key = normalizeSkillIdentity(skill.name);
                if (!skills.has(key))
                    skills.set(key, skill);
            }
            catch (error) {
                if (!isMissingFile(error))
                    throw error;
            }
        }
    }
    return [...skills.values()];
}
function scoreSkill(skill, queryTokens, queryText, input) {
    let score = 0;
    const reasons = [];
    const fields = [
        { label: "name", weight: 4, values: [skill.name] },
        { label: "description", weight: 2, values: [skill.metadata.description ?? ""] },
        { label: "keyword", weight: 5, values: skill.metadata.keywords ?? [] },
        { label: "trigger", weight: 6, values: skill.metadata.triggers ?? [] },
        { label: "tag", weight: 3, values: skill.metadata.tags ?? [] },
        { label: "category", weight: 4, values: skill.metadata.categories ?? [] },
    ];
    for (const field of fields) {
        const fieldTokens = tokenize(field.values.join(" "));
        for (const token of queryTokens) {
            if (!fieldTokens.includes(token))
                continue;
            score += field.weight;
            reasons.push(`matched ${field.label}:${token}`);
        }
    }
    for (const phrase of [...(skill.metadata.triggers ?? []), ...(skill.metadata.keywords ?? [])]) {
        const normalized = normalizeForSearch(phrase);
        if (normalized.length < 4 || !queryText.includes(normalized))
            continue;
        score += 4;
        reasons.push(`matched phrase:${phrase}`);
    }
    if (input.targetAgent && normalizedListIncludes(skill.metadata.agents, input.targetAgent)) {
        score += 8;
        reasons.push(`targets agent:${input.targetAgent}`);
    }
    if (input.category && normalizedListIncludes(skill.metadata.categories, input.category)) {
        score += 6;
        reasons.push(`targets category:${input.category}`);
    }
    return { score, reasons };
}
function normalizedListIncludes(values, needle) {
    const normalizedNeedle = normalizeForSearch(needle);
    return (values ?? []).some((value) => normalizeForSearch(value) === normalizedNeedle);
}
function tokenize(value) {
    const stop = new Set(["the", "and", "for", "with", "this", "that", "from", "into", "using", "use", "task"]);
    return unique(normalizeForSearch(value).split(/[^a-z0-9]+/).filter((token) => token.length >= 3 && !stop.has(token)));
}
function normalizeForSearch(value) {
    return value.toLowerCase().replace(/[_-]+/g, " ").trim();
}
function statusRank(status) {
    return status === "ready" ? 0 : 1;
}
function unique(values) {
    return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
}
function unquote(value) {
    if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) {
        return value.slice(1, -1);
    }
    return value;
}
function isMissingFile(error) {
    return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
}
//# sourceMappingURL=index.js.map