/**
 * LLM-Based Plugin Detection
 *
 * Replaces unreliable grep-based keyword matching with Claude LLM calls.
 * Uses Haiku model for fast, accurate intent detection.
 *
 * Key features:
 * - Understands context ("don't use React" → no React plugin)
 * - Cross-platform execution (Windows, macOS, Linux)
 * - Graceful degradation when Claude CLI unavailable
 * - Proper error handling throughout
 *
 * @module core/lazy-loading/llm-plugin-detector
 */
import { spawnSync } from 'child_process';
import * as os from 'os';
import * as fs from 'fs';
import * as path from 'path';
import { consoleLogger as logger } from '../../utils/logger.js';
import { getProjectRoot } from '../../utils/find-project-root.js';
// IMPORTANT: Use canonical Claude CLI detection from utils (handles shell functions, nvm, etc.)
import { detectClaudeCli, getCleanEnv } from '../../utils/claude-cli-detector.js';
// ============================================================
// Prompt safety constants and truncation utilities (v1.0.254)
//
// Prevents "Prompt is too long" errors by enforcing size limits
// on user prompts passed to internal Haiku calls and on the
// additionalContext injected by the UserPromptSubmit hook.
// ============================================================
/** Max chars of user prompt sent to Haiku for intent detection */
export const MAX_DETECTION_USER_PROMPT_LENGTH = 3000;
/** Max chars for the additionalContext returned from the UserPromptSubmit hook.
 * v1.0.260: Synced with hook (was 8000, reduced to 3000 to prevent prompt overflow).
 * Must match MAX_ADDITIONAL_CONTEXT_LENGTH in user-prompt-submit.sh. */
export const MAX_ADDITIONAL_CONTEXT_LENGTH = 3000;
/** Max chars of user prompt embedded in SKILL FIRST args.
 * v1.0.260: Synced with hook (was 2000, reduced to 800).
 * Note: v1.0.260 removed prompt embedding from SKILL FIRST entirely,
 * but this constant is kept for any remaining truncation use. */
export const MAX_SKILL_FIRST_PROMPT_LENGTH = 800;
/**
 * Truncate a user prompt for the detect-intent Haiku call.
 * The first N chars are sufficient for intent classification.
 */
export function truncateForDetection(prompt) {
    if (prompt.length <= MAX_DETECTION_USER_PROMPT_LENGTH)
        return prompt;
    return prompt.substring(0, MAX_DETECTION_USER_PROMPT_LENGTH) + '... [truncated]';
}
/**
 * Truncate a user prompt for embedding in SKILL FIRST args.
 * The original prompt is already in the user message — no need to duplicate it fully.
 */
export function truncateForSkillFirstArgs(prompt) {
    if (prompt.length <= MAX_SKILL_FIRST_PROMPT_LENGTH)
        return prompt;
    return prompt.substring(0, MAX_SKILL_FIRST_PROMPT_LENGTH) + '... [truncated — see original prompt above]';
}
/**
 * Truncate the additionalContext output from the UserPromptSubmit hook.
 * Prevents context inflation that can push the main model past limits.
 */
export function truncateAdditionalContext(context) {
    if (context.length <= MAX_ADDITIONAL_CONTEXT_LENGTH)
        return context;
    return context.substring(0, MAX_ADDITIONAL_CONTEXT_LENGTH) + '... [context truncated for safety]';
}
/**
 * Read plugin auto-load configuration from .specweave/config.json
 *
 * @returns Config object with enabled/suggestOnly flags
 */
export function readPluginAutoLoadConfig() {
    // Default: enabled, suggest-only (consent-first — never auto-install without asking)
    const defaultConfig = {
        enabled: true,
        suggestOnly: true,
    };
    // Check env var override first
    if (process.env.SPECWEAVE_DISABLE_AUTO_LOAD === '1') {
        logger.debug('[readPluginAutoLoadConfig] Auto-load disabled via SPECWEAVE_DISABLE_AUTO_LOAD=1');
        return { enabled: false, suggestOnly: true };
    }
    // Try to read from config file
    const configPath = path.join(getProjectRoot(), '.specweave', 'config.json');
    try {
        if (fs.existsSync(configPath)) {
            const content = fs.readFileSync(configPath, 'utf8');
            const config = JSON.parse(content);
            if (config.pluginAutoLoad) {
                const result = {
                    enabled: config.pluginAutoLoad.enabled !== false, // default true
                    suggestOnly: config.pluginAutoLoad.suggestOnly !== false, // default true (consent-first)
                };
                logger.debug(`[readPluginAutoLoadConfig] Config: enabled=${result.enabled}, suggestOnly=${result.suggestOnly}`);
                return result;
            }
        }
    }
    catch (error) {
        logger.debug(`[readPluginAutoLoadConfig] Error reading config: ${error}`);
    }
    return defaultConfig;
}
/**
 * Plugins that remain in specweave marketplace (installed locally)
 *
 * IMPORTANT: Plugin names use marketplace short names `sw-*` (not directory names `specweave-*`)
 * This matches the plugin names in marketplace.json and what `claude plugin install` expects.
 *
 * Note: Release, diagrams, and docs are now merged into CORE sw plugin (v1.0.130+)
 * - sw:npm, sw:release skills are built-in
 * - mermaid, c4, architecture diagram skills are built-in
 * - docs-writer, sync-docs skills are built-in
 *
 * v2.1.0: Domain skills live in vskill marketplace as per-category plugins.
 * Only workflow/integration plugins remain here.
 */
export const SPECWEAVE_PLUGINS = [
    'sw', // Core workflow
    'sw-github', // GitHub integration
    'sw-jira', // JIRA integration
    'sw-ado', // Azure DevOps
    'sw-release', // Release management
    'sw-diagrams', // Diagrams
    'sw-docs', // SpecWeave docs
    'sw-media', // AI image/video generation
];
/**
 * Combined list of all known plugins for validation.
 * Only specweave plugins — vskill marketplace plugins have been removed (v1.0.533).
 */
export const ALL_KNOWN_PLUGINS = [...SPECWEAVE_PLUGINS];
/**
 * All valid plugins — only specweave plugins (v1.0.533: vskill removed).
 */
export const ALL_VALID_PLUGINS = ALL_KNOWN_PLUGINS;
export const VSKILL_PLUGIN_REGISTRY = {
    mobile: {
        keywords: ['testflight', 'app store', 'play store', 'react native', 'react-native', 'expo', 'mobile app', 'flutter', 'xcode', 'cocoapods', 'fastlane', 'app-store-connect', 'app deployment'],
        shortKeywords: ['ios', 'apk'],
        description: 'App Store, TestFlight, mobile CI/CD',
        installCommand: 'vskill i anton-abyzov/vskill/appstore',
    },
    'google-workspace': {
        keywords: ['google sheets', 'google docs', 'google drive', 'google slides', 'google calendar', 'google workspace', 'gmail api', 'sheets api'],
        shortKeywords: [],
        description: 'Gmail, Drive, Sheets, Docs, Calendar',
        installCommand: 'vskill i anton-abyzov/vskill/gws',
    },
    marketing: {
        keywords: ['linkedin', 'instagram', 'social media', 'twitter', 'facebook', 'tiktok', 'content marketing', 'social post', 'blog post', 'newsletter', 'slack messaging'],
        shortKeywords: [],
        description: 'Social media posting, Slack messaging',
        installCommand: 'vskill i anton-abyzov/vskill/slack-messaging',
    },
    productivity: {
        keywords: ['notion', 'todoist', 'trello', 'asana', 'monday.com', 'obsidian', 'time tracking', 'project management', 'task management'],
        shortKeywords: [],
        description: 'Notion, Todoist, Trello, Asana, Obsidian',
        installCommand: 'vskill i anton-abyzov/vskill/survey-passing',
    },
    skills: {
        keywords: ['find plugin', 'search plugin', 'discover skill', 'browse marketplace', 'vskill search', 'plugin search'],
        shortKeywords: [],
        description: 'Skill discovery and installation',
        installCommand: 'vskill i anton-abyzov/vskill/scout',
    },
};
const VSKILL_MAX_SUGGESTIONS = 3;
export function detectVskillPlugins(prompt) {
    const normalized = prompt.toLowerCase();
    const matches = [];
    for (const [pluginName, entry] of Object.entries(VSKILL_PLUGIN_REGISTRY)) {
        let matchedKeyword = null;
        for (const kw of entry.shortKeywords) {
            const re = new RegExp(`\\b${kw}\\b`, 'i');
            if (re.test(normalized)) {
                matchedKeyword = kw;
                break;
            }
        }
        if (!matchedKeyword) {
            for (const kw of entry.keywords) {
                if (normalized.includes(kw.toLowerCase())) {
                    matchedKeyword = kw;
                    break;
                }
            }
        }
        if (matchedKeyword) {
            matches.push({ plugin: pluginName, matchedKeyword, installCommand: entry.installCommand });
        }
    }
    matches.sort((a, b) => b.matchedKeyword.length - a.matchedKeyword.length);
    return matches.slice(0, VSKILL_MAX_SUGGESTIONS);
}
/**
 * Check if a plugin is a SpecWeave plugin (installed from local specweave marketplace)
 */
export function isSpecWeavePlugin(plugin) {
    return SPECWEAVE_PLUGINS.includes(plugin);
}
/**
 * Check if a plugin is any known plugin (specweave only, v1.0.533).
 */
export function isKnownPlugin(plugin) {
    return ALL_KNOWN_PLUGINS.includes(plugin);
}
// Cache the CLI detection result for performance (detect once per process)
let cachedCliStatus = null;
/**
 * Clear the cached CLI status (useful for testing)
 */
export function clearCliCache() {
    cachedCliStatus = null;
}
/**
 * Get the current CLI detection status (for debugging/testing)
 */
export function getCliStatus() {
    return cachedCliStatus;
}
/**
 * Check if Claude CLI is available
 *
 * REFACTORED: Uses canonical implementation from utils/claude-cli-detector.ts
 * which handles all edge cases:
 * - Binary in PATH
 * - Shell functions in .zshrc/.bashrc
 * - Shell aliases
 * - npm global paths (including nvm versions)
 *
 * Also populates the cache for use by executeClaudeCli().
 *
 * @returns Status object with availability and path info
 */
export function isClaudeCliAvailable() {
    // Use cached result if available
    if (cachedCliStatus) {
        const result = {
            available: cachedCliStatus.available,
            path: cachedCliStatus.commandPath,
            version: cachedCliStatus.version,
            error: cachedCliStatus.available ? undefined : cachedCliStatus.errorMessage,
        };
        // Log when returning cached unavailable status (helps debug stale cache issues)
        if (!result.available) {
            logger.debug(`[isClaudeCliAvailable] Returning CACHED unavailable status: error=${cachedCliStatus.error}, errorMessage=${cachedCliStatus.errorMessage}, exitCode=${cachedCliStatus.exitCode}, platform=${cachedCliStatus.platform}`);
        }
        return result;
    }
    // Detect and cache
    logger.debug('[isClaudeCliAvailable] Running fresh detection (no cache)...');
    const canonicalStatus = detectClaudeCli();
    cachedCliStatus = canonicalStatus;
    // Log full detection result for debugging
    if (!canonicalStatus.available) {
        const debugInfo = [
            `error=${canonicalStatus.error}`,
            `errorMessage=${canonicalStatus.errorMessage}`,
            `commandExists=${canonicalStatus.commandExists}`,
            `pluginCommandsWork=${canonicalStatus.pluginCommandsWork}`,
            `exitCode=${canonicalStatus.exitCode}`,
            `platform=${canonicalStatus.platform}`,
            `detectionMethod=${canonicalStatus.detectionMethod}`,
            canonicalStatus.debugStdout ? `stdout=${canonicalStatus.debugStdout.substring(0, 100)}` : null,
            canonicalStatus.debugStderr ? `stderr=${canonicalStatus.debugStderr.substring(0, 100)}` : null,
        ].filter(Boolean).join(', ');
        logger.warn(`[isClaudeCliAvailable] Claude CLI NOT available: ${debugInfo}`);
    }
    else {
        logger.debug(`[isClaudeCliAvailable] Claude CLI available: version=${canonicalStatus.version}, path=${canonicalStatus.commandPath}, method=${canonicalStatus.detectionMethod}`);
    }
    // Map to our simpler interface
    return {
        available: canonicalStatus.available,
        path: canonicalStatus.commandPath,
        version: canonicalStatus.version,
        error: canonicalStatus.available ? undefined : canonicalStatus.errorMessage,
    };
}
/**
 * Build the system prompt for plugin detection, increment recommendation, and skill routing
 *
 * v1.0.155: MINIMAL detection - only detect technologies EXPLICITLY mentioned in the prompt
 */
function buildDetectionPrompt() {
    return `You detect which plugins to load based on the user's prompt.
Return specweave (sw-*) plugin names ONLY.

DETECTION RULES:
1. EXPLICIT mention of integrations - "GitHub sync" → sw-github, "JIRA" → sw-jira
2. Questions/discussions → ZERO plugins
3. ONLY suggest @specweave plugins (sw-*)
4. Domain plugins (backend, testing, etc.) are NOT available — do NOT suggest them

OUTPUT FORMAT (JSON only):
{"plugins":["sw-github"],"confidence":0.9,"reasoning":"one-line"}

═══════════════════════════════════════════════════════════════
AVAILABLE PLUGINS (specweave only — sw-*)
═══════════════════════════════════════════════════════════════

sw-github: GitHub issues, PRs, Actions, sync (ONLY if explicit)
sw-jira: JIRA, Atlassian (ONLY if explicit)
sw-ado: Azure DevOps, work items (ONLY if explicit)
sw-media: AI image generation, AI video generation, Remotion, text-to-image, text-to-video, Imagen, Veo, generate image, generate video, create video, media generation, Pollinations (ONLY if explicit)

DO NOT include in plugins array: backend, testing, infra, k8s, payments, ml, kafka, confluent, security, blockchain — these are vskill marketplace plugins handled separately (not sw-* plugins). Mobile and skills are also vskill plugins — do not include them in the plugins array.

═══════════════════════════════════════════════════════════════
INCREMENT RECOMMENDATION (v1.0.241 - DEFAULT: create increment)
═══════════════════════════════════════════════════════════════

ALSO analyze if user should create/reopen a SpecWeave increment.

⚠️ CRITICAL PRINCIPLE: Almost ALL implementation work should be tracked in an increment.
The DEFAULT is to recommend an increment (~95% of prompts that involve code changes).
Only skip for pure questions, greetings, exploration, or explicit user opt-out.

"increment" field with:
- action: "new" | "reopen" | "small_fix" | "hotfix" | "none"
- confidence: 0.0-1.0
- mandatory: true/false (YOU decide - not config-based!)
- suggestedName: kebab-case name (for "new" and "small_fix"). MUST be descriptive and actionable.
  For vague prompts, extract the CORE intent: "it's broken" → "fix-[feature]", "not working" → "fix-[feature]",
  "we need to improve X" → "improve-x", "the database is slow" → "fix-database-performance"
- reasoning: brief explanation

WHEN TO USE EACH ACTION:
┌─────────────┬─────────────────────────────────────────────────────────────┐
│ new         │ ANY feature, bug fix, refactoring, enhancement, or         │
│             │ implementation work that changes code behavior              │
│ hotfix      │ "urgent", "production bug", "critical fix"                 │
│ reopen      │ "fix the X feature", work related to recent increment      │
│ small_fix   │ ONLY: literal typo fix, version bump, single config value  │
│             │ change, comment update — truly trivial, <5 min changes     │
│ none        │ ONLY: pure questions ("what is X?"), exploration ("show me"),│
│             │ general chat, greetings, or explicit user opt-out.          │
│             │ NEVER use "none" for: "fix this", "it's broken",           │
│             │ "not working", "improve X", "change Y", "investigate",     │
│             │ "debug", "troubleshoot", "optimize", "secure", "audit",    │
│             │ "why does X fail/break", "think hard on why",              │
│             │ "solve", "resolve", "analyze" — these are work!            │
└─────────────┴─────────────────────────────────────────────────────────────┘

⚠️ IMPORTANT: When in doubt between "new" and "small_fix", ALWAYS choose "new".
Bug fixes, refactoring, adding error handling, improving validation, updating
tests, fixing logic errors — these are ALL "new", NOT "small_fix".
"small_fix" is ONLY for changes where you literally change 1-2 lines with zero
investigation needed (typo, version string, config value).

⚠️ INVESTIGATION/DEBUGGING/ANALYSIS: Prompts containing "investigate", "debug",
"troubleshoot", "diagnose", "trace", "profile", "root cause", "analyze", "audit",
"optimize", "improve", "secure", "harden", "solve", "resolve" are ALWAYS "new"
(NEVER "none"). Investigation across multiple components is implementation work
that needs increment tracking. "why does X fail" = work, NOT a question.

WHEN mandatory: true (Claude MUST create increment before implementing):
- Multi-file feature work (React + API + Database)
- Full-stack implementation requests
- New feature spanning multiple components
- Significant architectural changes
- confidence >= 0.85 AND action = "new"

WHEN mandatory: false (suggestion only, but still SUGGEST increment):
- Bug fixes and refactoring (action: "new", mandatory: false)
- Single-file improvements (action: "new", mandatory: false)
- Small but non-trivial changes (action: "small_fix", mandatory: false)
- Low confidence

EXPLICIT OPT-OUT → action: "none":
- "don't create an increment", "no increment needed", "skip workflow"
- "just a quick fix", "without tracking", "already tracking"

═══════════════════════════════════════════════════════════════
EXAMPLES
═══════════════════════════════════════════════════════════════

"Sync our GitHub issues"
{"plugins":["sw-github"],"confidence":0.95,"reasoning":"GitHub sync→sw-github","increment":{"action":"new","confidence":0.9,"mandatory":false,"suggestedName":"sync-github-issues","reasoning":"GitHub integration work"}}

"The auth feature is broken again"
{"plugins":[],"confidence":0.7,"reasoning":"No specific integration mentioned","increment":{"action":"reopen","confidence":0.8,"mandatory":false,"relatedKeyword":"auth","reasoning":"Related to previous auth work"}}

"Fix typo in README"
{"plugins":[],"confidence":0.9,"reasoning":"Typo fix","increment":{"action":"small_fix","confidence":0.9,"mandatory":false,"suggestedName":"fix-readme-typo","reasoning":"Trivial 1-line change"}}

"How do I use React hooks?"
{"plugins":[],"confidence":0.95,"reasoning":"Question only","increment":{"action":"none","confidence":0.99,"mandatory":false,"reasoning":"Question, no implementation"}}

"Investigate why the API sync keeps failing across multiple services"
{"plugins":[],"confidence":0.8,"reasoning":"Investigation/debugging work","increment":{"action":"new","confidence":0.85,"mandatory":false,"suggestedName":"investigate-api-sync-failure","reasoning":"Multi-component investigation requiring structured tracking"}}

═══════════════════════════════════════════════════════════════
LSP OPERATION DETECTION (v1.0.198 - unified detection)
═══════════════════════════════════════════════════════════════

ALSO analyze if the prompt requires LSP (Language Server Protocol) operations.

"lsp" field with:
- needed: true if user wants code intelligence (references, definition, etc.)
- operation: "references" | "definition" | "hover" | "symbols" | null
- language: "typescript" | "python" | "rust" | "go" | "csharp" | "java" | null
- warmupRequired: true (always true - session state unknown)

WHEN lsp.needed = true: "find references"→references, "go to definition"→definition, "show type"→hover, "list symbols"→symbols
WHEN lsp.needed = false (default): building features, general questions, no LSP keywords → omit lsp field

LSP EXAMPLES:

"Find all references to handleRequest"
{"plugins":[],"confidence":0.95,"reasoning":"Code navigation","lsp":{"needed":true,"operation":"references","language":"typescript","warmupRequired":true}}

"Build a React dashboard" (NO LSP needed)
{"plugins":[],"confidence":0.95,"reasoning":"React development"}`;
}
/**
 * Execute Claude CLI command safely
 *
 * Cross-platform safety measures:
 * - Uses spawnSync (not execSync) to avoid shell injection
 * - Proper timeout handling
 * - Buffer size limits
 * - Handles shell functions/aliases on macOS/Linux via interactive shell
 * - Windows-specific shell handling
 *
 * IMPORTANT: On macOS/Linux, if claude is installed as a shell function/alias
 * (defined in .zshrc/.bashrc), we need to use interactive shell to find it.
 * This is detected by detectClaudeCli() which we reuse here.
 *
 * @param args - Arguments to pass to claude command
 * @param timeout - Timeout in milliseconds
 * @returns Spawn result with stdout, stderr, status
 */
function executeClaudeCli(args, timeout = 30000) {
    const isWindows = process.platform === 'win32';
    // Get cached CLI status or detect
    if (!cachedCliStatus) {
        cachedCliStatus = detectClaudeCli();
    }
    // CRITICAL FIX: Prefer DIRECT binary path when available.
    //
    // Why? Users often have shell wrapper functions like:
    //   function claude() { command claude --dangerously-skip-permissions "$@" }
    //
    // These wrappers are great for interactive use but can interfere with
    // programmatic access. The `--dangerously-skip-permissions` flag, for example,
    // may cause issues when combined with certain arguments.
    //
    // Strategy:
    // 1. If we have a direct binary path (via `which` or file lookup), use it directly
    // 2. Only use interactive shell if claude was detected purely as a shell function
    //    (i.e., shellWorkaround is true and no direct binary path)
    // Use direct binary path if available (bypasses shell functions)
    if (cachedCliStatus.commandPath && !cachedCliStatus.shellWorkaround) {
        logger.debug(`[executeClaudeCli] Using direct binary: ${cachedCliStatus.commandPath}`);
        // CRITICAL: Use getCleanEnv() to remove NODE_OPTIONS debugger flags
        // that would cause Claude CLI to fail silently in test/debug environments
        const cleanEnv = getCleanEnv();
        return spawnSync(cachedCliStatus.commandPath, args, {
            encoding: 'utf8',
            timeout,
            maxBuffer: 1024 * 1024, // 1MB buffer
            windowsHide: true,
            cwd: os.tmpdir(),
            env: {
                ...cleanEnv,
                LANG: 'en_US.UTF-8',
                LC_ALL: 'en_US.UTF-8',
            },
        });
    }
    // On Unix with no direct binary (shell function only): use interactive shell
    if (!isWindows) {
        logger.debug('[executeClaudeCli] Using interactive shell (no direct binary)');
        return executeViaInteractiveShell(args, timeout);
    }
    // Windows: try with shell for PATH resolution
    // CRITICAL: Use getCleanEnv() to remove NODE_OPTIONS debugger flags
    const cleanEnv = getCleanEnv();
    return spawnSync('claude', args, {
        encoding: 'utf8',
        timeout,
        maxBuffer: 1024 * 1024,
        windowsHide: true,
        shell: true,
        cwd: os.tmpdir(),
        env: {
            ...cleanEnv,
            LANG: 'en_US.UTF-8',
            LC_ALL: 'en_US.UTF-8',
        },
    });
}
/**
 * Execute command via interactive shell (for shell functions/aliases)
 *
 * When claude is defined as a shell function or alias in .zshrc/.bashrc,
 * we need to run via interactive shell to have access to it.
 */
function executeViaInteractiveShell(args, timeout) {
    const userShell = process.env.SHELL || '/bin/bash';
    const isZsh = userShell.includes('zsh');
    const shell = isZsh ? 'zsh' : 'bash';
    // Build the command string - properly escape arguments
    const escapedArgs = args.map((arg) => {
        // Escape single quotes and wrap in single quotes
        return `'${arg.replace(/'/g, "'\\''")}'`;
    });
    const command = `claude ${escapedArgs.join(' ')}`;
    // CRITICAL: Use getCleanEnv() to remove NODE_OPTIONS debugger flags
    const cleanEnv = getCleanEnv();
    const result = spawnSync(shell, ['-ic', command], {
        encoding: 'utf8',
        timeout,
        maxBuffer: 1024 * 1024,
        windowsHide: true,
        cwd: os.tmpdir(),
        env: {
            ...cleanEnv,
            LANG: 'en_US.UTF-8',
            LC_ALL: 'en_US.UTF-8',
            BASH_SILENCE_DEPRECATION_WARNING: '1',
        },
    });
    return result;
}
/**
 * Create a failure result when LLM detection fails (v1.0.157)
 *
 * v1.0.157: Keyword fallback REMOVED - if LLM fails, return ZERO plugins.
 * This is safer than guessing wrong (which caused over-detection bugs).
 *
 * @param startTime - Start time for duration calculation
 * @param error - Error message from failed LLM detection
 * @returns Detection result with empty plugins
 */
function createFailureResult(startTime, error) {
    logger.warn(`LLM detection failed: ${error || 'unknown'} - returning empty plugins (no fallback)`);
    return {
        success: false,
        plugins: [],
        confidence: 0,
        error,
        durationMs: performance.now() - startTime,
    };
}
/**
 * Detect plugins needed for a user prompt using Claude LLM
 *
 * @param userPrompt - The user's prompt to analyze
 * @param timeout - Timeout in milliseconds (default: 30000)
 * @returns Detection result with plugins to install
 */
export async function detectPluginsViaLLM(userPrompt, timeout = 15000, // v1.0.159: Reduced to 15s with --setting-sources "" optimization (CLI starts in <1s)
activeIncrements = []) {
    const startTime = performance.now();
    // Check CLI availability first
    const cliStatus = isClaudeCliAvailable();
    if (!cliStatus.available) {
        // v1.0.157: No keyword fallback - if CLI unavailable, return empty plugins
        // This is safer than guessing wrong (which caused over-detection bugs)
        logger.warn('Claude CLI not available for LLM detection - returning empty plugins (no fallback)');
        return {
            success: false,
            plugins: [],
            confidence: 0,
            error: cliStatus.error,
            durationMs: performance.now() - startTime,
        };
    }
    // Build the full prompt
    const systemPrompt = buildDetectionPrompt();
    // Inject active increment context for reopen detection
    let incrementContext = '';
    if (activeIncrements.length > 0) {
        const incList = activeIncrements.map(i => `  - ${i.id} (${i.type}, ${i.status}): "${i.name}"`).join('\n');
        incrementContext = `\n\nACTIVE INCREMENTS (consider reopening one of these if the user's request relates to existing work):\n${incList}\n\nIf the user's prompt relates to any of these active increments, use action "reopen" with the relatedKeyword matching the increment name. Only suggest "new" if the work is clearly unrelated to ALL active increments.`;
    }
    // v1.0.254: Truncate user prompt to prevent "Prompt is too long" errors
    // The first 3000 chars are sufficient for intent/plugin detection
    const safePrompt = truncateForDetection(userPrompt);
    const fullPrompt = `${systemPrompt}${incrementContext}

User prompt to analyze:
"${safePrompt.replace(/"/g, '\\"')}"

Which plugins should be loaded?`;
    try {
        // Execute Claude CLI with haiku for fast detection
        // --bare skips all auto-discovery (hooks, plugins, CLAUDE.md, MCP servers)
        // This subsumes the previous --setting-sources "" optimization
        const result = executeClaudeCli(['--bare', '-p', fullPrompt, '--model', 'haiku', '--output-format', 'json'], timeout);
        // Handle spawn errors - return failure (v1.0.157 - no fallback)
        if (result.error) {
            const errorMsg = result.error.message || String(result.error);
            // Timeout error
            if (errorMsg.includes('ETIMEDOUT') || errorMsg.includes('TIMEOUT')) {
                return createFailureResult(startTime, `Detection timed out after ${timeout}ms`);
            }
            return createFailureResult(startTime, `Claude CLI error: ${errorMsg}`);
        }
        // Handle non-zero exit - return failure (v1.0.157 - no fallback)
        if (result.status !== 0) {
            const stderr = result.stderr || '';
            const stdout = result.stdout || '';
            // Check for specific errors
            if (stderr.includes('authentication') || stderr.includes('API key')) {
                return createFailureResult(startTime, 'Claude CLI authentication error. Run: claude login');
            }
            if (stderr.includes('rate limit')) {
                return createFailureResult(startTime, 'Rate limit exceeded. Try again later.');
            }
            // Prompt too long - common with Haiku
            if (stderr.includes('too long') || stdout.includes('too long')) {
                return createFailureResult(startTime, 'Prompt too long for Haiku model');
            }
            return createFailureResult(startTime, `Claude CLI exited with code ${result.status}: ${stderr || stdout}`);
        }
        // Parse the response
        let output = (result.stdout || '').trim();
        if (!output) {
            return createFailureResult(startTime, 'Empty response from Claude CLI');
        }
        // Handle --output-format json wrapper (v1.0.155)
        // The response is wrapped in: {"type":"result","result":"...actual response..."}
        try {
            const wrapper = JSON.parse(output);
            if (wrapper.type === 'result' && wrapper.result) {
                output = wrapper.result;
                logger.debug(`Extracted result from JSON wrapper, duration_ms: ${wrapper.duration_ms}`);
            }
        }
        catch {
            // Not a JSON wrapper, use raw output
            logger.debug('Output is not JSON wrapper, using raw output');
        }
        // Try to extract JSON from the response (handle markdown code blocks)
        let jsonStr = output;
        // Remove markdown code blocks if present
        const codeBlockMatch = output.match(/```(?:json)?\s*([\s\S]*?)```/);
        if (codeBlockMatch) {
            jsonStr = codeBlockMatch[1].trim();
        }
        // Find JSON object in the string
        const jsonMatch = jsonStr.match(/\{[\s\S]*\}/);
        // v1.0.157+: Return failure if no JSON (no keyword fallback - LLM-only detection)
        if (!jsonMatch) {
            logger.debug(`Invalid LLM response format: ${output.slice(0, 200)}`);
            return createFailureResult(startTime, 'Invalid response format (no JSON found)');
        }
        // Parse JSON
        let parsed;
        try {
            parsed = JSON.parse(jsonMatch[0]);
        }
        catch (parseError) {
            logger.debug(`JSON parse error: ${parseError}`);
            return createFailureResult(startTime, 'Failed to parse response JSON');
        }
        // Validate and filter plugins (v1.0.159: accept both SW and official plugins)
        const rawPlugins = parsed.plugins || [];
        let validPlugins = rawPlugins.filter((p) => typeof p === 'string' && ALL_VALID_PLUGINS.includes(p));
        // Log if we filtered out invalid plugins
        if (validPlugins.length !== rawPlugins.length) {
            const invalid = rawPlugins.filter((p) => !ALL_VALID_PLUGINS.includes(p));
            logger.debug(`Filtered out invalid plugins: ${invalid.join(', ')}`);
        }
        // v1.0.156: Trust Opus model - no arbitrary limits, no keyword fallback
        // Opus understands NECESSARY implications (dashboard → backend) vs OPTIONAL (→ infra)
        // Parse increment recommendation (v1.0.141+, v1.0.168: added mandatory field)
        let incrementRecommendation;
        if (parsed.increment) {
            const validActions = ['new', 'reopen', 'small_fix', 'hotfix', 'none'];
            const action = parsed.increment.action;
            if (validActions.includes(action)) {
                // v1.0.168: LLM decides if increment is mandatory (not config-based)
                const isMandatory = parsed.increment.mandatory === true ||
                    (action === 'new' && typeof parsed.increment.confidence === 'number' && parsed.increment.confidence >= 0.85);
                incrementRecommendation = {
                    action,
                    confidence: typeof parsed.increment.confidence === 'number' ? parsed.increment.confidence : 0.5,
                    mandatory: isMandatory,
                    suggestedName: parsed.increment.suggestedName,
                    relatedKeyword: parsed.increment.relatedKeyword,
                    reasoning: parsed.increment.reasoning || 'No reasoning provided',
                };
                logger.debug(`Increment recommendation: ${action} (confidence: ${incrementRecommendation.confidence}, mandatory: ${isMandatory})`);
            }
            else {
                logger.debug(`Invalid increment action: ${parsed.increment.action}`);
            }
        }
        // Parse skill routing recommendation (v1.0.150+)
        let skillRouting;
        if (parsed.routing && parsed.routing.skills) {
            const validTimings = ['immediate', 'after_increment', 'after_planning', 'with_primary', 'after_primary'];
            const validPriorities = ['primary', 'secondary'];
            // Validate and filter skills
            const validSkills = [];
            for (const skill of parsed.routing.skills) {
                // Validate required fields
                if (!skill.name || !skill.plugin || !skill.fullName) {
                    logger.debug(`Skipping skill with missing required fields: ${JSON.stringify(skill)}`);
                    continue;
                }
                // Validate plugin name (v2.1.0: accept both specweave and vskill plugins)
                if (!isKnownPlugin(skill.plugin)) {
                    logger.debug(`Skipping skill with invalid plugin: ${skill.plugin}`);
                    continue;
                }
                // Validate and default timing
                const invokeWhen = validTimings.includes(skill.invokeWhen)
                    ? skill.invokeWhen
                    : 'after_increment';
                // Validate and default priority
                const priority = validPriorities.includes(skill.priority)
                    ? skill.priority
                    : 'secondary';
                validSkills.push({
                    name: skill.name,
                    plugin: skill.plugin,
                    fullName: skill.fullName,
                    priority,
                    invokeWhen,
                    reason: skill.reason || 'No reason provided',
                });
            }
            if (validSkills.length > 0) {
                skillRouting = {
                    skills: validSkills,
                    workflow: {
                        suggestPlanMode: parsed.routing.workflow?.suggestPlanMode ?? false,
                        phases: parsed.routing.workflow?.phases ?? [],
                    },
                    confidence: typeof parsed.routing.confidence === 'number' ? parsed.routing.confidence : 0.5,
                    reasoning: parsed.routing.reasoning || 'No reasoning provided',
                };
                // Log routing decision
                const primarySkill = validSkills.find(s => s.priority === 'primary');
                const secondarySkills = validSkills.filter(s => s.priority === 'secondary');
                logger.debug(`Skill routing: primary=${primarySkill?.fullName || 'none'}, secondary=[${secondarySkills.map(s => s.fullName).join(', ')}]`);
            }
        }
        // Parse skill invocation recommendation (v1.0.168)
        let skillInvocation;
        if (parsed.skillInvocation && parsed.skillInvocation.skill) {
            skillInvocation = {
                skill: parsed.skillInvocation.skill,
                reason: parsed.skillInvocation.reason || 'Use this skill for specialized support',
                mandatory: parsed.skillInvocation.mandatory === true,
            };
            logger.debug(`Skill invocation: ${skillInvocation.skill} (mandatory: ${skillInvocation.mandatory})`);
        }
        // Parse LSP recommendation (v1.0.198+)
        let lspRecommendation;
        if (parsed.lsp && parsed.lsp.needed === true) {
            const validOperations = ['references', 'definition', 'hover', 'symbols', null];
            const validLanguages = ['typescript', 'python', 'rust', 'go', 'csharp', 'java', null];
            const operation = validOperations.includes(parsed.lsp.operation)
                ? parsed.lsp.operation
                : null;
            const language = validLanguages.includes(parsed.lsp.language)
                ? parsed.lsp.language
                : null;
            lspRecommendation = {
                needed: true,
                operation,
                language,
                warmupRequired: parsed.lsp.warmupRequired !== false, // Default to true
            };
            logger.debug(`LSP recommendation: operation=${operation}, language=${language}, warmupRequired=${lspRecommendation.warmupRequired}`);
        }
        return {
            success: true,
            plugins: validPlugins,
            confidence: typeof parsed.confidence === 'number' ? parsed.confidence : 0.5,
            reasoning: parsed.reasoning,
            durationMs: performance.now() - startTime,
            increment: incrementRecommendation,
            routing: skillRouting,
            skillInvocation,
            lsp: lspRecommendation,
        };
    }
    catch (error) {
        const errorMsg = error instanceof Error ? error.message : String(error);
        logger.error(`LLM detection failed: ${errorMsg}`);
        // v1.0.157+: Return failure on exception (no keyword fallback - LLM-only detection)
        return createFailureResult(startTime, `Detection failed: ${errorMsg}`);
    }
}
/** Track which plugins have already been reported this session (daily dedup) */
const _reportedPlugins = new Set();
/**
 * Report session-load installs for an already-installed plugin to verified-skill.com.
 * Reads the installed skill directories and sends a lightweight batch HTTP call.
 * Fire-and-forget: specweave is long-lived so the event loop stays alive.
 *
 * Rate-limited: once per plugin per session to avoid inflation on repeated loads.
 * Server-side dedup (ipHash+source+date) handles cross-session dedup.
 */
async function reportInstallsForPlugin(pluginName) {
    try {
        if (process.env.VSKILL_NO_TELEMETRY === '1')
            return;
        // Client-side dedup: once per plugin per session
        if (_reportedPlugins.has(pluginName))
            return;
        _reportedPlugins.add(pluginName);
        // Find installed skill directories for this plugin
        const projectRoot = getProjectRoot();
        const skillsDir = path.join(projectRoot, '.claude', 'skills', pluginName);
        if (!fs.existsSync(skillsDir))
            return;
        const skillDirs = fs.readdirSync(skillsDir, { withFileTypes: true })
            .filter(d => d.isDirectory() && fs.existsSync(path.join(skillsDir, d.name, 'SKILL.md')));
        if (skillDirs.length === 0)
            return;
        const skills = skillDirs.map(d => ({
            skillName: d.name,
        }));
        const controller = new AbortController();
        const timeout = setTimeout(() => controller.abort(), 5000);
        try {
            await fetch('https://verified-skill.com/api/v1/skills/installs', {
                method: 'POST',
                headers: { 'User-Agent': 'specweave-cli', 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    skills,
                    source: 'session',
                    platform: process.platform,
                }),
                signal: controller.signal,
            });
        }
        finally {
            clearTimeout(timeout);
        }
    }
    catch {
        // Silent — install tracking must never block plugin loading
    }
}
/**
 * Install a plugin using native Claude plugin system.
 *
 * @deprecated v1.0.535: On-demand plugin installation removed.
 * All plugins are now installed at init time via direct file copy
 * to .claude/skills/. This function is a no-op that returns success.
 *
 * @param pluginName - Name of the plugin to install
 * @param timeout - Timeout in milliseconds (unused)
 * @returns Installation result (always success/alreadyInstalled)
 */
export async function installPluginViaCli(pluginName, timeout = 30000, skillFilter) {
    // v1.0.535: No-op — plugins are pre-installed at init time
    return { success: true, plugin: pluginName, alreadyInstalled: true };
}
/**
 * Install multiple plugins via CLI
 *
 * @deprecated v1.0.535: No-op — plugins are pre-installed at init time.
 * @param plugins - Array of plugin names to install
 * @returns Array of installation results (all alreadyInstalled)
 */
export async function installPluginsViaCli(plugins, skillInvocation) {
    // v1.0.535: No-op — plugins are pre-installed at init time
    return plugins.map(plugin => ({ success: true, plugin, alreadyInstalled: true }));
}
/**
 * Full pipeline: detect plugins from prompt and optionally install them
 *
 * @deprecated v1.0.535: Plugin installation removed. Detection still works
 * for increment suggestions but no longer triggers installation.
 * All plugins are pre-installed at init time via direct file copy.
 *
 * @param userPrompt - The user's prompt
 * @returns Detection results (installations always empty)
 */
export async function detectAndInstallPlugins(userPrompt) {
    // Check config first
    const config = readPluginAutoLoadConfig();
    // If auto-load is completely disabled, skip detection
    if (!config.enabled) {
        logger.debug('[detectAndInstallPlugins] Auto-load disabled in config, skipping detection');
        return {
            detection: {
                success: true,
                plugins: [],
                confidence: 1.0,
                reasoning: 'Plugin auto-load disabled in config',
                durationMs: 0,
            },
            installations: [],
        };
    }
    // Step 1: Detect needed plugins (still used for increment suggestions)
    const detection = await detectPluginsViaLLM(userPrompt);
    // v1.0.535: No installation — plugins are pre-installed at init time
    return {
        detection,
        installations: [],
    };
}
/**
 * Format hook output for Claude Code
 *
 * @param result - Detection/installation result
 * @returns JSON string for hook output
 */
export function formatHookOutput(result) {
    const { detection } = result;
    // Always continue (don't block Claude Code)
    const output = {
        continue: true,
    };
    // Build system message based on results
    if (!detection.success) {
        if (detection.error?.includes('not found')) {
            output.systemMessage = `SpecWeave: Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code

Plugin auto-loading is disabled. Install Claude CLI to enable automatic plugin detection.`;
        }
        else if (detection.error?.includes('authentication')) {
            output.systemMessage = `SpecWeave: Claude CLI authentication required. Run: claude login`;
        }
        // Don't show message for other errors (silent degradation)
    }
    else if (detection.plugins.length > 0) {
        // v1.0.535: Plugin installation removed — plugins are pre-installed at init time.
        // No need for suggest-only or normal install mode messages.
    }
    return JSON.stringify(output);
}
//# sourceMappingURL=llm-plugin-detector.js.map