/**
 * Slash command registry for Franklin.
 * Extracted from loop.ts for maintainability.
 *
 * Two types of commands:
 * 1. "Handled" — execute directly, emit events, return { handled: true }
 * 2. "Rewrite" — transform input into a prompt for the agent, return { handled: false, rewritten }
 */
import fs from 'node:fs';
import path from 'node:path';
import { execSync } from 'node:child_process';
import { BLOCKRUN_DIR, VERSION } from '../config.js';
import { estimateHistoryTokens, getAnchoredTokenCount, getContextWindow, resetTokenAnchor } from './tokens.js';
import { forceCompact } from './compact.js';
import { getStatsSummary } from '../stats/tracker.js';
import { resolveModel } from '../ui/model-picker.js';
import { listSessions, loadSessionHistory, } from '../session/storage.js';
import { matchSkill } from '../skills/invoke.js';
// ─── Git helpers ──────────────────────────────────────────────────────────
function gitExec(cmd, cwd, timeout = 5000, maxBuffer) {
    return execSync(cmd, {
        cwd,
        encoding: 'utf-8',
        timeout,
        maxBuffer: maxBuffer || 1024 * 1024,
        stdio: ['pipe', 'pipe', 'pipe'],
    }).trim();
}
function gitCmd(ctx, cmd, timeout, maxBuffer) {
    try {
        return gitExec(cmd, ctx.config.workingDir || process.cwd(), timeout, maxBuffer);
    }
    catch (e) {
        // Prefer stderr (actual git error message) over the noisy "Command failed: ..." header
        const errObj = e;
        const stderr = errObj.stderr ? String(errObj.stderr).trim() : '';
        // Take only the first meaningful line (git sometimes dumps full usage on errors)
        const firstLine = (stderr || errObj.message || 'unknown').split('\n')[0].trim();
        ctx.onEvent({ kind: 'text_delta', text: `Git: ${firstLine}\n` });
        return null;
    }
}
function emitDone(ctx) {
    ctx.onEvent({ kind: 'turn_done', reason: 'completed' });
}
function buildExchanges(history) {
    const exchanges = [];
    let i = 0;
    while (i < history.length) {
        const msg = history[i];
        if (msg.role !== 'user') {
            i++;
            continue;
        }
        const userText = extractText(msg);
        if (!userText) {
            i++;
            continue;
        } // skip tool_result-only user messages
        const startIdx = i;
        let endIdx = i;
        let assistantText = '';
        const toolNames = [];
        let j = i + 1;
        while (j < history.length) {
            const next = history[j];
            if (next.role === 'user' && extractText(next))
                break; // next exchange
            if (next.role === 'assistant') {
                const t = extractText(next);
                if (t && !assistantText)
                    assistantText = t;
                if (Array.isArray(next.content)) {
                    for (const p of next.content) {
                        if (p.type === 'tool_use' && !toolNames.includes(p.name))
                            toolNames.push(p.name);
                    }
                }
            }
            endIdx = j;
            j++;
        }
        exchanges.push({
            userText: userText.slice(0, 120) + (userText.length > 120 ? '…' : ''),
            assistantText: (assistantText.slice(0, 80) + (assistantText.length > 80 ? '…' : '')) || '(no text)',
            toolNames,
            startIdx,
            endIdx,
        });
        i = j;
    }
    return exchanges;
}
function extractText(msg) {
    if (typeof msg.content === 'string')
        return msg.content.trim();
    if (!Array.isArray(msg.content))
        return '';
    for (const p of msg.content) {
        if (p.type === 'text' && p.text.trim())
            return p.text.trim();
    }
    return '';
}
// ─── Command Definitions ──────────────────────────────────────────────────
// Direct-handled commands (don't go to agent)
const DIRECT_COMMANDS = {
    '/noplan': (ctx) => {
        ctx.config.planDisabled = true;
        ctx.onEvent({ kind: 'text_delta', text: 'Plan-then-execute disabled for this session. Complex tasks will use a single model.\n' });
        emitDone(ctx);
    },
    '/stash': (ctx) => {
        const r = gitCmd(ctx, 'git stash push -m "franklin auto-stash"', 10000);
        if (r !== null)
            ctx.onEvent({ kind: 'text_delta', text: r ? `${r}\n` : 'No changes to stash.\n' });
        emitDone(ctx);
    },
    '/unstash': (ctx) => {
        const r = gitCmd(ctx, 'git stash pop', 10000);
        if (r !== null)
            ctx.onEvent({ kind: 'text_delta', text: r ? `${r}\n` : 'Stash applied.\n' });
        emitDone(ctx);
    },
    '/log': (ctx) => {
        const r = gitCmd(ctx, 'git log --oneline -15 --no-color');
        if (r !== null)
            ctx.onEvent({ kind: 'text_delta', text: r ? `\`\`\`\n${r}\n\`\`\`\n` : 'No commits yet.\n' });
        emitDone(ctx);
    },
    '/status': (ctx) => {
        const r = gitCmd(ctx, 'git status --short --branch');
        if (r !== null)
            ctx.onEvent({ kind: 'text_delta', text: r ? `\`\`\`\n${r}\n\`\`\`\n` : 'Working tree clean.\n' });
        emitDone(ctx);
    },
    '/diff': (ctx) => {
        // git diff with stat header then full diff
        const stat = gitCmd(ctx, 'git diff --stat --no-color');
        if (stat === null) {
            emitDone(ctx);
            return;
        }
        const full = gitCmd(ctx, 'git diff --no-color');
        if (full === null) {
            emitDone(ctx);
            return;
        }
        if (!stat && !full) {
            ctx.onEvent({ kind: 'text_delta', text: 'No unstaged changes.\n' });
        }
        else {
            ctx.onEvent({ kind: 'text_delta', text: `\`\`\`diff\n${[stat, full].filter(Boolean).join('\n---\n')}\n\`\`\`\n` });
        }
        emitDone(ctx);
    },
    '/undo': (ctx) => {
        const r = gitCmd(ctx, 'git reset --soft HEAD~1');
        if (r !== null)
            ctx.onEvent({ kind: 'text_delta', text: `Last commit undone. Changes preserved in staging.\n` });
        emitDone(ctx);
    },
    '/tokens': (ctx) => {
        const { estimated, apiAnchored } = getAnchoredTokenCount(ctx.history);
        const contextWindow = getContextWindow(ctx.config.model);
        const pct = (estimated / contextWindow) * 100;
        // Count tool results and thinking blocks
        let toolResults = 0;
        let thinkingBlocks = 0;
        let totalToolChars = 0;
        for (const msg of ctx.history) {
            if (typeof msg.content === 'string')
                continue;
            if (!Array.isArray(msg.content))
                continue;
            for (const part of msg.content) {
                if ('type' in part) {
                    if (part.type === 'tool_result') {
                        toolResults++;
                        // Sibling of PR #54's tokens.ts fix: image base64 must NOT
                        // count toward the displayed char total — `/context` would
                        // otherwise show ~70K chars per attached image and confuse
                        // the user about why the ring is at 1% but "total tool
                        // chars" is huge.
                        if (typeof part.content === 'string') {
                            totalToolChars += part.content.length;
                        }
                        else if (Array.isArray(part.content)) {
                            for (const block of part.content) {
                                const t = block.type;
                                if (t === 'text') {
                                    totalToolChars += (block.text || '').length;
                                }
                                else if (t === 'image') {
                                    totalToolChars += 6000; // ~1500 tokens × 4 chars/tok
                                }
                            }
                        }
                    }
                    if (part.type === 'thinking')
                        thinkingBlocks++;
                }
            }
        }
        ctx.onEvent({ kind: 'text_delta', text: `**Token Usage**\n` +
                `  Estimated:  ~${estimated.toLocaleString()} tokens ${apiAnchored ? '(API-anchored)' : '(estimated)'}\n` +
                `  Context:    ${(contextWindow / 1000).toFixed(0)}k window (${pct.toFixed(1)}% used)\n` +
                `  Messages:   ${ctx.history.length}\n` +
                `  Tool results: ${toolResults} (${(totalToolChars / 1024).toFixed(0)}KB)\n` +
                `  Thinking:   ${thinkingBlocks} blocks\n` +
                (pct > 80 ? '  ⚠ Near limit — run /compact\n' : '') +
                (pct > 60 ? '' : '  ✓ Healthy\n')
        });
        emitDone(ctx);
    },
    '/help': (ctx) => {
        const ultrathinkOn = ctx.config.ultrathink;
        let skillsBlock = '';
        if (ctx.skillRegistry) {
            const visible = ctx.skillRegistry
                .list()
                .filter((l) => !l.skill.disableModelInvocation);
            if (visible.length > 0) {
                skillsBlock =
                    `\n  **Skills:**\n` +
                        visible
                            .map((l) => `    /${l.skill.name.padEnd(22)} ${l.skill.description}`)
                            .join('\n') +
                        `\n`;
            }
        }
        ctx.onEvent({ kind: 'text_delta', text: `**RunCode Commands**\n\n` +
                `  **Coding:** /commit /review /test /fix /debug /explain /search /find /refactor /scaffold\n` +
                `  **Git:** /push /pr /undo /status /diff /log /branch /stash /unstash\n` +
                `  **Analysis:** /security /lint /optimize /todo /deps /clean /migrate /doc\n` +
                `  **Session:** /plan /ultraplan /execute /compact /retry /sessions /resume /session-search /context /tasks /history /transcript\n` +
                `  **Power:** /ultrathink [query] /ultraplan /noplan /moa [query] /dump\n` +
                `  **Info:** /model /auto /wallet /cost /tokens /learnings /brain /mcp /doctor /version /bug /help\n` +
                `  **UI:** /clear /exit\n` +
                skillsBlock +
                (ultrathinkOn ? `\n  Ultrathink: ON\n` : '')
        });
        emitDone(ctx);
    },
    '/history': (ctx) => {
        const { history, config } = ctx;
        const modelName = config.model.split('/').pop() || config.model;
        const exchanges = buildExchanges(history);
        let output = '**Conversation History**\n\n';
        if (exchanges.length === 0) {
            output += 'No history in the current session yet.\n';
        }
        else {
            for (let i = 0; i < exchanges.length; i++) {
                const ex = exchanges[i];
                const tools = ex.toolNames.length > 0 ? ` · used: ${ex.toolNames.join(', ')}` : '';
                output += `[${i + 1}] [user] ${ex.userText}\n`;
                output += `     [${modelName}] ${ex.assistantText}${tools}\n\n`;
            }
        }
        output += 'Use `/delete <number>` to remove exchanges (e.g., `/delete 2` or `/delete 3-5`).\n';
        output += 'Use `/transcript` for the full, un-truncated transcript.\n';
        ctx.onEvent({ kind: 'text_delta', text: output });
        emitDone(ctx);
    },
    '/transcript': (ctx) => {
        // Dump the FULL, un-truncated conversation as a single fresh stdout
        // block. Works around the limitation that the terminal's native
        // scrollback fills up faster than we can render long Franklin sessions
        // — by the time you scroll up to look for the first message, the
        // older Ink output has been pushed out of the terminal's ring buffer.
        // The fresh emit here becomes one contiguous block, so the user can
        // scroll *that* to read everything in order.
        const { history, config } = ctx;
        const modelName = config.model.split('/').pop() || config.model;
        const exchanges = buildExchanges(history);
        if (exchanges.length === 0) {
            ctx.onEvent({ kind: 'text_delta', text: 'No history in the current session yet.\n' });
            emitDone(ctx);
            return;
        }
        let output = `**Full Transcript** — ${exchanges.length} exchange${exchanges.length === 1 ? '' : 's'}, session \`${ctx.sessionId}\`\n\n`;
        output += '─'.repeat(70) + '\n\n';
        for (let i = 0; i < exchanges.length; i++) {
            const ex = exchanges[i];
            // Re-read full text from raw history (buildExchanges truncated).
            const userMsg = history[ex.startIdx];
            const fullUser = extractText(userMsg);
            // Find first assistant text in this exchange (full, not truncated).
            let fullAssistant = '';
            for (let j = ex.startIdx + 1; j <= ex.endIdx; j++) {
                const m = history[j];
                if (m.role === 'assistant') {
                    const t = extractText(m);
                    if (t && !fullAssistant)
                        fullAssistant = t;
                }
            }
            output += `❯ [${i + 1}] ${fullUser}\n\n`;
            if (fullAssistant)
                output += `${fullAssistant}\n`;
            if (ex.toolNames.length > 0) {
                output += `\n  _tools: ${ex.toolNames.join(', ')}_\n`;
            }
            output += `\n[${modelName}]\n\n`;
            output += '─'.repeat(70) + '\n\n';
        }
        output += `End of transcript — ${history.length} raw messages, ${exchanges.length} user/assistant exchanges.\n`;
        ctx.onEvent({ kind: 'text_delta', text: output });
        emitDone(ctx);
    },
    '/bug': (ctx) => {
        ctx.onEvent({ kind: 'text_delta', text: 'Report issues at: https://github.com/BlockRunAI/Franklin/issues\n' });
        emitDone(ctx);
    },
    '/version': (ctx) => {
        ctx.onEvent({ kind: 'text_delta', text: `RunCode v${VERSION}\n` });
        emitDone(ctx);
    },
    '/mcp': async (ctx) => {
        const { listMcpServers } = await import('../mcp/client.js');
        const servers = listMcpServers();
        if (servers.length === 0) {
            ctx.onEvent({ kind: 'text_delta', text: 'No MCP servers connected.\nAdd servers to `~/.blockrun/mcp.json` or `.mcp.json` in your project.\n' });
        }
        else {
            let text = `**${servers.length} MCP server(s) connected:**\n\n`;
            for (const s of servers) {
                text += `  **${s.name}** — ${s.toolCount} tools\n`;
                for (const t of s.tools)
                    text += `    · ${t}\n`;
            }
            ctx.onEvent({ kind: 'text_delta', text });
        }
        emitDone(ctx);
    },
    '/context': async (ctx) => {
        const { estimated, apiAnchored } = getAnchoredTokenCount(ctx.history);
        const contextWindow = getContextWindow(ctx.config.model);
        const pct = (estimated / contextWindow) * 100;
        const usagePct = pct.toFixed(1);
        const warning = pct > 80 ? '  ⚠ Near limit — consider /compact\n' : '';
        ctx.onEvent({ kind: 'text_delta', text: `**Session Context**\n` +
                `  Model:      ${ctx.config.model}\n` +
                `  Mode:       ${ctx.config.permissionMode || 'default'}\n` +
                `  Messages:   ${ctx.history.length}\n` +
                `  Tokens:     ~${estimated.toLocaleString()} / ${(contextWindow / 1000).toFixed(0)}k (${usagePct}%)${apiAnchored ? ' ✓' : ' ~'}\n` +
                warning +
                `  Session:    ${ctx.sessionId}\n` +
                `  Directory:  ${ctx.config.workingDir || process.cwd()}\n`
        });
        emitDone(ctx);
    },
    '/doctor': async (ctx) => {
        const checks = [];
        try {
            execSync('git --version', { stdio: 'pipe' });
            checks.push('✓ git available');
        }
        catch {
            checks.push('✗ git not found');
        }
        try {
            execSync('rg --version', { stdio: 'pipe' });
            checks.push('✓ ripgrep available');
        }
        catch {
            checks.push('⚠ ripgrep not found (using native grep fallback)');
        }
        const hasWallet = fs.existsSync(path.join(BLOCKRUN_DIR, 'wallet.json'))
            || fs.existsSync(path.join(BLOCKRUN_DIR, 'solana-wallet.json'));
        checks.push(hasWallet ? '✓ wallet configured' : '⚠ no wallet — run: franklin setup');
        checks.push(fs.existsSync(path.join(BLOCKRUN_DIR, 'franklin-config.json')) || fs.existsSync(path.join(BLOCKRUN_DIR, 'runcode-config.json')) ? '✓ config file exists' : '⚠ no config — using defaults');
        // Check MCP
        const { listMcpServers } = await import('../mcp/client.js');
        const mcpServers = listMcpServers();
        checks.push(mcpServers.length > 0
            ? `✓ MCP: ${mcpServers.length} server(s), ${mcpServers.reduce((a, s) => a + s.toolCount, 0)} tools`
            : '⚠ no MCP servers connected');
        checks.push(`✓ model: ${ctx.config.model}`);
        checks.push(`✓ history: ${ctx.history.length} messages, ~${estimateHistoryTokens(ctx.history).toLocaleString()} tokens`);
        checks.push(`✓ session: ${ctx.sessionId}`);
        checks.push(`✓ version: v${VERSION}`);
        ctx.onEvent({ kind: 'text_delta', text: `**Health Check**\n${checks.map(c => '  ' + c).join('\n')}\n` });
        emitDone(ctx);
    },
    '/plan': (ctx) => {
        if (ctx.config.permissionMode === 'plan') {
            ctx.onEvent({ kind: 'text_delta', text: 'Already in plan mode. Use /execute to exit.\n' });
        }
        else {
            ctx.config.permissionMode = 'plan';
            ctx.onEvent({ kind: 'text_delta', text: '**Plan mode active.** Tools restricted to read-only. Use /execute when ready to implement.\n' });
        }
        emitDone(ctx);
    },
    '/ultrathink': (ctx) => {
        const cfg = ctx.config;
        cfg.ultrathink = !cfg.ultrathink;
        if (cfg.ultrathink) {
            ctx.onEvent({ kind: 'text_delta', text: '**Ultrathink mode ON.** Extended reasoning active — the model will think deeply before responding.\n' +
                    'Use `/ultrathink` again to disable, or `/ultrathink <query>` to send a one-shot deep analysis.\n'
            });
        }
        else {
            ctx.onEvent({ kind: 'text_delta', text: '**Ultrathink mode OFF.** Normal response mode restored.\n' });
        }
        emitDone(ctx);
    },
    '/dump': (ctx) => {
        const instructions = ctx.config.systemInstructions;
        const joined = instructions.join('\n\n---\n\n');
        ctx.onEvent({ kind: 'text_delta', text: `**System Prompt** (${instructions.length} section${instructions.length !== 1 ? 's' : ''}):\n\n` +
                `\`\`\`\n${joined.slice(0, 4000)}${joined.length > 4000 ? `\n... (${joined.length - 4000} chars truncated)` : ''}\n\`\`\`\n`
        });
        emitDone(ctx);
    },
    '/execute': (ctx) => {
        if (ctx.config.permissionMode !== 'plan') {
            ctx.onEvent({ kind: 'text_delta', text: 'Not in plan mode. Use /plan to enter.\n' });
        }
        else {
            ctx.config.permissionMode = 'default';
            ctx.onEvent({ kind: 'text_delta', text: '**Execution mode.** All tools enabled with permissions.\n' });
        }
        emitDone(ctx);
    },
    '/sessions': async (ctx) => {
        const sessions = listSessions();
        if (sessions.length === 0) {
            ctx.onEvent({ kind: 'text_delta', text: 'No saved sessions.\n' });
        }
        else {
            const { formatTokens, formatUsd, shortModelName } = await import('../stats/format.js');
            let text = `**${sessions.length} saved sessions:**\n\n`;
            for (const s of sessions.slice(0, 10)) {
                const date = new Date(s.updatedAt).toLocaleString();
                const dir = s.workDir ? path.basename(s.workDir) : '';
                const current = s.id === ctx.sessionId ? ' (current)' : '';
                const model = shortModelName(s.model);
                const tokens = (s.inputTokens || s.outputTokens)
                    ? `  ${formatTokens(s.inputTokens ?? 0)} in / ${formatTokens(s.outputTokens ?? 0)} out`
                    : '';
                const cost = s.costUsd ? `  ${formatUsd(s.costUsd)}` : '';
                const saved = s.savedVsOpusUsd && s.savedVsOpusUsd > 0.001
                    ? `  saved ${formatUsd(s.savedVsOpusUsd)}`
                    : '';
                text += `  ${model} — ${s.messageCount} messages${tokens}${cost}${saved}\n`;
                text += `  ${date} · ${dir}${current}\n\n`;
            }
            if (sessions.length > 10)
                text += `  ... and ${sessions.length - 10} more\n`;
            text += 'Use /resume to restore the latest session, or /resume <session-id> for a specific one.\n';
            ctx.onEvent({ kind: 'text_delta', text });
        }
        emitDone(ctx);
    },
    '/cost': async (ctx) => {
        const { stats, saved } = getStatsSummary();
        const { getSessionModelBreakdown } = await import('../stats/session-tracker.js');
        const { formatTokens, formatUsd, shortModelName } = await import('../stats/format.js');
        const breakdown = getSessionModelBreakdown();
        let text = `**Session Cost**\n` +
            `  Requests: ${stats.totalRequests}\n` +
            `  Cost:     $${stats.totalCostUsd.toFixed(4)} USDC\n` +
            `  Saved:    $${saved.toFixed(2)} vs Opus tier\n` +
            `  Tokens:   ${formatTokens(stats.totalInputTokens)} in / ${formatTokens(stats.totalOutputTokens)} out\n`;
        if (breakdown.length > 0) {
            text += `\n  **By model:**\n`;
            for (const m of breakdown) {
                const name = shortModelName(m.model).padEnd(28);
                const cost = formatUsd(m.costUsd).padStart(8);
                const reqs = `${m.requests} req`.padStart(6);
                const tier = m.lastTier ? `  ${m.lastTier}` : '';
                text += `    ${name} ${cost}  ${reqs}${tier}\n`;
            }
        }
        ctx.onEvent({ kind: 'text_delta', text });
        emitDone(ctx);
    },
    '/clear': (ctx) => {
        ctx.history.length = 0;
        resetTokenAnchor();
        ctx.onEvent({ kind: 'text_delta', text: 'Conversation history cleared.\n' });
        emitDone(ctx);
    },
    '/failures': async (ctx) => {
        const { getFailureStats } = await import('../stats/failures.js');
        const stats = getFailureStats();
        if (stats.total === 0) {
            ctx.onEvent({ kind: 'text_delta', text: 'No failures recorded.\n' });
            emitDone(ctx);
            return;
        }
        let text = `**Failure Log** (${stats.total} total)\n\n`;
        if (stats.byType.size > 0) {
            text += '  **By type:**\n';
            for (const [type, count] of [...stats.byType.entries()].sort((a, b) => b[1] - a[1])) {
                text += `    ${type.padEnd(20)} ${count}\n`;
            }
        }
        if (stats.byTool.size > 0) {
            text += '\n  **By tool:**\n';
            for (const [tool, count] of [...stats.byTool.entries()].sort((a, b) => b[1] - a[1])) {
                text += `    ${tool.padEnd(20)} ${count}\n`;
            }
        }
        if (stats.recentFailures.length > 0) {
            text += '\n  **Recent:**\n';
            for (const f of stats.recentFailures.slice(-5)) {
                const date = new Date(f.timestamp).toLocaleDateString();
                const tool = f.toolName ? ` ${f.toolName}:` : '';
                text += `    [${date}]${tool} ${f.errorMessage.slice(0, 80)}\n`;
            }
        }
        ctx.onEvent({ kind: 'text_delta', text });
        emitDone(ctx);
    },
    '/compact': async (ctx) => {
        const beforeTokens = estimateHistoryTokens(ctx.history);
        const { history: compacted, compacted: didCompact } = await forceCompact(ctx.history, ctx.config.model, ctx.client, ctx.config.debug);
        if (didCompact) {
            ctx.history.length = 0;
            ctx.history.push(...compacted);
            resetTokenAnchor();
            const afterTokens = estimateHistoryTokens(ctx.history);
            const saved = beforeTokens - afterTokens;
            const pct = Math.round((saved / beforeTokens) * 100);
            ctx.onEvent({ kind: 'text_delta', text: `Compacted: ~${beforeTokens.toLocaleString()} → ~${afterTokens.toLocaleString()} tokens (saved ${pct}%)\n`
            });
        }
        else {
            ctx.onEvent({ kind: 'text_delta', text: `Nothing to compact — history is already minimal (${beforeTokens.toLocaleString()} tokens, ${ctx.history.length} messages).\n`
            });
        }
        emitDone(ctx);
    },
};
// Prompt-rewrite commands (transformed into agent prompts)
const REWRITE_COMMANDS = {
    '/commit': 'Review the current git diff and staged changes. Stage relevant files with `git add`, then create a commit with a concise message summarizing the changes. Do NOT push to remote.',
    '/push': 'Push the current branch to the remote repository using `git push`. Show the result.',
    '/pr': 'Create a pull request for the current branch. First check `git log --oneline main..HEAD` to see commits, then use `gh pr create` with a descriptive title and body summarizing the changes. If gh CLI is not available, show the manual steps.',
    '/review': 'Review the current git diff. For each changed file, check for: bugs, security issues, missing error handling, performance problems, and style issues. Provide a brief summary of findings.',
    '/fix': 'Look at the most recent error or issue we discussed and fix it. Check the relevant files, identify the root cause, and apply the fix.',
    '/test': 'Detect the project test framework (look for package.json scripts, pytest, etc.) and run the test suite. Show a summary of results.',
    '/debug': 'Look at the most recent error in this session. Read the relevant source files, analyze the root cause, and suggest a fix with specific code changes.',
    '/init': 'Read the project structure: check package.json (or equivalent), README, and key config files. Summarize: what this project is, main language/framework, entry points, and how to run/test it.',
    '/todo': 'Search the codebase for TODO, FIXME, HACK, and XXX comments using Grep. Show the results grouped by file.',
    '/deps': 'Read the project dependency file (package.json, requirements.txt, go.mod, Cargo.toml, etc.) and list key dependencies with their versions.',
    '/optimize': 'Analyze the codebase for performance issues. Check for: unnecessary re-renders, N+1 queries, missing indexes, unoptimized loops, large bundle sizes, and memory leaks. Provide specific recommendations.',
    '/security': 'Audit the codebase for security issues. Check for: SQL injection, XSS, command injection, hardcoded secrets, insecure dependencies, OWASP top 10 vulnerabilities. Report findings with severity.',
    '/lint': 'Check for code quality issues: unused imports, inconsistent naming, missing type annotations, long functions, duplicated code. Suggest improvements.',
    '/migrate': 'Check for pending database migrations, outdated dependencies, or breaking changes that need addressing. List required migration steps.',
    '/clean': 'Find and remove dead code: unused imports, unreachable code, commented-out blocks, unused variables and functions. Show what would be removed before making changes.',
    '/tasks': 'List all current tasks using the Task tool.',
    '/ultraplan': 'Enter ultraplan mode: create a detailed, step-by-step implementation plan before writing any code. ' +
        'First, thoroughly read ALL relevant files. Map out every dependency and potential side effect. ' +
        'Identify edge cases, security considerations, and performance implications. ' +
        'Then produce a numbered implementation plan with specific file paths, function names, and code changes. ' +
        'Do NOT write any code yet — only the plan.',
};
// Commands with arguments (prefix match → rewrite)
const ARG_COMMANDS = [
    { prefix: '/ultrathink ', rewrite: (a) => `Think deeply, carefully, and thoroughly before responding. ` +
            `Consider multiple approaches, check edge cases, reason through implications step by step, ` +
            `and challenge your initial assumptions. Take your time — quality of reasoning matters more than speed. ` +
            `Now respond to: ${a}`
    },
    { prefix: '/explain ', rewrite: (a) => `Read and explain the code in ${a}. Cover: what it does, key functions/classes, how it connects to the rest of the codebase.` },
    { prefix: '/search ', rewrite: (a) => `Search the codebase for "${a}" using Grep. Show the matching files and relevant code context.` },
    { prefix: '/find ', rewrite: (a) => `Find files matching the pattern "${a}" using Glob. Show the results.` },
    { prefix: '/refactor ', rewrite: (a) => `Refactor: ${a}. Read the relevant code first, then make targeted changes. Explain each change.` },
    { prefix: '/scaffold ', rewrite: (a) => `Create the scaffolding/boilerplate for: ${a}. Generate the file structure and initial code. Ask me if you need clarification on requirements.` },
    { prefix: '/doc ', rewrite: (a) => `Generate documentation for ${a}. Include: purpose, API/interface description, usage examples, and important notes.` },
    { prefix: '/moa ', rewrite: (a) => `Use the MixtureOfAgents tool to get a high-quality answer by querying multiple AI models in parallel: ${a}` },
    { prefix: '/moa', rewrite: () => `Use the MixtureOfAgents tool. Ask me what question I want answered by multiple models.` },
];
// ─── Main dispatch ────────────────────────────────────────────────────────
/**
 * Handle a slash command. Returns result indicating what happened.
 */
export async function handleSlashCommand(input, ctx) {
    // Direct-handled commands
    if (input in DIRECT_COMMANDS) {
        await DIRECT_COMMANDS[input](ctx);
        return { handled: true };
    }
    // /session-search <query> — full-text search past sessions
    if (input === '/session-search' ||
        input.startsWith('/session-search ') ||
        input === '/ssearch' ||
        input.startsWith('/ssearch ')) {
        const prefix = input.startsWith('/ssearch') ? '/ssearch' : '/session-search';
        const query = input === prefix ? '' : input.slice(prefix.length + 1).trim();
        if (!query) {
            ctx.onEvent({ kind: 'text_delta', text: 'Usage: /session-search <query>\n' +
                    'Finds past sessions whose messages match the query.\n' +
                    'Use quotes for phrase search: /session-search "payment loop"\n'
            });
            emitDone(ctx);
            return { handled: true };
        }
        const { searchSessions, formatSearchResults } = await import('../session/search.js');
        const matches = searchSessions(query, { limit: 10 });
        ctx.onEvent({ kind: 'text_delta', text: formatSearchResults(matches, query) });
        emitDone(ctx);
        return { handled: true };
    }
    // /insights [--days N] — rich usage insights
    if (input === '/insights' || input.startsWith('/insights ')) {
        const daysMatch = input.match(/--days\s+(\d+)/);
        const days = daysMatch ? parseInt(daysMatch[1], 10) : 30;
        const { generateInsights, formatInsights } = await import('../stats/insights.js');
        const report = generateInsights(days);
        ctx.onEvent({ kind: 'text_delta', text: formatInsights(report, days) });
        emitDone(ctx);
        return { handled: true };
    }
    // /learnings — view or clear per-user learnings
    if (input === '/learnings' || input.startsWith('/learnings ')) {
        const { loadLearnings, decayLearnings, saveLearnings } = await import('../learnings/store.js');
        const arg = input.slice('/learnings'.length).trim();
        if (arg === 'clear') {
            saveLearnings([]);
            ctx.onEvent({ kind: 'text_delta', text: 'All learnings cleared.\n' });
        }
        else {
            let learnings = loadLearnings();
            if (learnings.length === 0) {
                ctx.onEvent({ kind: 'text_delta', text: 'No learnings yet. Franklin learns your preferences over time.\n' });
            }
            else {
                learnings = decayLearnings(learnings);
                const sorted = [...learnings].sort((a, b) => (b.confidence * b.times_confirmed) - (a.confidence * a.times_confirmed));
                let text = `**Personal Learnings** (${sorted.length})\n\n`;
                for (const l of sorted) {
                    const conf = l.confidence >= 0.8 ? 'high' : l.confidence >= 0.5 ? 'mid' : 'low';
                    text += `  [${conf}] ${l.learning} (×${l.times_confirmed})\n`;
                }
                text += '\nUse `/learnings clear` to reset.\n';
                ctx.onEvent({ kind: 'text_delta', text });
            }
        }
        emitDone(ctx);
        return { handled: true };
    }
    // /brain — view knowledge graph entities
    if (input === '/brain' || input.startsWith('/brain ')) {
        const { searchEntities, loadEntities, getEntityObservations, getEntityRelations, getBrainStats, loadObservations } = await import('../brain/store.js');
        const arg = input.slice('/brain'.length).trim();
        if (!arg) {
            const stats = getBrainStats();
            if (stats.entities === 0) {
                ctx.onEvent({ kind: 'text_delta', text: 'Brain is empty. Franklin learns entities (people, projects, companies) from your conversations over time.\n' });
            }
            else {
                const entities = loadEntities().sort((a, b) => b.reference_count - a.reference_count);
                let text = `**Franklin Brain** (${stats.entities} entities, ${stats.observations} facts, ${stats.relations} relations)\n\n`;
                for (const e of entities.slice(0, 20)) {
                    text += `  ${e.type === 'person' ? '👤' : e.type === 'company' ? '🏢' : e.type === 'project' ? '📦' : '💡'} **${e.name}** (${e.type}, ×${e.reference_count})\n`;
                }
                if (entities.length > 20)
                    text += `  ... and ${entities.length - 20} more\n`;
                text += '\nSearch: `/brain <name>` for details.\n';
                ctx.onEvent({ kind: 'text_delta', text });
            }
        }
        else {
            const results = searchEntities(arg, 5);
            if (results.length === 0) {
                ctx.onEvent({ kind: 'text_delta', text: `No entities matching "${arg}".\n` });
            }
            else {
                let text = '';
                for (const e of results) {
                    text += `**${e.name}** (${e.type})\n`;
                    if (e.aliases.length > 0)
                        text += `  Aliases: ${e.aliases.join(', ')}\n`;
                    const obs = getEntityObservations(e.id).slice(0, 5);
                    for (const o of obs) {
                        text += `  - ${o.content}\n`;
                    }
                    const rels = getEntityRelations(e.id);
                    const allEntities = loadEntities();
                    for (const r of rels.slice(0, 3)) {
                        const other = allEntities.find(x => x.id === (r.from_id === e.id ? r.to_id : r.from_id));
                        if (other)
                            text += `  → ${r.type} ${other.name}\n`;
                    }
                    text += '\n';
                }
                ctx.onEvent({ kind: 'text_delta', text });
            }
        }
        emitDone(ctx);
        return { handled: true };
    }
    // /auto — hard-reset to smart routing regardless of current state.
    // Shortcut for `/model auto`. Fixes the common "I got stuck on a model
    // and want Franklin to pick again" scenario without typing the long form.
    if (input === '/auto') {
        ctx.config.model = 'blockrun/auto';
        ctx.config.baseModel = 'blockrun/auto';
        ctx.config.onModelChange?.('blockrun/auto', 'user');
        ctx.onEvent({ kind: 'text_delta', text: `Model → **Auto** (smart routing re-enabled)\n` });
        emitDone(ctx);
        return { handled: true };
    }
    // /model — show current model or switch with /model <name>
    if (input === '/model' || input.startsWith('/model ')) {
        if (input === '/model') {
            ctx.onEvent({ kind: 'text_delta', text: `Current model: **${ctx.config.model}**\n` +
                    `Switch with: \`/model <name>\` (e.g. \`/model sonnet\`, \`/model free\`, \`/model gemini\`)\n`
            });
        }
        else {
            const raw = input.slice(7).trim();
            // Reject obvious garbage before resolveModel gets it — prevents wedge
            // strings with shell metacharacters or newlines ending up in config.
            if (!/^[a-zA-Z0-9/_.-]+$/.test(raw)) {
                ctx.onEvent({ kind: 'text_delta', text: `Invalid model name. Use shortcut (sonnet, free, gemini) or full id (vendor/model).\n` });
                emitDone(ctx);
                return { handled: true };
            }
            const newModel = resolveModel(raw);
            ctx.config.model = newModel;
            ctx.config.baseModel = newModel; // Update recovery target so loop doesn't reset
            ctx.config.onModelChange?.(newModel, 'user');
            // Warn when switching from free to paid so users know charges start now
            const isFree = (m) => m.startsWith('nvidia/') || m === 'blockrun/free';
            const paidWarning = !isFree(newModel)
                ? ` ⚠️  (paid — charges from your wallet per call)`
                : '';
            ctx.onEvent({ kind: 'text_delta', text: `Model → **${newModel}**${paidWarning}\n` });
        }
        emitDone(ctx);
        return { handled: true };
    }
    // /branch has both no-arg and with-arg forms
    if (input === '/branch' || input.startsWith('/branch ')) {
        const cwd = ctx.config.workingDir || process.cwd();
        if (input === '/branch') {
            const r = gitCmd(ctx, 'git branch -v --no-color');
            if (r !== null)
                ctx.onEvent({ kind: 'text_delta', text: r ? `\`\`\`\n${r}\n\`\`\`\n` : 'No branches yet.\n' });
        }
        else {
            const branchName = input.slice(8).trim();
            const r = gitCmd(ctx, `git checkout -b ${branchName}`);
            if (r !== null)
                ctx.onEvent({ kind: 'text_delta', text: `Created and switched to branch: **${branchName}**\n` });
        }
        emitDone(ctx);
        return { handled: true };
    }
    // /wallet — show wallet info, import, or export
    if (input === '/wallet' || input.startsWith('/wallet ')) {
        const chain = (await import('../config.js')).loadChain();
        const args = input.slice(7).trim();
        // /wallet export [--show]  — key masked by default; --show prints the full key
        // Rationale: terminal scrollback, screen recordings, and shared tmux sessions
        // can leak keys. Default to masked so users can confirm which wallet they have
        // without exposing the key; they opt in to the full key with --show.
        if (args === 'export' || args === 'export --show') {
            const showKey = args === 'export --show';
            const mask = (key) => key.length > 10 ? key.slice(0, 6) + '…' + key.slice(-4) : '••••••';
            try {
                if (chain === 'solana') {
                    const { loadSolanaWallet, getOrCreateSolanaWallet } = await import('@blockrun/llm');
                    const key = loadSolanaWallet();
                    if (!key) {
                        ctx.onEvent({ kind: 'text_delta', text: 'No Solana wallet found. Run `/wallet` first.\n' });
                        emitDone(ctx);
                        return { handled: true };
                    }
                    const w = await getOrCreateSolanaWallet();
                    ctx.onEvent({ kind: 'text_delta', text: `**Wallet Export (Solana)**\n` +
                            `  Address:     ${w.address}\n` +
                            `  Private Key: ${showKey ? key : mask(key)}\n\n` +
                            (showKey
                                ? `⚠️  Anyone with this key controls your funds. Clear terminal history after copying.\n`
                                : `(key masked — use \`/wallet export --show\` to reveal)\n`)
                    });
                }
                else {
                    const { loadWallet, getOrCreateWallet } = await import('@blockrun/llm');
                    const key = loadWallet();
                    if (!key) {
                        ctx.onEvent({ kind: 'text_delta', text: 'No wallet found. Run `/wallet` first.\n' });
                        emitDone(ctx);
                        return { handled: true };
                    }
                    const w = getOrCreateWallet();
                    ctx.onEvent({ kind: 'text_delta', text: `**Wallet Export (Base)**\n` +
                            `  Address:     ${w.address}\n` +
                            `  Private Key: ${showKey ? key : mask(key)}\n\n` +
                            (showKey
                                ? `⚠️  Anyone with this key controls your funds. Clear terminal history after copying.\n`
                                : `(key masked — use \`/wallet export --show\` to reveal)\n`)
                    });
                }
            }
            catch (err) {
                ctx.onEvent({ kind: 'text_delta', text: `Export error: ${err.message}\n` });
            }
            emitDone(ctx);
            return { handled: true };
        }
        // /wallet import <private-key>
        if (args.startsWith('import')) {
            // Strip ALL whitespace (including newlines/tabs from accidental paste),
            // not just leading/trailing. Otherwise a pasted key with embedded newlines
            // sneaks through validators and corrupts the stored wallet file.
            const key = args.slice(6).replace(/\s/g, '');
            if (!key) {
                ctx.onEvent({ kind: 'text_delta', text: `**Usage:** \`/wallet import <private-key>\`\n\n` +
                        `  Base:   \`/wallet import 0x...\`  (hex, 66 chars)\n` +
                        `  Solana: \`/wallet import <bs58-key>\`  (base58 encoded)\n`
                });
                emitDone(ctx);
                return { handled: true };
            }
            // Shape-validate before touching disk
            if (chain === 'base') {
                if (!/^0x[0-9a-fA-F]{64}$/.test(key)) {
                    ctx.onEvent({ kind: 'text_delta', text: 'Import error: Base key must be 0x + 64 hex chars (66 total).\n' });
                    emitDone(ctx);
                    return { handled: true };
                }
            }
            else {
                // Solana bs58 keys are 87-88 chars; reject anything wildly off
                if (key.length < 80 || key.length > 100 || !/^[1-9A-HJ-NP-Za-km-z]+$/.test(key)) {
                    ctx.onEvent({ kind: 'text_delta', text: 'Import error: Solana key must be base58 (80-100 chars).\n' });
                    emitDone(ctx);
                    return { handled: true };
                }
            }
            try {
                if (chain === 'solana') {
                    const { saveSolanaWallet, solanaPublicKey } = await import('@blockrun/llm');
                    const address = await solanaPublicKey(key);
                    saveSolanaWallet(key);
                    ctx.onEvent({ kind: 'text_delta', text: `**Wallet Imported (Solana)**\n` +
                            `  Address: ${address}\n` +
                            `  Saved to: ~/.blockrun/solana-wallet.json\n\n` +
                            `⚠️  IMPORTANT: This session is still using the OLD wallet.\n` +
                            `    Run \`/exit\` now, then restart \`franklin\` to use the new wallet.\n`
                    });
                }
                else {
                    const { privateKeyToAccount } = await import('viem/accounts');
                    const { saveWallet } = await import('@blockrun/llm');
                    const account = privateKeyToAccount(key);
                    saveWallet(key);
                    ctx.onEvent({ kind: 'text_delta', text: `**Wallet Imported (Base)**\n` +
                            `  Address: ${account.address}\n` +
                            `  Saved to: ~/.blockrun/wallet.json\n\n` +
                            `⚠️  IMPORTANT: This session is still using the OLD wallet.\n` +
                            `    Run \`/exit\` now, then restart \`franklin\` to use the new wallet.\n`
                    });
                }
            }
            catch (err) {
                ctx.onEvent({ kind: 'text_delta', text: `Import error: ${err.message}\n` });
            }
            emitDone(ctx);
            return { handled: true };
        }
        // /wallet (no args) — show wallet info
        try {
            let address;
            let balance;
            const fetchTimeout = (ms) => new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), ms));
            if (chain === 'solana') {
                const { getOrCreateSolanaWallet, setupAgentSolanaWallet } = await import('@blockrun/llm');
                const w = await getOrCreateSolanaWallet();
                address = w.address;
                try {
                    const client = await setupAgentSolanaWallet({ silent: true });
                    const bal = await Promise.race([client.getBalance(), fetchTimeout(5000)]);
                    balance = `$${bal.toFixed(2)} USDC`;
                }
                catch {
                    balance = '(unavailable)';
                }
            }
            else {
                const { getOrCreateWallet, setupAgentWallet } = await import('@blockrun/llm');
                const w = getOrCreateWallet();
                address = w.address;
                try {
                    const client = setupAgentWallet({ silent: true });
                    const bal = await Promise.race([client.getBalance(), fetchTimeout(5000)]);
                    balance = `$${bal.toFixed(2)} USDC`;
                }
                catch {
                    balance = '(unavailable)';
                }
            }
            ctx.onEvent({ kind: 'text_delta', text: `**Wallet**\n` +
                    `  Chain:   ${chain}\n` +
                    `  Address: ${address}\n` +
                    `  Balance: ${balance}\n\n` +
                    `  \`/wallet import <key>\`  — import a personal wallet\n` +
                    `  \`/wallet export\`        — show private key\n`
            });
        }
        catch (err) {
            ctx.onEvent({ kind: 'text_delta', text: `Wallet error: ${err.message}\n` });
        }
        emitDone(ctx);
        return { handled: true };
    }
    // /delete <...>
    if (input.startsWith('/delete ')) {
        const arg = input.slice('/delete '.length).trim();
        if (!arg) {
            ctx.onEvent({ kind: 'text_delta', text: 'Usage: /delete <exchange> (e.g., /delete 3, /delete 2,5, /delete 4-7)\n' });
            emitDone(ctx);
            return { handled: true };
        }
        // Parse exchange numbers (1-based) into a set of 0-based exchange indices
        const exchangeIndicesToDelete = new Set();
        const parts = arg.split(',').map(p => p.trim());
        for (const part of parts) {
            if (part.includes('-')) {
                const [start, end] = part.split('-').map(n => parseInt(n, 10));
                if (!isNaN(start) && !isNaN(end) && start <= end) {
                    for (let i = start; i <= end; i++)
                        exchangeIndicesToDelete.add(i - 1);
                }
            }
            else {
                const n = parseInt(part, 10);
                if (!isNaN(n))
                    exchangeIndicesToDelete.add(n - 1);
            }
        }
        if (exchangeIndicesToDelete.size === 0) {
            ctx.onEvent({ kind: 'text_delta', text: 'No valid exchange numbers provided.\n' });
            emitDone(ctx);
            return { handled: true };
        }
        // Map exchange indices → raw history index ranges, then delete descending.
        // This preserves valid user/assistant alternation — each exchange covers the
        // full unit: user prompt + all tool calls/results + assistant replies.
        const exchanges = buildExchanges(ctx.history);
        const rawToDelete = new Set();
        const deletedNums = [];
        for (const exIdx of exchangeIndicesToDelete) {
            const ex = exchanges[exIdx];
            if (!ex)
                continue;
            for (let i = ex.startIdx; i <= ex.endIdx; i++)
                rawToDelete.add(i);
            deletedNums.push(exIdx + 1);
        }
        const sorted = Array.from(rawToDelete).sort((a, b) => b - a);
        for (const idx of sorted) {
            if (idx >= 0 && idx < ctx.history.length)
                ctx.history.splice(idx, 1);
        }
        if (deletedNums.length > 0) {
            resetTokenAnchor();
            ctx.onEvent({ kind: 'text_delta', text: `Deleted exchange(s) ${deletedNums.sort((a, b) => a - b).join(', ')} from history.\n` });
        }
        else {
            ctx.onEvent({ kind: 'text_delta', text: 'No matching exchanges found to delete.\n' });
        }
        emitDone(ctx);
        return { handled: true };
    }
    // /resume or /resume <id>
    if (input === '/resume' || input.startsWith('/resume ')) {
        const targetId = input === '/resume'
            ? listSessions().find((session) => session.id !== ctx.sessionId)?.id ?? ''
            : input.slice(8).trim();
        if (!targetId) {
            ctx.onEvent({ kind: 'text_delta', text: 'No previous session available to resume.\n' });
            emitDone(ctx);
            return { handled: true };
        }
        const restored = loadSessionHistory(targetId);
        if (restored.length === 0) {
            ctx.onEvent({ kind: 'text_delta', text: `Session "${targetId}" not found or empty.\n` });
        }
        else {
            ctx.history.length = 0;
            ctx.history.push(...restored);
            resetTokenAnchor();
            ctx.onEvent({ kind: 'text_delta', text: `Restored ${restored.length} messages from ${targetId}. Continue where you left off.\n` });
        }
        emitDone(ctx);
        return { handled: true };
    }
    // Simple rewrite commands (exact match)
    if (input in REWRITE_COMMANDS) {
        return { handled: false, rewritten: REWRITE_COMMANDS[input] };
    }
    // Argument-based rewrite commands (prefix match)
    for (const { prefix, rewrite } of ARG_COMMANDS) {
        if (input.startsWith(prefix)) {
            const arg = input.slice(prefix.length).trim();
            return { handled: false, rewritten: rewrite(arg) };
        }
    }
    // File-loaded skills — registered after built-ins so `/security` etc.
    // are never shadowed by a user-installed skill of the same name.
    if (ctx.skillRegistry) {
        const skillResult = matchSkill(input, ctx.skillRegistry, ctx.skillVars ?? {});
        if (skillResult) {
            return { handled: false, rewritten: skillResult.rewritten };
        }
    }
    // Not a recognized command — suggest closest match
    const skillNames = ctx.skillRegistry
        ? ctx.skillRegistry.list().map((s) => `/${s.skill.name}`)
        : [];
    const allCommands = [
        ...Object.keys(DIRECT_COMMANDS),
        ...Object.keys(REWRITE_COMMANDS),
        ...ARG_COMMANDS.map(c => c.prefix.trim()),
        ...skillNames,
        '/branch', '/resume', '/model', '/auto', '/wallet', '/cost', '/help', '/clear', '/retry', '/exit', '/session-search', '/ssearch', '/failures',
    ];
    const cmd = input.split(/\s/)[0];
    const close = allCommands.filter(c => {
        // Simple distance: share >= 50% of characters
        const shorter = Math.min(cmd.length, c.length);
        let matches = 0;
        for (let i = 0; i < shorter; i++) {
            if (cmd[i] === c[i])
                matches++;
        }
        return matches >= shorter * 0.5 && matches >= 3;
    });
    if (close.length > 0) {
        ctx.onEvent({ kind: 'text_delta', text: `Unknown command: ${cmd}. Did you mean: ${close.slice(0, 3).join(', ')}?\n` });
        emitDone(ctx);
        return { handled: true };
    }
    // Truly unknown — pass through as regular input
    return { handled: false };
}
