#!/usr/bin/env node

// dist/index.js
import { Command } from "commander";

// dist/commands/install.js
import { readFile, writeFile } from "fs/promises";
import { existsSync } from "fs";
import { join } from "path";
import { homedir } from "os";
function expandPath(p) {
  if (p.startsWith("~")) {
    return join(homedir(), p.slice(1));
  }
  if (p.startsWith("%APPDATA%")) {
    const appData = process.env.APPDATA || join(homedir(), "AppData", "Roaming");
    return join(appData, p.slice("%APPDATA%".length));
  }
  return p;
}
var AGENT_CONFIGS = [
  { name: "Claude Code", path: "~/.claude/claude_desktop_config.json", format: "claude" },
  { name: "Claude Code (Windows)", path: "%APPDATA%/Claude/claude_desktop_config.json", format: "claude" },
  { name: "Gemini CLI", path: "~/.gemini/settings.json", format: "gemini" },
  { name: "Cursor", path: "~/.cursor/mcp.json", format: "cursor" },
  { name: "Windsurf", path: "~/.codeium/windsurf/mcp_config.json", format: "cursor" }
];
var MCP_ENTRY = {
  command: "npx",
  args: ["-y", "vibe-splain", "serve"]
};
async function installCommand() {
  console.error("\n\u{1F527} VIBE-SPLAIN Installer\n");
  let patchedCount = 0;
  for (const agent of AGENT_CONFIGS) {
    const resolvedPath = expandPath(agent.path);
    if (!existsSync(resolvedPath)) {
      continue;
    }
    console.error(`  Found ${agent.name} config at ${resolvedPath}`);
    try {
      const raw = await readFile(resolvedPath, "utf8");
      let config;
      try {
        config = JSON.parse(raw);
      } catch {
        console.error(`    \u26A0 Could not parse JSON, skipping`);
        continue;
      }
      if (config.mcpServers?.["vibe-splain"]) {
        console.error(`    \u2713 Already configured, skipping`);
        patchedCount++;
        continue;
      }
      if (!config.mcpServers) {
        config.mcpServers = {};
      }
      config.mcpServers["vibe-splain"] = MCP_ENTRY;
      await writeFile(resolvedPath, JSON.stringify(config, null, 2), "utf8");
      console.error(`    \u2705 Patched successfully`);
      patchedCount++;
    } catch (err) {
      console.error(`    \u274C Error patching: ${err}`);
    }
  }
  if (patchedCount === 0) {
    console.error(`
\u26A0\uFE0F  No supported coding agent config found.`);
    console.error(`Add this manually to your agent's MCP config:
`);
    console.error(JSON.stringify({
      mcpServers: {
        "vibe-splain": MCP_ENTRY
      }
    }, null, 2));
    process.exit(1);
  } else {
    console.error(`
\u2705 Patched ${patchedCount} agent config(s). Restart your coding agent to activate.
`);
  }
}

// dist/mcp/server.js
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema } from "@modelcontextprotocol/sdk/types.js";

// ../brain/dist/scanner.js
import Parser from "web-tree-sitter";
import { join as join4, dirname, relative, extname, basename, sep } from "path";
import { fileURLToPath } from "url";
import { createRequire } from "module";
import { readFile as readFile4, readdir } from "fs/promises";
import { existsSync as existsSync2 } from "fs";

// ../brain/dist/graph.js
import { join as join2 } from "path";
import { readFile as readFile2, writeFile as writeFile2, mkdir } from "fs/promises";
async function writeGraph(projectRoot, graph) {
  const dir = join2(projectRoot, ".vibe-splainer");
  await mkdir(dir, { recursive: true });
  const graphPath = join2(dir, "graph.json");
  await writeFile2(graphPath, JSON.stringify(graph, null, 2), "utf8");
}

// ../brain/dist/analysis.js
import { join as join3 } from "path";
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
async function readAnalysis(projectRoot) {
  const p = join3(projectRoot, ".vibe-splainer", "analysis.json");
  try {
    const raw = await readFile3(p, "utf8");
    return JSON.parse(raw);
  } catch {
    return null;
  }
}
async function writeAnalysis(projectRoot, store) {
  const dir = join3(projectRoot, ".vibe-splainer");
  await mkdir2(dir, { recursive: true });
  await writeFile3(join3(dir, "analysis.json"), JSON.stringify(store, null, 2), "utf8");
}

// ../brain/dist/scanner.js
var __dirname = dirname(fileURLToPath(import.meta.url));
var require2 = createRequire(import.meta.url);
var parser = null;
var langCache = /* @__PURE__ */ new Map();
var EXT_LANG = {
  ".ts": "typescript",
  ".tsx": "tsx",
  ".js": "javascript",
  ".jsx": "tsx",
  ".mjs": "javascript",
  ".cjs": "javascript",
  ".py": "python",
  ".go": "go",
  ".rs": "rust",
  ".java": "java"
};
var LANG_WASM = {
  typescript: "tree-sitter-typescript.wasm",
  tsx: "tree-sitter-tsx.wasm",
  javascript: "tree-sitter-javascript.wasm",
  python: "tree-sitter-python.wasm",
  go: "tree-sitter-go.wasm",
  rust: "tree-sitter-rust.wasm",
  java: "tree-sitter-java.wasm"
};
var SUPPORTED_EXTENSIONS = new Set(Object.keys(EXT_LANG));
function resolveWasm(file) {
  try {
    const wasmsDir = dirname(require2.resolve("tree-sitter-wasms/package.json"));
    const p = join4(wasmsDir, "out", file);
    if (existsSync2(p))
      return p;
  } catch {
  }
  const local = join4(__dirname, "../wasm", file);
  return existsSync2(local) ? local : null;
}
async function getLanguage(lang) {
  const cached = langCache.get(lang);
  if (cached)
    return cached;
  const wasm = resolveWasm(LANG_WASM[lang]);
  if (!wasm) {
    console.error(`[vibe-splain] grammar missing for ${lang} (${LANG_WASM[lang]}); skipping language`);
    return null;
  }
  try {
    const loaded = await Parser.Language.load(wasm);
    langCache.set(lang, loaded);
    return loaded;
  } catch (err) {
    console.error(`[vibe-splain] failed to load grammar for ${lang}:`, err instanceof Error ? err.message : err);
    return null;
  }
}
async function initParser() {
  if (parser)
    return parser;
  await Parser.init();
  parser = new Parser();
  const ts = await getLanguage("typescript");
  if (ts)
    parser.setLanguage(ts);
  return parser;
}
async function parseAs(lang, source) {
  const p = await initParser();
  const language = await getLanguage(lang);
  if (!language)
    return null;
  p.setLanguage(language);
  try {
    return p.parse(source);
  } catch {
    return null;
  }
}
var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
  "node_modules",
  "dist",
  "build",
  ".next",
  "out",
  ".vibe-splainer",
  ".git",
  ".venv",
  "venv",
  "env",
  "__pycache__",
  ".idea",
  ".vscode",
  ".cache",
  "site-packages",
  "target",
  ".tox",
  ".mypy_cache",
  ".pytest_cache"
]);
var EXCLUDE_FILE_PATTERNS = [/\.lock$/, /\.min\.[a-z]+$/, /\.d\.ts$/];
var DEMOTE_SEGMENTS = /* @__PURE__ */ new Set([
  "docs",
  "doc",
  "examples",
  "example",
  "samples",
  "sample",
  "mockup",
  "mockups",
  "fixtures",
  "fixture",
  "__generated__",
  "__mocks__"
]);
var VENDOR_SEGMENTS = /* @__PURE__ */ new Set([
  "node_modules",
  "vendor",
  "vendored",
  "site-packages",
  "third_party",
  "third-party"
]);
async function collectFiles(dir, projectRoot, acc) {
  let entries;
  try {
    entries = await readdir(dir, { withFileTypes: true });
  } catch {
    return;
  }
  for (const entry of entries) {
    if (entry.name.startsWith(".") && entry.name !== ".") {
      if (entry.isDirectory())
        continue;
    }
    if (EXCLUDE_DIRS.has(entry.name))
      continue;
    const fullPath = join4(dir, entry.name);
    if (entry.isDirectory()) {
      await collectFiles(fullPath, projectRoot, acc);
    } else if (entry.isFile()) {
      const ext = extname(entry.name);
      if (!SUPPORTED_EXTENSIONS.has(ext))
        continue;
      if (EXCLUDE_FILE_PATTERNS.some((p) => p.test(entry.name)))
        continue;
      acc.push(fullPath);
    }
  }
}
function pathDemoteReason(relPath) {
  const segs = relPath.split(sep);
  for (const s of segs) {
    if (VENDOR_SEGMENTS.has(s))
      return `vendored code (${s})`;
    if (s.endsWith(".venv") || s === "venv" || s === "env")
      return "virtual environment";
  }
  for (const s of segs) {
    if (DEMOTE_SEGMENTS.has(s.toLowerCase()))
      return `non-application path segment (${s})`;
  }
  const base = basename(relPath);
  if (/\.min\./.test(base))
    return "minified bundle";
  if (/\.generated\./.test(base))
    return "generated file";
  return null;
}
function extractImports(source, lang) {
  const specs = [];
  if (lang === "python") {
    const re2 = /^[ \t]*(?:from[ \t]+([.\w]+)[ \t]+import|import[ \t]+([.\w][.\w ,]*))/gm;
    let m2;
    while ((m2 = re2.exec(source)) !== null) {
      if (m2[1]) {
        specs.push(m2[1]);
      } else if (m2[2]) {
        for (const part of m2[2].split(",")) {
          const name = part.trim().split(/\s+as\s+/)[0].trim();
          if (name)
            specs.push(name);
        }
      }
    }
    return specs;
  }
  if (lang === "go") {
    const re2 = /"([^"]+)"/g;
    const importBlock = source.match(/import\s*\(([\s\S]*?)\)/g) || [];
    for (const block of importBlock) {
      let m3;
      while ((m3 = re2.exec(block)) !== null)
        specs.push(m3[1]);
    }
    const single = /import\s+(?:\w+\s+)?"([^"]+)"/g;
    let m2;
    while ((m2 = single.exec(source)) !== null)
      specs.push(m2[1]);
    return specs;
  }
  if (lang === "rust") {
    const re2 = /\b(?:use|mod)\s+([\w:]+)/g;
    let m2;
    while ((m2 = re2.exec(source)) !== null)
      specs.push(m2[1]);
    return specs;
  }
  if (lang === "java") {
    const re2 = /import\s+(?:static\s+)?([\w.]+)/g;
    let m2;
    while ((m2 = re2.exec(source)) !== null)
      specs.push(m2[1]);
    return specs;
  }
  const re = /(?:import|export)\s[^;]*?from\s*['"]([^'"]+)['"]|(?:import|require)\s*\(\s*['"]([^'"]+)['"]/g;
  let m;
  while ((m = re.exec(source)) !== null) {
    specs.push(m[1] || m[2]);
  }
  return specs;
}
var JS_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
function resolveImport(spec, fromAbs, lang, projectRoot, fileSet, basenameIndex) {
  if (lang === "python") {
    return resolvePython(spec, fromAbs, projectRoot, fileSet);
  }
  if (lang === "typescript" || lang === "tsx" || lang === "javascript") {
    if (!spec.startsWith("."))
      return null;
    const base = join4(dirname(fromAbs), spec);
    return tryJsCandidates(base, projectRoot, fileSet);
  }
  return resolveGeneric(spec, projectRoot, fileSet, basenameIndex);
}
function tryJsCandidates(base, projectRoot, fileSet) {
  const candidates = [];
  for (const ext of JS_EXTS)
    candidates.push(base + ext);
  for (const ext of JS_EXTS)
    candidates.push(join4(base, "index" + ext));
  candidates.unshift(base);
  for (const c of candidates) {
    const rel = relative(projectRoot, c);
    if (fileSet.has(rel))
      return rel;
  }
  return null;
}
function resolvePython(spec, fromAbs, projectRoot, fileSet) {
  let modulePath;
  if (spec.startsWith(".")) {
    const dots = spec.match(/^\.+/)[0].length;
    let dir = dirname(fromAbs);
    for (let i = 1; i < dots; i++)
      dir = dirname(dir);
    const rest = spec.slice(dots).replace(/\./g, sep);
    modulePath = rest ? join4(dir, rest) : dir;
  } else {
    modulePath = join4(projectRoot, spec.replace(/\./g, sep));
  }
  const candidates = [modulePath + ".py", join4(modulePath, "__init__.py")];
  for (const c of candidates) {
    const rel = relative(projectRoot, c);
    if (fileSet.has(rel))
      return rel;
  }
  if (!spec.startsWith(".")) {
    const last = spec.split(".").pop();
    void last;
  }
  return null;
}
function resolveGeneric(spec, projectRoot, fileSet, basenameIndex) {
  const normalized = spec.replace(/^crate::/, "").replace(/::/g, "/").replace(/\./g, "/");
  const parts = normalized.split("/").filter(Boolean);
  if (parts.length === 0)
    return null;
  const last = parts[parts.length - 1];
  for (const rel of fileSet) {
    const noExt = rel.slice(0, rel.length - extname(rel).length);
    if (noExt.endsWith(parts.join(sep)))
      return rel;
  }
  const byBase = basenameIndex.get(last);
  if (byBase && byBase.length === 1)
    return byBase[0];
  return null;
}
var FUNCTION_TYPES = /* @__PURE__ */ new Set([
  "function_declaration",
  "function",
  "function_expression",
  "arrow_function",
  "method_definition",
  "function_definition",
  "method_declaration",
  "func_literal",
  "function_item",
  "closure_expression",
  "constructor_declaration",
  "generator_function_declaration",
  "generator_function"
]);
var NESTING_TYPES = /* @__PURE__ */ new Set([
  "function_declaration",
  "function",
  "arrow_function",
  "function_expression",
  "method_definition",
  "function_definition",
  "method_declaration",
  "function_item",
  "class_declaration",
  "class",
  "class_definition",
  "class_item",
  "if_statement",
  "if_expression",
  "for_statement",
  "for_in_statement",
  "for_expression",
  "enhanced_for_statement",
  "while_statement",
  "while_expression",
  "do_statement",
  "switch_statement",
  "match_expression",
  "match_arm",
  "try_statement",
  "catch_clause",
  "except_clause",
  "loop_expression",
  "block"
]);
var DECISION_TYPES = /* @__PURE__ */ new Set([
  "if_statement",
  "if_expression",
  "elif_clause",
  "for_statement",
  "for_in_statement",
  "for_expression",
  "enhanced_for_statement",
  "while_statement",
  "while_expression",
  "do_statement",
  "loop_expression",
  "case",
  "switch_case",
  "case_clause",
  "match_arm",
  "catch_clause",
  "except_clause",
  "communication_case",
  "conditional_expression",
  "ternary_expression"
]);
var CATCH_TYPES = /* @__PURE__ */ new Set(["catch_clause", "except_clause"]);
var LONG_FN_LOC = 60;
var DEEP_NESTING = 5;
var GOD_FILE_LOC = 400;
var GOD_FILE_EXPORTS = 8;
function nodeLOC(node) {
  return node.endPosition.row - node.startPosition.row + 1;
}
function countDecisions(node) {
  let count = 0;
  const walk = (n) => {
    if (DECISION_TYPES.has(n.type))
      count++;
    if (n.type === "binary_expression") {
      const op = n.children.find((c) => c.type === "&&" || c.type === "||");
      if (op)
        count++;
    }
    if (n.type === "boolean_operator")
      count++;
    for (const c of n.children)
      walk(c);
  };
  walk(node);
  return count;
}
function computeNesting(node, depth) {
  let maxDepth = depth;
  for (const child of node.children) {
    const nextDepth = NESTING_TYPES.has(child.type) ? depth + 1 : depth;
    maxDepth = Math.max(maxDepth, computeNesting(child, nextDepth));
  }
  return maxDepth;
}
function firstLine(s) {
  return s.split("\n")[0];
}
function stripLeadingComments(snippet) {
  const lines = snippet.split("\n");
  let i = 0;
  let inBlock = false;
  while (i < lines.length) {
    const t = lines[i].trim();
    if (inBlock) {
      if (t.includes("*/"))
        inBlock = false;
      i++;
      continue;
    }
    if (t === "") {
      i++;
      continue;
    }
    if (t.startsWith("//") || t.startsWith("#")) {
      i++;
      continue;
    }
    if (t.startsWith("/*")) {
      inBlock = !t.includes("*/");
      i++;
      continue;
    }
    if (t.startsWith('"""') || t.startsWith("'''")) {
      const q = t.slice(0, 3);
      if (t.length > 3 && t.endsWith(q)) {
        i++;
        continue;
      }
      i++;
      while (i < lines.length && !lines[i].includes(q))
        i++;
      i++;
      continue;
    }
    break;
  }
  return lines.slice(i).join("\n");
}
var TODO_RE = /\b(TODO|FIXME|HACK|XXX|KLUDGE)\b|@deprecated/;
var SUPPRESS_RE = /@ts-ignore|@ts-nocheck|eslint-disable|:\s*any\b|#\s*type:\s*ignore|type:\s*ignore|#\s*nosec/;
function collectFunctionNodes(root) {
  const out = [];
  const walk = (n) => {
    if (FUNCTION_TYPES.has(n.type))
      out.push(n);
    for (const c of n.children)
      walk(c);
  };
  walk(root);
  return out;
}
function catchIsSwallowed(node, lang) {
  const bodyText = node.text;
  const inner = bodyText.replace(/^[^{:]*[{:]/, "");
  const meaningful = inner.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//") && !l.startsWith("#") && l !== "}" && l !== "pass");
  if (meaningful.length === 0)
    return true;
  const onlyLogs = meaningful.every((l) => /^(console\.(log|error|warn|info)|print|println!?|System\.out|logger?\.)/.test(l) || l === "pass" || l === "{" || l === "});" || l === ")" || l === "`");
  return onlyLogs;
}
function analyzeAst(source, lang, tree) {
  const root = tree.rootNode;
  const lines = source.split("\n");
  const loc = lines.length;
  const cyclomatic = countDecisions(root);
  const maxNesting = computeNesting(root, 0);
  const smells = [];
  let todos = 0, suppressions = 0;
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    if (TODO_RE.test(line)) {
      todos++;
      smells.push({ kind: "todo", line: i + 1, endLine: i + 1, text: line.trim().slice(0, 200), severity: 2, note: "unfinished / known-bad marker" });
    }
    if (SUPPRESS_RE.test(line)) {
      suppressions++;
      smells.push({ kind: "suppression", line: i + 1, endLine: i + 1, text: line.trim().slice(0, 200), severity: 3, note: "type/lint safety suppressed" });
    }
  }
  let magicNumbers = 0;
  const magicWalk = (n) => {
    if (n.type === "number" || n.type === "integer_literal" || n.type === "float_literal" || n.type === "int_literal") {
      const v = n.text.replace(/_/g, "");
      if (!["0", "1", "2", "-1", "100", "1000"].includes(v) && /^\d{2,}$/.test(v)) {
        magicNumbers++;
      }
    }
    for (const c of n.children)
      magicWalk(c);
  };
  magicWalk(root);
  if (magicNumbers > 6) {
    smells.push({ kind: "magic-number", line: 1, endLine: 1, text: `${magicNumbers} unexplained numeric literals`, severity: 2, note: "many magic numbers \u2014 extract named constants" });
  }
  let swallowedCatches = 0;
  const catchWalk = (n) => {
    if (CATCH_TYPES.has(n.type) && catchIsSwallowed(n, lang)) {
      swallowedCatches++;
      smells.push({
        kind: "swallowed-catch",
        line: n.startPosition.row + 1,
        endLine: n.endPosition.row + 1,
        text: firstLine(n.text).trim().slice(0, 200),
        severity: 4,
        note: "catch block swallows error silently"
      });
    }
    for (const c of n.children)
      catchWalk(c);
  };
  catchWalk(root);
  const fnNodes = collectFunctionNodes(root);
  let longFunctions = 0;
  const scored = [];
  for (const fn of fnNodes) {
    const bodyLOC = nodeLOC(fn);
    const decisions = countDecisions(fn);
    scored.push({ node: fn, decisions, bodyLOC, score: decisions + bodyLOC });
    if (bodyLOC > LONG_FN_LOC) {
      longFunctions++;
      smells.push({
        kind: "long-function",
        line: fn.startPosition.row + 1,
        endLine: fn.endPosition.row + 1,
        text: firstLine(fn.text).trim().slice(0, 200),
        severity: 3,
        note: `function body is ${bodyLOC} lines`
      });
    }
  }
  if (maxNesting > DEEP_NESTING) {
    smells.push({ kind: "deep-nesting", line: 1, endLine: 1, text: `nesting depth ${maxNesting}`, severity: 3, note: `control flow nested ${maxNesting} levels deep` });
  }
  const exported = collectExports(root, lang);
  const publicSurface = exported.length;
  const signature = exported.map((e) => e.text).join("\n").slice(0, 4e3);
  if (loc > GOD_FILE_LOC && publicSurface > GOD_FILE_EXPORTS) {
    smells.push({
      kind: "god-file",
      line: 1,
      endLine: 1,
      text: `${loc} LOC, ${publicSurface} exports`,
      severity: 4,
      note: `god-file: ${loc} lines exporting ${publicSurface} symbols`
    });
  }
  scored.sort((a, b) => b.score - a.score);
  const hotSpans = scored.slice(0, 3).filter((s) => s.bodyLOC >= 4).map((s) => {
    const raw = source.split("\n").slice(s.node.startPosition.row, s.node.endPosition.row + 1).join("\n");
    const snippet = stripLeadingComments(raw).slice(0, 2e3);
    return {
      startLine: s.node.startPosition.row + 1,
      endLine: s.node.endPosition.row + 1,
      snippet,
      reason: `high complexity: ${s.decisions} decision branches across ${s.bodyLOC} lines`
    };
  });
  return {
    language: lang,
    loc,
    cyclomatic,
    maxNesting,
    publicSurface,
    exportedNames: exported.map((e) => e.name),
    signature,
    longFunctions,
    magicNumbers,
    swallowedCatches,
    smells,
    hotSpans
  };
}
function collectExports(root, lang) {
  const out = [];
  const seen = /* @__PURE__ */ new Set();
  const push = (name, node) => {
    if (!name || seen.has(name))
      return;
    seen.add(name);
    out.push({ name, text: firstLine(node.text).trim().slice(0, 200) });
  };
  if (lang === "python") {
    for (const c of root.children) {
      if (c.type === "function_definition" || c.type === "class_definition") {
        const name = c.childForFieldName("name")?.text;
        if (name && !name.startsWith("_"))
          push(name, c);
      }
    }
    return out;
  }
  if (lang === "go") {
    const walk2 = (n) => {
      if (n.type === "function_declaration" || n.type === "method_declaration" || n.type === "type_declaration") {
        const name = n.childForFieldName("name")?.text;
        if (name && /^[A-Z]/.test(name))
          push(name, n);
      }
      for (const c of n.children)
        walk2(c);
    };
    walk2(root);
    return out;
  }
  if (lang === "rust") {
    const walk2 = (n) => {
      if (/_item$/.test(n.type) && n.children.some((c) => c.type === "visibility_modifier")) {
        const name = n.childForFieldName("name")?.text;
        push(name, n);
      }
      for (const c of n.children)
        walk2(c);
    };
    walk2(root);
    return out;
  }
  if (lang === "java") {
    const walk2 = (n) => {
      if ((n.type === "method_declaration" || n.type === "class_declaration") && /\bpublic\b/.test(firstLine(n.text))) {
        const name = n.childForFieldName("name")?.text;
        push(name, n);
      }
      for (const c of n.children)
        walk2(c);
    };
    walk2(root);
    return out;
  }
  const walk = (n) => {
    if (n.type === "export_statement") {
      const decl = n.childForFieldName("declaration");
      if (decl) {
        const name = decl.childForFieldName("name")?.text;
        if (name)
          push(name, decl);
        for (const c of decl.namedChildren) {
          const dn = c.childForFieldName("name")?.text;
          if (dn)
            push(dn, c);
        }
      }
      for (const spec of n.descendantsOfType("export_specifier")) {
        push(spec.childForFieldName("name")?.text, spec);
      }
      if (n.text.includes("export default"))
        push("default", n);
    }
    for (const c of n.children)
      walk(c);
  };
  walk(root);
  return out;
}
function pageRank(nodes, outEdges, damping = 0.85, iters = 20) {
  const n = nodes.length;
  const rank = /* @__PURE__ */ new Map();
  if (n === 0)
    return rank;
  for (const node of nodes)
    rank.set(node, 1 / n);
  const inEdges = /* @__PURE__ */ new Map();
  for (const node of nodes)
    inEdges.set(node, []);
  const outCount = /* @__PURE__ */ new Map();
  for (const [from, tos] of outEdges) {
    const valid = [...tos].filter((t) => rank.has(t));
    outCount.set(from, valid.length);
    for (const to of valid)
      inEdges.get(to).push(from);
  }
  for (let it = 0; it < iters; it++) {
    const next = /* @__PURE__ */ new Map();
    let dangling = 0;
    for (const node of nodes) {
      if ((outCount.get(node) || 0) === 0)
        dangling += rank.get(node);
    }
    for (const node of nodes) {
      let sum = 0;
      for (const from of inEdges.get(node)) {
        sum += rank.get(from) / (outCount.get(from) || 1);
      }
      next.set(node, (1 - damping) / n + damping * (sum + dangling / n));
    }
    for (const node of nodes)
      rank.set(node, next.get(node));
  }
  let max = 0;
  for (const v of rank.values())
    max = Math.max(max, v);
  if (max > 0)
    for (const node of nodes)
      rank.set(node, rank.get(node) / max);
  return rank;
}
function detectCommunities(nodes, adjacency) {
  const label = /* @__PURE__ */ new Map();
  nodes.forEach((node, i) => label.set(node, i));
  const order = [...nodes];
  for (let pass = 0; pass < 10; pass++) {
    let changed = false;
    for (const node of order) {
      const neighbors = adjacency.get(node);
      if (!neighbors || neighbors.size === 0)
        continue;
      const counts = /* @__PURE__ */ new Map();
      for (const nb of neighbors) {
        const l = label.get(nb);
        counts.set(l, (counts.get(l) || 0) + 1);
      }
      let best = label.get(node), bestCount = -1;
      for (const [l, c] of counts) {
        if (c > bestCount || c === bestCount && l < best) {
          best = l;
          bestCount = c;
        }
      }
      if (best !== label.get(node)) {
        label.set(node, best);
        changed = true;
      }
    }
    if (!changed)
      break;
  }
  return label;
}
async function detectStackAndEntrypoints(projectRoot, files) {
  const stack = /* @__PURE__ */ new Set();
  const entrypoints = /* @__PURE__ */ new Set();
  const rel = (abs) => relative(projectRoot, abs);
  const pkgPath = join4(projectRoot, "package.json");
  if (existsSync2(pkgPath)) {
    try {
      const pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
      stack.add("Node.js");
      const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
      for (const known of ["react", "next", "vue", "svelte", "express", "fastify", "typescript", "vite"]) {
        if (deps[known])
          stack.add(known === "next" ? "Next.js" : known[0].toUpperCase() + known.slice(1));
      }
      const addEntry = (p) => {
        if (!p)
          return;
        const abs = join4(projectRoot, p);
        const r = relative(projectRoot, abs);
        if (files.includes(abs))
          entrypoints.add(r);
      };
      addEntry(pkg.main);
      if (typeof pkg.bin === "string")
        addEntry(pkg.bin);
      else if (pkg.bin)
        for (const v of Object.values(pkg.bin))
          addEntry(v);
    } catch {
    }
  }
  const pyproject = join4(projectRoot, "pyproject.toml");
  const setupPy = join4(projectRoot, "setup.py");
  const requirements = join4(projectRoot, "requirements.txt");
  if (existsSync2(pyproject) || existsSync2(setupPy) || existsSync2(requirements)) {
    stack.add("Python");
    let reqText = "";
    for (const f of [pyproject, requirements]) {
      if (existsSync2(f)) {
        try {
          reqText += await readFile4(f, "utf8");
        } catch {
        }
      }
    }
    for (const known of ["pygame", "PySide6", "PyQt5", "PyQt6", "flask", "django", "fastapi", "numpy", "pandas", "torch", "tensorflow"]) {
      if (new RegExp(known, "i").test(reqText))
        stack.add(known);
    }
  }
  if (existsSync2(join4(projectRoot, "go.mod")))
    stack.add("Go");
  if (existsSync2(join4(projectRoot, "Cargo.toml")))
    stack.add("Rust");
  if (existsSync2(join4(projectRoot, "pom.xml")) || existsSync2(join4(projectRoot, "build.gradle")))
    stack.add("Java");
  for (const abs of files) {
    const r = rel(abs);
    const base = basename(r);
    if (base === "main.py" || base === "__main__.py")
      entrypoints.add(r);
    if (/^index\.(ts|tsx|js|jsx|mjs|cjs)$/.test(base) && dirname(r).split(sep).length <= 2)
      entrypoints.add(r);
    if (base === "main.go" && r.includes("cmd" + sep))
      entrypoints.add(r);
    if (base === "main.go" && !r.includes(sep))
      entrypoints.add(r);
    if (base === "main.rs" || base === "lib.rs")
      entrypoints.add(r);
  }
  return { stack: [...stack], entrypoints };
}
var SMELL_WEIGHT = {
  "todo": 3,
  "suppression": 5,
  "swallowed-catch": 10,
  "deep-nesting": 6,
  "long-function": 5,
  "magic-number": 3,
  "god-file": 14
};
function computeHeat(smells) {
  let sum = 0;
  for (const s of smells)
    sum += s.severity * SMELL_WEIGHT[s.kind];
  return Math.min(100, sum);
}
async function scanProject(projectRoot) {
  await initParser();
  const abs = [];
  await collectFiles(projectRoot, projectRoot, abs);
  const fileSet = new Set(abs.map((f) => relative(projectRoot, f)));
  const basenameIndex = /* @__PURE__ */ new Map();
  for (const rel of fileSet) {
    const b = basename(rel).slice(0, basename(rel).length - extname(rel).length);
    if (!basenameIndex.has(b))
      basenameIndex.set(b, []);
    basenameIndex.get(b).push(rel);
  }
  const { stack, entrypoints } = await detectStackAndEntrypoints(projectRoot, abs);
  const work = [];
  const graph = { nodes: {}, edges: [] };
  for (const file of abs) {
    const rel = relative(projectRoot, file);
    const ext = extname(file);
    const lang = EXT_LANG[ext];
    if (!lang)
      continue;
    let source;
    try {
      source = await readFile4(file, "utf8");
    } catch {
      continue;
    }
    if (/if\s+__name__\s*==\s*['"]__main__['"]/.test(source) || /^#![^\n]*\b(node|python\d?)\b/.test(source)) {
      entrypoints.add(rel);
    }
    const tree = await parseAs(lang, source);
    if (!tree)
      continue;
    const ast = analyzeAst(source, lang, tree);
    const importSpecs = extractImports(source, lang);
    graph.nodes[rel] = { imports: importSpecs };
    work.push({ abs: file, rel, lang, source, ast, importSpecs, pathDemote: pathDemoteReason(rel) });
  }
  const importedBy = /* @__PURE__ */ new Map();
  const importsResolved = /* @__PURE__ */ new Map();
  const fanOut = /* @__PURE__ */ new Map();
  for (const w of work) {
    importedBy.set(w.rel, /* @__PURE__ */ new Set());
    importsResolved.set(w.rel, /* @__PURE__ */ new Set());
  }
  for (const w of work) {
    const distinctModules = /* @__PURE__ */ new Set();
    for (const spec of w.importSpecs) {
      distinctModules.add(spec);
      const target = resolveImport(spec, w.abs, w.lang, projectRoot, fileSet, basenameIndex);
      if (target && target !== w.rel && importedBy.has(target)) {
        importedBy.get(target).add(w.rel);
        importsResolved.get(w.rel).add(target);
        graph.edges.push({ from: w.rel, to: target });
      }
    }
    fanOut.set(w.rel, distinctModules.size);
  }
  const isRealSource = /* @__PURE__ */ new Map();
  const demoteReason = /* @__PURE__ */ new Map();
  for (const w of work) {
    if (w.pathDemote) {
      isRealSource.set(w.rel, false);
      demoteReason.set(w.rel, w.pathDemote);
    } else {
      isRealSource.set(w.rel, true);
      demoteReason.set(w.rel, null);
    }
  }
  for (const w of work) {
    if (!isRealSource.get(w.rel))
      continue;
    if (entrypoints.has(w.rel))
      continue;
    const inbound = [...importedBy.get(w.rel)].filter((src) => isRealSource.get(src));
    if (inbound.length === 0) {
      isRealSource.set(w.rel, false);
      demoteReason.set(w.rel, "no inbound references from application code");
    }
  }
  const realNodes = work.filter((w) => isRealSource.get(w.rel)).map((w) => w.rel);
  const realSet = new Set(realNodes);
  const outEdges = /* @__PURE__ */ new Map();
  const undirected = /* @__PURE__ */ new Map();
  for (const node of realNodes) {
    outEdges.set(node, /* @__PURE__ */ new Set());
    undirected.set(node, /* @__PURE__ */ new Set());
  }
  for (const w of work) {
    if (!realSet.has(w.rel))
      continue;
    for (const target of importsResolved.get(w.rel)) {
      if (!realSet.has(target))
        continue;
      outEdges.get(w.rel).add(target);
      undirected.get(w.rel).add(target);
      undirected.get(target).add(w.rel);
    }
  }
  const ranks = pageRank(realNodes, outEdges);
  const communities = detectCommunities(realNodes, undirected);
  const analyses = [];
  const persisted = {};
  for (const w of work) {
    const real = isRealSource.get(w.rel);
    const fanIn = [...importedBy.get(w.rel)].filter((src) => isRealSource.get(src)).length;
    const centrality = real ? ranks.get(w.rel) || 0 : 0;
    const gravitySignals = {
      fanIn,
      fanOut: fanOut.get(w.rel) || 0,
      centrality,
      cyclomatic: w.ast.cyclomatic,
      publicSurface: w.ast.publicSurface,
      loc: w.ast.loc
    };
    let gravityRaw = centrality * 50 + Math.log2(fanIn + 1) * 8 + Math.log2(w.ast.cyclomatic + 1) * 4 + Math.log2(w.ast.publicSurface + 1) * 3;
    if (!real)
      gravityRaw *= 0.2;
    const gravity = Math.max(0, Math.min(100, gravityRaw));
    const heatSignals = {
      todos: w.ast.smells.filter((s) => s.kind === "todo").length,
      suppressions: w.ast.smells.filter((s) => s.kind === "suppression").length,
      swallowedCatches: w.ast.swallowedCatches,
      maxNesting: w.ast.maxNesting,
      longFunctions: w.ast.longFunctions,
      magicNumbers: w.ast.magicNumbers
    };
    const heat = real ? computeHeat(w.ast.smells) : 0;
    const pillarHint = real ? `community-${communities.get(w.rel)}` : null;
    const fa = {
      path: w.abs,
      relativePath: w.rel,
      language: w.lang,
      isRealSource: real,
      demoteReason: demoteReason.get(w.rel) || null,
      gravity,
      heat,
      gravitySignals,
      heatSignals,
      smells: w.ast.smells,
      pillarHint
    };
    analyses.push(fa);
    persisted[w.rel] = {
      relativePath: w.rel,
      language: w.lang,
      isRealSource: real,
      demoteReason: demoteReason.get(w.rel) || null,
      gravity,
      heat,
      gravitySignals,
      heatSignals,
      smells: w.ast.smells,
      pillarHint,
      importedBy: [...importedBy.get(w.rel)].filter((src) => isRealSource.get(src)),
      imports: [...importsResolved.get(w.rel)]
    };
  }
  const realAnalyses = analyses.filter((a) => a.isRealSource).sort((a, b) => b.gravity - a.gravity);
  const wildCandidates = realAnalyses.filter((a) => a.heat >= 60 || a.smells.some((s) => s.severity >= 4)).sort((a, b) => b.heat - a.heat);
  const pillars = buildPillars(realAnalyses, communities, stack);
  const topGravity = realAnalyses.slice(0, 12).map((a) => a.relativePath);
  const topHeat = wildCandidates.slice(0, 12).map((a) => a.relativePath);
  const map = {
    stack,
    entrypoints: [...entrypoints],
    pillars,
    fileCount: work.length,
    realSourceCount: realAnalyses.length,
    topGravity,
    topHeat,
    brief: null
  };
  await writeGraph(projectRoot, graph);
  await writeAnalysis(projectRoot, { files: persisted });
  const uiUrl = `file://${join4(projectRoot, ".vibe-splainer", "ui", "index.html")}`;
  return {
    projectRoot,
    totalFilesScanned: work.length,
    realSourceCount: realAnalyses.length,
    files: realAnalyses,
    map,
    wildCandidates,
    uiUrl,
    graph
  };
}
function buildPillars(real, communities, stack) {
  const groups = /* @__PURE__ */ new Map();
  for (const a of real) {
    const c = communities.get(a.relativePath);
    if (c === void 0)
      continue;
    if (!groups.has(c))
      groups.set(c, []);
    groups.get(c).push(a);
  }
  const sorted = [...groups.entries()].map(([id, files]) => ({ id, files, weight: files.reduce((s, f) => s + f.gravity, 0) })).filter((g) => g.files.length >= 2).sort((a, b) => b.weight - a.weight).slice(0, 6);
  const pillars = sorted.map((g, idx) => {
    const top = [...g.files].sort((a, b) => b.gravity - a.gravity);
    const name = pillarName(top, idx);
    return {
      name,
      description: `Graph cluster of ${g.files.length} files centered on ${basename(top[0].relativePath)}.`,
      memberFiles: top.map((f) => f.relativePath)
    };
  });
  const seen = /* @__PURE__ */ new Set();
  for (const p of pillars) {
    let n = p.name, i = 2;
    while (seen.has(n)) {
      n = `${p.name} ${i++}`;
    }
    p.name = n;
    seen.add(n);
  }
  if (pillars.length === 0 && real.length > 0) {
    pillars.push({ name: "Core", description: "Primary application code.", memberFiles: real.slice(0, 20).map((f) => f.relativePath) });
  }
  return pillars;
}
function pillarName(files, idx) {
  const dirs = files.map((f) => dirname(f.relativePath)).filter((d) => d && d !== ".");
  if (dirs.length) {
    const counts = /* @__PURE__ */ new Map();
    for (const d of dirs) {
      const seg = d.split(sep).pop();
      counts.set(seg, (counts.get(seg) || 0) + 1);
    }
    const top = [...counts.entries()].sort((a, b) => b[1] - a[1])[0];
    if (top)
      return titleCase(top[0]);
  }
  return `Cluster ${idx + 1}`;
}
function titleCase(s) {
  return s.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
}
async function getFileAnalysis(absPath) {
  const ext = extname(absPath);
  const lang = EXT_LANG[ext];
  if (!lang)
    return null;
  let source;
  try {
    source = await readFile4(absPath, "utf8");
  } catch {
    return null;
  }
  const tree = await parseAs(lang, source);
  if (!tree)
    return null;
  const ast = analyzeAst(source, lang, tree);
  const lines = source.split("\n");
  const smellSpans = ast.smells.map((s) => {
    const start = Math.max(0, s.line - 1 - 3);
    const end = Math.min(lines.length, s.endLine + 3);
    return {
      startLine: start + 1,
      endLine: end,
      snippet: lines.slice(start, end).join("\n").slice(0, 1200),
      reason: `${s.kind}: ${s.note}`
    };
  });
  const heatSignals = {
    todos: ast.smells.filter((s) => s.kind === "todo").length,
    suppressions: ast.smells.filter((s) => s.kind === "suppression").length,
    swallowedCatches: ast.swallowedCatches,
    maxNesting: ast.maxNesting,
    longFunctions: ast.longFunctions,
    magicNumbers: ast.magicNumbers
  };
  return {
    language: lang,
    signature: ast.signature,
    hotSpans: ast.hotSpans,
    smellSpans,
    heatSignals,
    loc: ast.loc,
    cyclomatic: ast.cyclomatic
  };
}

// ../brain/dist/dossier.js
import { Mutex } from "async-mutex";
import { join as join5, dirname as dirname2 } from "path";
import { fileURLToPath as fileURLToPath2 } from "url";
import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
import { existsSync as existsSync3, cpSync } from "fs";
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
var dossierMutex = new Mutex();
async function readDossier(projectRoot) {
  const dossierPath = join5(projectRoot, ".vibe-splainer", "dossier.json");
  try {
    const raw = await readFile5(dossierPath, "utf8");
    return JSON.parse(raw);
  } catch {
    return null;
  }
}
async function writeDossier(projectRoot, dossier) {
  await dossierMutex.runExclusive(async () => {
    const dir = join5(projectRoot, ".vibe-splainer");
    await mkdir3(dir, { recursive: true });
    const dossierPath = join5(dir, "dossier.json");
    const tmp = dossierPath + ".tmp";
    await writeFile4(tmp, JSON.stringify(dossier, null, 2), "utf8");
    const { rename } = await import("fs/promises");
    await rename(tmp, dossierPath);
    await regenerateUI(projectRoot, dossier);
  });
}
async function regenerateUI(projectRoot, dossier) {
  const uiDir = join5(projectRoot, ".vibe-splainer", "ui");
  await mkdir3(uiDir, { recursive: true });
  let templateDir = join5(__dirname2, "ui");
  if (!existsSync3(templateDir)) {
    templateDir = join5(__dirname2, "../../cli/dist/ui");
  }
  if (!existsSync3(templateDir)) {
    console.error("[vibe-splain] UI template not found at", templateDir, "- skipping UI regeneration");
    return;
  }
  cpSync(templateDir, uiDir, { recursive: true });
  let html = await readFile5(join5(templateDir, "index.html"), "utf8");
  const injection = `<script>window.__VIBE_DOSSIER__ = ${JSON.stringify(dossier)};</script>`;
  html = html.replace("<!-- VIBE_DOSSIER_INJECTION_POINT -->", injection);
  await writeFile4(join5(uiDir, "index.html"), html, "utf8");
  console.error("[vibe-splain] UI regenerated at", join5(uiDir, "index.html"));
}
function validateMermaidNodeCount(diagram) {
  if (!diagram)
    return true;
  const nodePattern = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*[\[({|>]/gm;
  const statePattern = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*:/gm;
  const nodes = /* @__PURE__ */ new Set();
  for (const match of diagram.matchAll(nodePattern))
    nodes.add(match[1]);
  for (const match of diagram.matchAll(statePattern))
    nodes.add(match[1]);
  if (diagram.includes("[*]"))
    nodes.add("[*]");
  return nodes.size <= 7;
}

// ../brain/dist/watcher.js
import chokidar from "chokidar";
import { createHash } from "crypto";
import { readFile as readFile6 } from "fs/promises";
function startWatcher(projectRoot, watchedPaths) {
  const watcher = chokidar.watch(watchedPaths.length > 0 ? watchedPaths : projectRoot, {
    ignoreInitial: true,
    ignored: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.vibe-splainer/**"],
    persistent: true
  });
  watcher.on("change", async (filepath) => {
    try {
      const dossier = await readDossier(projectRoot);
      if (!dossier)
        return;
      const content = await readFile6(filepath, "utf8");
      const newHash = createHash("sha256").update(content).digest("hex");
      let mutated = false;
      for (const pillar of dossier.pillars) {
        for (const card of pillar.decisions) {
          if (card.evidence.some((e) => e.file === filepath || filepath.endsWith(e.file))) {
            if (card.lastScannedHash !== newHash) {
              card.status = "stale";
              if (!dossier.stalePaths.includes(filepath))
                dossier.stalePaths.push(filepath);
              mutated = true;
            }
          }
        }
      }
      if (mutated)
        await writeDossier(projectRoot, dossier);
    } catch (err) {
      console.error("[vibe-splain] Watcher error:", err);
    }
  });
  console.error("[vibe-splain] File watcher started");
}

// dist/mcp/tools/scan_project.js
var scanProjectTool = {
  name: "scan_project",
  description: "Scans a codebase (TS/JS/Python/Go/Rust/Java) and returns a structural analysis. CALL THIS FIRST, then call get_project_map. Files are scored on two axes: GRAVITY (importance \u2014 fan-in + PageRank centrality) and HEAT (smell/tech-debt). Mockups, vendored code, and orphan files are demoted (isRealSource:false) so cards target the real application. After scanning, call get_project_map to get the fixed pillar set, Start-Here (top gravity) and Wild-Discovery (top heat) lists. The uiUrl is a file:// link \u2014 share it with the user.",
  inputSchema: {
    type: "object",
    properties: {
      projectRoot: {
        type: "string",
        description: "Absolute path to the project root directory to scan"
      }
    },
    required: ["projectRoot"]
  }
};
async function handleScanProject(args) {
  const projectRoot = args.projectRoot;
  if (!projectRoot)
    throw new Error("projectRoot is required");
  console.error(`[vibe-splain] Scanning project: ${projectRoot}`);
  const result = await scanProject(projectRoot);
  const existing = await readDossier(projectRoot);
  const brief = existing?.map?.brief ?? null;
  const dossier = {
    version: "2.0.0",
    scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
    projectRoot,
    map: { ...result.map, brief },
    pillars: existing?.pillars ?? [],
    wildDiscoveries: existing?.wildDiscoveries ?? [],
    stalePaths: existing?.stalePaths ?? []
  };
  for (const def of result.map.pillars) {
    if (!dossier.pillars.find((p) => p.name === def.name)) {
      dossier.pillars.push({ name: def.name, cardCount: 0, decisions: [] });
    }
  }
  await writeDossier(projectRoot, dossier);
  startWatcher(projectRoot, result.files.map((f) => f.path));
  console.error(`[vibe-splain] Scan complete. ${result.totalFilesScanned} files, ${result.realSourceCount} real-source, ${result.wildCandidates.length} wild candidates.`);
  return {
    projectRoot: result.projectRoot,
    totalFilesScanned: result.totalFilesScanned,
    realSourceCount: result.realSourceCount,
    stack: result.map.stack,
    entrypoints: result.map.entrypoints,
    pillars: result.map.pillars.map((p) => ({ name: p.name, fileCount: p.memberFiles.length })),
    startHere: result.map.topGravity,
    wildDiscoveryCandidates: result.wildCandidates.map((f) => ({
      relativePath: f.relativePath,
      heat: Math.round(f.heat),
      gravity: Math.round(f.gravity),
      topSmells: f.smells.filter((s) => s.severity >= 3).slice(0, 3).map((s) => s.note)
    })),
    nextStep: "Call get_project_map, write a project brief via set_project_brief, THEN write cards starting from the Start-Here files.",
    uiUrl: result.uiUrl
  };
}

// dist/mcp/tools/get_project_map.js
var getProjectMapTool = {
  name: "get_project_map",
  description: "Returns the project map produced by scan_project: the detected stack, entrypoints, the FIXED set of architectural pillars (you may NOT invent others \u2014 write_decision_card rejects unknown pillars), the Start-Here files (highest gravity = most depended-upon), and the Wild-Discovery candidates (highest heat = most tech debt). BEFORE writing any card you MUST: read this map, write a 3-5 sentence project brief, and persist it via set_project_brief.",
  inputSchema: {
    type: "object",
    properties: {
      projectRoot: { type: "string", description: "Absolute path to the project root" }
    },
    required: ["projectRoot"]
  }
};
async function handleGetProjectMap(args) {
  const projectRoot = args.projectRoot;
  if (!projectRoot)
    throw new Error("projectRoot is required");
  const dossier = await readDossier(projectRoot);
  if (!dossier || !dossier.map) {
    return { error: "No project map found. Run scan_project first." };
  }
  const m = dossier.map;
  return {
    stack: m.stack,
    entrypoints: m.entrypoints,
    fileCount: m.fileCount,
    realSourceCount: m.realSourceCount,
    pillars: m.pillars.map((p) => ({
      name: p.name,
      description: p.description,
      memberFiles: p.memberFiles
    })),
    legalPillarNames: m.pillars.map((p) => p.name),
    startHere: m.topGravity,
    wildDiscoveryCandidates: m.topHeat,
    brief: m.brief,
    nextStep: m.brief ? "Brief is set. Work the Start-Here files first via get_file_context, then write_decision_card." : "Write a 3-5 sentence brief and call set_project_brief BEFORE any card."
  };
}

// dist/mcp/tools/set_project_brief.js
var setProjectBriefTool = {
  name: "set_project_brief",
  description: "Persists your 3-5 sentence project brief into the dossier (and regenerates the UI). Call this AFTER get_project_map and BEFORE writing any decision card. The brief must say: what this project IS, the real stack, and \u2014 critically \u2014 which files are the actual application vs. mockups/generated/vendored noise.",
  inputSchema: {
    type: "object",
    properties: {
      projectRoot: { type: "string", description: "Absolute path to the project root" },
      brief: { type: "string", description: "3-5 sentence project brief. What is this, the real stack, app vs. noise." }
    },
    required: ["projectRoot", "brief"]
  }
};
async function handleSetProjectBrief(args) {
  const projectRoot = args.projectRoot;
  const brief = args.brief;
  if (!projectRoot || !brief)
    throw new Error("projectRoot and brief are required");
  const dossier = await readDossier(projectRoot);
  if (!dossier || !dossier.map) {
    return { error: "No project map found. Run scan_project first." };
  }
  dossier.map.brief = brief;
  await writeDossier(projectRoot, dossier);
  const documented = new Set([...dossier.pillars.flatMap((p) => p.decisions), ...dossier.wildDiscoveries].map((c) => c.primaryFile).filter(Boolean));
  const startHere = dossier.map.topGravity.filter((f) => !documented.has(f));
  const wild = dossier.map.topHeat.filter((f) => !documented.has(f));
  const worklist = [.../* @__PURE__ */ new Set([...startHere, ...wild])];
  return {
    success: true,
    brief,
    remainingFiles: worklist,
    legalPillarNames: dossier.map.pillars.map((p) => p.name),
    nextStep: worklist.length === 0 ? "All files documented. Share the file:// UI link from scan_project." : `Brief saved. DO NOT STOP and DO NOT ask the user what to do next. Now loop: for EACH of the ${worklist.length} files in remainingFiles, call get_file_context then write_decision_card. Start with "${worklist[0]}". Continue until every file has a card, then share the file:// UI link.`
  };
}

// dist/mcp/tools/get_file_context.js
import { readFile as readFile7 } from "fs/promises";
import { join as join6, relative as relative2, isAbsolute } from "path";
var getFileContextTool = {
  name: "get_file_context",
  description: "Returns PRE-EXTRACTED evidence for a file so you do not have to read the whole thing and paraphrase its header comment. Returns: gravity/heat scores + signals, importedBy (named fan-in \u2014 use this for blastRadius), hotSpans (the gnarliest function bodies, comment-stripped, each with a reason), smellSpans (located tech debt with \xB13 lines of context), and signature (the exported API surface). Base your evidence on hotSpans/smellSpans \u2014 NEVER on header comments. Pass { full: true } only if you truly need the raw source.",
  inputSchema: {
    type: "object",
    properties: {
      projectRoot: { type: "string", description: "Absolute path to the project root" },
      filePath: { type: "string", description: "Relative or absolute path to the file" },
      full: { type: "boolean", description: "Set true to also return the raw source. Default false." }
    },
    required: ["projectRoot", "filePath"]
  }
};
async function handleGetFileContext(args) {
  const projectRoot = args.projectRoot;
  const filePath = args.filePath;
  const full = args.full === true;
  if (!projectRoot || !filePath)
    throw new Error("projectRoot and filePath are required");
  const fullPath = isAbsolute(filePath) ? filePath : join6(projectRoot, filePath);
  const relPath = relative2(projectRoot, fullPath);
  const evidence = await getFileAnalysis(fullPath);
  if (!evidence) {
    throw new Error(`Could not analyze ${relPath} (unsupported language or parse failure).`);
  }
  const store = await readAnalysis(projectRoot);
  const persisted = store?.files[relPath];
  const result = {
    filePath: relPath,
    language: evidence.language,
    gravity: persisted ? Math.round(persisted.gravity) : null,
    heat: persisted ? Math.round(persisted.heat) : null,
    isRealSource: persisted?.isRealSource ?? null,
    demoteReason: persisted?.demoteReason ?? null,
    gravitySignals: persisted?.gravitySignals ?? null,
    heatSignals: evidence.heatSignals,
    importedBy: persisted?.importedBy ?? [],
    imports: persisted?.imports ?? [],
    signature: evidence.signature,
    hotSpans: evidence.hotSpans,
    smellSpans: evidence.smellSpans
  };
  if (full) {
    result.source = await readFile7(fullPath, "utf8");
  }
  return result;
}

// dist/mcp/tools/write_decision_card.js
import { v4 as uuidv4 } from "uuid";
import { createHash as createHash2 } from "crypto";
import { readFile as readFile8 } from "fs/promises";
import { join as join7 } from "path";
var CATEGORIES = ["Bottleneck", "Hack", "Smart-Move", "Risk", "Convention", "Dead-Weight"];
var writeDecisionCardTool = {
  name: "write_decision_card",
  description: "Persists ONE Decision Card about ONE file. This is a hostile architecture review, not documentation. The thesis must be a VERDICT, not a description. The pillar MUST be one of the names from get_project_map (free-form is rejected). One card per file (duplicates rejected). Evidence must come from get_file_context hotSpans/smellSpans \u2014 never the header comment, never the whole file.",
  inputSchema: {
    type: "object",
    properties: {
      projectRoot: { type: "string" },
      pillar: { type: "string", description: "MUST be one of the pillar names from get_project_map. Free-form values are rejected." },
      primaryFile: { type: "string", description: "The single file this card is about (relative path). Used to reject duplicate cards." },
      title: { type: "string" },
      thesis: { type: "string", description: "ONE sharp sentence. A verdict, not a description. Take a position. Bad: 'This file implements a panel system.' Good: 'A 600-line god-component that owns drag, zoom, persistence AND the host bridge \u2014 the single highest-risk refactor in the app.'" },
      category: { type: "string", enum: CATEGORIES },
      severity: { type: "integer", minimum: 1, maximum: 5 },
      narrative: { type: "string", description: "3-5 sentences. WHY it exists and WHY it's built this way. Do NOT restate the file's header comments." },
      tradeoff: { type: "string", description: "What was given up, or why the obvious approach was rejected. Null only if genuinely none." },
      blastRadius: { type: "string", description: "What breaks if this changes. Ground it in the fan-in (importedBy) from get_file_context." },
      confidence: { type: "string", enum: ["low", "medium", "high"] },
      evidence: {
        type: "array",
        items: {
          type: "object",
          properties: {
            file: { type: "string" },
            startLine: { type: "number" },
            endLine: { type: "number" },
            snippet: { type: "string" }
          },
          required: ["file", "startLine", "endLine", "snippet"]
        },
        description: "Use hotSpans/smellSpans from get_file_context. NEVER cite header comments or the whole file."
      },
      diagram: { type: "string", description: "Optional. stateDiagram-v2 / flowchart TD / linear. Max 7 nodes." }
    },
    required: ["projectRoot", "pillar", "primaryFile", "title", "thesis", "category", "severity", "narrative", "confidence", "evidence"]
  }
};
async function handleWriteDecisionCard(args) {
  const projectRoot = args.projectRoot;
  const pillar = args.pillar;
  const primaryFile = args.primaryFile;
  const title = args.title;
  const thesis = args.thesis;
  const category = args.category;
  const severity = args.severity;
  const narrative = args.narrative;
  const tradeoff = args.tradeoff || null;
  const blastRadius = args.blastRadius || null;
  const confidence = args.confidence || "medium";
  const evidence = args.evidence;
  const diagram = args.diagram || null;
  if (!projectRoot || !pillar || !primaryFile || !title || !thesis || !category || !narrative || !evidence) {
    throw new Error("projectRoot, pillar, primaryFile, title, thesis, category, narrative, and evidence are required");
  }
  if (!CATEGORIES.includes(category)) {
    throw new Error(`Invalid category "${category}". Must be one of: ${CATEGORIES.join(", ")}`);
  }
  if (diagram && !validateMermaidNodeCount(diagram)) {
    throw new Error("Mermaid diagram exceeds maximum of 7 nodes. Simplify the diagram.");
  }
  const dossier = await readDossier(projectRoot);
  if (!dossier || !dossier.map) {
    throw new Error("No project map found. Run scan_project and set_project_brief before writing cards.");
  }
  const legalPillars = dossier.map.pillars.map((p) => p.name);
  if (!legalPillars.includes(pillar)) {
    throw new Error(`Pillar "${pillar}" is not a legal pillar. Use one of: ${legalPillars.join(", ")}. Pillars are fixed by the scan \u2014 you may not invent new ones.`);
  }
  const existing = [...dossier.pillars.flatMap((p) => p.decisions), ...dossier.wildDiscoveries].find((c) => c.primaryFile === primaryFile);
  if (existing) {
    if (existing.status === "fresh") {
      throw new Error(`A card already exists for "${primaryFile}". One card per file. To revise it, call mark_stale on this file and rewrite, or pick a different file.`);
    }
    for (const p of dossier.pillars)
      p.decisions = p.decisions.filter((c) => c.id !== existing.id);
    dossier.wildDiscoveries = dossier.wildDiscoveries.filter((c) => c.id !== existing.id);
  }
  const store = await readAnalysis(projectRoot);
  const persisted = store?.files[primaryFile];
  const gravity = persisted ? Math.round(persisted.gravity) : void 0;
  const heat = persisted ? Math.round(persisted.heat) : void 0;
  let combinedContent = "";
  for (const e of evidence) {
    try {
      combinedContent += await readFile8(join7(projectRoot, e.file), "utf8");
    } catch {
      combinedContent += e.snippet;
    }
  }
  const hash = createHash2("sha256").update(combinedContent).digest("hex");
  const card = {
    id: uuidv4(),
    pillar,
    title,
    thesis,
    category,
    severity,
    narrative,
    tradeoff,
    blastRadius,
    confidence,
    evidence,
    diagram,
    gravity,
    heat,
    primaryFile,
    status: "fresh",
    lastScannedHash: hash
  };
  const isWild = severity >= 4 || heat !== void 0 && heat >= 60;
  if (isWild) {
    dossier.wildDiscoveries.push(card);
  }
  let bucket = dossier.pillars.find((p) => p.name === pillar);
  if (!bucket) {
    bucket = { name: pillar, cardCount: 0, decisions: [] };
    dossier.pillars.push(bucket);
  }
  bucket.decisions.push(card);
  bucket.cardCount = bucket.decisions.length;
  await writeDossier(projectRoot, dossier);
  console.error(`[vibe-splain] Card written: "${title}" [${category} sev${severity}] in "${pillar}"${isWild ? " (Wild Discovery)" : ""}`);
  const documented = new Set([...dossier.pillars.flatMap((p) => p.decisions), ...dossier.wildDiscoveries].map((c) => c.primaryFile).filter(Boolean));
  const remaining = [.../* @__PURE__ */ new Set([...dossier.map.topGravity, ...dossier.map.topHeat])].filter((f) => !documented.has(f));
  return {
    success: true,
    cardId: card.id,
    pillar,
    primaryFile,
    category,
    severity,
    wildDiscovery: isWild,
    gravity,
    heat,
    remainingFiles: remaining,
    nextStep: remaining.length === 0 ? "Every Start-Here and Wild-Discovery file now has a card. Share the file:// UI link from scan_project. Done." : `Card saved. DO NOT STOP. ${remaining.length} files left. Next: call get_file_context then write_decision_card for "${remaining[0]}".`
  };
}

// dist/mcp/tools/get_strategic_overview.js
var getStrategicOverviewTool = {
  name: "get_strategic_overview",
  description: "Returns the current state of the dossier without evidence snippets (to save tokens). Use this to see what has already been analyzed and what is stale. Check stalePaths to know which files need re-analysis.",
  inputSchema: {
    type: "object",
    properties: {
      projectRoot: {
        type: "string",
        description: "Absolute path to the project root"
      }
    },
    required: ["projectRoot"]
  }
};
async function handleGetStrategicOverview(args) {
  const projectRoot = args.projectRoot;
  if (!projectRoot)
    throw new Error("projectRoot is required");
  const dossier = await readDossier(projectRoot);
  if (!dossier) {
    return { error: "No dossier found. Run scan_project first." };
  }
  return {
    version: dossier.version,
    scannedAt: dossier.scannedAt,
    projectRoot: dossier.projectRoot,
    pillars: dossier.pillars.map((p) => ({
      name: p.name,
      cardCount: p.cardCount,
      decisions: p.decisions.map((d) => ({
        id: d.id,
        title: d.title,
        status: d.status,
        pillar: d.pillar,
        // Omit evidence snippets to save tokens
        evidenceFileCount: d.evidence.length,
        hasDiagram: !!d.diagram
      }))
    })),
    wildDiscoveriesCount: dossier.wildDiscoveries.length,
    stalePaths: dossier.stalePaths
  };
}

// dist/mcp/tools/inspect_pillar.js
var inspectPillarTool = {
  name: "inspect_pillar",
  description: "Returns all Decision Cards for a specific pillar including full evidence snippets. Use when you need deep detail on a specific area of the codebase.",
  inputSchema: {
    type: "object",
    properties: {
      projectRoot: {
        type: "string",
        description: "Absolute path to the project root"
      },
      pillarName: {
        type: "string",
        description: "Name of the pillar to inspect"
      }
    },
    required: ["projectRoot", "pillarName"]
  }
};
async function handleInspectPillar(args) {
  const projectRoot = args.projectRoot;
  const pillarName2 = args.pillarName;
  if (!projectRoot || !pillarName2)
    throw new Error("projectRoot and pillarName are required");
  const dossier = await readDossier(projectRoot);
  if (!dossier) {
    return { error: "No dossier found. Run scan_project first." };
  }
  const pillar = dossier.pillars.find((p) => p.name === pillarName2);
  if (!pillar) {
    return {
      error: `Pillar "${pillarName2}" not found. Available pillars: ${dossier.pillars.map((p) => p.name).join(", ")}`
    };
  }
  return pillar;
}

// dist/mcp/tools/get_wild_discoveries.js
var getWildDiscoveriesTool = {
  name: "get_wild_discoveries",
  description: "Returns files with extremely high cognitive complexity (weight \u2265 25) that don't fit standard patterns. These are the most surprising and important parts of the codebase to understand.",
  inputSchema: {
    type: "object",
    properties: {
      projectRoot: {
        type: "string",
        description: "Absolute path to the project root"
      }
    },
    required: ["projectRoot"]
  }
};
async function handleGetWildDiscoveries(args) {
  const projectRoot = args.projectRoot;
  if (!projectRoot)
    throw new Error("projectRoot is required");
  const dossier = await readDossier(projectRoot);
  if (!dossier) {
    return { error: "No dossier found. Run scan_project first." };
  }
  return {
    wildDiscoveries: dossier.wildDiscoveries,
    count: dossier.wildDiscoveries.length
  };
}

// dist/mcp/tools/mark_stale.js
var markStaleTool = {
  name: "mark_stale",
  description: "Manually marks Decision Cards as stale when you detect a file has changed. The file watcher does this automatically, but call this if you modify a file yourself during a session.",
  inputSchema: {
    type: "object",
    properties: {
      projectRoot: {
        type: "string",
        description: "Absolute path to the project root"
      },
      filePaths: {
        type: "array",
        items: { type: "string" },
        description: "Array of relative file paths to mark as stale"
      }
    },
    required: ["projectRoot", "filePaths"]
  }
};
async function handleMarkStale(args) {
  const projectRoot = args.projectRoot;
  const filePaths = args.filePaths;
  if (!projectRoot || !filePaths)
    throw new Error("projectRoot and filePaths are required");
  const dossier = await readDossier(projectRoot);
  if (!dossier) {
    return { error: "No dossier found. Run scan_project first." };
  }
  let staleCount = 0;
  for (const filePath of filePaths) {
    const matches = (card) => card.primaryFile === filePath || filePath.endsWith(card.primaryFile || "\0") || card.evidence.some((e) => e.file === filePath || filePath.endsWith(e.file));
    for (const pillar of dossier.pillars) {
      for (const card of pillar.decisions) {
        if (matches(card)) {
          card.status = "stale";
          staleCount++;
        }
      }
    }
    for (const card of dossier.wildDiscoveries) {
      if (matches(card))
        card.status = "stale";
    }
    if (!dossier.stalePaths.includes(filePath)) {
      dossier.stalePaths.push(filePath);
    }
  }
  await writeDossier(projectRoot, dossier);
  return {
    success: true,
    staleCardsMarked: staleCount,
    totalStalePaths: dossier.stalePaths.length
  };
}

// dist/mcp/server.js
var ALL_TOOLS = [
  scanProjectTool,
  getProjectMapTool,
  setProjectBriefTool,
  getFileContextTool,
  writeDecisionCardTool,
  getStrategicOverviewTool,
  inspectPillarTool,
  getWildDiscoveriesTool,
  markStaleTool
];
var TOOL_HANDLERS = {
  scan_project: handleScanProject,
  get_project_map: handleGetProjectMap,
  set_project_brief: handleSetProjectBrief,
  get_file_context: handleGetFileContext,
  write_decision_card: handleWriteDecisionCard,
  get_strategic_overview: handleGetStrategicOverview,
  inspect_pillar: handleInspectPillar,
  get_wild_discoveries: handleGetWildDiscoveries,
  mark_stale: handleMarkStale
};
async function startMCPServer() {
  await initParser();
  console.error("[vibe-splain] Tree-Sitter parser initialized");
  const server = new Server({ name: "vibe-splain", version: "2.0.0" }, { capabilities: { tools: {}, prompts: {} } });
  server.setRequestHandler(ListPromptsRequestSchema, async () => ({
    prompts: [
      {
        name: "build_dossier",
        description: "Build a full architectural dossier using vibe-splain (replaces the need to copy-paste the README prompt)"
      }
    ]
  }));
  server.setRequestHandler(GetPromptRequestSchema, async (request) => {
    if (request.params.name !== "build_dossier") {
      throw new Error(`Unknown prompt: ${request.params.name}`);
    }
    return {
      description: "Build a full architectural dossier",
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: `You are a skeptical staff engineer doing a HOSTILE architecture review of this codebase.
You are NOT writing documentation. You are finding the load-bearing walls, the landmines,
and the clever moves, and you are taking positions on them.

PROCESS \u2014 follow in order:
1. Call scan_project, then get_project_map. The map gives you: the detected stack,
   the FIXED set of pillars (you may not invent others), the Start-Here files (highest
   gravity = most depended-upon), and Wild-Discovery candidates (highest heat = most smell).
2. Read the map's stack and entrypoints. Write a 3-5 sentence project brief: what IS this,
   what's the real stack, and \u2014 critically \u2014 which files are the actual application vs.
   mockups/generated/vendored noise. Pass it via set_project_brief. Do this BEFORE any card.
3. Work the Start-Here files first (highest gravity), then the Wild-Discovery files.
   For each, call get_file_context. It returns hotSpans (the gnarliest functions) and
   smellSpans (located tech debt) \u2014 base your evidence on THOSE, never on header comments.
4. Write one decision card per file via write_decision_card.

This is an AUTONOMOUS loop. Every tool response includes a \`nextStep\` and often a
\`remainingFiles\` list \u2014 OBEY them. Do NOT stop, summarize, or ask the user "how would
you like to proceed" until every Start-Here and Wild-Discovery file has a card. Writing
the brief is the START of the work, not the end. Keep calling get_file_context +
write_decision_card until remainingFiles is empty.

RULES FOR EVERY CARD \u2014 non-negotiable:
- The \`thesis\` is a VERDICT in one sentence. Take a position. If you can't, you don't
  understand the file yet \u2014 read more.
- Pick a \`category\`: Bottleneck, Hack, Smart-Move, Risk, Convention, or Dead-Weight.
- \`blastRadius\` must reference the real fan-in (get_file_context.importedBy).
- NEVER paraphrase the file's own comments. If the insight is already in a // block,
  it is not insight \u2014 go deeper into the logic.
- Evidence = 5-20 lines of the ACTUAL interesting code (hotSpans/smellSpans). Never the
  whole file, never the doc-header.
- For every Wild-Discovery candidate, name the specific smell and rate its severity.

\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
EXAMPLE \u2014 what GOOD vs BAD looks like:

BAD (rejected \u2014 this is a book report):
  title: "Panel Component Framework"
  narrative: "This module establishes the structural framework for the panel-based
  interface. It defines the generic Panel shell that standardizes look and feel..."
  \u2192 Restates the header comment. No position. No risk. No tradeoff. Worthless.

GOOD (accepted):
  title: "Panel shell carries 14 props and 6 tools in one file"
  thesis: "cipher-panels-a.jsx is a god-file: one 600-line module owns the shared shell
           AND three unrelated generators, so any panel change risks all of them."
  category: "Risk"  severity: 4
  narrative: "Panel was built as a single shell to guarantee visual consistency, but the
              three generators (Palette/Vibe/Pocket) were folded in beside it instead of
              split out. The shell threads 14 props through every tool, so the generators
              are now coupled to the shell's drag/compact state they don't use."
  tradeoff: "Bought consistency and one import site; paid with a module no one can change
             safely and props that leak shell concerns into pure generators."
  blastRadius: "Imported by cipher-shell.jsx (the app root) \u2014 a regression here is a
                full-app regression."
  evidence: [ the 14-param Panel signature; the prop-drill into PalettePanel ]
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500

When done, share the exact file:// UI link returned by scan_project. Never invent a URL.`
          }
        }
      ]
    };
  });
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
    tools: ALL_TOOLS
  }));
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const { name, arguments: args } = request.params;
    const handler = TOOL_HANDLERS[name];
    if (!handler) {
      return {
        content: [{ type: "text", text: `Unknown tool: ${name}` }],
        isError: true
      };
    }
    try {
      const result = await handler(args || {});
      return {
        content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
      };
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      console.error(`[vibe-splain] Tool ${name} error:`, message);
      return {
        content: [{ type: "text", text: `Error: ${message}` }],
        isError: true
      };
    }
  });
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("[vibe-splain] MCP server running on stdio");
}

// dist/commands/serve.js
async function serveCommand() {
  console.error("[vibe-splain] Starting MCP server...");
  await startMCPServer();
}

// dist/index.js
var program = new Command();
program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("1.0.0");
program.command("install").description("Patch coding agent MCP config files to register vibe-splain").action(installCommand);
program.command("serve").description("Start the MCP server (called by the coding agent, not by you)").action(serveCommand);
program.parse();
