/**
 * Claude Code hook subcommands for nexus-prime.
 *
 * Registered as `nexus-prime hook bootstrap|memory|mindkit|ghost-pass|session-dna`.
 * Each subcommand is invoked by a Claude Code hook entry in ~/.claude/settings.json.
 *
 * Fast path: reads the daemon lock file (sync, ~0 ms) then POSTs to the daemon's
 * /rpc/call-tool endpoint over localhost (typically < 30 ms round-trip).
 * Graceful skip when the daemon is not running — never blocks Claude Code.
 *
 * Claude Code stdin shapes:
 *   UserPromptSubmit  →  { session_id, cwd, hook_event_name, user_message }
 *   PreToolUse        →  { session_id, cwd, hook_event_name, tool_name, tool_input }
 *   PostToolUse       →  { session_id, cwd, hook_event_name, tool_name, tool_input, tool_result, tool_status }
 *   Stop              →  { session_id, cwd }
 */
import fs from 'fs';
import path from 'path';
import os from 'os';
import { spawn } from 'child_process';
function findDaemonLock(cwd) {
    const nexusPrimeDir = path.join(os.homedir(), '.nexus-prime');
    if (!fs.existsSync(nexusPrimeDir))
        return null;
    try {
        for (const entry of fs.readdirSync(nexusPrimeDir)) {
            const lockPath = path.join(nexusPrimeDir, entry, 'nexus-daemon.lock.json');
            if (!fs.existsSync(lockPath))
                continue;
            try {
                const record = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
                if (record.version !== 1 || !record.port || !record.token || !record.pid)
                    continue;
                const ws = record.workspaceRoot ?? '';
                const repo = record.repoRoot ?? '';
                if (!ws && !repo)
                    continue;
                if (cwd === ws || cwd === repo ||
                    cwd.startsWith(ws + '/') || cwd.startsWith(repo + '/')) {
                    return record;
                }
            }
            catch { /* skip corrupt lock */ }
        }
    }
    catch { /* ~/.nexus-prime not readable */ }
    return null;
}
async function callDaemonTool(record, toolName, args, timeoutMs) {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
        const resp = await fetch(`http://127.0.0.1:${record.port}/rpc/call-tool`, {
            method: 'POST',
            headers: {
                'content-type': 'application/json',
                'x-nexus-daemon-token': record.token,
            },
            body: JSON.stringify({ name: toolName, arguments: args, timeoutMs }),
            signal: controller.signal,
        });
        if (!resp.ok)
            return null;
        return (await resp.json());
    }
    catch {
        return null;
    }
    finally {
        clearTimeout(timer);
    }
}
// --------------------------------------------------------------------------
// Stdin reader
// --------------------------------------------------------------------------
async function readStdinJson() {
    return new Promise((resolve) => {
        if (process.stdin.isTTY) {
            resolve({});
            return;
        }
        let data = '';
        process.stdin.setEncoding('utf8');
        process.stdin.on('data', (chunk) => { data += chunk; });
        process.stdin.on('end', () => {
            try {
                resolve(JSON.parse(data));
            }
            catch {
                resolve({});
            }
        });
        // Safety valve: give up waiting for stdin after 3 s
        setTimeout(() => {
            try {
                resolve(data ? JSON.parse(data) : {});
            }
            catch {
                resolve({});
            }
        }, 3_000).unref?.();
    });
}
// --------------------------------------------------------------------------
// Hook subcommands
// --------------------------------------------------------------------------
function spawnDaemon(cwd) {
    const entrypoint = process.argv[1];
    if (!entrypoint)
        return;
    try {
        const child = spawn(process.execPath, [entrypoint, 'daemon', '__serve'], {
            cwd,
            env: { ...process.env, NEXUS_WORKSPACE_ROOT: cwd },
            detached: true,
            stdio: 'ignore',
        });
        child.unref();
    }
    catch { /* never block the hook */ }
}
async function waitForDaemonLock(cwd, timeoutMs) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
        const lock = findDaemonLock(cwd);
        if (lock)
            return lock;
        await new Promise(r => setTimeout(r, 100).unref());
    }
    return null;
}
/** UserPromptSubmit — call nexus_session_bootstrap so the model never has to. */
export async function runHookBootstrap() {
    const data = await readStdinJson();
    const cwd = String(data.cwd ?? process.cwd());
    const goal = String(data.user_message ?? data.prompt ?? '').trim();
    if (!goal)
        return;
    let lock = findDaemonLock(cwd);
    if (!lock) {
        // Daemon not running — spawn it and wait up to 1.5 s for the lock to appear.
        // If it doesn't come up in time, fall back silently (never block the model).
        spawnDaemon(cwd);
        lock = await waitForDaemonLock(cwd, 1_500);
    }
    if (!lock)
        return;
    await callDaemonTool(lock, 'nexus_session_bootstrap', {
        goal: goal.slice(0, 500),
    }, 10_000);
}
/**
 * PostToolUse (Edit|Write|MultiEdit|Bash) — compact audit memory.
 *
 * The hook stores only compact, hidden lifecycle breadcrumbs. Real findings
 * should still be stored via nexus_store_memory by the agent itself. Set
 * NEXUS_HOOK_AUTO_MEMORY=0 to disable this hook in a local session.
 */
export async function runHookMemory() {
    if (process.env.NEXUS_HOOK_AUTO_MEMORY === '0')
        return;
    const data = await readStdinJson();
    const cwd = String(data.cwd ?? process.cwd());
    const toolName = String(data.tool_name ?? '');
    // Skip read-only and discovery tools — no meaningful state change
    const skipPrefixes = ['Read', 'Glob', 'Grep', 'LS', 'nexus_list', 'nexus_describe',
        'nexus_runtime_health', 'nexus_doctor', 'nexus_recall', 'nexus_token_report'];
    if (!toolName || skipPrefixes.some(p => toolName.startsWith(p)))
        return;
    const lock = findDaemonLock(cwd);
    if (!lock)
        return;
    const input = (data.tool_input ?? {});
    const inputSummary = Object.entries(input)
        .filter(([k]) => !['content', 'new_string', 'old_string', 'result', 'command_result'].includes(k))
        .map(([k, v]) => `${k}=${String(v).slice(0, 80)}`)
        .join(', ')
        .slice(0, 200);
    const repoName = path.basename(cwd);
    await callDaemonTool(lock, 'nexus_store_memory', {
        content: `${toolName}: ${inputSummary}`,
        priority: 0.2,
        tags: ['#auto', '#memory-hook', '#system-hidden', `tool:${toolName}`, `repo:${repoName}`],
    }, 5_000);
}
/**
 * PreToolUse (Edit|Write|MultiEdit) — governance check.
 * Outputs deny JSON + exits 2 when mindkit returns `blocked: true`.
 */
export async function runHookMindkit() {
    const data = await readStdinJson();
    const cwd = String(data.cwd ?? process.cwd());
    const toolName = String(data.tool_name ?? '');
    const input = (data.tool_input ?? {});
    const files = [];
    if (typeof input.file_path === 'string')
        files.push(input.file_path);
    if (Array.isArray(input.edits)) {
        for (const e of input.edits) {
            if (typeof e?.file_path === 'string')
                files.push(e.file_path);
        }
    }
    if (!files.length)
        return;
    const lock = findDaemonLock(cwd);
    if (!lock)
        return; // no daemon — skip rather than block
    const resp = await callDaemonTool(lock, 'nexus_mindkit_check', {
        action: `${toolName}: ${files.join(', ')}`,
        filesToModify: files,
        isDestructive: ['Write', 'Edit', 'MultiEdit'].includes(toolName),
    }, 6_000);
    // Parse the tool result text to check for a block verdict
    const text = extractResultText(resp);
    if (text && isBlockedResult(text)) {
        process.stdout.write(JSON.stringify({
            hookSpecificOutput: {
                hookEventName: 'PreToolUse',
                permissionDecision: 'deny',
                permissionDecisionReason: extractBlockReason(text),
            },
        }));
        process.exit(2);
    }
}
/**
 * PreToolUse (MultiEdit with >= 3 files) — ghost-pass dry-run.
 * Does not block — runs informatively. Block enforcement is a future enhancement.
 */
export async function runHookGhostPass() {
    const data = await readStdinJson();
    const cwd = String(data.cwd ?? process.cwd());
    const input = (data.tool_input ?? {});
    const edits = Array.isArray(input.edits) ? input.edits : [];
    if (edits.length < 3)
        return;
    const files = edits
        .map(e => e?.file_path)
        .filter((f) => typeof f === 'string');
    const lock = findDaemonLock(cwd);
    if (!lock)
        return;
    await callDaemonTool(lock, 'nexus_ghost_pass', {
        goal: `MultiEdit: ${files.length} files`,
        files,
    }, 10_000);
}
/** Stop — generate session DNA summary. */
export async function runHookSessionDna() {
    const data = await readStdinJson();
    const cwd = String(data.cwd ?? process.cwd());
    const lock = findDaemonLock(cwd);
    if (!lock)
        return;
    await callDaemonTool(lock, 'nexus_session_dna', { action: 'generate' }, 15_000);
}
// --------------------------------------------------------------------------
// Helpers for parsing tool results
// --------------------------------------------------------------------------
function extractResultText(resp) {
    if (!resp)
        return null;
    const result = resp.result;
    if (!result)
        return null;
    const content = Array.isArray(result.content) ? result.content : [];
    const first = content[0];
    return typeof first?.text === 'string' ? first.text : null;
}
function isBlockedResult(text) {
    try {
        const parsed = JSON.parse(text);
        return (parsed?.blocked === true ||
            parsed?.verdict === 'block' ||
            parsed?.decision === 'block' ||
            String(parsed?.status ?? '').toLowerCase() === 'blocked');
    }
    catch {
        return text.toLowerCase().includes('"blocked":true') ||
            text.toLowerCase().includes('"verdict":"block"');
    }
}
function extractBlockReason(text) {
    try {
        const parsed = JSON.parse(text);
        return String(parsed?.reason ?? parsed?.message ?? parsed?.summary ?? 'Blocked by nexus mindkit governance check.');
    }
    catch {
        return 'Blocked by nexus mindkit governance check.';
    }
}
