#!/usr/bin/env node
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));

// src/history-routing.ts
var STRONG_MEMORY_FIRST_PATTERNS = [
  /\bwhy\b/i,
  /\breason(?:ing)?\b/i,
  /\brationale\b/i,
  /\bintent\b/i,
  /\bdecision(?: trail)?\b/i,
  /\bhidden assumptions?\b/i,
  /\bwhat led to\b/i,
  /\btrying to solve\b/i,
  /\bearlier prompts?\b/i,
  /\brequirements?\b/i,
  /\btemporary patch\b/i,
  /\bworkaround\b/i,
  /\blong[-\s]?term design\b/i,
  /\bfailed attempts?\b/i,
  /\btried before\b/i,
  /\bprevious attempts?\b/i,
  /\babandon(?:ed)?\b/i,
  /\broll(?:ed)? back\b/i,
  /\bregressions?\b/i,
  /\berrors?\b.*\bkept happening\b/i,
  /\bbefore i (?:touch|change|modify|refactor)\b/i,
  /\bmerge request discussions?\b/i,
  /\brecovery\b/i,
  /\bdrift\b/i,
  /\bcontext did the agent have\b/i,
  /\buser (?:ask|request|approval)\b/i
];
var MEMORY_FIRST_PATTERNS = [
  /\brecent changes?\b/i,
  /\bwhat led to\b/i,
  /\bproblem\b/i,
  /\bchange step\b/i,
  /\bhistorical\b/i,
  /\bhistory\b/i,
  ...STRONG_MEMORY_FIRST_PATTERNS
];
function shouldPreferRemixMemory(intent) {
  return intent === "memory_first";
}

// src/hook-state.ts
var import_promises2 = __toESM(require("fs/promises"), 1);
var import_node_os2 = __toESM(require("os"), 1);
var import_node_path2 = __toESM(require("path"), 1);
var import_node_crypto = require("crypto");

// src/orphan-prompt-capture.ts
var import_promises = __toESM(require("fs/promises"), 1);
var import_node_os = __toESM(require("os"), 1);
var import_node_path = __toESM(require("path"), 1);
var ORPHAN_PROMPT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;

// src/hook-state.ts
function stateRoot() {
  const configured = process.env.REMIX_CURSOR_PLUGIN_HOOK_STATE_ROOT?.trim();
  return configured || import_node_path2.default.join(import_node_os2.default.tmpdir(), "remix-cursor-plugin-hooks");
}
function statePath(sessionId) {
  return import_node_path2.default.join(stateRoot(), `${sessionId}.json`);
}
function normalizeIntent(value) {
  return value === "memory_first" || value === "collab_state" || value === "git_facts" ? value : "neutral";
}
function normalizeString(value) {
  return typeof value === "string" && value.trim() ? value.trim() : null;
}
function normalizeStringArray(value) {
  if (!Array.isArray(value)) return [];
  return Array.from(
    new Set(
      value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim())
    )
  );
}
function normalizeManualRecordingScope(value) {
  if (value === "full_turn") {
    return "full_turn";
  }
  return null;
}
function normalizeTouchedRepo(value, repoRoot) {
  if (!value || typeof value !== "object") return null;
  const parsed = value;
  const normalizedRepoRoot = normalizeString(parsed.repoRoot) ?? repoRoot.trim();
  if (!normalizedRepoRoot) return null;
  return {
    repoRoot: normalizedRepoRoot,
    projectId: normalizeString(parsed.projectId),
    currentAppId: normalizeString(parsed.currentAppId),
    upstreamAppId: normalizeString(parsed.upstreamAppId),
    firstTouchedAt: normalizeString(parsed.firstTouchedAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
    lastTouchedAt: normalizeString(parsed.lastTouchedAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
    lastObservedWriteAt: normalizeString(parsed.lastObservedWriteAt),
    touchedBy: normalizeStringArray(parsed.touchedBy),
    hasObservedWrite: Boolean(parsed.hasObservedWrite),
    manuallyRecorded: Boolean(parsed.manuallyRecorded),
    manuallyRecordedAt: normalizeString(parsed.manuallyRecordedAt),
    manuallyRecordedByTool: normalizeString(parsed.manuallyRecordedByTool),
    manualRecordingScope: normalizeManualRecordingScope(parsed.manualRecordingScope),
    manualRemoteChangeRecordedAt: normalizeString(parsed.manualRemoteChangeRecordedAt),
    stopAttempted: Boolean(parsed.stopAttempted),
    stopRecorded: Boolean(parsed.stopRecorded),
    stopRecordedAt: normalizeString(parsed.stopRecordedAt),
    stopRecordedMode: parsed.stopRecordedMode === "changed_turn" || parsed.stopRecordedMode === "no_diff_turn" ? parsed.stopRecordedMode : null,
    recordingFailureMessage: normalizeString(parsed.recordingFailureMessage),
    recordingFailureHint: normalizeString(parsed.recordingFailureHint),
    recordingFailedAt: normalizeString(parsed.recordingFailedAt)
  };
}
function normalizeTouchedRepos(value) {
  if (!value || typeof value !== "object") return {};
  const entries = Object.entries(value).map(([repoRoot, repo]) => normalizeTouchedRepo(repo, repoRoot)).filter((repo) => repo !== null).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));
  return Object.fromEntries(entries.map((repo) => [repo.repoRoot, repo]));
}
async function loadPendingTurnState(sessionId) {
  const raw = await import_promises2.default.readFile(statePath(sessionId), "utf8").catch(() => null);
  if (!raw) return null;
  try {
    const parsed = JSON.parse(raw);
    if (!parsed || typeof parsed !== "object") return null;
    if (typeof parsed.sessionId !== "string" || typeof parsed.turnId !== "string" || typeof parsed.prompt !== "string") {
      return null;
    }
    return {
      sessionId: parsed.sessionId,
      turnId: parsed.turnId,
      prompt: parsed.prompt,
      initialCwd: normalizeString(parsed.initialCwd),
      intent: normalizeIntent(parsed.intent),
      submittedAt: typeof parsed.submittedAt === "string" ? parsed.submittedAt : (/* @__PURE__ */ new Date()).toISOString(),
      consultedMemory: Boolean(parsed.consultedMemory),
      touchedRepos: normalizeTouchedRepos(parsed.touchedRepos),
      turnFailureMessage: normalizeString(parsed.turnFailureMessage),
      turnFailureHint: normalizeString(parsed.turnFailureHint),
      turnFailedAt: normalizeString(parsed.turnFailedAt)
    };
  } catch {
    return null;
  }
}

// src/hook-utils.ts
var import_promises3 = __toESM(require("fs/promises"), 1);
var import_node_path3 = __toESM(require("path"), 1);
async function readJsonStdin() {
  const chunks = [];
  for await (const chunk of process.stdin) {
    chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
  }
  const raw = Buffer.concat(chunks).toString("utf8").trim();
  if (!raw) return {};
  try {
    const parsed = JSON.parse(raw);
    return parsed && typeof parsed === "object" ? parsed : {};
  } catch {
    return {};
  }
}
function getNestedRecord(value) {
  return value && typeof value === "object" ? value : null;
}
function extractSessionId(payload) {
  return extractString(payload, ["conversation_id", "session_id"]);
}
function extractCwd(payload) {
  const direct = extractString(payload, ["cwd"]);
  if (direct) return direct;
  const roots = payload.workspace_roots ?? payload.workspaceRoots;
  if (Array.isArray(roots)) {
    for (const root of roots) {
      if (typeof root === "string" && root.trim()) return root.trim();
    }
  }
  const envCwd = process.env.CURSOR_PROJECT_DIR ?? process.env.CLAUDE_PROJECT_DIR;
  return envCwd?.trim() || null;
}
function extractToolInput(payload) {
  return getNestedRecord(payload.tool_input) ?? getNestedRecord(payload.toolInput) ?? payload;
}
function extractString(input, keys) {
  for (const key of keys) {
    const value = input[key];
    if (typeof value === "string" && value.trim()) {
      return value.trim();
    }
  }
  return null;
}
function extractToolCwd(payload) {
  const toolInput = extractToolInput(payload);
  return extractString(toolInput, ["cwd"]) ?? extractString(payload, ["cwd"]);
}
async function findBoundRepo(startPath) {
  if (!startPath) return null;
  let current = import_node_path3.default.resolve(startPath);
  let stats = await import_promises3.default.stat(current).catch(() => null);
  if (stats?.isFile()) {
    current = import_node_path3.default.dirname(current);
  }
  while (true) {
    const bindingPath = import_node_path3.default.join(current, ".remix", "config.json");
    const bindingStats = await import_promises3.default.stat(bindingPath).catch(() => null);
    if (bindingStats?.isFile()) return current;
    const parent = import_node_path3.default.dirname(current);
    if (parent === current) return null;
    current = parent;
  }
}

// src/hook-pre-git.ts
function emitCursorResponse(response) {
  process.stdout.write(JSON.stringify(response));
}
var GIT_ADVISORIES = [
  {
    subcommand: "commit",
    re: /\bgit\b(?:\s+-[^\s]+)*\s+commit\b/i,
    category: "mutation",
    message: "This repository is bound to Remix. Raw `git commit` is allowed. Remix is source-blind: any local content change since the last recorded turn is captured by automatic hook recording, regardless of what produced it. After committing, run `remix_collab_status` if you need to inspect state; `recommendedAction: record` means the completed-turn hook will capture the revision boundary.",
    blocked: false
  },
  {
    subcommand: "push",
    re: /\bgit\b(?:\s+-[^\s]+)*\s+push\b/i,
    category: "mutation",
    message: "This repository is bound to Remix. Raw `git push` is allowed and does not change local content, so no Remix recovery action is needed for the push itself. If you also pulled or merged something locally before pushing, automatic hook recording will capture that local boundary at turn completion.",
    blocked: false
  },
  {
    subcommand: "pull",
    re: /\bgit\b(?:\s+-[^\s]+)*\s+pull\b/i,
    category: "mutation",
    message: "This repository is bound to Remix. Raw `git pull` from GitHub is allowed. Remix is source-blind: anything `git pull` brings into the working tree is captured by automatic hook recording like any other local change. After pulling, run `remix_collab_status` if you need to inspect state; `recommendedAction: record` means the completed-turn hook will capture the boundary. Use `remix_collab_sync_*` only for pulling Remix server deltas, not for raw GitHub pulls.",
    blocked: false
  },
  {
    subcommand: "merge",
    re: /\bgit\b(?:\s+-[^\s]+)*\s+merge\b/i,
    category: "mutation",
    message: "This repository is bound to Remix. Raw `git merge` is allowed. Remix is source-blind: a merge is just another local content change and is captured by automatic hook recording; its `preTurnEvents` metadata records that a merge happened. After merging, run `remix_collab_status` if you need to inspect state; `recommendedAction: record` means the completed-turn hook will capture the boundary.",
    blocked: false
  },
  {
    subcommand: "rebase",
    re: /\bgit\b(?:\s+-[^\s]+)*\s+rebase\b/i,
    category: "mutation",
    message: "This repository is bound to Remix. Raw `git rebase` is allowed. Remix is source-blind: a rebase is just another local content change and is captured by automatic hook recording. After rebasing, run `remix_collab_status` if you need to inspect state; `recommendedAction: record` means the completed-turn hook will capture the boundary.",
    blocked: false
  },
  {
    subcommand: "reset",
    re: /\bgit\b(?:\s+-[^\s]+)*\s+reset\b/i,
    category: "mutation",
    message: "This repository is bound to Remix. Raw `git reset` is allowed but risky because it can discard or rewrite local state. Remix is source-blind: whatever the workspace looks like after the reset is what automatic hook recording will capture. Run `remix_collab_status` afterward if you need to inspect state; `recommendedAction: record` means the completed-turn hook will capture the revision boundary.",
    blocked: false
  },
  {
    subcommand: "log",
    re: /\bgit\b(?:\s+-[^\s]+)*\s+log\b/i,
    category: "history_read",
    message: "This repository is bound to Remix. Raw `git log` is a fallback for exact commit history, not the default starting point for historical reasoning. Use Remix memory first when the goal is to understand intent, prior prompts, failed attempts, or decision trail context.",
    blocked: false
  },
  {
    subcommand: "show",
    re: /\bgit\b(?:\s+-[^\s]+)*\s+show\b/i,
    category: "history_read",
    message: "This repository is bound to Remix. Raw `git show` is useful for exact patch inspection, but historical reasoning should start from Remix memory so you can identify the right change step or merge context before expanding into raw diffs.",
    blocked: false
  },
  {
    subcommand: "blame",
    re: /\bgit\b(?:\s+-[^\s]+)*\s+blame\b/i,
    category: "history_read",
    message: "This repository is bound to Remix. Raw `git blame` can identify line ownership, but it does not explain the user ask, reasoning, or failed attempts behind a change. Use Remix memory first when you need historical intent.",
    blocked: false
  },
  {
    subcommand: "diff",
    re: /\bgit\b(?:\s+-[^\s]+)*\s+diff\b/i,
    category: "history_read",
    message: "This repository is bound to Remix. Raw `git diff` is useful for exact patch detail, but Remix memory should be the default first read for understanding why a change happened or which historical step matters before inspecting raw diffs.",
    blocked: false
  },
  {
    subcommand: "rev-list",
    re: /\bgit\b(?:\s+-[^\s]+)*\s+rev-list\b/i,
    category: "history_read",
    message: "This repository is bound to Remix. Raw `git rev-list` is commit-level history detail; use Remix memory first when the question is about reasoning, chronology, or related historical attempts rather than exact ancestry mechanics.",
    blocked: false
  }
];
function getGitAdvisory(command) {
  for (const advisory of GIT_ADVISORIES) {
    if (advisory.re.test(command)) {
      return {
        subcommand: advisory.subcommand,
        category: advisory.category,
        message: advisory.message,
        blocked: advisory.blocked
      };
    }
  }
  return null;
}
function buildMemoryFirstMessage(state) {
  if (!state) {
    return "Use `remix_collab_memory_summary`, `remix_collab_memory_search`, or `remix_collab_memory_timeline` first, then fall back to raw git only if you still need exact commit or patch details.";
  }
  if (state.intent === "git_facts") {
    return null;
  }
  if (state.intent === "collab_state") {
    return "This turn looks like Remix collaboration-state work. Start with `remix_collab_status`, then use memory reads if you need related historical context before raw git history.";
  }
  if (shouldPreferRemixMemory(state.intent) && !state.consultedMemory) {
    return "This turn is classified as a Remix memory-first history question, and no memory tool has been used yet. Start with `remix_collab_memory_summary`, `remix_collab_memory_search`, or `remix_collab_memory_timeline`. Only use `remix_collab_memory_change_step_diff` after identifying the relevant `changeStepId`, and use raw git after that only if exact repository facts are still needed.";
  }
  if (!state.consultedMemory) {
    return "Prefer Remix memory before raw git history in a bound repo: start with `remix_collab_memory_summary`, `remix_collab_memory_search`, or `remix_collab_memory_timeline`, then use raw git only if you still need exact repository facts.";
  }
  return null;
}
async function main() {
  const payload = await readJsonStdin();
  const toolInput = extractToolInput(payload);
  const command = extractString(toolInput, ["command", "cmd", "bash_command"]);
  if (!command) {
    return;
  }
  const advisory = getGitAdvisory(command);
  if (!advisory) {
    return;
  }
  const cwd = extractToolCwd(payload) ?? extractCwd(payload) ?? process.cwd();
  const boundRepo = await findBoundRepo(cwd);
  if (!boundRepo) {
    return;
  }
  const sessionId = extractSessionId(payload);
  const turnState = sessionId ? await loadPendingTurnState(sessionId) : null;
  if (advisory.category === "history_read" && turnState?.intent === "git_facts") {
    return;
  }
  const memoryFirstMessage = advisory.category === "history_read" ? buildMemoryFirstMessage(turnState) : null;
  if (advisory.category === "history_read" && !memoryFirstMessage) {
    return;
  }
  const messageBody = [
    advisory.blocked ? "Remix guardrail:" : "Remix advisory:",
    `Detected raw git ${advisory.subcommand} usage in a repo bound to Remix: ${command}`,
    advisory.message,
    ...memoryFirstMessage ? [memoryFirstMessage] : []
  ].join("\n");
  if (advisory.blocked) {
    emitCursorResponse({
      permission: "deny",
      agent_message: messageBody,
      user_message: messageBody
    });
    process.exitCode = 2;
    return;
  }
  emitCursorResponse({
    permission: "allow",
    agent_message: messageBody
  });
}
main().catch((error) => {
  const message = error instanceof Error ? error.message : String(error);
  process.stderr.write(`${message}
`);
  process.exitCode = 0;
});
//# sourceMappingURL=hook-pre-git.cjs.map