#!/usr/bin/env python3
"""Linear Canal — Cursor beforeShellExecution hook.

Fires before any shell command is executed in the agent. Blocks raw file
exploration commands (grep, find, rg, etc.) when a Linear Canal index is
present, redirecting the agent to use MCP tools instead.

Stdin:   {"command": "...", "cwd": "...", "workspace_roots": [...], ...}
Stdout:  {"permission": "allow"}           — allow the command
         {"permission": "deny",            — block the command
          "user_message": "...",           — shown in the Cursor UI
          "agent_message": "..."}          — fed back to the agent
"""
from __future__ import annotations

import json
import os
import sys

# Commands that indicate raw file exploration — block these
_BLOCKED_BASENAMES = {
    "grep", "grep.exe",
    "find", "find.exe",
    "rg", "rg.exe",          # ripgrep
    "ripgrep", "ripgrep.exe",
    "fd", "fd.exe",           # fd-find
    "ack",
    "ag",                     # the_silver_searcher
    "fzf",
}

_ALLOW = json.dumps({"continue": True})


def _has_index(workspace_roots: list[str]) -> bool:
    return any(
        os.path.isdir(os.path.join(r, ".linear_canal_data"))
        for r in workspace_roots
    )


def main() -> None:
    try:
        body = json.load(sys.stdin)
    except Exception:
        sys.stdout.write(_ALLOW)
        return

    workspace_roots: list[str] = body.get("workspace_roots") or [
        os.environ.get("CURSOR_WORKSPACE_ROOT", ".")
    ]

    # Only intervene when a Linear Canal index is present
    if not _has_index(workspace_roots):
        sys.stdout.write(_ALLOW)
        return

    raw_command: str = body.get("command") or ""
    # Extract the bare executable name (handles full paths like /usr/bin/grep)
    exe = os.path.basename(raw_command.split()[0]).lower() if raw_command.strip() else ""

    if exe not in _BLOCKED_BASENAMES:
        sys.stdout.write(_ALLOW)
        return

    msg = (
        "[Linear Canal] Raw file search blocked.\n\n"
        "This project is indexed by Linear Canal. "
        "Use the MCP tools instead — they are 80-95% cheaper in tokens:\n\n"
        "  lc_context('<describe your task>')   — context bundle for your task\n"
        "  lc_search('<concept or symbol>')     — semantic search across the codebase\n"
        "  lc_impact('<file_path>')             — change impact before editing a file\n\n"
        f"The blocked command was: {raw_command!r}\n\n"
        "If you genuinely need a shell command (e.g. running tests), "
        "prefix it with 'RUN:' in your message to bypass this check."
    )
    # Bypass: if the raw command starts with 'RUN:' marker we allow it
    if raw_command.lstrip().upper().startswith("RUN:"):
        sys.stdout.write(_ALLOW)
        return

    # beforeShellExecution output per Cursor hooks spec:
    # permission: "deny" blocks the command (equivalent to exit code 2)
    # user_message: shown in the Cursor UI
    # agent_message: fed back to the agent loop
    sys.stdout.write(json.dumps({
        "permission": "deny",
        "user_message": "[Linear Canal] Raw file search blocked — use lc_context / lc_search / lc_impact instead.",
        "agent_message": msg,
    }))


if __name__ == "__main__":
    main()
