#!/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 __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
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
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

// src/hook-user-prompt.ts
var hook_user_prompt_exports = {};
__export(hook_user_prompt_exports, {
  runHookUserPrompt: () => runHookUserPrompt
});
module.exports = __toCommonJS(hook_user_prompt_exports);

// 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
];
var COLLAB_STATE_PATTERNS = [
  /\bcollab status\b/i,
  /\bsync\b/i,
  /\breconcile\b/i,
  /\bmerge request\b/i,
  /\brequest merge\b/i,
  /\breview\b/i,
  /\bbind(?:ing)?\b/i,
  /\bremix\b/i,
  /\bupstream\b/i
];
var GIT_FACT_PATTERNS = [
  /\bgit (?:log|show|diff|blame|rev-list|whatchanged)\b/i,
  /\bcommit hash(?:es)?\b/i,
  /\bexact commits?\b/i,
  /\braw git\b/i,
  /\bgit history\b/i,
  /\bblame this\b/i,
  /\bwho changed (?:this line|this file|that line)\b/i,
  /\bbranch ancestr(?:y|ies)\b/i,
  /\bpatch[-\s]?level\b/i
];
function hasMatch(prompt, patterns) {
  return patterns.some((pattern) => pattern.test(prompt));
}
function classifyTurnIntent(prompt) {
  const normalizedPrompt = prompt.trim();
  if (!normalizedPrompt) {
    return "neutral";
  }
  const hasStrongMemorySignals = hasMatch(normalizedPrompt, STRONG_MEMORY_FIRST_PATTERNS);
  const hasMemorySignals = hasMatch(normalizedPrompt, MEMORY_FIRST_PATTERNS);
  const hasGitFactSignals = hasMatch(normalizedPrompt, GIT_FACT_PATTERNS);
  if (hasGitFactSignals && !hasStrongMemorySignals) {
    return "git_facts";
  }
  if (hasMemorySignals) {
    return "memory_first";
  }
  if (hasMatch(normalizedPrompt, COLLAB_STATE_PATTERNS)) {
    return "collab_state";
  }
  if (hasMatch(normalizedPrompt, GIT_FACT_PATTERNS)) {
    return "git_facts";
  }
  return "neutral";
}
function buildPromptRoutingAdvisory(intent) {
  if (intent === "memory_first") {
    return [
      "Remix advisory:",
      "This prompt looks like a historical reasoning request in a repo bound to Remix.",
      "Start with `remix_collab_memory_summary`, `remix_collab_memory_search`, or `remix_collab_memory_timeline` before raw git history. Only fetch `remix_collab_memory_change_step_diff` after identifying a relevant `changeStepId`."
    ].join("\n");
  }
  if (intent === "collab_state") {
    return [
      "Remix advisory:",
      "This prompt looks like a repo collaboration-state request in a repo bound to Remix.",
      "Start with `remix_collab_status`, then follow the recommended sync, reconcile, merge-request, or memory reads from there."
    ].join("\n");
  }
  return null;
}

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

// src/hook-state.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 import_node_crypto = require("crypto");
function stateRoot() {
  const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_STATE_ROOT?.trim();
  return configured || import_node_path.default.join(import_node_os.default.tmpdir(), "remix-claude-plugin-hooks");
}
function statePath(sessionId) {
  return import_node_path.default.join(stateRoot(), `${sessionId}.json`);
}
function stateLockPath(sessionId) {
  return import_node_path.default.join(stateRoot(), `${sessionId}.lock`);
}
function stateLockMetaPath(sessionId) {
  return import_node_path.default.join(stateLockPath(sessionId), "owner.json");
}
async function writeJsonAtomic(filePath, value) {
  await import_promises.default.mkdir(import_node_path.default.dirname(filePath), { recursive: true });
  const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
  await import_promises.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
  await import_promises.default.rename(tmpPath, filePath);
}
var STATE_LOCK_WAIT_MS = 2e3;
var STATE_LOCK_POLL_MS = 25;
var STATE_LOCK_STALE_MS = 3e4;
var STATE_LOCK_HEARTBEAT_MS = 5e3;
async function sleep(ms) {
  await new Promise((resolve) => setTimeout(resolve, ms));
}
async function readStateLockMetadata(sessionId) {
  const raw = await import_promises.default.readFile(stateLockMetaPath(sessionId), "utf8").catch(() => null);
  if (!raw) return null;
  try {
    const parsed = JSON.parse(raw);
    if (typeof parsed.ownerId !== "string" || typeof parsed.pid !== "number" || typeof parsed.createdAt !== "string" || typeof parsed.heartbeatAt !== "string") {
      return null;
    }
    return {
      ownerId: parsed.ownerId,
      pid: parsed.pid,
      createdAt: parsed.createdAt,
      heartbeatAt: parsed.heartbeatAt
    };
  } catch {
    return null;
  }
}
async function writeStateLockMetadata(sessionId, metadata) {
  await writeJsonAtomic(stateLockMetaPath(sessionId), metadata);
}
async function tryRemoveStaleStateLock(sessionId) {
  const lockPath = stateLockPath(sessionId);
  const metadata = await readStateLockMetadata(sessionId);
  const staleByHeartbeat = metadata && Date.now() - new Date(metadata.heartbeatAt).getTime() > STATE_LOCK_STALE_MS;
  if (staleByHeartbeat) {
    await import_promises.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
    return true;
  }
  if (!metadata) {
    const lockStat = await import_promises.default.stat(lockPath).catch(() => null);
    if (lockStat && Date.now() - lockStat.mtimeMs > STATE_LOCK_STALE_MS) {
      await import_promises.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
      return true;
    }
  }
  return false;
}
async function acquireStateLock(sessionId) {
  const lockPath = stateLockPath(sessionId);
  const deadline = Date.now() + STATE_LOCK_WAIT_MS;
  await import_promises.default.mkdir(stateRoot(), { recursive: true });
  while (true) {
    try {
      await import_promises.default.mkdir(lockPath);
      const ownerId = (0, import_node_crypto.randomUUID)();
      const createdAt = (/* @__PURE__ */ new Date()).toISOString();
      const metadata = {
        ownerId,
        pid: process.pid,
        createdAt,
        heartbeatAt: createdAt
      };
      await writeStateLockMetadata(sessionId, metadata);
      let released = false;
      const heartbeat = setInterval(() => {
        if (released) return;
        void writeStateLockMetadata(sessionId, {
          ...metadata,
          heartbeatAt: (/* @__PURE__ */ new Date()).toISOString()
        }).catch(() => void 0);
      }, STATE_LOCK_HEARTBEAT_MS);
      heartbeat.unref?.();
      return async () => {
        if (released) return;
        released = true;
        clearInterval(heartbeat);
        const currentMetadata = await readStateLockMetadata(sessionId);
        if (currentMetadata?.ownerId === ownerId) {
          await import_promises.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
        }
      };
    } catch (error) {
      const code = error && typeof error === "object" && "code" in error ? error.code : null;
      if (code !== "EEXIST") {
        throw error;
      }
      if (await tryRemoveStaleStateLock(sessionId)) {
        continue;
      }
      if (Date.now() >= deadline) {
        throw new Error(`Timed out acquiring hook state lock for session ${sessionId}.`);
      }
      await sleep(STATE_LOCK_POLL_MS);
    }
  }
}
async function withStateLock(sessionId, fn) {
  const release = await acquireStateLock(sessionId);
  try {
    return await fn();
  } finally {
    await release();
  }
}
async function savePendingTurnState(state) {
  await writeJsonAtomic(statePath(state.sessionId), state);
}
async function createPendingTurnState(params) {
  return withStateLock(params.sessionId, async () => {
    const state = {
      sessionId: params.sessionId,
      turnId: (0, import_node_crypto.randomUUID)(),
      prompt: params.prompt,
      initialCwd: params.initialCwd?.trim() || null,
      intent: params.intent,
      submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
      consultedMemory: false,
      touchedRepos: {},
      turnFailureMessage: null,
      turnFailureHint: null,
      turnFailedAt: null
    };
    await savePendingTurnState(state);
    return state;
  });
}

// package.json
var package_default = {
  name: "@remixhq/claude-plugin",
  version: "0.1.17",
  description: "Claude Code plugin for Remix collaboration workflows",
  homepage: "https://github.com/RemixDotOne/remix-claude-plugin",
  license: "MIT",
  repository: {
    type: "git",
    url: "https://github.com/RemixDotOne/remix-claude-plugin.git"
  },
  type: "module",
  engines: {
    node: ">=20"
  },
  publishConfig: {
    access: "public"
  },
  files: [
    "dist",
    ".claude-plugin/plugin.json",
    ".mcp.json",
    "skills",
    "hooks",
    "agents"
  ],
  scripts: {
    build: "tsup",
    postbuild: `node -e "const fs=require('node:fs'); for (const p of ['dist/mcp-server.cjs','dist/hook-pre-git.cjs','dist/hook-user-prompt.cjs','dist/hook-post-collab.cjs','dist/hook-stop-collab.cjs']) fs.chmodSync(p, 0o755);"`,
    dev: "tsx src/mcp-server.ts",
    typecheck: "tsc -p tsconfig.json --noEmit",
    prepack: "npm run build"
  },
  dependencies: {
    "@remixhq/core": "^0.1.12",
    "@remixhq/mcp": "^0.1.12"
  },
  devDependencies: {
    "@types/node": "^25.4.0",
    tsup: "^8.5.1",
    tsx: "^4.21.0",
    typescript: "^5.9.3"
  }
};

// src/metadata.ts
var pluginMetadata = {
  name: package_default.name,
  version: package_default.version,
  description: package_default.description,
  pluginId: "remix",
  agentName: "remix-collab"
};

// src/hook-diagnostics.ts
var MAX_LOG_BYTES = 512 * 1024;
function resolveClaudeRoot() {
  const configured = process.env.CLAUDE_CONFIG_DIR?.trim();
  return configured || import_node_path2.default.join(import_node_os2.default.homedir(), ".claude");
}
function resolvePluginDataDirName() {
  return `${pluginMetadata.pluginId}-${pluginMetadata.pluginId}`;
}
function getHookDiagnosticsDirPath() {
  const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_DIAGNOSTICS_DIR?.trim();
  return configured || import_node_path2.default.join(resolveClaudeRoot(), "plugins", "data", resolvePluginDataDirName());
}
function getHookDiagnosticsLogPath() {
  return import_node_path2.default.join(getHookDiagnosticsDirPath(), "hooks.ndjson");
}
function toFieldValue(value) {
  if (value === null) return null;
  if (typeof value === "string") return value;
  if (typeof value === "number" && Number.isFinite(value)) return value;
  if (typeof value === "boolean") return value;
  return void 0;
}
function normalizeFields(fields) {
  if (!fields) return {};
  const normalizedEntries = Object.entries(fields).map(([key, value]) => {
    const normalized = toFieldValue(value);
    return normalized === void 0 ? null : [key, normalized];
  }).filter((entry) => entry !== null);
  return Object.fromEntries(normalizedEntries);
}
async function rotateLogIfNeeded(logPath) {
  const stat = await import_promises2.default.stat(logPath).catch(() => null);
  if (!stat || stat.size < MAX_LOG_BYTES) {
    return;
  }
  const rotatedPath = `${logPath}.1`;
  await import_promises2.default.rm(rotatedPath, { force: true }).catch(() => void 0);
  await import_promises2.default.rename(logPath, rotatedPath).catch(() => void 0);
}
function summarizeText(value) {
  if (typeof value !== "string" || !value.trim()) {
    return {
      present: false,
      length: 0,
      sha256Prefix: null
    };
  }
  const trimmed = value.trim();
  return {
    present: true,
    length: trimmed.length,
    sha256Prefix: (0, import_node_crypto2.createHash)("sha256").update(trimmed).digest("hex").slice(0, 12)
  };
}
async function appendHookDiagnosticsEvent(params) {
  try {
    const logPath = getHookDiagnosticsLogPath();
    await import_promises2.default.mkdir(import_node_path2.default.dirname(logPath), { recursive: true });
    await rotateLogIfNeeded(logPath);
    const event = {
      ts: (/* @__PURE__ */ new Date()).toISOString(),
      hook: params.hook,
      pluginVersion: pluginMetadata.version,
      pid: process.pid,
      sessionId: params.sessionId?.trim() || null,
      turnId: params.turnId?.trim() || null,
      stage: params.stage.trim(),
      result: params.result,
      reason: params.reason?.trim() || null,
      toolName: params.toolName?.trim() || null,
      repoRoot: params.repoRoot?.trim() || null,
      message: params.message?.trim() || null,
      fields: normalizeFields(params.fields)
    };
    await import_promises2.default.appendFile(logPath, `${JSON.stringify(event)}
`, "utf8");
  } catch {
  }
}

// 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 extractString(input, keys) {
  for (const key of keys) {
    const value = input[key];
    if (typeof value === "string" && value.trim()) {
      return value.trim();
    }
  }
  return null;
}
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-user-prompt.ts
function buildGlobalFinalRecordingReminder() {
  return [
    "Remix recording rule:",
    "If you work in any Remix-bound repo during this turn, you must record exactly one final Remix turn before your final user-facing response.",
    "Use `remix_collab_finalize_turn`. Include the exact user prompt and your final assistant response. If no Remix-bound repo is involved in this turn, ignore this reminder."
  ].join("\n");
}
function buildBoundRepoFinalRecordingReminder() {
  return [
    "Remix bound-repo reminder:",
    "This turn is in a Remix-bound repo. You must record exactly one final Remix turn before your final user-facing response.",
    "In this repo, Remix MCP tools are the required workflow layer for ordinary collaboration work. Use `remix_collab_finalize_turn`. It chooses changed-turn vs no-diff automatically from the live worktree, and it can accept an explicit external diff when needed.",
    "Branch reminder: switching git branches can change the valid Remix lane binding for this checkout. Run `remix_collab_status` after branch switches and before mutation commands. If the branch is unbound or mismatched, resolve that first.",
    "Do not finish the response before recording."
  ].join("\n");
}
function combineAdvisories(...messages) {
  const parts = messages.map((message) => message?.trim()).filter((message) => Boolean(message));
  if (parts.length === 0) return null;
  return parts.join("\n\n");
}
async function runHookUserPrompt(payload) {
  const sessionId = extractString(payload, ["session_id"]);
  const prompt = extractString(payload, ["prompt"]);
  const cwd = extractString(payload, ["cwd"]);
  await appendHookDiagnosticsEvent({
    hook: "UserPromptSubmit",
    sessionId,
    stage: "payload_received",
    result: "start",
    fields: {
      hasSessionId: Boolean(sessionId),
      hasPrompt: Boolean(prompt),
      hasCwd: Boolean(cwd)
    }
  });
  if (!sessionId) {
    const reminder = buildGlobalFinalRecordingReminder();
    await appendHookDiagnosticsEvent({
      hook: "UserPromptSubmit",
      stage: "payload_validation",
      result: "info",
      reason: "missing_session_id",
      fields: {
        hasPrompt: Boolean(prompt),
        advisoryLength: reminder.length
      }
    });
    process.stdout.write(reminder);
    return;
  }
  if (!prompt) {
    await appendHookDiagnosticsEvent({
      hook: "UserPromptSubmit",
      sessionId,
      stage: "payload_validation",
      result: "skip",
      reason: "missing_prompt",
      fields: {
        hasCwd: Boolean(cwd)
      }
    });
    return;
  }
  const promptSummary = summarizeText(prompt);
  await appendHookDiagnosticsEvent({
    hook: "UserPromptSubmit",
    sessionId,
    stage: "payload_parsed",
    result: "info",
    fields: {
      hasCwd: Boolean(cwd),
      promptLength: promptSummary.length,
      promptHash: promptSummary.sha256Prefix
    }
  });
  const intent = classifyTurnIntent(prompt);
  const state = await createPendingTurnState({
    sessionId,
    prompt,
    initialCwd: cwd,
    intent
  });
  await appendHookDiagnosticsEvent({
    hook: "UserPromptSubmit",
    sessionId,
    turnId: state.turnId,
    stage: "state_created",
    result: "success",
    fields: {
      intent,
      hasInitialCwd: Boolean(cwd)
    }
  });
  const boundRepo = await findBoundRepo(cwd);
  if (!boundRepo) {
    await appendHookDiagnosticsEvent({
      hook: "UserPromptSubmit",
      sessionId,
      turnId: state.turnId,
      stage: "bound_repo_lookup",
      result: "skip",
      reason: "repo_not_bound",
      fields: {
        hasCwd: Boolean(cwd)
      }
    });
    return;
  }
  const advisory = boundRepo ? buildPromptRoutingAdvisory(intent) : null;
  const combinedAdvisory = combineAdvisories(
    buildGlobalFinalRecordingReminder(),
    advisory,
    boundRepo ? buildBoundRepoFinalRecordingReminder() : null
  );
  if (combinedAdvisory) {
    await appendHookDiagnosticsEvent({
      hook: "UserPromptSubmit",
      sessionId,
      turnId: state.turnId,
      stage: "advisory_emitted",
      result: "info",
      repoRoot: boundRepo,
      fields: {
        advisoryLength: combinedAdvisory.length,
        intent
      }
    });
    process.stdout.write(combinedAdvisory);
    return;
  }
  await appendHookDiagnosticsEvent({
    hook: "UserPromptSubmit",
    sessionId,
    turnId: state.turnId,
    stage: "completed",
    result: "success",
    repoRoot: boundRepo,
    fields: {
      intent
    }
  });
}
async function main() {
  const payload = await readJsonStdin();
  await runHookUserPrompt(payload);
}
main().catch((error) => {
  const message = error instanceof Error ? error.message : String(error);
  void appendHookDiagnosticsEvent({
    hook: "UserPromptSubmit",
    stage: "unhandled_error",
    result: "error",
    reason: "exception",
    message
  });
  process.stderr.write(`${message}
`);
  process.exitCode = 0;
});
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  runHookUserPrompt
});
//# sourceMappingURL=hook-user-prompt.cjs.map