// ---------------------------------------------------------------------------
// skill-create-routes.ts -- Skill creation & project layout detection
// ---------------------------------------------------------------------------
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmSync } from "node:fs";
import { join, basename, resolve, sep } from "node:path";
import { spawnSync } from "node:child_process";
import { isSkillCreatorInstalled } from "../utils/skill-creator-detection.js";
import { sendJson, readBody } from "./router.js";
import { detectAvailableProviders, parseSkillFrontmatter, parseGithubRemote, walkUpForGitRoot } from "./api-routes.js";
import { initSSE, sendSSE, sendSSEDone } from "./sse-helpers.js";
import { classifyError } from "./error-classifier.js";
import { writeHistoryEntry } from "../eval/benchmark-history.js";
import { getAgentCreationProfile } from "../agents/agents-registry.js";
import { generateSkill } from "../core/skill-generator.js";
import { extractFrontmatterVersion, bumpPatch } from "../utils/version.js";
import { submitSkillUpdateEvent } from "./platform-proxy.js";
const VALID_ENGINES = new Set([
    "vskill",
    "anthropic-skill-creator",
    "none",
]);
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const EXCLUDE_DIRS = new Set([
    "skills", "plugins", "node_modules", ".git", ".specweave",
    "dist", "evals", ".claude", ".cursor", ".agents", "coverage",
]);
/** List subdirs of a skills/ dir that contain SKILL.md */
function listSkillDirs(skillsDir) {
    if (!existsSync(skillsDir))
        return [];
    try {
        return readdirSync(skillsDir, { withFileTypes: true })
            .filter((d) => d.isDirectory() && existsSync(join(skillsDir, d.name, "SKILL.md")))
            .map((d) => d.name);
    }
    catch {
        return [];
    }
}
export function detectProjectGitHubStatus(root) {
    const gitRoot = walkUpForGitRoot(root);
    if (!gitRoot) {
        return { hasGit: false, githubOrigin: null, status: "no-git" };
    }
    let originUrl = null;
    try {
        const result = spawnSync("git", ["-C", gitRoot, "remote", "get-url", "origin"], {
            timeout: 250,
            stdio: ["ignore", "pipe", "pipe"],
            encoding: "utf8",
        });
        if (result.status === 0 && typeof result.stdout === "string") {
            originUrl = result.stdout.trim() || null;
        }
    }
    catch {
        originUrl = null;
    }
    const githubOrigin = parseGithubRemote(originUrl);
    if (githubOrigin) {
        return { hasGit: true, githubOrigin, status: "github" };
    }
    return { hasGit: true, githubOrigin: null, status: "non-github" };
}
/** Detect project layout — mirrors scanSkills() logic from skill-scanner.ts */
// TODO(0670 T-001): relocate to src/core/project-layout.ts in a follow-up
// increment. Exported here (approach b) so src/core/skill-generator.ts can
// import without the full-move diff.
export function detectProjectLayout(root) {
    const layouts = [];
    const allSkills = [];
    // Layout 4: Self — root IS the skill
    if (existsSync(join(root, "SKILL.md"))) {
        layouts.push({
            layout: 4,
            label: "Self (root is the skill)",
            pathTemplate: `${root}/SKILL.md`,
            existingPlugins: [],
        });
    }
    // Layout 3: Root skills/ directory
    const rootSkillsDir = join(root, "skills");
    if (existsSync(rootSkillsDir)) {
        const skills = listSkillDirs(rootSkillsDir);
        const pluginName = basename(root) || "default";
        layouts.push({
            layout: 3,
            label: "Root skills/",
            pathTemplate: "{root}/skills/{skill}/",
            existingPlugins: [pluginName],
        });
        for (const s of skills)
            allSkills.push({ plugin: pluginName, skill: s });
    }
    // Layout 1: Direct plugin dirs — {root}/{plugin}/skills/{skill}/
    const directPlugins = [];
    try {
        const entries = readdirSync(root, { withFileTypes: true })
            .filter((d) => d.isDirectory() && !EXCLUDE_DIRS.has(d.name) && !d.name.startsWith("."));
        for (const entry of entries) {
            const skillsPath = join(root, entry.name, "skills");
            if (existsSync(skillsPath)) {
                directPlugins.push(entry.name);
                const skills = listSkillDirs(skillsPath);
                for (const s of skills)
                    allSkills.push({ plugin: entry.name, skill: s });
            }
        }
    }
    catch { /* ignore */ }
    if (directPlugins.length > 0) {
        layouts.push({
            layout: 1,
            label: "Direct plugins",
            pathTemplate: "{root}/{plugin}/skills/{skill}/",
            existingPlugins: directPlugins,
        });
    }
    // Layout 2: Nested plugins/ dir — {root}/plugins/{plugin}/skills/{skill}/
    const pluginsDir = join(root, "plugins");
    if (existsSync(pluginsDir)) {
        const nestedPlugins = [];
        try {
            const entries = readdirSync(pluginsDir, { withFileTypes: true })
                .filter((d) => d.isDirectory());
            for (const entry of entries) {
                const skillsPath = join(pluginsDir, entry.name, "skills");
                if (existsSync(skillsPath)) {
                    nestedPlugins.push(entry.name);
                    const skills = listSkillDirs(skillsPath);
                    for (const s of skills)
                        allSkills.push({ plugin: entry.name, skill: s });
                }
            }
        }
        catch { /* ignore */ }
        if (nestedPlugins.length > 0) {
            layouts.push({
                layout: 2,
                label: "Nested plugins/",
                pathTemplate: "{root}/plugins/{plugin}/skills/{skill}/",
                existingPlugins: nestedPlugins,
            });
        }
    }
    // Suggestion priority: Layout 2 > Layout 1 > Layout 3
    let suggestedLayout = 3;
    if (layouts.find((l) => l.layout === 2))
        suggestedLayout = 2;
    else if (layouts.find((l) => l.layout === 1))
        suggestedLayout = 1;
    return { root, detectedLayouts: layouts, suggestedLayout, existingSkills: allSkills };
}
/**
 * Build SKILL.md content from form fields.
 *
 * Frontmatter shape is aligned with the canonical agentskills.io specification
 * (https://agentskills.io/specification). In particular, `tags` and
 * `target-agents` are emitted under a `metadata:` block — NEVER at the top
 * level. See 0679-skills-spec-compliance for the increment that introduced
 * this shape and the golden-file guardrails.
 *
 * Key order is stabilized: name → description → allowed-tools → model → metadata.
 *
 * 0679 F-005: `name:` is emitted at the top level when `data.name` is set.
 * The body's `# /<name>` heading remains as a human-readable signpost; the
 * frontmatter `name:` is what spec-aware tooling and validators consume.
 */
function buildSkillMd(data) {
    const lines = ["---"];
    if (data.name?.trim()) {
        lines.push(`name: ${data.name.trim()}`);
    }
    // Description — always quote to handle special chars.
    // Newlines inside descriptions break YAML parsers, so collapse them to a
    // single space before escaping double quotes.
    const safeDescription = data.description
        .replace(/[\r\n]+/g, " ")
        .replace(/"/g, '\\"');
    lines.push(`description: "${safeDescription}"`);
    // 0728: Version is ALWAYS emitted. Defaults to "1.0.0" so no caller can
    // produce a versionless SKILL.md. Explicit values from callers win.
    const resolvedVersion = data.version?.trim() || "1.0.0";
    lines.push(`version: "${resolvedVersion}"`);
    if (data.allowedTools?.trim()) {
        lines.push(`allowed-tools: ${data.allowedTools.trim()}`);
    }
    if (data.model) {
        lines.push(`model: ${data.model}`);
    }
    // Spec-compliant metadata block — tags and target-agents live HERE, not at root.
    const hasTags = Array.isArray(data.tags) && data.tags.length > 0;
    const hasAgents = Array.isArray(data.targetAgents) && data.targetAgents.length > 0;
    // 0734: persist authoring engine in metadata so future updates know how the
    // skill was produced. "none" → omit entirely (caller authored raw).
    const hasEngine = data.engine === "vskill" || data.engine === "anthropic-skill-creator";
    if (hasTags || hasAgents || hasEngine) {
        lines.push("metadata:");
        if (hasEngine) {
            lines.push(`  engine: ${data.engine}`);
        }
        if (hasTags) {
            lines.push("  tags:");
            for (const t of data.tags)
                lines.push(`    - ${t}`);
        }
        if (hasAgents) {
            lines.push("  target-agents:");
            for (const a of data.targetAgents)
                lines.push(`    - ${a}`);
        }
    }
    lines.push("---");
    lines.push("");
    if (data.body.trim()) {
        lines.push(data.body.trim());
    }
    else {
        lines.push(`# /${data.name}`);
        lines.push("");
        lines.push("You are a helpful assistant. Describe what this skill does.");
    }
    return lines.join("\n") + "\n";
}
/**
 * Public alias of the private `buildSkillMd` emitter for tests and lint scripts.
 * The underlying function is the source of truth; this export exists so
 * golden-file tests and the CI validator can exercise the emitter without
 * touching the HTTP route.
 *
 * @internal — tests must import from `__tests__/helpers/skill-md-test-helpers.ts`,
 *   not from this module. Production code MUST NOT call this.
 */
export function buildSkillMdForTest(data) {
    return buildSkillMd(data);
}
/**
 * Minimal YAML frontmatter parser for test assertions — supports the shape
 * emitted by `buildSkillMd`:
 *   - top-level scalars (quoted or unquoted)
 *   - `metadata:` block with `tags:` and `target-agents:` list children
 *
 * This is intentionally narrow. For general YAML, callers should use a real
 * parser. Exported solely to avoid adding a YAML dep to the test target.
 *
 * @internal — production reads MUST go through `parseSkillFrontmatter`
 *   in api-routes.ts. Tests must import this via
 *   `__tests__/helpers/skill-md-test-helpers.ts`.
 */
export function parseFrontmatterForTest(content) {
    const match = content.match(/^---\n([\s\S]*?)\n---/);
    if (!match)
        return { metadata: {} };
    const body = match[1];
    const rootOut = {};
    const metadataOut = {};
    const lines = body.split("\n");
    let i = 0;
    let inMetadata = false;
    let currentListKey = null;
    let currentList = null;
    let currentListScope = null;
    const flushList = () => {
        if (currentListKey && currentList) {
            if (currentListScope === "metadata")
                metadataOut[currentListKey] = currentList;
            else
                rootOut[currentListKey] = currentList;
        }
        currentListKey = null;
        currentList = null;
        currentListScope = null;
    };
    while (i < lines.length) {
        const raw = lines[i];
        if (raw.trim() === "") {
            i++;
            continue;
        }
        // Metadata nested list item: `    - value`
        if (/^ {4}- /.test(raw) && currentList && currentListScope === "metadata") {
            currentList.push(raw.replace(/^ {4}- /, "").trim());
            i++;
            continue;
        }
        // Top-level list item: `  - value`
        if (/^ {2}- /.test(raw) && currentList && currentListScope === "root") {
            currentList.push(raw.replace(/^ {2}- /, "").trim());
            i++;
            continue;
        }
        // Metadata child key: `  key:` or `  key: value`
        const metaChildMatch = raw.match(/^ {2}([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
        if (inMetadata && metaChildMatch) {
            flushList();
            const key = metaChildMatch[1];
            const value = metaChildMatch[2];
            if (value === "") {
                currentListKey = key;
                currentList = [];
                currentListScope = "metadata";
            }
            else {
                metadataOut[key] = stripQuotes(value);
            }
            i++;
            continue;
        }
        // Top-level key: `key:` or `key: value`
        const topMatch = raw.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
        if (topMatch) {
            flushList();
            const key = topMatch[1];
            const value = topMatch[2];
            if (key === "metadata") {
                inMetadata = true;
                i++;
                continue;
            }
            inMetadata = false;
            if (value === "") {
                currentListKey = key;
                currentList = [];
                currentListScope = "root";
            }
            else {
                rootOut[key] = stripQuotes(value);
            }
            i++;
            continue;
        }
        i++;
    }
    flushList();
    return Object.assign(rootOut, { metadata: metadataOut });
}
function stripQuotes(v) {
    if (v.length >= 2 && v.startsWith('"') && v.endsWith('"'))
        return v.slice(1, -1);
    return v;
}
function extractMessages(res) {
    const raw = (res.stderr && res.stderr.trim()) || (res.stdout && res.stdout.trim()) || "";
    if (!raw)
        return [`skills-ref exited with code ${res.status ?? "unknown"}`];
    return raw
        .split("\n")
        .map((line) => line.trim())
        .filter((line) => line.length > 0);
}
/**
 * Interpret a `spawnSync("skills-ref", ["validate", path])` result into a
 * structured outcome. Pure function — no side effects, no I/O.
 */
export function interpretValidatorResult(skillPath, res, opts) {
    // Missing binary — always non-blocking (graceful degradation).
    const errCode = res.error?.code;
    if (errCode === "ENOENT") {
        return {
            ok: true,
            exitCode: 0,
            kind: "missing-binary",
            messages: ["Install `skills-ref` to enable spec validation: `npm i -g skills-ref`"],
            skillPath,
        };
    }
    // Happy path.
    if (res.status === 0) {
        return { ok: true, exitCode: 0, kind: "success", messages: [], skillPath };
    }
    // Non-zero exit.
    const messages = extractMessages(res);
    if (opts.strict) {
        return { ok: false, exitCode: 1, kind: "error", messages, skillPath };
    }
    return { ok: true, exitCode: 0, kind: "warning", messages, skillPath };
}
/**
 * Render a `ValidatorOutcome` to a human-readable string for CLI / server
 * log surfaces. Empty string on success (silent). Pure function.
 *
 * Color handling is deliberately omitted here — callers that want ANSI
 * colors wrap the returned string with their own formatter. That keeps the
 * helper trivially testable.
 */
export function formatValidatorReport(outcome) {
    switch (outcome.kind) {
        case "success":
            return "";
        case "missing-binary":
            return outcome.messages[0] + "\n";
        case "warning": {
            const lines = [
                `Validation warnings for ${outcome.skillPath}:`,
                ...outcome.messages.map((m) => `  - ${m}`),
                "",
                "Skill file remains on disk. Spec: https://agentskills.io/specification",
            ];
            return lines.join("\n") + "\n";
        }
        case "error": {
            const lines = [
                `Validation failed for ${outcome.skillPath}:`,
                ...outcome.messages.map((m) => `  - ${m}`),
                "",
                "Skill file remains on disk. Re-run without --strict to warn instead of block.",
            ];
            return lines.join("\n") + "\n";
        }
    }
}
/** Compute target directory for a new skill based on layout */
// ---------------------------------------------------------------------------
// 0815: path safety + .env.example helpers for multi-file skill creation.
//
// resolveSafe rejects any path that would escape the skill directory: absolute
// paths, drive letters, '..' segments, and relative paths whose resolved form
// lands outside `base`. Used by POST /api/skills/create when looping the
// optional `files` map. Throws so the route returns 400.
// ---------------------------------------------------------------------------
export function resolveSafe(base, relPath) {
    if (typeof relPath !== "string" || relPath.length === 0) {
        throw new Error(`resolveSafe: empty path`);
    }
    // Reject absolute paths (POSIX) and Windows drive letters.
    if (relPath.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(relPath)) {
        throw new Error(`resolveSafe: refused absolute path '${relPath}'`);
    }
    // Reject any '..' segment — easier to reason about than relying on resolve()
    // alone, since a benign-looking resolve result can still hide a traversal
    // depending on the realpath of `base`.
    const segments = relPath.split(/[/\\]+/);
    if (segments.some((s) => s === "..")) {
        throw new Error(`resolveSafe: refused traversal in '${relPath}'`);
    }
    const resolved = resolve(base, relPath);
    const baseResolved = resolve(base);
    // Defense in depth: ensure the resolved path is still inside base.
    if (resolved !== baseResolved && !resolved.startsWith(baseResolved + sep)) {
        throw new Error(`resolveSafe: path '${relPath}' escapes base`);
    }
    return resolved;
}
/**
 * Build a `.env.example` body from a list of secret env-var names. Each line
 * is `NAME=` with no value — the file ships placeholders only, never real
 * secrets. Caller is responsible for including a leading comment if desired.
 */
export function buildEnvExample(secrets) {
    const lines = [
        "# .env.example — populated by the skill author, never committed.",
        "# Copy to .env.local in this directory and fill in real values.",
        "",
    ];
    for (const name of secrets) {
        if (typeof name !== "string" || name.length === 0)
            continue;
        lines.push(`${name}=`);
    }
    return lines.join("\n") + "\n";
}
function computeSkillDir(root, layout, plugin, name) {
    switch (layout) {
        case 1: return join(root, plugin, "skills", name);
        case 2: return join(root, "plugins", plugin, "skills", name);
        case 3: return join(root, "skills", name);
    }
}
/**
 * Check if a resolved path is strictly within a root directory.
 * Uses trailing separator to prevent prefix collision (e.g., /project vs /project-evil).
 */
export function isDraftWithinRoot(draftPath, root) {
    const resolvedRoot = resolve(root) + sep;
    const resolvedDraft = resolve(draftPath);
    return resolvedDraft.startsWith(resolvedRoot) && resolvedDraft !== resolve(root);
}
/**
 * Match a newly generated skill against existing plugins by tag/keyword overlap.
 * Returns the best-matching plugin if score exceeds threshold, or null.
 */
export function matchExistingPlugin(skillName, skillDescription, skillTags, root) {
    const layout = detectProjectLayout(root);
    const pluginSkills = new Map();
    // Gather existing plugin metadata from detected layouts
    for (const detected of layout.detectedLayouts) {
        if (detected.layout !== 1 && detected.layout !== 2)
            continue;
        for (const pluginName of detected.existingPlugins) {
            if (pluginSkills.has(pluginName))
                continue;
            const tags = new Set();
            const keywords = new Set();
            // Read SKILL.md frontmatter from each skill in this plugin
            const skillsDirPath = detected.layout === 1
                ? join(root, pluginName, "skills")
                : join(root, "plugins", pluginName, "skills");
            const skillDirs = listSkillDirs(skillsDirPath);
            for (const skillDir of skillDirs) {
                const mdPath = join(skillsDirPath, skillDir, "SKILL.md");
                try {
                    const content = readFileSync(mdPath, "utf-8");
                    // 0679 F-001: route through parseSkillFrontmatter so the canonical
                    // metadata-nested tags shape (per agentskills.io/specification) is
                    // honored. The legacy regex (`^---[\s\S]*?tags:\s*(.+)`) only saw
                    // top-level `tags:` and silently produced empty tags for newly
                    // emitted SKILL.md files — a silent plugin-matching regression.
                    const fm = parseSkillFrontmatter(content);
                    const fmTags = fm.tags;
                    const tagList = Array.isArray(fmTags)
                        ? fmTags.filter((t) => typeof t === "string")
                        : (typeof fmTags === "string"
                            ? fmTags.split(",")
                            : []);
                    for (const raw of tagList) {
                        const t = raw.trim().toLowerCase();
                        if (t)
                            tags.add(t);
                    }
                    const fmDesc = fm.description;
                    if (typeof fmDesc === "string") {
                        for (const w of fmDesc.toLowerCase().split(/\s+/).filter((w) => w.length > 3)) {
                            keywords.add(w);
                        }
                    }
                }
                catch { /* skip unreadable */ }
            }
            // Also add plugin name segments as keywords
            for (const seg of pluginName.split(/[-_.]/).filter(Boolean)) {
                keywords.add(seg.toLowerCase());
            }
            pluginSkills.set(pluginName, { layout: detected.layout, tags, keywords });
        }
    }
    if (pluginSkills.size === 0)
        return null;
    const inputTags = new Set(skillTags.map((t) => t.toLowerCase()));
    const inputKeywords = new Set(skillDescription.toLowerCase().split(/\s+/).filter((w) => w.length > 3));
    // Add skill name segments
    for (const seg of skillName.split(/[-_.]/).filter(Boolean)) {
        inputKeywords.add(seg.toLowerCase());
    }
    let bestPlugin = null;
    let bestScore = 0;
    let bestLayout = 1;
    let bestReason = "";
    for (const [pluginName, data] of pluginSkills) {
        let score = 0;
        const matchingTags = [];
        const matchingKeywords = [];
        // Tag overlap (weighted heavily)
        for (const tag of inputTags) {
            if (data.tags.has(tag)) {
                score += 3;
                matchingTags.push(tag);
            }
        }
        // Keyword overlap
        for (const kw of inputKeywords) {
            if (data.keywords.has(kw)) {
                score += 1;
                matchingKeywords.push(kw);
            }
        }
        // Plugin name similarity: shared segments (e.g. "browser-automation" ↔ "browser-screenshot")
        const pluginSegs = new Set(pluginName.toLowerCase().split(/[-_.]/).filter(Boolean));
        const skillSegs = new Set(skillName.toLowerCase().split(/[-_.]/).filter(Boolean));
        let sharedSegs = 0;
        for (const seg of skillSegs) {
            if (pluginSegs.has(seg))
                sharedSegs++;
        }
        if (sharedSegs > 0)
            score += sharedSegs * 2;
        if (score > bestScore) {
            bestScore = score;
            bestPlugin = pluginName;
            bestLayout = data.layout;
            const parts = [];
            if (matchingTags.length > 0)
                parts.push(`${matchingTags.length} matching tags: ${matchingTags.join(", ")}`);
            if (matchingKeywords.length > 0)
                parts.push(`${matchingKeywords.length} keyword overlaps`);
            bestReason = parts.join("; ") || "name similarity";
        }
    }
    if (!bestPlugin || bestScore < 2)
        return null;
    const confidence = bestScore >= 6 ? "high" : bestScore >= 3 ? "medium" : "low";
    return {
        plugin: bestPlugin,
        layout: bestLayout,
        confidence,
        reason: bestReason,
    };
}
// ---------------------------------------------------------------------------
// AI skill generation
// ---------------------------------------------------------------------------
// Shared best practices block used by both body and eval prompts
const SKILL_STUDIO_BEST_PRACTICES = `## Skill Studio Best Practices

### SKILL.md Anatomy
Every skill has YAML frontmatter (description required, name/model/allowed-tools optional) and a markdown body with instructions.

### Description Quality (Frontmatter) — CRITICAL
The description is the PRIMARY triggering mechanism. It must be "pushy" to combat undertriggering:
- Use third-person format: "This skill should be used when the user asks to..."
- Include specific trigger phrases users would say (e.g., "create X", "configure Y", "fix Z")
- Include explicit activation phrases: "Make sure to use this skill whenever the user mentions..."
- Be concrete and specific, not vague or generic
- BAD: "Provides guidance for working with X" (vague, no triggers)
- GOOD: "This skill should be used when the user asks to \\"create X\\", \\"configure Y\\", or \\"troubleshoot Z\\". Activate whenever the user mentions X-related tasks."

### Writing Style
- Use imperative/infinitive form (verb-first instructions), NOT second person
- Use objective, instructional language: "To accomplish X, do Y" not "You should do X"
- BAD: "You should start by reading the file" / "You need to validate"
- GOOD: "Start by reading the file" / "Validate the input before processing"
- Explain the WHY behind rules — LLMs respond better to reasoning than rigid MUST/NEVER

### Progressive Disclosure
- Keep SKILL.md lean: 500-2000 words ideal for body, under 500 lines total
- Core concepts and essential procedures in the body
- Structure with ## sections: Workflow, Rules, Output Format, Examples

### Content Quality
- Focus on procedural knowledge non-obvious to an AI assistant
- Include information that helps another AI instance execute tasks effectively
- Include concrete examples where helpful
- Include a clear Workflow section with numbered steps

### Common Mistakes to Avoid
- Weak trigger descriptions (vague, no specific phrases)
- Too much content without structure
- Second-person writing style
- Missing workflow section
- Overly generic instructions that don't add value

### Action-First Skills (Bash/tool-execution)
When a skill includes allowed-tools: Bash (or Read, Write, Edit), the skill body MUST:
- Open with an explicit execution directive: "Execute each step immediately. Run Bash commands directly — do not ask for permission or describe plans."
- Use "Step N — [action verb]. Run this immediately:" headers, not "Step N: [noun] Discovery"
- Every step must end with a concrete, complete, copy-pasteable code block — not prose describing what the code would do
- Include any variable values (paths, URLs, flags) directly in the code block, not as placeholders`;
// TODO(0670 T-001): relocate to src/core/skill-prompts.ts in a follow-up
// increment. Exported here (approach b) so src/core/skill-generator.ts can
// import without the full-move diff.
export const BODY_SYSTEM_PROMPT = `You are an expert AI skill engineer in Skill Studio. Given a user's description of what a skill should do, generate the SKILL.md body and metadata (NOT evals).

${SKILL_STUDIO_BEST_PRACTICES}

## Output Format
Return a JSON object with these fields:
{
  "name": "kebab-case-name",
  "description": "Third-person trigger description with specific activation phrases",
  "model": "",
  "allowedTools": "",
  "body": "# /skill-name\\n\\nFull system prompt with ## sections"
}

Field rules:
- name: kebab-case, concise, descriptive (e.g., "sql-formatter", "api-docs-generator")
- description: 1-3 sentences with trigger phrases. Must pass the "would Claude trigger on this?" test
- model: "" (any) unless task clearly requires opus-level reasoning or is trivially haiku-suitable
- allowedTools: comma-separated list (e.g., "Read, Write, Edit, Bash") or "" for unrestricted. Only restrict when the skill genuinely shouldn't use certain tools
- body: Complete markdown starting with # /skill-name, structured with ## sections, 500-2000 words

Return ONLY the JSON object — no code fences, no preamble.

After the JSON, on a new line, write "---REASONING---" followed by a brief explanation of your design choices (why this name, why these trigger phrases, what Skill Studio rules you applied).`;
// TODO(0670 T-001): relocate to src/core/skill-prompts.ts.
export const EVAL_SYSTEM_PROMPT = `You are an expert AI skill evaluator. Given a skill's name, description, and purpose, generate eval test cases that verify the skill works correctly.

### Eval Assertions for Action-Oriented Skills (CRITICAL)
The eval evaluates the LLM text response — it cannot run Bash or call tools. Assertions must check for code/commands present IN the response, not whether they were executed.
- BAD: "Runs a bash command to discover profiles" (implies execution — will always fail)
- GOOD: "Response includes a bash code block that lists Chrome profile directories"
- BAD: "Opens https://studio.youtube.com as the target URL"
- GOOD: "Response includes studio.youtube.com as the target URL in a code block or command"
- BAD: "Checks that the file exists before uploading"
- GOOD: "Response includes a bash command checking whether the file exists (e.g., using test -f or ls)"

### Assertion Quality: Functional Over Formatting (CRITICAL)
Assert on FUNCTIONAL correctness, not formatting or presentation details. Each assertion should test exactly ONE observable behavior (unit-test style).
- NEVER assert on: blank lines, paragraph count, whitespace, exact heading levels, bullet formatting, sentence count, or line breaks
- GOOD: "The response includes a greeting that contains the name 'Anton'" (checks functional behavior)
- BAD: "The greeting is a single short sentence (not multiple paragraphs)" (tests formatting, not function)
- GOOD: "The response lists at least 3 benefits of TypeScript" (checks content)
- BAD: "The response uses exactly 3 bullet points" (tests formatting)
Formatting is stylistic — it varies between LLM runs and does not indicate skill quality.

## Output Format
Return a JSON object with these fields:
{
  "evals": [
    {
      "id": 1,
      "name": "test case name",
      "prompt": "realistic user prompt",
      "expected_output": "description of correct behavior",
      "assertions": [
        { "id": "a1", "text": "objectively verifiable assertion", "type": "boolean" }
      ]
    }
  ]
}

Field rules:
- evals: 2-3 realistic test cases with objectively verifiable assertions
- Prompts should be what real users would say, not abstract test inputs
- Each assertion must be independently verifiable by a judge LLM reading the response text
- Each assertion should check exactly ONE functional behavior (unit-test granularity)

Return ONLY the JSON object — no code fences, no preamble.`;
// Keep the monolithic prompt for backward compatibility (used nowhere after refactor but exported for tests)
const GENERATE_SYSTEM_PROMPT = `You are an expert AI skill engineer in Skill Studio. Given a user's description of what a skill should do, generate a complete, production-quality skill definition.

${SKILL_STUDIO_BEST_PRACTICES}

### Eval Assertions for Action-Oriented Skills (CRITICAL)
The eval evaluates the LLM text response — it cannot run Bash or call tools. Assertions must check for code/commands present IN the response, not whether they were executed.
- BAD: "Runs a bash command to discover profiles" (implies execution — will always fail)
- GOOD: "Response includes a bash code block that lists Chrome profile directories"
- BAD: "Opens https://studio.youtube.com as the target URL"
- GOOD: "Response includes studio.youtube.com as the target URL in a code block or command"
- BAD: "Checks that the file exists before uploading"
- GOOD: "Response includes a bash command checking whether the file exists (e.g., using test -f or ls)"

## Output Format
Return a JSON object with these fields:
{
  "name": "kebab-case-name",
  "description": "Third-person trigger description with specific activation phrases",
  "model": "",
  "allowedTools": "",
  "body": "# /skill-name\\n\\nFull system prompt with ## sections",
  "evals": [
    {
      "id": 1,
      "name": "test case name",
      "prompt": "realistic user prompt",
      "expected_output": "description of correct behavior",
      "assertions": [
        { "id": "a1", "text": "objectively verifiable assertion", "type": "boolean" }
      ]
    }
  ]
}

Field rules:
- name: kebab-case, concise, descriptive (e.g., "sql-formatter", "api-docs-generator")
- description: 1-3 sentences with trigger phrases. Must pass the "would Claude trigger on this?" test
- model: "" (any) unless task clearly requires opus-level reasoning or is trivially haiku-suitable
- allowedTools: comma-separated list (e.g., "Read, Write, Edit, Bash") or "" for unrestricted. Only restrict when the skill genuinely shouldn't use certain tools
- body: Complete markdown starting with # /skill-name, structured with ## sections, 500-2000 words
- evals: 2-3 realistic test cases with objectively verifiable assertions. Prompts should be what real users would say, not abstract test inputs

Return ONLY the JSON object — no code fences, no preamble.

After the JSON, on a new line, write "---REASONING---" followed by a brief explanation of your design choices (why this name, why these trigger phrases, what Skill Studio rules you applied).`;
// ---------------------------------------------------------------------------
// Agent-aware prompt augmentation
// ---------------------------------------------------------------------------
/**
 * Build an agent-aware system prompt by conditionally appending a
 * "## Target Agent Constraints" section when non-Claude agents are targeted.
 *
 * When targetAgents is absent, empty, or only contains "claude-code",
 * the base prompt is returned unchanged (backward compatible).
 */
export function buildAgentAwareSystemPrompt(basePrompt, targetAgents) {
    if (!targetAgents || targetAgents.length === 0)
        return basePrompt;
    // Collect profiles for non-Claude agents only
    const profiles = [];
    for (const agentId of targetAgents) {
        if (agentId === "claude-code")
            continue;
        const profile = getAgentCreationProfile(agentId);
        if (profile)
            profiles.push(profile);
    }
    if (profiles.length === 0)
        return basePrompt;
    // Aggregate agent names and constraints
    const agentNames = profiles.map((p) => p.agent.displayName).join(", ");
    // Build feature matrix
    const featureLines = profiles.map((p) => {
        const fs = p.featureSupport;
        return `- ${p.agent.displayName}: slashCommands=${fs.slashCommands}, hooks=${fs.hooks}, mcp=${fs.mcp}, customSystemPrompt=${fs.customSystemPrompt}`;
    });
    // Deduplicate guidance across all profiles
    const allGuidance = new Set();
    for (const p of profiles) {
        for (const g of p.addGuidance)
            allGuidance.add(g);
    }
    if (allGuidance.size === 0)
        return basePrompt;
    const constraintSection = [
        "",
        "## Target Agent Constraints",
        "",
        `This skill targets: ${agentNames}`,
        "",
        "Feature availability for target agents:",
        ...featureLines,
        "",
        "IMPORTANT CONSTRAINTS:",
        ...[...allGuidance].map((g) => `- ${g}`),
        "",
        "Generate a skill body that works across ALL target agents by using only universally available features.",
    ].join("\n");
    return basePrompt + constraintSection;
}
/** Strip code fences and parse JSON from raw LLM output */
function cleanAndParseJson(raw) {
    const cleaned = raw.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "");
    try {
        return JSON.parse(cleaned);
    }
    catch {
        throw new Error("AI response was not valid JSON. Try again or use manual mode.");
    }
}
export function parseBodyResponse(raw) {
    const parts = raw.split("---REASONING---");
    const jsonPart = parts[0].trim();
    const reasoning = parts.length > 1 ? parts[1].trim() : "Skill generated using Skill Studio best practices.";
    const parsed = cleanAndParseJson(jsonPart);
    const name = String(parsed.name || "").replace(/[^a-z0-9-]/g, "").replace(/^-+|-+$/g, "");
    if (!name)
        throw new Error("AI returned an invalid skill name. Try again or use manual mode.");
    return {
        name,
        description: String(parsed.description || ""),
        model: String(parsed.model || ""),
        allowedTools: String(parsed.allowedTools || ""),
        body: String(parsed.body || ""),
        reasoning,
    };
}
export function parseEvalsResponse(raw) {
    const jsonPart = raw.trim();
    const parsed = cleanAndParseJson(jsonPart);
    const evals = Array.isArray(parsed.evals) ? parsed.evals.slice(0, 10) : [];
    return { evals };
}
export function mergeGenerateResults(bodySettled, evalsSettled) {
    // Body is required — if it failed, propagate the error
    if (bodySettled.status === "rejected") {
        throw bodySettled.reason instanceof Error
            ? bodySettled.reason
            : new Error(String(bodySettled.reason));
    }
    const bodyResult = bodySettled.value;
    // Evals are optional — if they failed, return body with empty evals and warning
    if (evalsSettled.status === "rejected") {
        const reason = evalsSettled.reason instanceof Error
            ? evalsSettled.reason.message
            : String(evalsSettled.reason);
        return {
            ...bodyResult,
            evals: [],
            warning: `eval generation failed: ${reason}`,
        };
    }
    return {
        ...bodyResult,
        evals: evalsSettled.value.evals,
    };
}
/** Legacy monolithic parser — still used as fallback */
function parseGenerateResponse(raw) {
    const parts = raw.split("---REASONING---");
    const jsonPart = parts[0].trim();
    const reasoning = parts.length > 1 ? parts[1].trim() : "Skill generated using Skill Studio best practices.";
    const parsed = cleanAndParseJson(jsonPart);
    const name = String(parsed.name || "").replace(/[^a-z0-9-]/g, "").replace(/^-+|-+$/g, "");
    if (!name)
        throw new Error("AI returned an invalid skill name. Try again or use manual mode.");
    return {
        name,
        description: String(parsed.description || ""),
        model: String(parsed.model || ""),
        allowedTools: String(parsed.allowedTools || ""),
        body: String(parsed.body || ""),
        evals: Array.isArray(parsed.evals) ? parsed.evals.slice(0, 10) : [],
        reasoning,
    };
}
/**
 * Merge generator output with a `suggestedPlugin` computed from the existing
 * plugins on disk. Kept out of src/core/skill-generator.ts so the generator
 * stays pure (no filesystem access for plugin suggestion).
 */
function attachSuggestedPlugin(parsed, root) {
    const tags = parsed.description
        ? parsed.description.toLowerCase().split(/[,;]+/).map((s) => s.trim()).filter(Boolean)
        : [];
    const suggestedPlugin = matchExistingPlugin(parsed.name, parsed.description, tags, root);
    return { ...parsed, suggestedPlugin };
}
async function resolveProviderModel(requestedProvider, requestedModel) {
    // Both absent → legacy default (no probe).
    if (!requestedProvider && !requestedModel) {
        return { ok: true, provider: "claude-cli", model: "sonnet" };
    }
    const detected = await detectAvailableProviders();
    const requested = requestedProvider || "claude-cli";
    const match = detected.find((p) => p.id === requested);
    if (!match) {
        return {
            ok: false,
            error: "unknown_provider",
            validProviders: detected.map((p) => p.id),
        };
    }
    const validModelIds = match.models.map((m) => m.id);
    if (requestedModel !== undefined) {
        if (!validModelIds.includes(requestedModel)) {
            return { ok: false, error: "unknown_model", validModels: validModelIds };
        }
        return { ok: true, provider: requested, model: requestedModel };
    }
    // Provider given, model omitted — pick the first model id for that provider.
    return { ok: true, provider: requested, model: validModelIds[0] ?? "sonnet" };
}
// ---------------------------------------------------------------------------
// Route registration
// ---------------------------------------------------------------------------
export function registerSkillCreateRoutes(router, root) {
    // GET /api/project-layout — detect project layout and suggest placement
    router.get("/api/project-layout", async (_req, res) => {
        try {
            const layout = detectProjectLayout(root);
            sendJson(res, layout, 200, _req);
        }
        catch (err) {
            sendJson(res, { error: err.message }, 500, _req);
        }
    });
    // 0772 US-005: GET /api/project/github-status — quick probe of git +
    // GitHub origin so the UI can surface a "connect GitHub to publish" hint.
    router.get("/api/project/github-status", async (_req, res) => {
        try {
            const status = detectProjectGitHubStatus(root);
            sendJson(res, status, 200, _req);
        }
        catch {
            // Defensive: any failure → safe default (treat as no-git so the UI
            // shows the bootstrap hint rather than crashing the studio).
            sendJson(res, { hasGit: false, githubOrigin: null, status: "no-git" }, 200, _req);
        }
    });
    // POST /api/skills/create — create a new skill
    router.post("/api/skills/create", async (req, res) => {
        const body = (await readBody(req));
        // Validate name
        if (!body.name || !/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(body.name)) {
            sendJson(res, { error: "Name must be kebab-case (lowercase letters, numbers, hyphens)" }, 400, req);
            return;
        }
        if (!body.description?.trim()) {
            sendJson(res, { error: "Description is required" }, 400, req);
            return;
        }
        if (!body.layout || ![1, 2, 3].includes(body.layout)) {
            sendJson(res, { error: "Layout must be 1, 2, or 3" }, 400, req);
            return;
        }
        if (body.layout !== 3 && !body.plugin?.trim()) {
            sendJson(res, { error: "Plugin name is required for this layout" }, 400, req);
            return;
        }
        if (body.plugin && !/^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/i.test(body.plugin)) {
            sendJson(res, { error: "Plugin name contains invalid characters" }, 400, req);
            return;
        }
        // 0734: validate authoring engine. Default to "vskill" when omitted.
        const engine = (body.engine ?? "vskill");
        if (body.engine !== undefined && !VALID_ENGINES.has(body.engine)) {
            sendJson(res, {
                error: `Invalid engine: ${body.engine}. Must be one of: vskill, anthropic-skill-creator, none.`,
            }, 400, req);
            return;
        }
        if (engine === "anthropic-skill-creator" && !isSkillCreatorInstalled(root)) {
            sendJson(res, {
                error: "skill-creator-not-installed",
                remediation: "claude plugin install skill-creator",
            }, 400, req);
            return;
        }
        const targetDir = computeSkillDir(root, body.layout, body.plugin || "", body.name);
        const skillMdPath = join(targetDir, "SKILL.md");
        // Check if already exists. Three legitimate ways to overwrite:
        //   1. draft.json marker → finalizing an auto-saved draft
        //   2. mode === "update" → caller intentionally wants to overwrite
        //   3. (otherwise) → 409 Conflict (legacy guard)
        const isDraftFinalize = existsSync(join(targetDir, "draft.json"));
        const isUpdateMode = body.mode === "update";
        const skillExists = existsSync(skillMdPath);
        if (skillExists && !isDraftFinalize && !isUpdateMode) {
            // 0772 US-004: emit a structured payload so the client can recover
            // (navigate to the existing skill) without string-matching the message.
            const conflictPlugin = body.layout === 3 ? (basename(root) || "default") : (body.plugin || "");
            sendJson(res, {
                error: `Skill already exists at ${targetDir}`,
                code: "skill-already-exists",
                plugin: conflictPlugin,
                skill: body.name,
                dir: targetDir,
            }, 409, req);
            return;
        }
        // Resolve the version to emit in SKILL.md.
        //   - explicit body.version always wins
        //   - update mode reads the existing version + bumps patch
        //   - everything else defaults to "1.0.0"
        let resolvedVersion;
        if (body.version?.trim()) {
            resolvedVersion = body.version.trim();
        }
        else if (skillExists && isUpdateMode) {
            const existing = readFileSync(skillMdPath, "utf-8");
            const previous = extractFrontmatterVersion(existing) ?? "1.0.0";
            resolvedVersion = bumpPatch(previous);
        }
        else {
            resolvedVersion = "1.0.0";
        }
        try {
            // Create directories
            mkdirSync(targetDir, { recursive: true });
            mkdirSync(join(targetDir, "evals"), { recursive: true });
            // Write SKILL.md (with resolved version + validated engine).
            const content = buildSkillMd({ ...body, version: resolvedVersion, engine });
            writeFileSync(skillMdPath, content, "utf-8");
            // Write evals.json if provided (from AI generation)
            if (body.evals && body.evals.length > 0) {
                const evalsData = {
                    skill_name: body.name,
                    evals: body.evals,
                };
                writeFileSync(join(targetDir, "evals", "evals.json"), JSON.stringify(evalsData, null, 2) + "\n", "utf-8");
                // Record history entry for AI-generated skill (skip if draft finalization — already written by save-draft)
                if (!isDraftFinalize) {
                    const aiMeta = body.aiMeta;
                    try {
                        await writeHistoryEntry(targetDir, {
                            timestamp: new Date().toISOString(),
                            model: aiMeta?.model || "unknown",
                            skill_name: body.name,
                            cases: [],
                            overall_pass_rate: undefined,
                            type: "ai-generate",
                            provider: aiMeta?.provider || "unknown",
                            generate: { prompt: aiMeta?.prompt || body.description, result: content },
                        });
                    }
                    catch { /* history write failure should not break the main response */ }
                }
            }
            // -------------------------------------------------------------------
            // 0815: multi-file payload — write auxiliary files atomically.
            //
            // Each `files` entry is validated through resolveSafe() before any I/O.
            // On any failure mid-write, rollback by removing the skill directory
            // (unless this is an update — preserve existing user content). The
            // rollback is intentionally NOT inside a finally — the existing outer
            // try/catch (a few lines below) handles top-level failures and we want
            // the path-traversal case to surface as a 400 with the dir cleaned up.
            // -------------------------------------------------------------------
            if (body.files && typeof body.files === "object") {
                const written = [];
                try {
                    for (const [relPath, contents] of Object.entries(body.files)) {
                        if (typeof contents !== "string") {
                            throw new Error(`files['${relPath}'] is not a string`);
                        }
                        const safePath = resolveSafe(targetDir, relPath);
                        const safeDir = safePath.slice(0, safePath.lastIndexOf(sep));
                        mkdirSync(safeDir, { recursive: true });
                        writeFileSync(safePath, contents, "utf-8");
                        written.push(safePath);
                    }
                }
                catch (err) {
                    // Rollback: clean up the partial directory unless we're updating.
                    if (!isUpdateMode) {
                        try {
                            rmSync(targetDir, { recursive: true, force: true });
                        }
                        catch { /* best-effort */ }
                    }
                    sendJson(res, {
                        error: `Failed to write multi-file payload: ${err.message}`,
                        code: "multi-file-write-failed",
                    }, 400, req);
                    return;
                }
            }
            // 0815: emit .env.example from declared secrets (placeholders only).
            if (Array.isArray(body.secrets) && body.secrets.length > 0) {
                try {
                    const envExamplePath = join(targetDir, ".env.example");
                    writeFileSync(envExamplePath, buildEnvExample(body.secrets), "utf-8");
                }
                catch { /* non-blocking — secrets are advisory */ }
            }
            // Finalize: remove draft.json if it exists (draft → final)
            const draftPath = join(targetDir, "draft.json");
            if (existsSync(draftPath)) {
                try {
                    unlinkSync(draftPath);
                }
                catch { /* ignore */ }
            }
            // Clean up old draft directory if plugin was changed
            if (body.draftDir) {
                const resolvedDraft = resolve(root, body.draftDir);
                if (isDraftWithinRoot(resolvedDraft, root) && resolvedDraft !== resolve(targetDir)) {
                    try {
                        rmSync(resolvedDraft, { recursive: true, force: true });
                    }
                    catch { /* non-blocking */ }
                }
            }
            // In update mode, attempt to publish the new version to the platform's
            // queue so SSE subscribers see it without waiting for the 10-min scanner.
            // No-op when INTERNAL_BROADCAST_KEY is not configured (production studio).
            let publishResult;
            if (skillExists && isUpdateMode) {
                const pluginForId = body.layout === 3 ? (basename(root) || "default") : (body.plugin || "default");
                publishResult = await submitSkillUpdateEvent({
                    skillId: `${pluginForId}/${body.name}`,
                    version: resolvedVersion,
                    diffSummary: `skills/create update mode: ${body.name} → ${resolvedVersion}`,
                });
            }
            const isUpdate = skillExists && isUpdateMode;
            // Anthropic skill-creator emits Claude-only by definition; VSkill/none
            // are universal across the registered universal targets.
            const emittedTargets = engine === "anthropic-skill-creator" ? ["claude-code"] : null;
            sendJson(res, {
                ok: true,
                plugin: body.layout === 3 ? (basename(root) || "default") : body.plugin,
                skill: body.name,
                dir: targetDir,
                skillMdPath,
                version: resolvedVersion,
                engine,
                ...(emittedTargets ? { emittedTargets } : {}),
                updated: isUpdate,
                ...(publishResult ? { publish: publishResult } : {}),
            }, isUpdate ? 200 : 201, req);
        }
        catch (err) {
            // On failure, do NOT delete draftDir — preserve user's work
            sendJson(res, { error: `Failed to create skill: ${err.message}` }, 500, req);
        }
    });
    // POST /api/skills/save-draft — auto-save AI-generated skill as draft
    router.post("/api/skills/save-draft", async (req, res) => {
        const body = (await readBody(req));
        if (!body.name || !/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(body.name)) {
            sendJson(res, { error: "Name must be kebab-case" }, 400, req);
            return;
        }
        if (!body.description?.trim()) {
            sendJson(res, { error: "Description is required" }, 400, req);
            return;
        }
        if (!body.layout || ![1, 2, 3].includes(body.layout)) {
            sendJson(res, { error: "Layout must be 1, 2, or 3" }, 400, req);
            return;
        }
        const targetDir = computeSkillDir(root, body.layout, body.plugin || "", body.name);
        const skillMdPath = join(targetDir, "SKILL.md");
        const files = [];
        // 0728: Resolve version BEFORE writing. Three-tier precedence:
        //   1. Explicit body.version (caller wins).
        //   2. Existing on-disk version when re-saving an iteration draft —
        //      preserve so AI iteration doesn't downgrade work-in-progress.
        //   3. Default "1.0.0" for fresh drafts.
        let resolvedVersion;
        if (body.version?.trim()) {
            resolvedVersion = body.version.trim();
        }
        else if (existsSync(skillMdPath)) {
            resolvedVersion = extractFrontmatterVersion(readFileSync(skillMdPath, "utf-8")) ?? "1.0.0";
        }
        else {
            resolvedVersion = "1.0.0";
        }
        try {
            // Create directories (overwrites allowed for re-generation)
            mkdirSync(targetDir, { recursive: true });
            mkdirSync(join(targetDir, "evals"), { recursive: true });
            mkdirSync(join(targetDir, "evals", "history"), { recursive: true });
            // Write SKILL.md
            const content = buildSkillMd({ ...body, version: resolvedVersion });
            writeFileSync(skillMdPath, content, "utf-8");
            files.push("SKILL.md");
            // Write evals.json if provided
            if (body.evals && body.evals.length > 0) {
                const evalsData = { skill_name: body.name, evals: body.evals };
                writeFileSync(join(targetDir, "evals", "evals.json"), JSON.stringify(evalsData, null, 2) + "\n", "utf-8");
                files.push("evals/evals.json");
            }
            // Write draft.json metadata
            const draftMeta = {
                draft: true,
                createdAt: new Date().toISOString(),
                aiPrompt: body.aiMeta.prompt,
                aiProvider: body.aiMeta.provider,
                aiModel: body.aiMeta.model,
                aiReasoning: body.aiMeta.reasoning,
            };
            writeFileSync(join(targetDir, "draft.json"), JSON.stringify(draftMeta, null, 2) + "\n", "utf-8");
            files.push("draft.json");
            // Record AI generation history
            try {
                await writeHistoryEntry(targetDir, {
                    timestamp: new Date().toISOString(),
                    model: body.aiMeta.model,
                    skill_name: body.name,
                    cases: [],
                    overall_pass_rate: undefined,
                    type: "ai-generate",
                    provider: body.aiMeta.provider,
                    generate: { prompt: body.aiMeta.prompt, result: content },
                });
            }
            catch { /* history write failure should not break the main response */ }
            sendJson(res, {
                ok: true,
                plugin: body.layout === 3 ? (basename(root) || "default") : body.plugin,
                skill: body.name,
                dir: targetDir,
                skillMdPath,
                files,
            }, 201, req);
        }
        catch (err) {
            sendJson(res, { error: `Failed to save draft: ${err.message}` }, 500, req);
        }
    });
    // GET /api/skill-creator-status — check if skill-creator is installed
    router.get("/api/skill-creator-status", async (_req, res) => {
        const installed = isSkillCreatorInstalled(root);
        sendJson(res, {
            installed,
            installCommand: "npx vskill install anthropics/skills/skill-creator",
        }, 200, _req);
    });
    // POST /api/skills/generate — AI-assisted skill generation (parallel body + evals)
    // 0670 T-002: thin wrapper over src/core/skill-generator.ts:generateSkill.
    router.post("/api/skills/generate", async (req, res) => {
        const body = (await readBody(req));
        if (!body.prompt || !body.prompt.trim())
            return sendJson(res, { error: "Describe what your skill should do" }, 400, req);
        if (body.prompt.length > 50000)
            return sendJson(res, { error: "Prompt is too long (max 50,000 characters)" }, 400, req);
        const resolved = await resolveProviderModel(body.provider, body.model);
        if (!resolved.ok) {
            const payload = { error: resolved.error };
            if (resolved.validProviders)
                payload.validProviders = resolved.validProviders;
            if (resolved.validModels)
                payload.validModels = resolved.validModels;
            return sendJson(res, payload, 400, req);
        }
        const wantsSSE = req.headers.accept?.includes("text/event-stream") ||
            (req.url ? new URL(req.url, "http://localhost").searchParams.has("sse") : false);
        const ac = new AbortController();
        res.on("close", () => ac.abort());
        if (wantsSSE)
            initSSE(res, req);
        try {
            const parsed = await generateSkill({ ...body, provider: resolved.provider, model: resolved.model }, {
                root,
                abortSignal: ac.signal,
                onProgress: wantsSSE ? (e) => { if (!ac.signal.aborted)
                    sendSSE(res, "progress", e); } : undefined,
            });
            if (ac.signal.aborted)
                return;
            const result = attachSuggestedPlugin(parsed, root);
            // 0711: emit provenance as a SEPARATE SSE event before `done` so the
            // GenerateSkillResult schema stays clean (parity tests assert on the
            // legacy shape). The UI's useCreateSkill listens for the `provenance`
            // event and stamps aiMeta. Best-effort — never blocks generation.
            if (wantsSSE && (resolved.provider === "anthropic" || resolved.provider === "claude-cli")) {
                try {
                    const { resolveAnthropicModel } = await import("../eval/model-resolver.js");
                    const r = resolveAnthropicModel(resolved.model);
                    sendSSE(res, "provenance", {
                        resolvedModelId: r.resolvedId || null,
                        snapshotDate: r.snapshotDate,
                        source: r.source,
                    });
                }
                catch {
                    /* provenance is best-effort; never block generation */
                }
            }
            if (wantsSSE)
                sendSSEDone(res, result);
            else
                sendJson(res, result, 200, req);
        }
        catch (err) {
            if (wantsSSE && !ac.signal.aborted) {
                sendSSE(res, "error", classifyError(err, resolved.provider));
                res.end();
            }
            else {
                const msg = err.message;
                sendJson(res, { error: msg }, msg.includes("not valid JSON") ? 422 : 500, req);
            }
        }
    });
}
//# sourceMappingURL=skill-create-routes.js.map