/**
 * `opensquid hooks <subcommand>` — Claude Code settings.json hooks
 * installer.
 *
 * Subcommands:
 *   install    — append the opensquid PreToolUse hook to
 *                ~/.claude/settings.json (idempotent — re-running
 *                refreshes the hook command pointer without disturbing
 *                other hooks)
 *   uninstall  — remove the opensquid PreToolUse hook
 *   doctor     — report what's currently installed
 *
 * The PreToolUse hook runs the local opensquid build's `hook
 * pre-tool-use` handler (see src/hooks/pre-tool-use.ts), which
 * intercepts known-anti-pattern actions (amend, push, substrate-impure
 * engine commits) before Claude Code executes them.
 */
import { promises as fs } from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import * as url from "node:url";
export class HooksCliError extends Error {
    hint;
    constructor(message, hint) {
        super(message);
        this.hint = hint;
        this.name = "HooksCliError";
    }
}
/**
 * Per-event opensquid hook id. Was a single shared id pre-#118, which
 * meant uninstall could not selectively target one event AND legacy
 * entries written by older opensquid versions never got recognized
 * (since they had a different id, or none at all).
 */
const HOOK_IDS = {
    PreToolUse: "opensquid-pre-tool-use",
    Stop: "opensquid-stop",
    StopFalseStopGuard: "opensquid-stop-false-stop-guard",
    UserPromptSubmit: "opensquid-user-prompt-submit",
    SessionEnd: "opensquid-session-end",
};
/** Legacy id used through 2026-05-15 — purgeOurHook still recognizes it. */
const LEGACY_HOOK_ID = "opensquid-drift-pretooluse";
/**
 * Path-substring fallback that identifies opensquid hook entries even
 * when no `_id` marker is present (older installs, manual edits, etc.).
 * Matches anything that runs `opensquid/dist/index.js`, regardless of
 * whether the subcommand is the legacy `hook <event>` (≤0.7.34) or the
 * new unified `anti-drift <event>` (0.7.35+).
 */
const COMMAND_FINGERPRINT = "/opensquid/dist/index.js";
/**
 * D9 false-stop guard prompt — fires as a `type: "prompt"` Stop hook
 * alongside the existing `type: "command"` Stop hook. Claude Code
 * evaluates this prompt against the assistant's just-completed turn
 * via the configured model and uses the YES/NO answer to allow or
 * block the stop. When blocked, the agent is re-prompted to continue.
 *
 * The prompt is intentionally framed so the default (YES, allow stop)
 * fires on the vast majority of turns; only trailing politeness
 * reflexes ("Run it?", "Want me to start B4?", "Should I continue?")
 * tip it to NO.
 *
 * Catches drift D9 per loop/docs/opensquid-anti-drift-unified-evaluator-design.md
 * rule #10b. Locked 2026-05-17 — user requirement: complete prevention.
 */
const FALSE_STOP_GUARD_PROMPT = `The assistant just finished a turn with this message:

$ARGUMENTS

Did the assistant message above end with a REAL blocking question that requires user input before the agent can continue? Most messages end normally (no question, or with a question mark that's part of a real decision the user must make) and should answer YES (allow the stop).

Only answer NO when the message ends with a politeness reflex or suggestion the agent should just execute past — patterns like "Run it?", "Want me to do X?", "Should I continue?", "Ready for the next step?", "Want me to start Y?". These are forbidden by the user's locked automation-mode rule.

OUTPUT FORMAT (required): Begin your response with the literal prefix "🦑 [opensquid D9-guard] " so the user can visually identify this as an automated hook output (not agent text). After the prefix, respond YES or NO followed by a one-sentence justification.

Examples of well-formed responses:
- "🦑 [opensquid D9-guard] YES — message ends with a factual statement of completion, not a question."
- "🦑 [opensquid D9-guard] NO — message ends with 'Want me to start B4?' which is a politeness reflex the agent should execute past."`;
const FALSE_STOP_GUARD_MODEL = "claude-haiku-4-5";
function settingsPath() {
    return path.join(os.homedir(), ".claude", "settings.json");
}
async function loadSettings() {
    try {
        const raw = await fs.readFile(settingsPath(), "utf8");
        const parsed = JSON.parse(raw);
        return parsed && typeof parsed === "object" ? parsed : {};
    }
    catch {
        return {};
    }
}
async function saveSettings(settings) {
    const p = settingsPath();
    await fs.mkdir(path.dirname(p), { recursive: true });
    await fs.writeFile(p, JSON.stringify(settings, null, 2) + "\n", "utf8");
}
/** Absolute path to the opensquid CLI binary that hosts this code. */
function opensquidBinPath() {
    // import.meta.url points at dist/hooks-cli.js after build; the entry
    // point is dist/index.js in the same directory.
    const here = url.fileURLToPath(import.meta.url);
    return path.join(path.dirname(here), "index.js");
}
function buildHookCommand(hookName) {
    // 0.7.35 cutover: register hooks pointing at the unified anti-drift
    // evaluator (src/anti-drift/evaluator.ts) instead of the per-file
    // legacy handlers (src/hooks/<event>.ts). The legacy `hook <event>`
    // dispatch is preserved in index.ts for backward compat with
    // settings.json entries that haven't been re-installed yet.
    return `node ${opensquidBinPath()} anti-drift ${hookName}`;
}
/** True when a hook entry is recognizably opensquid's. */
export function isOurHook(h) {
    if (h._id === LEGACY_HOOK_ID)
        return true;
    for (const id of Object.values(HOOK_IDS)) {
        if (h._id === id)
            return true;
    }
    // Fallback: command-path fingerprint catches un-marked legacy entries
    // that purgeOurHook used to silently skip (the bug from #118 dogfood).
    // Case-insensitive — macOS APFS default is case-preserving but case-
    // insensitive, so an install at /projects/OpenSquid/ should match too.
    // Prompt-typed hooks have no command field — they're recognized only
    // by their _id (handled above); manual / unmarked prompt entries are
    // not our concern (no installer wrote them).
    if (h.type === "command" &&
        typeof h.command === "string" &&
        h.command.toLowerCase().includes(COMMAND_FINGERPRINT.toLowerCase())) {
        return true;
    }
    return false;
}
/** Remove any existing opensquid hook from a matcher list. */
function purgeOurHook(matchers) {
    return matchers
        .map((m) => ({
        ...m,
        hooks: m.hooks.filter((h) => !isOurHook(h)),
    }))
        .filter((m) => m.hooks.length > 0);
}
async function cmdInstall() {
    const settings = await loadSettings();
    const hooks = settings.hooks ?? {};
    // PreToolUse — single matcher ".*" is sufficient. It catches Bash
    // (drift checks live in pre-tool-use.ts handler keyed off tool_name)
    // AND every other tool (so the honesty ledger has full evidence for
    // Stop-hook reconciliation). Pre-#118 we registered TWO matchers
    // (Bash + .*) which double-fired on Bash and caused duplicate ledger
    // entries.
    const preToolUse = purgeOurHook(hooks.PreToolUse ?? []);
    preToolUse.push({
        matcher: ".*",
        hooks: [
            {
                type: "command",
                command: buildHookCommand("pre-tool-use"),
                _id: HOOK_IDS.PreToolUse,
            },
        ],
    });
    // Stop — claim-vs-action reconciliation + auto-classify spawn at turn end.
    const stop = purgeOurHook(hooks.Stop ?? []);
    stop.push({
        hooks: [{ type: "command", command: buildHookCommand("stop"), _id: HOOK_IDS.Stop }],
    });
    // D9 false-stop guard — fires as a native `type: "prompt"` hook so
    // Claude Code's framework handles the LLM evaluation (no subprocess
    // wrapper needed in opensquid). Catches trailing politeness reflexes
    // like "Run it?" / "Want me to start B4?" and blocks the stop,
    // re-prompting the agent to continue per the user's locked
    // automation-mode rule. Counts against the user's Claude Code
    // subscription model quota (haiku-class, cheap). 0.7.20 / drift D9.
    stop.push({
        hooks: [
            {
                type: "prompt",
                prompt: FALSE_STOP_GUARD_PROMPT,
                model: FALSE_STOP_GUARD_MODEL,
                _id: HOOK_IDS.StopFalseStopGuard,
            },
        ],
    });
    // UserPromptSubmit — surface previous turn's broken promises +
    // auto-classify candidates at the start of the next turn.
    const ups = purgeOurHook(hooks.UserPromptSubmit ?? []);
    ups.push({
        hooks: [
            {
                type: "command",
                command: buildHookCommand("user-prompt-submit"),
                _id: HOOK_IDS.UserPromptSubmit,
            },
        ],
    });
    // SessionEnd — clearSession to bound disk usage from per-session
    // ledger accumulation.
    const sessionEnd = purgeOurHook(hooks.SessionEnd ?? []);
    sessionEnd.push({
        hooks: [
            {
                type: "command",
                command: buildHookCommand("session-end"),
                _id: HOOK_IDS.SessionEnd,
            },
        ],
    });
    settings.hooks = {
        ...hooks,
        PreToolUse: preToolUse,
        Stop: stop,
        UserPromptSubmit: ups,
        SessionEnd: sessionEnd,
    };
    await saveSettings(settings);
    console.log(`[opensquid hooks install] wrote ${settingsPath()}`);
    console.log(`  PreToolUse       → ${buildHookCommand("pre-tool-use")}`);
    console.log(`  Stop             → ${buildHookCommand("stop")}`);
    console.log(`  Stop (D9 guard)  → type:prompt model:${FALSE_STOP_GUARD_MODEL}`);
    console.log(`  UserPromptSubmit → ${buildHookCommand("user-prompt-submit")}`);
    console.log(`  SessionEnd       → ${buildHookCommand("session-end")}`);
    console.log(`  next: restart Claude Code so the new settings load.`);
}
async function cmdUninstall() {
    const settings = await loadSettings();
    const hooks = settings.hooks;
    if (!hooks) {
        console.log(`[opensquid hooks uninstall] no hooks configured`);
        return;
    }
    let totalRemoved = 0;
    for (const event of ["PreToolUse", "Stop", "UserPromptSubmit", "SessionEnd"]) {
        const matchers = hooks[event];
        if (!matchers)
            continue;
        totalRemoved += countOurHooks(matchers);
        const purged = purgeOurHook(matchers);
        if (purged.length === 0) {
            delete hooks[event];
        }
        else {
            hooks[event] = purged;
        }
    }
    await saveSettings(settings);
    console.log(`[opensquid hooks uninstall] removed ${totalRemoved} opensquid hook(s)`);
}
async function cmdDoctor() {
    const settings = await loadSettings();
    const hooks = settings.hooks ?? {};
    console.log(`[opensquid hooks doctor]`);
    console.log(`  settings.json: ${settingsPath()}`);
    for (const event of ["PreToolUse", "Stop", "UserPromptSubmit", "SessionEnd"]) {
        const matchers = hooks[event] ?? [];
        const ours = countOurHooks(matchers);
        console.log(`  ${event.padEnd(18)} total=${matchers.length}  opensquid=${ours}`);
    }
}
function countOurHooks(matchers) {
    let n = 0;
    for (const m of matchers) {
        for (const h of m.hooks) {
            if (isOurHook(h))
                n++;
        }
    }
    return n;
}
export async function runHooksCli(cmd, _argv) {
    switch (cmd) {
        case "install":
            await cmdInstall();
            return;
        case "uninstall":
            await cmdUninstall();
            return;
        case "doctor":
            await cmdDoctor();
            return;
    }
}
//# sourceMappingURL=hooks-cli.js.map