#!/usr/bin/env node

import { resolve, basename } from "node:path";

// sverklo requires Node 24+. Node 22 LTS has node:sqlite behind the
// --experimental-sqlite flag and ships SQLite without FTS5 (which we
// use for code search). Node 24 unflagged node:sqlite and includes
// FTS5 in the bundled SQLite. npm enforces `engines.node` at install
// time too, but this guard gives a clearer message at runtime in case
// the package was installed without engine-strict.
{
  const major = Number(process.versions.node.split(".")[0]);
  if (major < 24) {
    console.error(
      `sverklo requires Node 24 or newer (you're on ${process.versions.node}).\n` +
        `node:sqlite is flagged on Node 22 LTS and ships without FTS5.\n` +
        `Run: nvm install 24 && nvm use 24`,
    );
    process.exit(1);
  }
}

const args = process.argv.slice(2);
const command = args[0];

/**
 * Resolve a project path from a subcommand's flag list. If the first
 * non-flag arg is set, treat it as the project path; otherwise fall
 * back to cwd. Errors and exits when the resolved path doesn't exist.
 * Use this in any subcommand that historically hard-coded process.cwd().
 */
async function resolveProjectPath(flags: string[]): Promise<string> {
  const { existsSync, statSync } = await import("node:fs");
  const positional = flags.find((a) => !a.startsWith("-"));
  const target = resolve(positional ?? process.cwd());
  if (!existsSync(target)) {
    console.error(`\n✗ project path not found: ${target}\n`);
    process.exit(2);
  }
  if (!statSync(target).isDirectory()) {
    console.error(`\n✗ project path is not a directory: ${target}\n`);
    process.exit(2);
  }
  return target;
}

// Global --help / -h interceptor.
//
// Without this, `--help` falls through to whatever subcommand the user
// typed. That used to be catastrophic: `sverklo wiki --help` wrote 61
// markdown files into the user's repo, `sverklo init --help` rewrote
// `~/.gemini/antigravity/mcp_config.json`, `sverklo register --help`
// registered the literal string "--help" as a repo at /private/tmp/--help.
// Catching --help/-h here, BEFORE any subcommand's destructive setup
// runs, makes the gesture safe.
if (command && command !== "--help" && command !== "-h") {
  const wantsHelp = args.slice(1).some((a) => a === "--help" || a === "-h");
  if (wantsHelp) {
    const HELP_BLURBS: Record<string, string> = {
      init: "Set up sverklo in your project (.mcp.json + CLAUDE.md, auto-detects Claude Code/Cursor/Windsurf/Antigravity).",
      doctor: "Diagnose MCP setup issues. Run after `init` to verify the agent can reach sverklo.",
      audit: "Run codebase audit and emit a graded report. Flags: --format markdown|html|json|graph|arch|obsidian, --output PATH, --open, --badge, --publish.",
      "audit-diff": "Incremental architectural quality gate. Audits `git diff` for new cycles + fan-in spikes. Flags: --against REF, --fan-in-threshold N, --format human|json, --show-existing, --verbose. Exits 1 on regression.",
      review: "Risk-scored diff review (CI-friendly). Flags: --ref REF, --ci, --format markdown|json, --max-files N, --fail-on low|medium|high.",
      wiki: "Generate a markdown wiki from the indexed codebase. Flags: --output DIR (default ./sverklo-wiki), --format markdown|html.",
      "concept-index": "Label clusters with an LLM (requires Ollama). Flags: --model NAME, --base-url URL, --force, --max N.",
      "enrich-symbols": "Add LLM-generated purpose to top-PageRank symbols (requires Ollama). Flags: --top N, --model NAME, --base-url URL, --force.",
      "enrich-patterns": "Tag top-PageRank symbols with design patterns (requires Ollama). Flags: --top N, --model NAME, --base-url URL, --min-conf X, --force.",
      register: "Add a directory to the global registry. Usage: sverklo register [path] (defaults to cwd).",
      unregister: "Remove a repo from the global registry. Usage: sverklo unregister <name>.",
      list: "List all registered repositories.",
      workspace: "Manage cross-repo workspaces. Subcommands: create, list, index, add, remove.",
      ui: "Open the web dashboard. Usage: sverklo ui [project-path].",
      dashboard: "Alias for `sverklo ui`.",
      wakeup: "Print compressed project context (for system-prompt injection in non-MCP clients).",
      digest: "5-line summary of what changed in this project. Flags: --since 7d, --format markdown|plain.",
      receipt: "Token-spend receipt for your recent Claude Code sessions. Shows where tokens went and projected yearly cost. Flags: --since 7d, --format plain|json.",
      memory: "Manage the memory store. Subcommands: show, edit, export.",
      grammars: "Manage tree-sitter grammars for the SVERKLO_PARSER=tree-sitter opt-in path. Subcommands: install.",
      "audit-prompt": "Print a ready-to-paste codebase-audit prompt (hybrid agent workflow).",
      "review-prompt": "Print a ready-to-paste PR/MR-review prompt (hybrid agent workflow).",
      bench: "Run reproducible benchmarks on gin/nestjs/react.",
      benchmark: "Alias for `sverklo bench`.",
      history: "Show audit grade history and trend over time.",
      activity: "Show recent activity log (always-on audit trail).",
      profile: "Suggest the smallest tool-set profile based on real usage. Sub-actions: suggest [path] [--days N] (default 30) | list. Reads activity.jsonl per project.",
      trace: "Show recent tool call traces (set SVERKLO_TRACE=1).",
      telemetry: "Manage opt-in telemetry (off by default). Subcommands: status, enable, disable.",
      setup: "Download the embedding model (~90MB). With --global: write global MCP config for Claude Code.",
      install: "Alias for `sverklo setup`.",
      prune: "", // prune already prints its own --help inside the block
    };

    // Pass-throughs: subcommands that handle --help themselves.
    // workspace handles subcommand-specific --help (issue #38).
    const SELF_HANDLES_HELP = new Set(["prune", "memory", "workspace"]);
    if (!SELF_HANDLES_HELP.has(command)) {
      const blurb = HELP_BLURBS[command];
      if (blurb) {
        console.log(`\nsverklo ${command} — ${blurb}\n\nSee \`sverklo --help\` for the full command list.\n`);
      } else {
        console.log(`\nsverklo: unknown subcommand \`${command}\`.\n\nRun \`sverklo --help\` for the list of subcommands.\n`);
      }
      process.exit(0);
    }
  }
}

if (command === "--version" || command === "-v" || command === "-V") {
  const { readFileSync } = await import("node:fs");
  const { join, dirname } = await import("node:path");
  const { fileURLToPath } = await import("node:url");
  const binDir = dirname(fileURLToPath(import.meta.url));
  // Try both ../package.json (source) and ../../package.json (dist)
  for (const rel of ["..", "../.."]) {
    try {
      const pkg = JSON.parse(readFileSync(join(binDir, rel, "package.json"), "utf-8"));
      console.log(`sverklo v${pkg.version}`);
      process.exit(0);
    } catch {}
  }
  console.log("sverklo (version unknown)");
  process.exit(0);
}

if (command === "init") {
  // Parse flags: --auto-capture, --mine-chats
  const flags = args.filter((a) => a.startsWith("--"));
  const positional = args.filter((a) => !a.startsWith("--"));
  const autoCapture = flags.includes("--auto-capture");
  const mineChats = flags.includes("--mine-chats");
  const projectPath = resolve(positional[1] || process.cwd());
  const { initProject } = await import("../src/init.js");
  await initProject(projectPath, { autoCapture, mineChats });

  // Auto-register in the global registry
  const { basename } = await import("node:path");
  const { registerRepo, deriveRepoName } = await import("../src/registry/registry.js");
  const repoName = deriveRepoName(projectPath);
  registerRepo(repoName, projectPath);
  console.log(`  Global registry — registered as "${repoName}"`);

  process.exit(0);
}

if (command === "register") {
  // Reject flag-shaped positionals (e.g. someone typed `register --foo` and
  // we'd otherwise create a repo named "--foo" pointing at /private/tmp/--foo).
  if (args[1] && args[1].startsWith("-")) {
    console.error(`✗ register expects a directory path, got flag-shaped arg: ${args[1]}`);
    console.error("  Usage: sverklo register [path] [name]");
    process.exit(2);
  }
  const targetPath = resolve(args[1] || process.cwd());
  const { registerRepo, deriveRepoName, getRegistryPath } = await import("../src/registry/registry.js");
  const repoName = args[2] || deriveRepoName(targetPath);
  registerRepo(repoName, targetPath);
  console.log(`Registered "${repoName}" -> ${targetPath}`);
  console.log(`Registry: ${getRegistryPath()}`);
  process.exit(0);
}

if (command === "unregister") {
  const name = args[1];
  if (!name) {
    console.error("Usage: sverklo unregister <name>");
    console.error("Use `sverklo list` to see registered repos.");
    process.exit(1);
  }
  const { unregisterRepo, getRegistry } = await import("../src/registry/registry.js");
  const repos = getRegistry();
  if (!repos[name]) {
    console.error(`Repo "${name}" not found in registry.`);
    const available = Object.keys(repos);
    if (available.length > 0) {
      console.error(`Available: ${available.join(", ")}`);
    }
    process.exit(1);
  }
  unregisterRepo(name);
  console.log(`Unregistered "${name}"`);
  process.exit(0);
}

// Issue #37 (HaleTom 2026-05-13): users perceive `sverklo init` as
// "full re-index every time" because there's no separate command for
// forcing a rebuild — they end up running init repeatedly when they
// just want fresh data. `Indexer.index()` is ALREADY incremental
// (mtime + content-hash skip on unchanged files), but the only way
// to force a full rebuild today is `rm -rf` of the index directory.
// `sverklo reindex` makes the force-rebuild path discoverable.
if (command === "reindex" || command === "re-index") {
  const positional = args.filter((a) => !a.startsWith("--"));
  const flags = args.filter((a) => a.startsWith("--"));
  const projectPath = resolve(positional[1] || process.cwd());
  const force = flags.includes("--force") || flags.includes("-f");
  // --timing prints per-phase elapsed ms (provider_init, discover,
  // parse_chunk_insert, embed, graph_pagerank, doc_link). Useful for
  // debugging "why is reindex slow" — without it the user sees one
  // wall-clock number with no breakdown. Dogfood perf review 2026-05-14.
  if (flags.includes("--timing")) {
    process.env.SVERKLO_TIMING = "1";
  }

  const { getProjectConfig } = await import("../src/utils/config.js");
  const { Indexer } = await import("../src/indexer/indexer.js");

  const config = getProjectConfig(projectPath);
  const indexer = new Indexer(config);

  if (force) {
    console.log(`Clearing index at ${projectPath}…`);
    indexer.clearIndex();
    console.log("Reindexing from scratch…");
  } else {
    console.log(`Reindexing ${projectPath} (incremental — only changed files)…`);
    console.log("Use --force to clear and rebuild from scratch.");
  }

  const start = Date.now();
  await indexer.index();
  const dur = ((Date.now() - start) / 1000).toFixed(1);
  const status = indexer.getStatus();
  console.log("");
  console.log(`✓ Done in ${dur}s`);
  console.log(`  ${status.fileCount} files · ${status.chunkCount} chunks`);
  indexer.close();
  process.exit(0);
}

if (command === "list") {
  const { getRegistry, getRegistryPath } = await import("../src/registry/registry.js");
  const repos = getRegistry();
  const entries = Object.entries(repos);
  if (entries.length === 0) {
    console.log("No repositories registered.");
    console.log("Register with: sverklo register [path] or sverklo init");
  } else {
    console.log(`Registered repositories (${entries.length}):`);
    console.log("");
    const { statSync, existsSync } = await import("node:fs");
    const { getProjectConfig } = await import("../src/utils/config.js");
    const now = Date.now();
    for (const [name, entry] of entries) {
      // Issue #49: the registry's `lastIndexed` is set on `register`
      // but never updated after subsequent reindexes, so it was
      // showing a stale time. The actual index db's mtime is the
      // correct source of truth — derive from there if it exists,
      // otherwise fall back to the registry stamp.
      let timestamp = new Date(entry.lastIndexed).getTime();
      try {
        const cfg = getProjectConfig(entry.path);
        if (existsSync(cfg.dbPath)) {
          timestamp = statSync(cfg.dbPath).mtimeMs;
        }
      } catch {
        // Project path may not be accessible from here — keep the
        // registry stamp as the fallback.
      }
      const age = now - timestamp;
      const ageStr = age < 60_000 ? `${Math.floor(age / 1000)}s ago`
        : age < 3_600_000 ? `${Math.floor(age / 60_000)} min ago`
        : age < 86_400_000 ? `${Math.floor(age / 3_600_000)} hours ago`
        : `${Math.floor(age / 86_400_000)} days ago`;
      console.log(`  ${name}`);
      console.log(`    path: ${entry.path}`);
      console.log(`    last indexed: ${ageStr}`);
      console.log("");
    }
    console.log(`Registry: ${getRegistryPath()}`);
  }
  process.exit(0);
}

if (command === "bench" || command === "benchmark") {
  // `sverklo bench self` — consumer-runnable self-benchmark against
  // the user's OWN repo. Dogfood perf review 2026-05-14 flagged that
  // README claims like "26s cold-start on 4000-file repos" aren't
  // reproducible from the shipped npm binary because `sverklo bench`
  // (without `self`) requires the source checkout. `self` runs against
  // whatever directory the user points it at and reports cold-start +
  // 5 warm-call latencies. No external clones, no scripts/ shipped.
  const sub = args[1];
  if (sub === "self") {
    const positional = args.slice(2).filter((a) => !a.startsWith("--"));
    const projectPath = resolve(positional[0] || process.cwd());

    const { getProjectConfig } = await import("../src/utils/config.js");
    const { Indexer } = await import("../src/indexer/indexer.js");

    console.log(`sverklo bench self — measuring ${projectPath}`);
    console.log("");

    const config = getProjectConfig(projectPath);
    const indexer = new Indexer(config);

    // Cold-start: clear + rebuild
    console.log("[1/2] cold-start (clear index, rebuild from scratch)…");
    indexer.clearIndex();
    const t0 = Date.now();
    await indexer.index();
    const coldMs = Date.now() - t0;
    const status = indexer.getStatus();
    console.log(`      ${coldMs}ms for ${status.fileCount} files · ${status.chunkCount} chunks`);
    console.log("");

    // Warm calls: search, lookup, refs, deps, impact — 5 each, take median
    console.log("[2/2] warm-call latencies (5 calls each, median ms):");

    const { handleLookup } = await import("../src/server/tools/lookup.js");
    const { hybridSearch } = await import("../src/search/hybrid-search.js");

    type Call = { name: string; fn: () => Promise<unknown> };
    const calls: Call[] = [
      {
        name: "search",
        fn: () => hybridSearch(indexer, { query: "indexer", tokenBudget: 1000 }),
      },
      {
        name: "lookup",
        fn: () => handleLookup(indexer, { symbol: "Indexer" }),
      },
    ];

    for (const c of calls) {
      const times: number[] = [];
      for (let i = 0; i < 5; i++) {
        const start = Date.now();
        try {
          await c.fn();
        } catch {
          /* swallow — we want latency, not correctness here */
        }
        times.push(Date.now() - start);
      }
      times.sort((a, b) => a - b);
      const median = times[2];
      console.log(`  ${c.name.padEnd(10)} median=${median}ms  (samples: ${times.join(", ")})`);
    }

    console.log("");
    console.log("Run with --timing for per-phase cold-start breakdown:");
    console.log(`  sverklo reindex ${projectPath} --force --timing`);
    indexer.close();
    process.exit(0);
  }

  // Reproducible benchmark runner. Clones pinned versions of gin, nestjs,
  // and react into ~/.sverklo-bench-cache, runs the perf profiler against
  // each, and prints a summary. Everything in BENCHMARKS.md should come
  // out of this command so readers can reproduce the numbers with one
  // invocation. Inspired by ripgrep's benchsuite.
  const { spawn } = await import("node:child_process");
  const { dirname } = await import("node:path");
  const { fileURLToPath } = await import("node:url");
  const { existsSync } = await import("node:fs");
  const here = dirname(fileURLToPath(import.meta.url));
  // Try source layout first (running from checkout), then installed dist.
  const candidates = [
    resolve(here, "..", "scripts", "bench-reproducer.mjs"),
    resolve(here, "..", "..", "scripts", "bench-reproducer.mjs"),
  ];
  const scriptPath = candidates.find((p) => existsSync(p));
  if (!scriptPath) {
    console.error(
      "sverklo bench: the gin/nestjs/react cross-repo benchmark requires the source\n" +
        "checkout (scripts/bench-reproducer.mjs isn't shipped in the npm package to\n" +
        "keep it small). For consumer-runnable perf numbers on YOUR repo, use:\n\n" +
        "  sverklo bench self [path]   # measures cold-start + warm-call latency\n" +
        "                              # on a directory you point at, no clones\n\n" +
        "For the full cross-repo benchmark with reproducible upstream pins:\n\n" +
        "  git clone https://github.com/sverklo/sverklo && cd sverklo\n" +
        "  npm install && npm run build && npm run bench",
    );
    process.exit(1);
  }
  const child = spawn("node", [scriptPath, ...args.slice(1)], { stdio: "inherit" });
  child.on("exit", (code) => process.exit(code ?? 0));
  // Keep alive until spawn exit
  await new Promise(() => {});
}

if (command === "audit-diff") {
  // Incremental architectural quality gate. Runs Tarjan SCC + fan-in
  // spike detection over the boundary subgraph derived from `git diff
  // --name-only`. Exit 1 on new violations, 0 on pass, 2 on config error.
  const { handleAuditDiff } = await import("../src/audit-diff/index.js");
  const exitCode = await handleAuditDiff(args.slice(1));
  process.exit(exitCode);
}

if (command === "audit-prompt" || command === "review-prompt") {
  // Emit a ready-to-paste prompt that encodes the hybrid workflow
  // (prefer sverklo tools for discovery, built-in tools for exact
  // patterns and line-level reading). Pipe into `pbcopy` on macOS or
  // `xclip -sel clip` on Linux, or paste directly into your agent.
  const { renderAuditPrompt } = await import("../src/audit-prompt.js");
  const mode = command === "review-prompt" ? "review" : "audit";
  process.stdout.write(renderAuditPrompt(mode));
  process.exit(0);
}

if (command === "doctor" || command === "diagnose" || command === "check") {
  const projectPath = resolve(args[1] || process.cwd());
  const { runDoctor } = await import("../src/doctor.js");
  runDoctor(projectPath);
  process.exit(0);
}

if (command === "workspace") {
  const sub = args[1];

  // Issue #38 (HaleTom 2026-05-13): subcommand-specific --help. The
  // top-level fallthrough below prints the command list, but until now
  // `sverklo workspace <subcmd> --help` printed the same generic
  // blurb. Each subcommand now gets its own usage/example block.
  const wantsHelp = args.slice(1).some((a) => a === "--help" || a === "-h");
  if (wantsHelp || sub === "help") {
    const helpText: Record<string, string> = {
      init: `sverklo workspace init — Create a cross-repo workspace (YAML)

Usage:
  sverklo workspace init <name> <path1> [path2 ...]

Args:
  <name>       Workspace name (alphanumerics, dash, underscore; 1-64 chars).
  <path...>    One or more project directories to include.

Examples:
  sverklo workspace init backend ./api ./worker ./shared
  sverklo workspace init team-platform $PWD/api $PWD/web

Creates ~/.sverklo/workspaces/<name>.yaml. Use \`sverklo workspace
index <name>\` to scan contracts + dependencies across projects.`,

      status: `sverklo workspace status — Show workspace health

Usage:
  sverklo workspace status [name]

Args:
  [name]   Optional workspace name. If omitted, shows all workspaces.

Reports: project paths, last-indexed time, contract counts, edge counts.`,

      index: `sverklo workspace index — Index every project in a workspace

Usage:
  sverklo workspace index <name>

Args:
  <name>   Workspace name (must exist; see \`sverklo workspace list\`).

Runs incremental indexing per project. Cross-repo contract extraction
(GraphQL/OpenAPI/protobuf) runs after each project completes.`,

      create: `sverklo workspace create — Create a workspace (legacy JSON shape)

Usage:
  sverklo workspace create <name> [path1] [path2] ...

Args:
  <name>       Workspace name.
  [paths...]   Project paths. Defaults to current directory if omitted.

Note: \`init\` is the modern equivalent and uses YAML. \`create\` is
preserved for back-compat with the v0.18 JSON registry.`,

      add: `sverklo workspace add — Add a repo to an existing workspace

Usage:
  sverklo workspace add <name> [path]

Args:
  <name>     Workspace name.
  [path]     Repo path to add. Defaults to current directory.`,

      remove: `sverklo workspace remove — Remove a repo from a workspace

Usage:
  sverklo workspace remove <name> <path>

Args:
  <name>     Workspace name.
  <path>     Repo path to remove.`,

      list: `sverklo workspace list — List all workspaces

Usage:
  sverklo workspace list

Prints names + repo counts.`,

      show: `sverklo workspace show — Show repos in a workspace

Usage:
  sverklo workspace show <name>

Args:
  <name>     Workspace name.`,

      memory: `sverklo workspace memory — Manage cross-repo workspace memory

Usage:
  sverklo workspace memory <name> list                     List all memories
  sverklo workspace memory <name> add <content> [--tags T] Add a memory
  sverklo workspace memory <name> search <query>           Search memories
  sverklo workspace memory <name> forget <id>              Delete a memory by id

The workspace memory lives at ~/.sverklo/workspaces/<name>/memories.db
and is shared across every repo in the workspace.`,
    };
    if (sub && helpText[sub]) {
      console.log(helpText[sub]);
      process.exit(0);
    }
    // Fall through to the generic command list below.
  }

  // --- Cross-repo workspace commands (new YAML-based) ---

  if (sub === "init") {
    const name = args[2];
    const paths = args.slice(3);
    if (!name || paths.length === 0) {
      console.error("Usage: sverklo workspace init <name> <path1> <path2> ...");
      process.exit(1);
    }
    const { workspaceInit } = await import("../src/workspace/cli.js");
    await workspaceInit(name, paths);
    process.exit(0);
  }

  if (sub === "status") {
    const name = args[2]; // optional
    const { workspaceStatus } = await import("../src/workspace/cli.js");
    const output = await workspaceStatus(name);
    console.log(output);
    process.exit(0);
  }

  if (sub === "index") {
    const name = args[2];
    if (!name) { console.error("Usage: sverklo workspace index <name>"); process.exit(1); }
    const { workspaceIndex } = await import("../src/workspace/cli.js");
    await workspaceIndex(name);
    process.exit(0);
  }

  // --- Legacy workspace commands (JSON-based, kept for backwards compat) ---

  const {
    createWorkspace,
    loadWorkspace,
    listWorkspaces,
    addRepoToWorkspace,
    removeRepoFromWorkspace,
  } = await import("../src/workspace.js");

  if (sub === "create") {
    const name = args[2];
    if (!name) { console.error("Usage: sverklo workspace create <name> [path1] [path2]..."); process.exit(1); }
    const repos = args.slice(3).length > 0 ? args.slice(3) : [process.cwd()];
    const ws = createWorkspace(name, repos);
    console.log(`Created workspace '${name}' with ${ws.repos.length} repo(s):`);
    for (const r of ws.repos) console.log(`  · ${r.path}`);
    process.exit(0);
  }

  if (sub === "list") {
    const all = listWorkspaces();
    if (all.length === 0) {
      console.log("No workspaces. Create one with: sverklo workspace create <name> [paths...]");
    } else {
      console.log("Workspaces:");
      for (const name of all) {
        const ws = loadWorkspace(name);
        if (ws) console.log(`  · ${name} (${ws.repos.length} repos)`);
      }
    }
    process.exit(0);
  }

  if (sub === "add") {
    const name = args[2];
    const path = args[3] || process.cwd();
    if (!name) { console.error("Usage: sverklo workspace add <name> [path]"); process.exit(1); }
    const ws = addRepoToWorkspace(name, path);
    console.log(`Workspace '${name}' now has ${ws.repos.length} repos`);
    process.exit(0);
  }

  if (sub === "remove") {
    const name = args[2];
    const path = args[3];
    if (!name || !path) { console.error("Usage: sverklo workspace remove <name> <path>"); process.exit(1); }
    const ws = removeRepoFromWorkspace(name, path);
    if (ws) console.log(`Workspace '${name}' now has ${ws.repos.length} repos`);
    else console.error(`Workspace '${name}' not found`);
    process.exit(0);
  }

  if (sub === "show") {
    const name = args[2];
    if (!name) { console.error("Usage: sverklo workspace show <name>"); process.exit(1); }
    const ws = loadWorkspace(name);
    if (!ws) { console.error(`Workspace '${name}' not found`); process.exit(1); }
    console.log(`Workspace: ${ws.name}`);
    console.log(`Repos (${ws.repos.length}):`);
    for (const r of ws.repos) console.log(`  · ${r.alias || ""} ${r.path}`);
    process.exit(0);
  }

  if (sub === "memory") {
    // sverklo workspace memory <name> <list|add|search|forget> [...]
    //
    // Per-workspace shared memory store at
    // ~/.sverklo/workspaces/<name>/memories.db. CLI ships in v0.17;
    // sverklo_remember scope:workspace is the v0.18 follow-up.
    const name = args[2];
    const op = args[3];
    if (!name || !op) {
      console.error("Usage: sverklo workspace memory <name> <list|add|search|forget> [args]");
      process.exit(1);
    }
    const {
      openWorkspaceMemory,
      addWorkspaceMemory,
      searchWorkspaceMemory,
      workspaceMemoryExists,
    } = await import("../src/workspace/memory.js");

    if (op === "add") {
      const content = args.slice(4).join(" ");
      if (!content) {
        console.error('Usage: sverklo workspace memory <name> add "memory text"');
        process.exit(1);
      }
      const ws = openWorkspaceMemory(name);
      const id = addWorkspaceMemory(ws, { content });
      console.log(`Saved workspace memory #${id} → ${ws.dbPath}`);
      ws.close();
      process.exit(0);
    }
    if (op === "list") {
      if (!workspaceMemoryExists(name)) {
        console.log(`No memories yet for workspace "${name}". Add one with:`);
        console.log(`  sverklo workspace memory ${name} add "your decision here"`);
        process.exit(0);
      }
      const ws = openWorkspaceMemory(name);
      const rows = ws.memoryStore.getAll(50);
      console.log(`\nWorkspace "${name}" memories (${rows.length}):\n`);
      for (const m of rows) {
        const age = new Date(m.created_at).toISOString().slice(0, 10);
        console.log(`  #${m.id} [${m.category}/${m.kind}] ${age}`);
        console.log(`    ${m.content.replace(/\n/g, " ").slice(0, 100)}${m.content.length > 100 ? "…" : ""}`);
      }
      ws.close();
      process.exit(0);
    }
    if (op === "search") {
      const query = args.slice(4).join(" ");
      if (!query) {
        console.error('Usage: sverklo workspace memory <name> search "query"');
        process.exit(1);
      }
      if (!workspaceMemoryExists(name)) {
        console.log(`No memories for workspace "${name}".`);
        process.exit(0);
      }
      const ws = openWorkspaceMemory(name);
      const rows = searchWorkspaceMemory(ws, query, 20);
      console.log(`\n${rows.length} match${rows.length === 1 ? "" : "es"}:\n`);
      for (const m of rows) {
        console.log(`  #${m.id} [${m.category}/${m.kind}]`);
        console.log(`    ${m.content.replace(/\n/g, " ").slice(0, 200)}${m.content.length > 200 ? "…" : ""}`);
      }
      ws.close();
      process.exit(0);
    }
    if (op === "forget") {
      const idStr = args[4];
      if (!idStr) {
        console.error("Usage: sverklo workspace memory <name> forget <id>");
        process.exit(1);
      }
      const id = parseInt(idStr, 10);
      if (!Number.isFinite(id)) {
        console.error(`✗ "${idStr}" is not a valid id`);
        process.exit(2);
      }
      const ws = openWorkspaceMemory(name);
      const ok = ws.memoryStore.delete(id);
      ws.close();
      console.log(ok ? `Forgot memory #${id}.` : `Memory #${id} not found.`);
      process.exit(ok ? 0 : 1);
    }

    console.error(`Unknown workspace memory op: ${op}`);
    process.exit(1);
  }

  console.log(`
sverklo workspace — manage multi-repo workspaces

Usage:
  sverklo workspace init <name> <p1> <p2> ...   Create cross-repo workspace (YAML)
  sverklo workspace status [name]                Show workspace health & staleness
  sverklo workspace index <name>                 Index all projects in a workspace
  sverklo workspace create <name> [paths...]     Create a workspace (legacy JSON)
  sverklo workspace add <name> [path]            Add a repo to a workspace
  sverklo workspace remove <name> <path>         Remove a repo from a workspace
  sverklo workspace list                         List all workspaces
  sverklo workspace show <name>                  Show repos in a workspace
`);
  process.exit(0);
}

if (command === "wakeup" || command === "wake-up") {
  const projectPath = resolve(args[1] || process.cwd());
  const { existsSync } = await import("node:fs");
  const { join } = await import("node:path");
  const { homedir } = await import("node:os");
  const modelDir = join(homedir(), ".sverklo", "models");
  if (!existsSync(join(modelDir, "model.onnx"))) {
    const { setupModels } = await import("../src/indexer/setup.js");
    await setupModels().catch(() => {});
  }
  const { getProjectConfig } = await import("../src/utils/config.js");
  const { Indexer } = await import("../src/indexer/indexer.js");
  const { generateWakeup } = await import("../src/server/tools/wakeup.js");
  const config = getProjectConfig(projectPath);
  const indexer = new Indexer(config);
  // Use existing index — don't re-run
  const output = generateWakeup(indexer, { maxTokens: 500 });
  indexer.close();
  console.log(output);
  process.exit(0);
}

if (command === "setup" || command === "install") {
  if (args.includes("--global")) {
    // Write global MCP config for Claude Code pointing to the global sverklo server
    const { existsSync, readFileSync, writeFileSync, mkdirSync } = await import("node:fs");
    const { join } = await import("node:path");
    const { homedir } = await import("node:os");
    const { findOnPath } = await import("../src/utils/find-on-path.js");

    // Issue #43: `command -v` is POSIX-only and printed misleading
    // errors on Windows. Walk PATH cross-platform instead.
    const sverkloBin = findOnPath("sverklo") ?? "sverklo";

    // Claude Code global settings: ~/.claude/settings.json
    const claudeSettingsDir = join(homedir(), ".claude");
    const claudeSettingsPath = join(claudeSettingsDir, "settings.json");
    mkdirSync(claudeSettingsDir, { recursive: true });

    type ClaudeSettings = {
      mcpServers?: Record<string, { command: string; args?: string[] }>;
      permissions?: { allow?: string[] };
      [key: string]: unknown;
    };

    let claudeSettings: ClaudeSettings = {};
    if (existsSync(claudeSettingsPath)) {
      try {
        claudeSettings = JSON.parse(readFileSync(claudeSettingsPath, "utf-8"));
      } catch {
        claudeSettings = {};
      }
    }

    if (!claudeSettings.mcpServers) claudeSettings.mcpServers = {};
    claudeSettings.mcpServers.sverklo = {
      command: sverkloBin,
      args: [],  // No path arg = global mode
    };

    // Auto-allow sverklo tools
    if (!claudeSettings.permissions) claudeSettings.permissions = {};
    if (!claudeSettings.permissions.allow) claudeSettings.permissions.allow = [];
    const allowList = claudeSettings.permissions.allow;
    if (!allowList.some((p: string) => p === "mcp__sverklo__*" || p.startsWith("mcp__sverklo__"))) {
      allowList.push("mcp__sverklo__*");
    }

    writeFileSync(claudeSettingsPath, JSON.stringify(claudeSettings, null, 2) + "\n");
    console.log(`Global MCP config written to ${claudeSettingsPath}`);
    console.log(`  Server command: ${sverkloBin} (no args = global mode)`);
    console.log("");
    console.log("The global sverklo server will serve all repos in ~/.sverklo/registry.json.");
    console.log("Register repos with: sverklo register /path/to/project");
    console.log("Or run `sverklo init` in each project directory.");
    process.exit(0);
  }

  const { setupModels } = await import("../src/indexer/setup.js");
  await setupModels();
  process.exit(0);
}

if (command === "telemetry") {
  const sub = args[1];
  const tel = await import("../src/telemetry/index.js");

  if (sub === "enable") {
    console.log("");
    console.log("Sverklo telemetry is currently OFF. Enabling sends:");
    console.log("");
    console.log("  install_id  one random UUID stored at ~/.sverklo/install-id");
    console.log("  version     current sverklo version");
    console.log("  os          darwin / linux / win32");
    console.log("  node_major  the Node major version sverklo is running on");
    console.log("  event       one of 17 fixed event types");
    console.log("  tool        sverklo_* tool name (when applicable)");
    console.log("  outcome     ok / error / timeout");
    console.log("  duration_ms tool execution time");
    console.log("");
    console.log("It does NOT send:");
    console.log("  - code, queries, file paths, symbol names, or memory contents");
    console.log("  - IP addresses, hostnames, or project identifiers");
    console.log("  - git remote URLs, branch names, or SHAs");
    console.log("");
    console.log("Every event is mirrored to ~/.sverklo/telemetry.log so you can see");
    console.log("exactly what gets sent. The endpoint source code lives at");
    console.log("https://github.com/sverklo/sverklo/tree/main/telemetry-endpoint");
    console.log("and the sending code is at src/telemetry/index.ts (under 250 lines).");
    console.log("");

    // Read y/n from stdin if interactive, otherwise --yes flag.
    // Pass the prompt directly to readline.question() — doing a prior
    // stdout.write() and then question("") races with the TTY handoff
    // on some terminal/Node combinations and the prompt never shows.
    const autoYes = args.includes("--yes") || args.includes("-y");
    let confirmed = autoYes;
    if (!autoYes) {
      if (!process.stdin.isTTY) {
        console.log("Non-interactive stdin — pass --yes to confirm enable.");
        console.log("Cancelled. Telemetry remains OFF.");
        process.exit(0);
      }
      const readline = await import("node:readline/promises");
      const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
      // Clean exit on SIGINT so ctrl-C doesn't leave the terminal in a bad state.
      rl.on("SIGINT", () => {
        rl.close();
        console.log("");
        console.log("Cancelled. Telemetry remains OFF.");
        process.exit(0);
      });
      try {
        const answer = (await rl.question("Type 'yes' to enable, anything else to cancel: ")).trim().toLowerCase();
        confirmed = answer === "yes" || answer === "y";
      } catch {
        // User hit ctrl-D / ctrl-C / the terminal closed — treat as cancel.
        confirmed = false;
      } finally {
        rl.close();
      }
    }
    if (!confirmed) {
      console.log("");
      console.log("Cancelled. Telemetry remains OFF.");
      process.exit(0);
    }

    const id = await tel.enable();
    console.log("");
    console.log(`Telemetry enabled. install_id: ${id}`);
    console.log(`Local mirror: ${tel.logPath}`);
    console.log("Disable any time with:  sverklo telemetry disable");
    process.exit(0);
  }

  if (sub === "disable") {
    await tel.disable();
    console.log("");
    console.log("Telemetry disabled. The disabled sentinel is permanent —");
    console.log("you'll need to run `sverklo telemetry enable` again to re-opt-in.");
    process.exit(0);
  }

  if (sub === "status") {
    const s = tel.status();
    console.log("");
    console.log(`telemetry: ${s.enabled ? "ON" : "OFF"}`);
    if (s.installId) console.log(`install_id: ${s.installId}`);
    console.log(`endpoint:  ${s.endpoint}`);
    console.log(`local log: ${s.logPath}`);
    console.log("");
    if (!s.enabled) {
      console.log("Enable with:  sverklo telemetry enable");
    } else {
      console.log("Disable with: sverklo telemetry disable");
      console.log("Tail log:     sverklo telemetry log");
    }
    process.exit(0);
  }

  if (sub === "log") {
    const { existsSync, readFileSync } = await import("node:fs");
    if (!existsSync(tel.logPath)) {
      console.log("No telemetry log yet. Enable with: sverklo telemetry enable");
      process.exit(0);
    }
    process.stdout.write(readFileSync(tel.logPath, "utf-8"));
    process.exit(0);
  }

  if (sub === "test") {
    // Diagnostic-only: send one event regardless of opt-in/opt-out state.
    // Does not write to ~/.sverklo/install-id, telemetry.log, or telemetry.enabled.
    // Uses a fixed sentinel install_id so the dashboard can filter test events
    // out if it ever wants to. Useful when:
    //   - debugging the network path between a user and the endpoint
    //   - confirming the dashboard pipeline works without flipping the opt-in
    //   - validating a self-hosted SVERKLO_TELEMETRY_ENDPOINT relay
    const TEST_INSTALL_ID = "00000000-0000-4000-8000-00000000c11d";
    const endpoint = process.env.SVERKLO_TELEMETRY_ENDPOINT || "https://t.sverklo.com/v1/event";
    const { platform } = await import("node:os");
    const p = platform();
    const os = (p === "darwin" || p === "linux" || p === "win32") ? p : "other";
    const nodeMajor = parseInt(process.versions.node.split(".")[0], 10) || 0;
    // Read sverklo version from package.json so the diagnostic carries
    // the same version field a real event would.
    let version = "0.0.0";
    try {
      const { readFileSync } = await import("node:fs");
      const { fileURLToPath } = await import("node:url");
      const { dirname, join } = await import("node:path");
      const here = dirname(fileURLToPath(import.meta.url));
      for (const rel of ["..", "../..", "../../..", "../../../.."]) {
        try {
          const pkg = JSON.parse(readFileSync(join(here, rel, "package.json"), "utf-8"));
          if (pkg.name === "sverklo" && pkg.version) { version = pkg.version; break; }
        } catch {}
      }
    } catch {}
    const payload = {
      install_id: TEST_INSTALL_ID,
      version,
      os,
      node_major: nodeMajor,
      event: "init.run",
      tool: null,
      outcome: "ok",
      duration_ms: 0,
    };
    console.log("");
    console.log("Sending one diagnostic event (does NOT enable telemetry):");
    console.log(`  endpoint:   ${endpoint}`);
    console.log(`  install_id: ${TEST_INSTALL_ID}  (sentinel — never written to disk)`);
    console.log(`  payload:    ${JSON.stringify(payload)}`);
    console.log("");
    const t0 = Date.now();
    try {
      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), 5000);
      const res = await fetch(endpoint, {
        method: "POST",
        headers: { "content-type": "application/json", "user-agent": `sverklo-test/${version}` },
        body: JSON.stringify(payload),
        signal: controller.signal,
      });
      clearTimeout(timer);
      const elapsed = Date.now() - t0;
      if (res.status === 204) {
        console.log(`Server accepted (HTTP 204) in ${elapsed}ms.`);
        console.log("");
        console.log("Check the dashboard at https://t.sverklo.com/v1/adoption/ui");
        console.log("(cache TTL: 60s; with days=2+ the cache key is fresh).");
        process.exit(0);
      }
      console.log(`Server responded HTTP ${res.status} in ${elapsed}ms.`);
      const body = await res.text().catch(() => "");
      if (body) console.log(`  body: ${body.slice(0, 200)}`);
      process.exit(1);
    } catch (e: unknown) {
      const elapsed = Date.now() - t0;
      const msg = e instanceof Error ? e.message : String(e);
      console.log(`Network error after ${elapsed}ms: ${msg}`);
      console.log("");
      console.log("If you're behind a proxy or firewall, set SVERKLO_TELEMETRY_ENDPOINT");
      console.log("to a reachable relay and re-run.");
      process.exit(1);
    }
  }

  console.log(`
sverklo telemetry — opt-in, privacy-preserving, off by default

Usage:
  sverklo telemetry enable    Opt in (interactive prompt; prints exact schema first)
  sverklo telemetry disable   Opt out permanently (sends one final opt_out event)
  sverklo telemetry status    Show current state
  sverklo telemetry log       Print the local mirror of every event sent
  sverklo telemetry test      Send one diagnostic event (bypasses opt-in)

Design doc: https://github.com/sverklo/sverklo/blob/main/TELEMETRY.md
`);
  process.exit(0);
}

if (command === "profile") {
  // sverklo profile suggest [path] [--days N]
  // sverklo profile list
  //
  // Reads ~/.sverklo/<project-hash>/activity.jsonl, filters tool.call events,
  // aggregates per-tool counts, and recommends the smallest named profile
  // (defined in src/server/tool-overrides.ts) that covers ≥95% of the user's
  // actual tool calls. Suggested in response to MCP discourse around tool-list
  // bloat (see /blog/we-already-shipped-mcp-code-mode/).
  const action = args[1] || "suggest";
  const { PROFILES } = await import("../src/server/tool-overrides.js");

  if (action === "list") {
    console.log("\n  Sverklo tool profiles\n");
    console.log("  " + "-".repeat(70));
    const profileNames = ["core", "nav", "review", "lean", "research"];
    for (const name of profileNames) {
      const tools = PROFILES[name];
      if (!tools) continue;
      console.log(`  ${name.padEnd(10)} ${String(tools.length).padStart(2)} tools`);
      console.log(`             ${tools.map((t: string) => t.replace(/^sverklo_/, "")).join(", ")}`);
      console.log();
    }
    console.log("  full        36 tools  (all sverklo_* tools — default)");
    console.log("\n  Set with: SVERKLO_PROFILE=core sverklo init");
    console.log("  Or in .sverklo.yaml: profile: core");
    console.log("  See: https://sverklo.com/blog/we-already-shipped-mcp-code-mode/\n");
    process.exit(0);
  }

  if (action !== "suggest") {
    console.error(`Unknown profile action: ${action}. Use "suggest" or "list".`);
    process.exit(2);
  }

  // Optional --days N flag, default 30
  const daysIdx = args.indexOf("--days");
  const daysWindow =
    daysIdx >= 0 && args[daysIdx + 1] ? parseInt(args[daysIdx + 1], 10) || 30 : 30;
  const projectPath = await resolveProjectPath(args.slice(2));

  // Two read paths. New one: structured tool-stats.json (post-v0.20.7
  // ports of pi-mcp-adapter's atomic-debounced writer). Cumulative
  // counts since the file was created — fast, no parse loop.
  // Legacy fallback: activity.jsonl scan with --days N filtering.
  // We try structured first; fall back if the file doesn't exist.
  const { readToolStats } = await import("../src/utils/tool-stats.js");
  const structuredStats = readToolStats(projectPath);

  let counts: Record<string, number> = {};
  let total = 0;
  let source = "";

  if (structuredStats && Object.keys(structuredStats.tools).length > 0) {
    // Structured doc — use it directly. The --days window doesn't apply
    // to cumulative stats; we use the full lifetime instead, and tell
    // the user when the doc started accumulating.
    for (const [tool, stat] of Object.entries(structuredStats.tools)) {
      counts[tool] = stat.calls;
    }
    total = structuredStats.totalCalls;
    const sinceStr = new Date(structuredStats.startedAt).toISOString().slice(0, 10);
    source = `tool-stats.json (cumulative since ${sinceStr})`;
  } else {
    // Legacy path: scan activity.jsonl with --days filter.
    const { getAllActivityEntries } = await import("../src/utils/activity-log.js");
    const entries = getAllActivityEntries(projectPath);
    const cutoffMs = Date.now() - daysWindow * 24 * 60 * 60 * 1000;
    const calls = entries.filter(
      (e) => e.event === "tool.call" && e.ts >= cutoffMs && typeof e.detail.tool === "string"
    );
    if (calls.length === 0) {
      console.log("\n  No tool calls recorded for this project yet.\n");
      console.log("  Tool-call telemetry is captured automatically when the MCP server handles tool calls.");
      console.log("  Use sverklo for ~1 week of normal coding sessions, then re-run this.\n");
      console.log("  Static profiles are listed by:\n");
      console.log("    sverklo profile list\n");
      process.exit(0);
    }
    for (const c of calls) {
      const tool = String(c.detail.tool);
      counts[tool] = (counts[tool] || 0) + 1;
    }
    total = calls.length;
    source = `activity.jsonl (last ${daysWindow} days)`;
  }

  const ranked = Object.entries(counts).sort((a, b) => b[1] - a[1]);

  console.log(
    `\n  Sverklo profile suggestion — based on ${total.toLocaleString()} tool calls\n  Source: ${source}\n`
  );
  console.log("  " + "-".repeat(70));
  console.log("  tool".padEnd(40) + "calls".padStart(10) + "  share".padStart(10));
  console.log("  " + "-".repeat(70));
  for (const [tool, n] of ranked) {
    const pct = ((n / total) * 100).toFixed(1) + "%";
    const display = tool.startsWith("sverklo_") ? tool : tool;
    console.log("  " + display.padEnd(38) + String(n).padStart(10) + pct.padStart(10));
  }
  console.log("  " + "-".repeat(70));

  // Compute coverage for every named profile so the user can see the
  // trade-off, not just a single suggestion. The 95% threshold is the
  // "safe to switch" line — below that, switching means a small number
  // of real usage calls fall outside the profile.
  const profileOrder: string[] = ["core", "nav", "review", "lean", "research"];
  type ProfileFit = { name: string; size: number; coveragePct: number; missing: string[] };
  const fits: ProfileFit[] = [];
  for (const name of profileOrder) {
    const profileTools = PROFILES[name];
    if (!profileTools) continue;
    const profileSet = new Set(profileTools);
    let covered = 0;
    const missing: string[] = [];
    for (const [tool, n] of ranked) {
      if (profileSet.has(tool)) covered += n;
      else if (tool.startsWith("sverklo_")) missing.push(tool);
    }
    fits.push({ name, size: profileTools.length, coveragePct: covered / total, missing });
  }

  console.log("\n  Profile coverage on your usage:");
  console.log("  " + "-".repeat(70));
  for (const fit of fits) {
    const pct = (fit.coveragePct * 100).toFixed(1) + "%";
    const flag = fit.coveragePct >= 0.95 ? " ✓ safe" : fit.coveragePct >= 0.90 ? " ~ close" : "";
    console.log(
      `  ${fit.name.padEnd(10)} ${String(fit.size).padStart(2)} tools   covers ${pct.padStart(6)}${flag}`
    );
  }
  console.log("  " + "-".repeat(70));

  const safest = fits.find((f) => f.coveragePct >= 0.95);
  const closest = [...fits].sort((a, b) => b.coveragePct - a.coveragePct)[0];

  console.log();
  if (safest) {
    const pctLabel = (safest.coveragePct * 100).toFixed(1) + "%";
    console.log(
      `  Suggested: SVERKLO_PROFILE=${safest.name} (${safest.size} tools, ${pctLabel} coverage)`
    );
    console.log(`    Add to your shell: export SVERKLO_PROFILE=${safest.name}`);
    console.log(`    Or in .sverklo.yaml: profile: ${safest.name}`);
  } else if (closest && closest.coveragePct >= 0.7) {
    const pctLabel = (closest.coveragePct * 100).toFixed(1) + "%";
    console.log(
      `  Closest match: SVERKLO_PROFILE=${closest.name} (${closest.size} tools, ${pctLabel} coverage)`
    );
    if (closest.missing.length > 0) {
      const top = closest.missing.slice(0, 3);
      const totalMissingCalls = top.reduce((s, t) => s + (counts[t] || 0), 0);
      const missingPct = ((totalMissingCalls / total) * 100).toFixed(1) + "%";
      console.log(
        `  But you also use ${top.join(", ")} (${missingPct} of calls) which aren't in this profile.`
      );
      console.log("  Two paths:");
      console.log(
        `    a) Accept the gap — set SVERKLO_PROFILE=${closest.name}; the missing tools won't be exposed.`
      );
      console.log(
        `    b) Custom set — keep full and use SVERKLO_DISABLED_TOOLS=<tools-you-never-call>`
      );
      console.log(
        "       to drop the long-tail. The lowest-count tools above are good candidates."
      );
    }
  } else {
    console.log(
      "  Your usage spans tools across multiple named profiles — no single profile fits well."
    );
    console.log("  Either keep the default (full) profile, or use SVERKLO_DISABLED_TOOLS to");
    console.log("  drop the long-tail tools you rarely call.");
  }
  console.log(
    "\n  For host-side lazy-loading (Claude Code Tool Search / Claude API defer_loading),"
  );
  console.log("  see https://sverklo.com/recipes/defer-loading/\n");
  process.exit(0);
}

if (command === "activity") {
  const projectPath = resolve(args[1] || process.cwd());
  const count = parseInt(args[2] || "30", 10) || 30;
  const { getActivityLog } = await import("../src/utils/activity-log.js");
  const entries = getActivityLog(projectPath, count);

  if (entries.length === 0) {
    console.log("No activity recorded yet. Activity is logged automatically when the MCP server handles tool calls.");
    process.exit(0);
  }

  console.log(`\n  Sverklo Activity Log (last ${entries.length} entries)\n`);
  console.log("  " + "-".repeat(70));

  for (const entry of entries) {
    const time = new Date(entry.ts).toISOString().replace("T", " ").replace("Z", "");
    const detail = Object.entries(entry.detail)
      .map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`)
      .join("  ");
    console.log(`  ${time}  ${entry.event}  ${detail}`);
  }

  console.log("  " + "-".repeat(70) + "\n");
  process.exit(0);
}

if (command === "trace") {
  const { existsSync, readFileSync } = await import("node:fs");
  const { TRACE_PATH } = await import("../src/utils/trace.js");

  if (!existsSync(TRACE_PATH)) {
    console.log("No trace log found at " + TRACE_PATH);
    console.log("Traces are recorded when SVERKLO_DEBUG=1 or SVERKLO_TRACE=1 is set.");
    process.exit(0);
  }

  const content = readFileSync(TRACE_PATH, "utf-8").trim();
  if (!content) {
    console.log("Trace log is empty.");
    process.exit(0);
  }

  const lines = content.split("\n");
  const count = parseInt(args[1] || "20", 10) || 20;
  const recent = lines.slice(-count);

  console.log(`\n  Sverklo Trace Log (last ${Math.min(count, recent.length)} entries)\n`);
  console.log("  " + "-".repeat(70));

  for (const line of recent) {
    try {
      const entry = JSON.parse(line);
      const time = new Date(entry.ts).toISOString().replace("T", " ").replace("Z", "");

      if (entry.phase === "request") {
        const argStr = Object.keys(entry.args || {}).length > 0
          ? " " + JSON.stringify(entry.args)
          : "";
        console.log(`  ${time}  ${entry.trace}  -> ${entry.tool}${argStr}`);
      } else if (entry.phase === "response") {
        console.log(`  ${time}  ${entry.trace}  <- ${entry.duration_ms}ms  ${entry.result_chars} chars`);
      } else if (entry.phase === "error") {
        console.log(`  ${time}  ${entry.trace}  !! ${entry.duration_ms}ms  ${entry.error}`);
      }
    } catch {
      // Skip malformed lines
    }
  }

  console.log("  " + "-".repeat(70));
  console.log(`  Log: ${TRACE_PATH}\n`);
  process.exit(0);
}

if (command === "review") {
  // CI-friendly review subcommand: indexes the repo, runs review_diff,
  // prints markdown (or JSON) to stdout, and optionally exits non-zero if
  // the highest risk level exceeds a threshold.
  //
  //   sverklo review [--ref <ref>] [--ci] [--format markdown|json]
  //                  [--max-files 25] [--fail-on critical|high|medium|low|none]

  const flags = args.slice(1);
  const flagVal = (name: string, fallback: string): string => {
    const idx = flags.indexOf(name);
    return idx !== -1 && flags[idx + 1] ? flags[idx + 1] : fallback;
  };

  const ref = flagVal("--ref", "");
  const ci = flags.includes("--ci");
  const format = flagVal("--format", "markdown") as
    | "markdown"
    | "json"
    | "github-review-json";
  const maxFiles = parseInt(flagVal("--max-files", "25"), 10) || 25;
  const failOn = flagVal("--fail-on", "none") as
    | "critical"
    | "high"
    | "medium"
    | "low"
    | "none";

  // Auto-detect ref: if inside a PR (GH Actions sets GITHUB_BASE_REF),
  // use origin/$GITHUB_BASE_REF..HEAD. Otherwise default to main..HEAD.
  const effectiveRef =
    ref ||
    (process.env.GITHUB_BASE_REF
      ? `origin/${process.env.GITHUB_BASE_REF}..HEAD`
      : "main..HEAD");

  // Strip value-taking flags so `--format github-review-json` doesn't
  // leave "github-review-json" looking like a positional path.
  const valueFlags = new Set(["--ref", "--format", "--max-files", "--fail-on"]);
  const cleanFlags: string[] = [];
  for (let i = 0; i < flags.length; i++) {
    if (valueFlags.has(flags[i])) {
      i++;
      continue;
    }
    if (Array.from(valueFlags).some((f) => flags[i].startsWith(`${f}=`))) continue;
    cleanFlags.push(flags[i]);
  }
  const projectPath = await resolveProjectPath(cleanFlags);

  // Ensure model is available
  const { existsSync: modelExists } = await import("node:fs");
  const { join: joinPath } = await import("node:path");
  const { homedir: hd } = await import("node:os");
  const mDir = joinPath(hd(), ".sverklo", "models");
  if (!modelExists(joinPath(mDir, "model.onnx"))) {
    if (ci) process.stderr.write("[sverklo] Downloading embedding model...\n");
    const { setupModels } = await import("../src/indexer/setup.js");
    await setupModels().catch(() => {});
  }

  const { getProjectConfig } = await import("../src/utils/config.js");
  const { Indexer } = await import("../src/indexer/indexer.js");
  const { handleReviewDiff } = await import(
    "../src/server/tools/review-diff.js"
  );
  const { buildReviewJson } = await import(
    "../src/server/tools/review-format.js"
  );

  const config = getProjectConfig(projectPath);
  const indexer = new Indexer(config);
  await indexer.index();

  const reviewArgs = {
    ref: effectiveRef,
    max_files: maxFiles,
    token_budget: 8000,
  };

  // Run early so we can both decide the threshold AND emit the format.
  // For github-review-json we want the structured payload; for the
  // other two formats we just need the markdown.
  let markdown = "";
  let structured: ReturnType<typeof buildReviewJson> | null = null;
  if (format === "github-review-json") {
    structured = buildReviewJson(indexer, reviewArgs);
    if ("error" in structured) {
      process.stderr.write(`✗ ${structured.error}\n`);
      indexer.close();
      process.exit(2);
    }
    markdown = structured.summary;
  } else {
    markdown = handleReviewDiff(indexer, reviewArgs);
  }

  indexer.close();

  if (format === "json") {
    const riskLevels = ["low", "medium", "high", "critical"] as const;
    type RiskLevel = (typeof riskLevels)[number];
    let maxRisk: RiskLevel = "low";
    for (const level of riskLevels) {
      if (markdown.includes(`(${level})`)) maxRisk = level;
    }
    const output = {
      ref: effectiveRef,
      max_risk: maxRisk,
      review: markdown,
    };
    process.stdout.write(JSON.stringify(output, null, 2) + "\n");
  } else if (format === "github-review-json") {
    process.stdout.write(JSON.stringify(structured, null, 2) + "\n");
  } else {
    process.stdout.write(markdown + "\n");
  }

  // Check fail-on threshold
  if (failOn !== "none") {
    const levelOrder: Record<string, number> = {
      low: 1,
      medium: 2,
      high: 3,
      critical: 4,
    };
    const threshold = levelOrder[failOn] || 0;
    const riskLevels = ["critical", "high", "medium", "low"];
    let maxFound = 0;
    for (const level of riskLevels) {
      if (markdown.includes(`(${level})`)) {
        maxFound = Math.max(maxFound, levelOrder[level] || 0);
      }
    }
    if (maxFound >= threshold) {
      process.stderr.write(
        `[sverklo] Risk threshold exceeded: found issues at or above '${failOn}'\n`
      );
      process.exit(1);
    }
  }

  process.exit(0);
}

if (command === "history") {
  const projectPath = resolve(args[1] || process.cwd());
  const { getAuditHistory, formatTrend } = await import("../src/utils/audit-history.js");
  const history = getAuditHistory(projectPath);

  if (history.length === 0) {
    console.log("No audit history yet. Run `sverklo audit` first.");
    process.exit(0);
  }

  // Issue #20: basename() is platform-aware.
  const projectName = basename(projectPath) || "unknown";
  console.log(`\nAudit History — ${projectName}\n`);

  // Dimension short names for the compact display
  const SHORT: Record<string, string> = {
    "Dead code": "dead",
    "Circular deps": "deps",
    "Coupling": "coup",
    "Security": "sec",
  };

  const recent = history.slice(-20);
  for (const entry of recent) {
    const sha = entry.sha.slice(0, 7);
    const dims = entry.dimensions
      .map((d) => `${SHORT[d.name] || d.name}:${d.grade}`)
      .join("  ");
    console.log(`${entry.date}  ${sha}  ${entry.grade}  (${entry.numericScore.toFixed(1)})  ${dims}`);
  }

  // Trend line
  if (recent.length >= 2) {
    const grades = recent.map((e) => e.grade);
    console.log(`\nTrend: ${formatTrend(grades)}`);
  }

  console.log("");
  process.exit(0);
}

if (command === "audit") {
  // CLI audit subcommand: indexes the repo, runs audit analysis,
  // outputs markdown, HTML, or JSON.
  //
  //   sverklo audit [--format markdown|html|json] [--output <path>] [--open] [--badge] [--publish]

  const flags = args.slice(1);
  const flagVal = (name: string, fallback: string): string => {
    const idx = flags.indexOf(name);
    return idx !== -1 && flags[idx + 1] ? flags[idx + 1] : fallback;
  };

  const format = flagVal("--format", "markdown") as "markdown" | "html" | "json" | "graph" | "arch" | "obsidian";
  const outputPath = flagVal("--output", format === "html" ? "sverklo-audit.html" : "");
  const shouldOpen = flags.includes("--open");
  const shouldBadge = flags.includes("--badge");
  const shouldPublish = flags.includes("--publish");
  const deepSecurity = flags.includes("--deep-security");

  const projectPath = await resolveProjectPath(flags);

  const { existsSync: modelExists } = await import("node:fs");
  const { join: joinPath } = await import("node:path");
  const { homedir: hd } = await import("node:os");
  const mDir = joinPath(hd(), ".sverklo", "models");
  if (!modelExists(joinPath(mDir, "model.onnx"))) {
    // stderr because `audit --format json` (and any other JSON-output
    // subcommand) pipes stdout to a parser; this preamble would
    // contaminate the JSON. PR #33 CI surfaced this.
    console.error("Downloading embedding model (~90MB)...");
    const { setupModels } = await import("../src/indexer/setup.js");
    await setupModels().catch(() => {});
  }

  const { getProjectConfig } = await import("../src/utils/config.js");
  const { Indexer } = await import("../src/indexer/indexer.js");
  const { handleAudit } = await import("../src/server/tools/audit.js");

  const config = getProjectConfig(projectPath);
  const indexer = new Indexer(config);
  await indexer.index();

  // Run analysis once for history tracking (handleAudit also calls it internally)
  const { analyzeCodebase: runAnalysis } = await import("../src/server/audit-analysis.js");
  const auditAnalysis = runAnalysis(indexer);

  // Auto-save to audit history
  const { appendAuditHistory } = await import("../src/utils/audit-history.js");
  appendAuditHistory(projectPath, auditAnalysis);

  let mdOutput = handleAudit(indexer, { token_budget: 16000 });

  // Deep security scan (semgrep) — optional enhancement
  if (deepSecurity) {
    const { isSemgrepInstalled, runSemgrep, formatSemgrepSection, semgrepSeverityToAudit } =
      await import("../src/utils/semgrep.js");
    if (!(await isSemgrepInstalled())) {
      console.error("semgrep not found. Install: brew install semgrep (or pip install semgrep)");
      process.exit(1);
    }
    console.log("Running deep security scan (semgrep)...");
    const findings = await runSemgrep(projectPath);
    if (findings.length > 0) {
      mdOutput += "\n" + formatSemgrepSection(findings);
      // Merge into auditAnalysis security issues for grade recalculation
      for (const f of findings) {
        auditAnalysis.securityIssues.push({
          file: f.path,
          line: f.line,
          pattern: `semgrep: ${f.rule}`,
          severity: semgrepSeverityToAudit(f.severity),
          snippet: f.message.slice(0, 120),
        });
      }
    } else {
      mdOutput += "\n## Deep Security Scan (semgrep)\n\nNo additional concerns found.\n";
    }
  }

  if (format === "graph") {
    const { analyzeCodebase } = await import("../src/server/audit-analysis.js");
    const { generateAuditGraph } = await import("../src/server/audit-graph.js");
    const analysis = analyzeCodebase(indexer);
    const html = generateAuditGraph(indexer, analysis, config.name);
    indexer.close();
    const { writeFileSync } = await import("node:fs");
    const out = outputPath || "sverklo-graph.html";
    writeFileSync(out, html);
    console.log(`Dependency graph written to ${out}`);
    if (shouldOpen) {
      const { execSync } = await import("node:child_process");
      const cmd = process.platform === "darwin" ? "open" : "xdg-open";
      try { execSync(`${cmd} ${out}`); } catch { /* ignore */ }
    }
    process.exit(0);
  }

  if (format === "arch") {
    const { analyzeCodebase } = await import("../src/server/audit-analysis.js");
    const { generateAuditArch } = await import("../src/server/audit-arch.js");
    const analysis = analyzeCodebase(indexer);
    const html = generateAuditArch(indexer, analysis, config.name);
    indexer.close();
    const { writeFileSync } = await import("node:fs");
    const out = outputPath || "sverklo-arch.html";
    writeFileSync(out, html);
    console.log(`Architecture diagram written to ${out}`);
    if (shouldOpen) {
      const { execSync } = await import("node:child_process");
      const cmd = process.platform === "darwin" ? "open" : "xdg-open";
      try { execSync(`${cmd} ${out}`); } catch { /* ignore */ }
    }
    process.exit(0);
  }

  if (format === "obsidian") {
    const { analyzeCodebase } = await import("../src/server/audit-analysis.js");
    const { generateAuditObsidian } = await import("../src/server/audit-obsidian.js");
    const analysis = analyzeCodebase(indexer);
    const md = generateAuditObsidian(indexer, analysis, config.name);
    indexer.close();
    const { writeFileSync } = await import("node:fs");
    const out = outputPath || "sverklo-obsidian.md";
    writeFileSync(out, md);
    console.log(`Obsidian vault file written to ${out}`);
    if (shouldOpen) {
      const { execSync } = await import("node:child_process");
      const cmd = process.platform === "darwin" ? "open" : "xdg-open";
      try { execSync(`${cmd} ${out}`); } catch { /* ignore */ }
    }
    process.exit(0);
  }

  indexer.close();

  if (shouldBadge || shouldPublish) {
    // Extract grade from the audit output (first line: "# Sverklo Project Audit — Grade: X")
    const gradeMatch = mdOutput.match(/Grade:\s*([ABCDF])/);
    const grade = gradeMatch ? gradeMatch[1] : "?";

    if (shouldPublish) {
      // Detect owner/repo from git remote
      const { execSync: exec } = await import("node:child_process");
      let owner = "", repo = "";
      try {
        const remote = exec("git remote get-url origin", { cwd: projectPath, encoding: "utf8" }).trim();
        const m = remote.match(/[/:]([^/]+)\/([^/.]+?)(?:\.git)?$/);
        if (m) { owner = m[1]; repo = m[2]; }
      } catch { /* no git remote */ }

      if (!owner || !repo) {
        console.error("Could not detect owner/repo from git remote. Run from a git repo with a remote.");
        process.exit(1);
      }

      // Extract dimensions from audit output
      const dimLines = mdOutput.match(/\| (Dead code|Circular deps|Coupling|Security) \| ([ABCDF]) \| (.+?) \|/g) || [];
      const dimensions = dimLines.map(line => {
        const m = line.match(/\| (.+?) \| ([ABCDF]) \| (.+?) \|/);
        return m ? { name: m[1], grade: m[2], detail: m[3] } : null;
      }).filter(Boolean);

      console.log(`Publishing grade ${grade} for ${owner}/${repo}...`);
      try {
        const res = await fetch("https://t.sverklo.com/v1/badge/publish", {
          method: "POST",
          headers: { "content-type": "application/json" },
          body: JSON.stringify({ owner, repo, grade, dimensions, version: "0.8.0" }),
        });
        if (res.ok) {
          const badgeUrl = `https://sverklo.com/api/badge/${owner}/${repo}.svg`;
          console.log(`\nPublished! Your dynamic badge:`);
          console.log(`\n[![Sverklo Health: ${grade}](${badgeUrl})](https://sverklo.com/report/${owner}/${repo})\n`);
          console.log(`Badge URL: ${badgeUrl}`);
        } else {
          console.error(`Publish failed: ${res.status} ${await res.text()}`);
        }
      } catch (e) {
        console.error(`Publish failed: ${e}`);
      }
      process.exit(0);
    }

    // --badge only (static, no publish)
    const colorMap: Record<string, string> = { A: "brightgreen", B: "green", C: "yellow", D: "orange", F: "red" };
    const color = colorMap[grade] || "lightgrey";
    const badge = `[![Sverklo Health: ${grade}](https://img.shields.io/badge/sverklo-${grade}-${color})](https://sverklo.com)`;
    console.log("\n── Sverklo Health Badge ──\n");
    console.log("Add this to your README.md:\n");
    console.log(badge);
    console.log("\nFor a dynamic badge that auto-updates, run: sverklo audit --publish\n");
    console.log("── Learn more: https://sverklo.com/badge/ ──\n");
    process.exit(0);
  }

  if (format === "json") {
    // v1.0.0: structured fields. Earlier 0.4.0 emitted only `content`
    // (markdown), forcing consumers to parse the headline + table — fragile
    // and broke the published GitHub Action's PR-comment builder until
    // 2026-05-09. The new shape adds `grade` (overall A–F) and
    // `dimensions: [{name, grade, score, detail}]` directly from
    // analyzeCodebase(), so consumers can skip the markdown parser.
    // `content` is preserved for backwards compatibility — older parsers
    // still work.
    const json = JSON.stringify({
      format: "sverklo-audit",
      version: "1.0.0",
      grade: auditAnalysis.healthScore.grade,
      numeric_score: auditAnalysis.healthScore.numericScore,
      dimensions: auditAnalysis.healthScore.dimensions,
      security_issues_count: auditAnalysis.securityIssues.length,
      circular_deps_count: auditAnalysis.circularDeps.length,
      content: mdOutput,
    }, null, 2);
    if (outputPath) {
      const { writeFileSync } = await import("node:fs");
      writeFileSync(outputPath, json);
      console.log(`Audit written to ${outputPath}`);
    } else {
      process.stdout.write(json + "\n");
    }
  } else if (format === "html") {
    const { generateAuditHtml } = await import("../src/server/audit-html.js");
    const html = generateAuditHtml(mdOutput, config.name, projectPath);
    const { writeFileSync } = await import("node:fs");
    const out = outputPath || "sverklo-audit.html";
    writeFileSync(out, html);
    console.log(`Audit report written to ${out}`);
    if (shouldOpen) {
      const { execSync } = await import("node:child_process");
      const cmd = process.platform === "darwin" ? "open" : "xdg-open";
      try { execSync(`${cmd} ${out}`); } catch { /* ignore */ }
    }
  } else {
    if (outputPath) {
      const { writeFileSync } = await import("node:fs");
      writeFileSync(outputPath, mdOutput);
      console.log(`Audit written to ${outputPath}`);
    } else {
      process.stdout.write(mdOutput + "\n");
    }
  }

  process.exit(0);
}

if (command === "ui" || command === "dashboard") {
  const projectPath = resolve(args[1] || process.cwd());
  const { existsSync } = await import("node:fs");
  const { join } = await import("node:path");
  const { homedir } = await import("node:os");
  const modelDir = join(homedir(), ".sverklo", "models");
  if (!existsSync(join(modelDir, "model.onnx"))) {
    console.log("Downloading embedding model (~90MB)...");
    const { setupModels } = await import("../src/indexer/setup.js");
    await setupModels().catch(() => {});
  }
  const { getProjectConfig } = await import("../src/utils/config.js");
  const { Indexer } = await import("../src/indexer/indexer.js");
  const { startHttpServer } = await import("../src/server/http-server.js");
  const config = getProjectConfig(projectPath);
  const indexer = new Indexer(config);
  await indexer.index();
  startHttpServer(indexer);
  const port = 3847;
  console.log(`\nSverklo Dashboard: http://localhost:${port}\n`);
  // Open browser
  const { exec } = await import("node:child_process");
  exec(`open http://localhost:${port} 2>/dev/null || xdg-open http://localhost:${port} 2>/dev/null`);
  // Keep alive
  process.on("SIGINT", () => { indexer.close(); process.exit(0); });
  await new Promise(() => {}); // block forever
}

if (command === "wiki") {
  // Generate a markdown wiki from the indexed codebase.
  //
  //   sverklo wiki [--output <dir>] [--format markdown|html]

  const flags = args.slice(1);
  const flagVal = (name: string, fallback: string): string => {
    const idx = flags.indexOf(name);
    return idx !== -1 && flags[idx + 1] ? flags[idx + 1] : fallback;
  };

  const output = resolve(flagVal("--output", "./sverklo-wiki"));
  const format = flagVal("--format", "markdown") as "markdown" | "html";
  const projectPath = await resolveProjectPath(flags);

  // Ensure model is available
  const { existsSync: modelExists } = await import("node:fs");
  const { join: joinPath } = await import("node:path");
  const { homedir: hd } = await import("node:os");
  const mDir = joinPath(hd(), ".sverklo", "models");
  if (!modelExists(joinPath(mDir, "model.onnx"))) {
    // stderr because `audit --format json` (and any other JSON-output
    // subcommand) pipes stdout to a parser; this preamble would
    // contaminate the JSON. PR #33 CI surfaced this.
    console.error("Downloading embedding model (~90MB)...");
    const { setupModels } = await import("../src/indexer/setup.js");
    await setupModels().catch(() => {});
  }

  const { getProjectConfig } = await import("../src/utils/config.js");
  const { Indexer } = await import("../src/indexer/indexer.js");
  const { generateWiki } = await import("../src/wiki/wiki-generator.js");

  const config = getProjectConfig(projectPath);
  const indexer = new Indexer(config);
  await indexer.index();

  await generateWiki(indexer, { outputDir: output, format });
  indexer.close();
  process.exit(0);
}

if (command === "enrich-patterns") {
  // P2-17: closed-taxonomy design-pattern annotation pass.
  //   sverklo enrich-patterns [--top 200] [--model qwen2.5-coder:7b]
  //                           [--min-conf 0.6] [--force]
  const flags = args.slice(1);
  const flagVal = (name: string, fallback?: string): string | undefined => {
    const idx = flags.indexOf(name);
    if (idx !== -1 && flags[idx + 1]) return flags[idx + 1];
    const prefixed = flags.find((f) => f.startsWith(`${name}=`));
    if (prefixed) return prefixed.slice(name.length + 1);
    return fallback;
  };
  const topN = Number(flagVal("--top", "200"));
  const model = flagVal("--model", "qwen2.5-coder:7b")!;
  const baseUrl = flagVal("--base-url", "http://localhost:11434")!;
  const minConfStr = flagVal("--min-conf", "0.6");
  const minConfidence = Number(minConfStr);
  const force = flags.includes("--force");
  const projectPath = await resolveProjectPath(flags);

  const reach = await fetch(`${baseUrl}/api/tags`).catch(() => null);
  if (!reach || !reach.ok) {
    console.error(`\n✗ Could not reach Ollama at ${baseUrl}.\n`);
    process.exit(1);
  }

  const { existsSync: mEP } = await import("node:fs");
  const { join: jpP } = await import("node:path");
  const { homedir: hdP } = await import("node:os");
  const mDP = jpP(hdP(), ".sverklo", "models");
  if (!mEP(jpP(mDP, "model.onnx"))) {
    console.log("Downloading embedding model (~90MB)...");
    const { setupModels } = await import("../src/indexer/setup.js");
    await setupModels().catch(() => {});
  }

  const { getProjectConfig } = await import("../src/utils/config.js");
  const { Indexer } = await import("../src/indexer/indexer.js");
  const { labelPatterns } = await import("../src/indexer/pattern-labeler.js");

  const config = getProjectConfig(projectPath);
  const indexer = new Indexer(config);
  await indexer.index();

  console.log(
    `Annotating top ${topN} symbols with pattern taxonomy via ${model} ` +
      `(min conf ${minConfidence})${force ? " (forced)" : ""}...`
  );
  const r = await labelPatterns(indexer, {
    topN, model, baseUrl, minConfidence, force,
    onProgress: (done, total, sym) => {
      if (done % 10 === 0 || done + 1 === total) {
        process.stdout.write(
          `  [${done + 1}/${total}] ${sym.slice(0, 40)}${done + 1 === total ? "\n" : "\r"}`
        );
      }
    },
  });
  console.log(
    `\nDone: scanned ${r.scanned}, labeled ${r.labeled}, ` +
      `dropped ${r.skipped_by_taxonomy} taxonomy / ${r.skipped_low_conf} low-conf, failed ${r.failed}.`
  );
  if (r.failures.length > 0) {
    console.log("\nFailures (first 5):");
    for (const f of r.failures.slice(0, 5)) console.log(`  - ${f.symbol}: ${f.reason}`);
  }
  indexer.close();
  // Exit non-zero when nothing succeeded — CI shouldn't green-light a
  // total no-op (e.g. Ollama 404 on the requested model).
  process.exit(r.failed > 0 && r.labeled === 0 ? 1 : 0);
}

if (command === "enrich-symbols") {
  // P1-12: write a one-line purpose onto chunks.purpose for the top-N
  // PageRank symbols. Uses Ollama; cached by content-hash.
  //
  //   sverklo enrich-symbols [--top 200] [--model qwen2.5-coder:7b] [--force]
  const flags = args.slice(1);
  const flagVal = (name: string, fallback?: string): string | undefined => {
    const idx = flags.indexOf(name);
    if (idx !== -1 && flags[idx + 1]) return flags[idx + 1];
    const prefixed = flags.find((f) => f.startsWith(`${name}=`));
    if (prefixed) return prefixed.slice(name.length + 1);
    return fallback;
  };

  const topN = Number(flagVal("--top", "200"));
  const model = flagVal("--model", "qwen2.5-coder:7b")!;
  const baseUrl = flagVal("--base-url", "http://localhost:11434")!;
  const force = flags.includes("--force");
  const projectPath = await resolveProjectPath(flags);

  // Reach check up front.
  const reach = await fetch(`${baseUrl}/api/tags`).catch(() => null);
  if (!reach || !reach.ok) {
    console.error(
      `\n✗ Could not reach Ollama at ${baseUrl}. Install + run Ollama, then \`ollama pull ${model}\`.\n`
    );
    process.exit(1);
  }

  const { existsSync: mE2 } = await import("node:fs");
  const { join: jp2 } = await import("node:path");
  const { homedir: hd2 } = await import("node:os");
  const mD2 = jp2(hd2(), ".sverklo", "models");
  if (!mE2(jp2(mD2, "model.onnx"))) {
    console.log("Downloading embedding model (~90MB)...");
    const { setupModels } = await import("../src/indexer/setup.js");
    await setupModels().catch(() => {});
  }

  const { getProjectConfig } = await import("../src/utils/config.js");
  const { Indexer } = await import("../src/indexer/indexer.js");
  const { enrichSymbolPurposes } = await import("../src/indexer/symbol-purpose.js");

  const config = getProjectConfig(projectPath);
  const indexer = new Indexer(config);
  await indexer.index();

  console.log(`Enriching top ${topN} symbols via ${model}${force ? " (forced)" : ""}...`);
  const r = await enrichSymbolPurposes(indexer, {
    topN,
    model,
    baseUrl,
    force,
    onProgress: (done, total, sym) => {
      if (done % 10 === 0 || done + 1 === total) {
        process.stdout.write(
          `  [${done + 1}/${total}] ${sym.slice(0, 40)}${done + 1 === total ? "\n" : "\r"}`
        );
      }
    },
  });

  console.log(`\nDone: ${r.enriched} enriched, ${r.skipped} skipped, ${r.failed} failed.`);
  if (r.failures.length > 0) {
    console.log("\nFailures (first 5):");
    for (const f of r.failures.slice(0, 5)) {
      console.log(`  - ${f.symbol}: ${f.reason}`);
    }
  }
  indexer.close();
  process.exit(r.failed > 0 && r.enriched === 0 ? 1 : 0);
}

if (command === "concept-index") {
  // Offline pass that labels every cluster with a short phrase + summary
  // via a locally-hosted Ollama chat model. Runs once per repo; later
  // runs skip clusters whose membership fingerprint hasn't changed.
  //
  //   sverklo concept-index
  //     [--model qwen2.5-coder:7b]
  //     [--base-url http://localhost:11434]
  //     [--force]
  //     [--max N]
  const flags = args.slice(1);
  const flagVal = (name: string, fallback?: string): string | undefined => {
    const idx = flags.indexOf(name);
    if (idx !== -1 && flags[idx + 1]) return flags[idx + 1];
    const prefixed = flags.find((f) => f.startsWith(`${name}=`));
    if (prefixed) return prefixed.slice(name.length + 1);
    return fallback;
  };

  const model = flagVal("--model", "qwen2.5-coder:7b")!;
  const baseUrl = flagVal("--base-url", "http://localhost:11434")!;
  const force = flags.includes("--force");
  const maxStr = flagVal("--max");
  const maxClusters = maxStr ? Number(maxStr) : undefined;

  const projectPath = await resolveProjectPath(flags);

  // Model check — needed for embedding the concept labels.
  const { existsSync: mExists } = await import("node:fs");
  const { join: jp } = await import("node:path");
  const { homedir: hdC } = await import("node:os");
  const mD = jp(hdC(), ".sverklo", "models");
  if (!mExists(jp(mD, "model.onnx"))) {
    console.log("Downloading embedding model (~90MB)...");
    const { setupModels } = await import("../src/indexer/setup.js");
    await setupModels().catch(() => {});
  }

  // Ollama reachability check up front — fail fast with a helpful message
  // before we spend time indexing.
  const reach = await fetch(`${baseUrl}/api/tags`).catch(() => null);
  if (!reach || !reach.ok) {
    console.error(
      `\n✗ Could not reach Ollama at ${baseUrl}.` +
        `\n\nTo fix:` +
        `\n  1. Install Ollama: https://ollama.com` +
        `\n  2. Pull a chat model:  ollama pull ${model}` +
        `\n  3. Start the daemon:   ollama serve   (or just \`ollama run ${model}\`)` +
        `\n\nThen re-run: sverklo concept-index\n`
    );
    process.exit(1);
  }

  const { getProjectConfig } = await import("../src/utils/config.js");
  const { Indexer } = await import("../src/indexer/indexer.js");
  const { detectClusters } = await import("../src/search/cluster.js");
  const { labelClusters } = await import("../src/indexer/concept-labeler.js");

  const config = getProjectConfig(projectPath);
  const indexer = new Indexer(config);
  await indexer.index();

  // Build clusters from the file graph.
  const files = indexer.fileStore.getAll().map((f) => ({
    id: f.id,
    path: f.path,
    pagerank: f.pagerank,
    language: f.language || "unknown",
  }));
  const edges = indexer.graphStore.getAll().map((e) => ({
    source: e.source_file_id,
    target: e.target_file_id,
    weight: e.reference_count,
  }));
  const clusters = detectClusters(files, edges);

  console.log(
    `Labeling ${maxClusters ? Math.min(maxClusters, clusters.length) : clusters.length} ` +
      `cluster(s) via ${model} at ${baseUrl}${force ? " (forced)" : ""}...`
  );

  const r = await labelClusters(indexer, clusters, indexer.conceptStore, {
    model,
    baseUrl,
    force,
    maxClusters,
    onProgress: (done, total, clusterId) => {
      if (done % 5 === 0 || done + 1 === total) {
        process.stdout.write(
          `  [${done + 1}/${total}] cluster ${clusterId}${done + 1 === total ? "\n" : "\r"}`
        );
      }
    },
  });

  console.log(`\nDone: ${r.labeled} labeled, ${r.skipped} skipped (unchanged), ${r.failed} failed.`);
  if (r.failures.length > 0) {
    console.log("\nFailures (first 5):");
    for (const f of r.failures.slice(0, 5)) {
      console.log(`  - cluster ${f.cluster_id}: ${f.reason}`);
    }
  }
  indexer.close();
  process.exit(r.failed > 0 && r.labeled === 0 ? 1 : 0);
}

if (command === "grammars") {
  // Installs / refreshes the WASM grammars used by SVERKLO_PARSER=tree-sitter
  // into ~/.sverklo/grammars/. v0.17 opt-in path; the regex parser
  // works without grammars, so this is purely additive.
  const sub = args[1];
  if (sub !== "install" && sub !== "list") {
    console.log(
      `\nsverklo grammars — manage tree-sitter grammars for the SVERKLO_PARSER=tree-sitter opt-in parser\n\n` +
      `Usage:\n` +
      `  sverklo grammars install [--lang typescript,python,go] [--force]\n` +
      `  sverklo grammars list\n\n` +
      `Languages supported: typescript, tsx, javascript, python, go, rust\n` +
      `Grammars land in ~/.sverklo/grammars/. Total ~6 MB across all six.\n`
    );
    process.exit(0);
  }
  const flags = args.slice(2);
  const langArg = flags.find((f, i) => flags[i - 1] === "--lang") ?? "";
  const langs = langArg
    ? langArg.split(",").map((s) => s.trim())
    : undefined;
  const force = flags.includes("--force");

  const { installGrammars, grammarsDir, GRAMMARS } = await import(
    "../src/indexer/grammars-install.js"
  );

  if (sub === "list") {
    console.log(`\nGrammars dir: ${grammarsDir()}\n`);
    const { existsSync, statSync } = await import("node:fs");
    const { join: jp } = await import("node:path");
    for (const g of GRAMMARS) {
      const p = jp(grammarsDir(), g.wasm);
      if (existsSync(p)) {
        console.log(`  ✓ ${g.lang.padEnd(12)}${(statSync(p).size / 1024).toFixed(0)} KB`);
      } else {
        console.log(`  ✗ ${g.lang.padEnd(12)}not installed`);
      }
    }
    console.log(`\nRun \`sverklo grammars install\` to fetch missing grammars.\n`);
    process.exit(0);
  }

  console.log(`\nInstalling tree-sitter grammars into ${grammarsDir()}\n`);
  const results = await installGrammars({
    langs,
    force,
    onProgress: (m: string) => console.log(m),
  });

  const fresh = results.filter((r) => r.status === "fresh").length;
  const cached = results.filter((r) => r.status === "cached").length;
  const errors = results.filter((r) => r.status === "error");

  console.log(
    `\nDone: ${fresh} downloaded, ${cached} cached, ${errors.length} failed.`
  );
  if (errors.length > 0) {
    for (const e of errors) console.log(`  ✗ ${e.lang}: ${e.error}`);
    console.log(
      `\nRetry with \`sverklo grammars install --force\` after checking your network.\n`
    );
  } else {
    console.log(`\nNext: SVERKLO_PARSER=tree-sitter sverklo audit . to use them.\n`);
  }
  process.exit(errors.length > 0 ? 1 : 0);
}

if (command === "memory") {
  // `sverklo memory` subcommands:
  //   export  — push memory rows to markdown/Notion/JSON files
  //   show    — print memory rows as markdown to stdout
  //   edit    — open memory rows in $EDITOR; round-trip content edits back
  //             to SQLite. Never deletes by omission.
  const sub = args[1];
  if (sub !== "export" && sub !== "show" && sub !== "edit") {
    console.log(
      `\nsverklo memory — read, edit, and export the memory store\n\n` +
      `Subcommands:\n` +
      `  show     print all memories as markdown to stdout\n` +
      `  edit     open memories in $EDITOR; round-trip text edits back\n` +
      `  export   write per-category .md files / JSON / push to Notion\n\n` +
      `Usage:\n` +
      `  sverklo memory show [--include-invalidated]\n` +
      `  sverklo memory edit [--editor PATH]\n` +
      `  sverklo memory export --format markdown|notion|json --to PATH [flags]\n\n` +
      `Run \`sverklo memory <subcommand> --help\` for the full flag list.\n`
    );
    process.exit(0);
  }

  if (sub === "show") {
    // Render every active memory as markdown to stdout. AI Edge's
    // "open Memory.md and read it" workflow, but driven by SQLite —
    // bi-temporal history, git provenance, no manual upkeep.
    const flags = args.slice(2);
    if (flags.includes("--help") || flags.includes("-h")) {
      console.log(
        `\nsverklo memory show — print memories as markdown to stdout\n\n` +
        `Flags:\n` +
        `  --include-invalidated  include superseded rows (full bi-temporal timeline)\n` +
        `  --kind episodic|semantic|procedural   filter by cognitive axis\n`
      );
      process.exit(0);
    }
    const includeInvalidated = flags.includes("--include-invalidated");
    const kindFlag = (() => {
      const idx = flags.indexOf("--kind");
      if (idx !== -1 && flags[idx + 1]) return flags[idx + 1];
      const prefixed = flags.find((f) => f.startsWith("--kind="));
      return prefixed ? prefixed.slice("--kind=".length) : undefined;
    })();
    if (kindFlag && !["episodic", "semantic", "procedural"].includes(kindFlag)) {
      console.error(`✗ --kind must be episodic|semantic|procedural, got "${kindFlag}"`);
      process.exit(2);
    }

    const valueFlags = new Set(["--kind"]);
    const cleanFlags: string[] = [];
    for (let i = 0; i < flags.length; i++) {
      if (valueFlags.has(flags[i])) { i++; continue; }
      if (Array.from(valueFlags).some((f) => flags[i].startsWith(`${f}=`))) continue;
      cleanFlags.push(flags[i]);
    }
    const projectPath = await resolveProjectPath(cleanFlags);

    const { getProjectConfig } = await import("../src/utils/config.js");
    const { Indexer } = await import("../src/indexer/indexer.js");
    const { renderMarkdownCombined } = await import("../src/memory/export.js");

    const config = getProjectConfig(projectPath);
    const indexer = new Indexer(config);
    await indexer.index();

    const rows = includeInvalidated
      ? indexer.memoryStore.getTimeline(10_000)
      : indexer.memoryStore.getAll(10_000);
    const filtered = kindFlag ? rows.filter((m) => m.kind === kindFlag) : rows;
    process.stdout.write(renderMarkdownCombined(filtered));
    indexer.close();
    process.exit(0);
  }

  if (sub === "edit") {
    // Render memories to a temp markdown file, open in $EDITOR, parse
    // changed content back into SQLite. Strict safety policy: omission
    // never deletes (use `sverklo memory demote` for that); a parse
    // error aborts without writing.
    const flags = args.slice(2);
    if (flags.includes("--help") || flags.includes("-h")) {
      console.log(
        `\nsverklo memory edit — open memories in $EDITOR; round-trip text edits\n\n` +
        `Flags:\n` +
        `  --editor PATH    editor to invoke (default: $EDITOR or vi)\n\n` +
        `Safety:\n` +
        `  - Removing a memory's heading from the file does NOT delete it.\n` +
        `    Use \`sverklo memory demote <id>\` (planned) or \`sverklo_demote\`\n` +
        `    from MCP for explicit deletion.\n` +
        `  - Adding a new memory by hand is not supported here. Use\n` +
        `    \`sverklo_remember\` from MCP or call the API directly.\n` +
        `  - If the parser can't make sense of your edits, the change\n` +
        `    is rejected and your SQLite store is left untouched.\n`
      );
      process.exit(0);
    }
    const editorOverride = (() => {
      const idx = flags.indexOf("--editor");
      if (idx !== -1 && flags[idx + 1]) return flags[idx + 1];
      const prefixed = flags.find((f) => f.startsWith("--editor="));
      return prefixed ? prefixed.slice("--editor=".length) : undefined;
    })();
    const editor = editorOverride ?? process.env.EDITOR ?? "vi";

    const valueFlags = new Set(["--editor"]);
    const cleanFlags: string[] = [];
    for (let i = 0; i < flags.length; i++) {
      if (valueFlags.has(flags[i])) { i++; continue; }
      if (Array.from(valueFlags).some((f) => flags[i].startsWith(`${f}=`))) continue;
      cleanFlags.push(flags[i]);
    }
    const projectPath = await resolveProjectPath(cleanFlags);

    const { getProjectConfig } = await import("../src/utils/config.js");
    const { Indexer } = await import("../src/indexer/indexer.js");
    const { renderMarkdownCombined, parseMarkdownEdits } = await import(
      "../src/memory/export.js"
    );
    const { writeFileSync, readFileSync, mkdtempSync } = await import("node:fs");
    const { tmpdir } = await import("node:os");
    const { join: joinPath } = await import("node:path");
    const { spawnSync: spawn } = await import("node:child_process");

    const config = getProjectConfig(projectPath);
    const indexer = new Indexer(config);
    await indexer.index();

    const rows = indexer.memoryStore.getAll(10_000);
    if (rows.length === 0) {
      console.log("(no memories to edit)");
      indexer.close();
      process.exit(0);
    }

    const dir = mkdtempSync(joinPath(tmpdir(), "sverklo-memory-edit-"));
    const filePath = joinPath(dir, "memory.md");
    const original = renderMarkdownCombined(rows);
    writeFileSync(filePath, original, "utf-8");

    console.log(`Opening ${filePath} in ${editor}...`);
    const result = spawn(editor, [filePath], { stdio: "inherit" });
    if (result.status !== 0) {
      console.error(`✗ editor exited with status ${result.status}; no changes applied`);
      indexer.close();
      process.exit(1);
    }

    const after = readFileSync(filePath, "utf-8");
    if (after === original) {
      console.log("(no edits — exiting)");
      indexer.close();
      process.exit(0);
    }

    const parsed = parseMarkdownEdits(after);
    if (parsed === null) {
      console.error("✗ couldn't parse the edited file (heading structure broken). No changes applied.");
      console.error(`  Your edits are preserved at ${filePath} in case you want to recover them.`);
      indexer.close();
      process.exit(1);
    }

    // Compute the diff: ids whose content changed.
    const byId = new Map(rows.map((r) => [r.id, r]));
    const updates: Array<{ id: number; content: string; before: string }> = [];
    for (const edit of parsed) {
      const original = byId.get(edit.id);
      if (!original) continue; // edit references an unknown id — skip silently
      if (original.content === edit.content) continue;
      updates.push({ id: edit.id, content: edit.content, before: original.content });
    }
    if (updates.length === 0) {
      console.log("(content unchanged — only metadata or formatting edits, ignoring)");
      indexer.close();
      process.exit(0);
    }

    for (const u of updates) {
      indexer.memoryStore.update(u.id, u.content);
    }
    console.log(`✓ updated ${updates.length} memor${updates.length === 1 ? "y" : "ies"}:`);
    for (const u of updates) {
      const beforeSnip = u.before.slice(0, 60).replace(/\n/g, " ");
      const afterSnip = u.content.slice(0, 60).replace(/\n/g, " ");
      console.log(`  #${u.id}: "${beforeSnip}${u.before.length > 60 ? "..." : ""}"`);
      console.log(`       → "${afterSnip}${u.content.length > 60 ? "..." : ""}"`);
    }
    console.log(
      `\nNote: omitted memories are preserved. Use \`sverklo_demote <id>\` from MCP\n` +
      `to explicitly archive a memory.`
    );
    indexer.close();
    process.exit(0);
  }

  const flags = args.slice(2);
  if (flags.includes("--help") || flags.includes("-h")) {
    console.log(
      `\nsverklo memory export — push memory rows to markdown / Notion / JSON\n\n` +
      `Required: --format markdown|notion|json --to PATH\n` +
      `See \`sverklo memory\` for the full flag list.\n`
    );
    process.exit(0);
  }
  const flagVal = (name: string): string | undefined => {
    const idx = flags.indexOf(name);
    if (idx !== -1 && flags[idx + 1]) return flags[idx + 1];
    const prefixed = flags.find((f) => f.startsWith(`${name}=`));
    return prefixed ? prefixed.slice(name.length + 1) : undefined;
  };

  const format = flagVal("--format") as "markdown" | "notion" | "json" | undefined;
  if (!format || !["markdown", "notion", "json"].includes(format)) {
    console.error("✗ --format markdown|notion|json is required");
    process.exit(2);
  }
  const to = flagVal("--to");
  if (!to) {
    console.error("✗ --to PATH is required");
    process.exit(2);
  }
  const kindRaw = flagVal("--kind");
  if (kindRaw && !["episodic", "semantic", "procedural"].includes(kindRaw)) {
    console.error(`✗ --kind must be episodic|semantic|procedural, got "${kindRaw}"`);
    process.exit(2);
  }
  const includeInvalidated = flags.includes("--include-invalidated");
  const notionDatabase = flagVal("--notion-database");
  if (format === "notion" && !notionDatabase) {
    console.error("✗ --notion-database ID is required for --format notion");
    process.exit(2);
  }

  // Strip value-taking flags before resolving the project path
  const valueFlags = new Set(["--format", "--to", "--kind", "--notion-database"]);
  const cleanFlags: string[] = [];
  for (let i = 0; i < flags.length; i++) {
    if (valueFlags.has(flags[i])) { i++; continue; }
    if (Array.from(valueFlags).some((f) => flags[i].startsWith(`${f}=`))) continue;
    cleanFlags.push(flags[i]);
  }
  const projectPath = await resolveProjectPath(cleanFlags);

  const { resolve: resolvePath } = await import("node:path");
  const outPath = resolvePath(to);

  const { getProjectConfig } = await import("../src/utils/config.js");
  const { Indexer } = await import("../src/indexer/indexer.js");
  const { runMemoryExport } = await import("../src/memory/export.js");

  const config = getProjectConfig(projectPath);
  const indexer = new Indexer(config);
  await indexer.index();

  try {
    const report = runMemoryExport(indexer, {
      format,
      to: outPath,
      kind: kindRaw as "episodic" | "semantic" | "procedural" | undefined,
      includeInvalidated,
      notionDatabase,
    });
    console.log(
      `\nExported ${report.rowsExported} memories (${format}):\n` +
      report.written.map((p) => `  ${p}`).join("\n") +
      `\n\nBy category: ${Object.entries(report.byCategory).map(([k, v]) => `${k}=${v}`).join(", ") || "(none)"}\n`
    );
  } catch (err) {
    const e = err as { message?: string };
    console.error(`✗ export failed: ${e.message ?? String(err)}`);
    process.exit(1);
  }

  indexer.close();
  process.exit(0);
}

if (command === "digest") {
  // Habit-loop scaffold: 5-line summary of what changed in this project
  // since the user last paid attention. Designed to be cheap to render
  // and easy to wire into a shell `cd` hook or a Slack post.
  //
  //   sverklo digest [--since 7d] [--format markdown|plain]
  const flags = args.slice(1);
  const flagVal = (name: string): string | undefined => {
    const idx = flags.indexOf(name);
    if (idx !== -1 && flags[idx + 1]) return flags[idx + 1];
    const prefixed = flags.find((f) => f.startsWith(`${name}=`));
    return prefixed ? prefixed.slice(name.length + 1) : undefined;
  };

  // Parse --since: accept "7d", "30d", or a bare number (interpreted as days).
  const sinceRaw = flagVal("--since") ?? "7d";
  const sinceMatch = /^(\d+)d?$/.exec(sinceRaw);
  if (!sinceMatch) {
    console.error(`✗ --since expects N or Nd (e.g. 7 or 7d), got "${sinceRaw}"`);
    process.exit(2);
  }
  const sinceDays = parseInt(sinceMatch[1], 10);

  const formatRaw = flagVal("--format") ?? "markdown";
  if (formatRaw !== "markdown" && formatRaw !== "plain") {
    console.error(`✗ --format must be markdown or plain, got "${formatRaw}"`);
    process.exit(2);
  }

  // Strip value-taking flags so the bare "7" in `--since 7` isn't
  // mistaken for a positional project path.
  const consumedFlags = new Set(["--since", "--format"]);
  const cleanFlags: string[] = [];
  for (let i = 0; i < flags.length; i++) {
    if (consumedFlags.has(flags[i])) {
      i++; // skip the value too
      continue;
    }
    if (Array.from(consumedFlags).some((f) => flags[i].startsWith(`${f}=`))) continue;
    cleanFlags.push(flags[i]);
  }
  const projectPath = await resolveProjectPath(cleanFlags);
  const { getProjectConfig } = await import("../src/utils/config.js");
  const { Indexer } = await import("../src/indexer/indexer.js");
  const { generateDigest } = await import("../src/digest.js");

  const config = getProjectConfig(projectPath);
  const indexer = new Indexer(config);
  await indexer.index();
  console.log(generateDigest(indexer, { sinceDays, format: formatRaw }));
  indexer.close();
  process.exit(0);
}

if (command === "receipt") {
  // Spotify-Wrapped-style summary of token spend across recent Claude
  // Code sessions. Reads ~/.claude/projects/**/*.jsonl, aggregates tool
  // calls and usage, and prints a screenshotable receipt. The point is
  // to make the cost concrete enough that the share-instinct kicks in.
  //
  //   sverklo receipt [--since 7d] [--format plain|json]
  const flags = args.slice(1);
  const flagVal = (name: string): string | undefined => {
    const idx = flags.indexOf(name);
    if (idx !== -1 && flags[idx + 1]) return flags[idx + 1];
    const prefixed = flags.find((f) => f.startsWith(`${name}=`));
    return prefixed ? prefixed.slice(name.length + 1) : undefined;
  };
  const sinceRaw = flagVal("--since") ?? "7d";
  const sinceMatch = /^(\d+)d?$/.exec(sinceRaw);
  if (!sinceMatch) {
    console.error(`✗ --since expects N or Nd (e.g. 7 or 7d), got "${sinceRaw}"`);
    process.exit(2);
  }
  const sinceDays = parseInt(sinceMatch[1], 10);
  const formatRaw = flagVal("--format") ?? "plain";
  if (formatRaw !== "plain" && formatRaw !== "json") {
    console.error(`✗ --format must be plain or json, got "${formatRaw}"`);
    process.exit(2);
  }
  const { runReceipt } = await import("../src/receipt.js");
  console.log(runReceipt({ sinceDays, format: formatRaw }));
  process.exit(0);
}

if (command === "prune") {
  // Sprint 9-C: access-decay pruning + episodic consolidation. Pure
  // bookkeeping pass over the memory store; never deletes (uses bi-
  // temporal supersedes-by). LLM distillation is opt-in via --with-ollama.
  //
  //   sverklo prune
  //     [--dry-run]
  //     [--max-age-days N]            (default 30)
  //     [--stale-threshold X]         (default 0.05)
  //     [--similarity-threshold X]    (default 0.88)
  //     [--min-cluster-size N]        (default 3)
  //     [--with-ollama --model X --base-url URL]
  const flags = args.slice(1);

  if (flags.includes("--help") || flags.includes("-h")) {
    console.log(
      `\nsverklo prune — decay stale memories + consolidate similar episodic ones (offline)\n\n` +
      `Usage:\n` +
      `  sverklo prune [flags]\n\n` +
      `Flags:\n` +
      `  --dry-run                      report what would change without writing\n` +
      `  --max-age-days N               consolidate episodic memories older than N (default 30)\n` +
      `  --stale-threshold X            decay-score cutoff below which a memory is marked stale (default 0.05)\n` +
      `  --similarity-threshold X       cosine threshold for clustering (0..1, default 0.88)\n` +
      `  --min-cluster-size N           smallest cluster that triggers consolidation (default 3)\n` +
      `  --with-ollama                  use Ollama to distil cluster summaries (falls back to deterministic note)\n` +
      `  --model NAME                   Ollama model id (default qwen2.5-coder:7b)\n` +
      `  --base-url URL                 Ollama base URL (default http://localhost:11434)\n` +
      `  -h, --help                     show this help\n\n` +
      `Notes:\n` +
      `  Originals are never deleted — superseded memories keep their lineage via valid_until_sha\n` +
      `  and superseded_by, so timeline views stay intact.\n`
    );
    process.exit(0);
  }

  const flagVal = (name: string): string | undefined => {
    const idx = flags.indexOf(name);
    if (idx !== -1 && flags[idx + 1]) return flags[idx + 1];
    const prefixed = flags.find((f) => f.startsWith(`${name}=`));
    if (prefixed) return prefixed.slice(name.length + 1);
    return undefined;
  };
  const num = (name: string, predicate: (n: number) => boolean): number | undefined => {
    const v = flagVal(name);
    if (v === undefined) return undefined;
    const n = Number(v);
    if (!Number.isFinite(n) || !predicate(n)) {
      console.error(`\n✗ ${name} expects a valid number (got "${v}")\n`);
      process.exit(2);
    }
    return n;
  };

  const projectPath = await resolveProjectPath(flags);
  const { getProjectConfig } = await import("../src/utils/config.js");
  const { Indexer } = await import("../src/indexer/indexer.js");
  const { runPrune } = await import("../src/memory/prune.js");

  // When --with-ollama is set, fail fast if the daemon isn't reachable
  // — same contract as `concept-index`. Otherwise the user thinks
  // distillation ran when it silently fell back to the deterministic
  // summary (or did nothing because no clusters existed).
  const withOllama = flags.includes("--with-ollama");
  const ollamaBaseUrl = flagVal("--base-url") ?? "http://localhost:11434";
  if (withOllama) {
    const reach = await fetch(`${ollamaBaseUrl}/api/tags`).catch(() => null);
    if (!reach || !reach.ok) {
      console.error(
        `\n✗ --with-ollama set but ${ollamaBaseUrl} is unreachable.` +
          `\n  Start Ollama (\`ollama serve\`) or drop --with-ollama to use the deterministic summary.\n`
      );
      process.exit(1);
    }
  }

  const config = getProjectConfig(projectPath);
  const indexer = new Indexer(config);
  await indexer.index();

  const report = await runPrune(indexer, {
    dryRun: flags.includes("--dry-run"),
    maxAgeDays: num("--max-age-days", (n) => n > 0),
    staleScoreThreshold: num("--stale-threshold", (n) => n >= 0),
    similarityThreshold: num("--similarity-threshold", (n) => n > 0 && n <= 1),
    minClusterSize: num("--min-cluster-size", (n) => n >= 2),
    withOllama,
    ollamaModel: flagVal("--model"),
    ollamaBaseUrl: flagVal("--base-url"),
  });

  console.log(
    `\n${report.dryRun ? "[dry-run] " : ""}Memory prune complete:\n` +
      `  scanned:               ${report.scanned}${report.truncated ? ` (of ${report.totalActive} active — capped)` : ""}\n` +
      `  marked stale (decay):  ${report.decayed}\n` +
      `  clusters consolidated: ${report.consolidatedClusters}\n` +
      `  members superseded:    ${report.consolidatedMembers}\n` +
      (report.newSemanticMemoryIds.length > 0
        ? `  new semantic ids:      ${report.newSemanticMemoryIds.join(", ")}\n`
        : "") +
      (report.truncated
        ? `\n⚠ Only the ${report.scanned} most-recent memories were scanned. Re-run from a smaller working set or wait for a future flag to lift this cap.\n`
        : "")
  );

  indexer.close();
  process.exit(0);
}

if (command === "--help" || command === "-h") {
  console.log(`
sverklo — code intelligence for AI agents

Just installed? Run these two:
  sverklo init               Set up sverklo in your project (.mcp.json + CLAUDE.md)
  sverklo doctor             Verify MCP dispatch end-to-end (initialize + tools/list + tools/call)

Then restart your AI agent (Claude Code, Cursor, Windsurf, etc.) — sverklo tools become available automatically.

Usage:
  sverklo init               Set up sverklo in your project (.mcp.json + CLAUDE.md)
  sverklo doctor             Diagnose MCP setup issues
  sverklo reindex [path]     Incremental rebuild of the index (changed files only)
                             Use --force to clear and rebuild from scratch.
                             Use --timing to see per-phase elapsed ms.
  sverklo [project-path]     Start the MCP server (stdio transport, single project)
  sverklo                    Start in global mode (serves all registered repos)
  sverklo register [path]    Add a directory to the global registry
  sverklo unregister <name>  Remove a repo from the global registry
  sverklo list               List all registered repositories
  sverklo workspace <subcmd> Manage cross-repo workspaces (see \`workspace --help\`)

Audit / review:
  sverklo audit [path]       Run codebase audit and emit a graded report
  sverklo review             Run risk-scored diff review (CI-friendly; auto-detects PR ref)
  sverklo audit-prompt       Print a ready-to-paste codebase-audit prompt (hybrid workflow)
  sverklo review-prompt      Print a ready-to-paste PR/MR-review prompt (hybrid workflow)
  sverklo history            Show audit grade history and trend over time
  sverklo receipt            Token-spend receipt for your recent AI-agent sessions
  sverklo bench              Run reproducible benchmarks on gin/nestjs/react (checkout only)

Memory + offline maintenance:
  sverklo wakeup             Print compressed project context (for system-prompt injection)
  sverklo wiki               Generate a markdown wiki from the indexed codebase
  sverklo digest             5-line summary of what changed in this project (--since 7d)
  sverklo memory export      Export memories to markdown / Notion / JSON
  sverklo grammars install   Install tree-sitter grammars for the v0.17 opt-in parser
  sverklo prune              Decay stale memories + consolidate similar episodic ones
  sverklo concept-index      Label clusters with an LLM (requires Ollama)
  sverklo enrich-symbols     Add LLM-generated purpose to top-PageRank symbols (requires Ollama)
  sverklo enrich-patterns    Tag top-PageRank symbols with design patterns (requires Ollama)

Setup / runtime:
  sverklo setup              Download the embedding model (~90MB)
  sverklo setup --global     Write global MCP config for Claude Code (multi-repo)
  sverklo ui [project-path]  Open the web dashboard
  sverklo activity           Show recent activity log (always-on audit trail)
  sverklo trace              Show recent tool call traces (set SVERKLO_TRACE=1)
  sverklo telemetry <subcmd> Manage opt-in telemetry (off by default)
  sverklo --help             Show this help

Quick start (single project):
  npm install -g sverklo
  cd your-project && sverklo init
  claude   # start coding — sverklo tools are preferred automatically

Quick start (multi-repo, global):
  sverklo register /path/to/project-a
  sverklo register /path/to/project-b
  sverklo setup --global    # writes ~/.claude/settings.json
  claude                    # sverklo serves both repos via one MCP server

Environment:
  SVERKLO_DEBUG=1   Enable debug logging to stderr
`);
  process.exit(0);
}

// Issue #12: runtime mode resolution. Embedded is the default and does
// what sverklo has always done. Shared and cloud are reserved names
// that print a clear "not yet implemented" message.
const { resolveMode, notYetImplemented, SverkloModeError } = await import("../src/modes.js");
let modeResolution;
try {
  modeResolution = resolveMode(args);
} catch (err) {
  if (err instanceof SverkloModeError) {
    console.error(err.message);
    process.exit(2);
  }
  throw err;
}

if (modeResolution.mode !== "embedded") {
  process.stderr.write(notYetImplemented(modeResolution.mode));
  process.exit(2);
}

// Strip any --mode=... arg before resolving the project path so it
// doesn't get treated as a directory name.
const positionalArgs = args.filter((a) => !a.startsWith("--mode=") && !a.startsWith("--global"));
const hasExplicitPath = positionalArgs.length > 0 && positionalArgs[0] !== undefined;
const isGlobalFlag = args.includes("--global");

// Bug-bash 2 finding: `sverklo search "query"` silently fell through
// to MCP-server-start with "search" treated as a project path. The
// server then hung on stdin waiting for MCP protocol traffic. First-
// time users would conclude sverklo was broken. Detect that case
// up-front and emit a friendly error.
const MCP_TOOL_VERBS = new Set([
  "search", "lookup", "refs", "find-references", "impact", "deps", "dependencies",
  "overview", "context", "ask", "recall", "remember", "forget", "promote", "demote",
  "memories", "ast-grep", "ast_grep", "review-diff", "review_diff", "test-map", "test_map",
  "diff-search", "diff_search", "status", "pin", "unpin", "clusters", "concepts",
  "patterns", "critique", "verify", "investigate", "search-iterative", "search_iterative",
  "grep-results", "grep_results", "head-results", "head_results",
  "ctx-peek", "ctx_peek", "ctx-slice", "ctx_slice", "ctx-grep", "ctx_grep",
  "ctx-stats", "ctx_stats",
]);
if (hasExplicitPath) {
  const candidate = positionalArgs[0];
  if (MCP_TOOL_VERBS.has(candidate.toLowerCase())) {
    process.stderr.write(`Error: \`sverklo ${candidate}\` is an MCP tool, not a CLI command.\n\n`);
    process.stderr.write(`MCP tools are called by AI agents (Claude Code, Cursor, Windsurf, Zed) —\n`);
    process.stderr.write(`not from the command line. To use them:\n\n`);
    process.stderr.write(`  1. cd to your project\n`);
    process.stderr.write(`  2. sverklo init                  # set up MCP integration\n`);
    process.stderr.write(`  3. claude                        # ask in natural language\n\n`);
    process.stderr.write(`If you wanted to start the MCP server directly (e.g. for a custom\n`);
    process.stderr.write(`client), run \`sverklo\` with no args from the project root.\n\n`);
    process.stderr.write(`For CLI commands (init, audit, doctor, ui, …) run \`sverklo --help\`.\n`);
    process.exit(2);
  }
  // Validate the path actually exists, otherwise the user typo'd a
  // command verb and starting an MCP server on a non-existent dir
  // would silently hang on stdin like the case above.
  const { statSync: _stat } = await import("node:fs");
  try {
    if (!_stat(resolve(candidate)).isDirectory()) throw new Error("not a directory");
  } catch {
    process.stderr.write(`Error: \`${candidate}\` is not a valid project path or known command.\n\n`);
    process.stderr.write(`Run \`sverklo --help\` to see available commands, or \`sverklo init\`\n`);
    process.stderr.write(`from a project directory to set up MCP integration.\n`);
    process.exit(2);
  }
}

// Auto-download model if missing (no separate setup step needed)
const { existsSync } = await import("node:fs");
const { join } = await import("node:path");
const { homedir } = await import("node:os");
const modelDir = join(homedir(), ".sverklo", "models");
if (!existsSync(join(modelDir, "model.onnx"))) {
  process.stderr.write("[sverklo] First run — downloading embedding model (~90MB)...\n");
  const { setupModels } = await import("../src/indexer/setup.js");
  await setupModels().catch(() => {
    process.stderr.write("[sverklo] Model download failed. Search will use lightweight embeddings.\n");
  });
}

// Global mode: when no project path is given (or --global is passed),
// check if the registry has repos and start the multi-repo MCP server.
if (isGlobalFlag || !hasExplicitPath) {
  const { getRegistry } = await import("../src/registry/registry.js");
  const repos = getRegistry();
  const repoCount = Object.keys(repos).length;

  if (repoCount > 0 || isGlobalFlag) {
    // Start in global (multi-repo) mode
    const { startGlobalMcpServer } = await import("../src/index.js");
    startGlobalMcpServer().catch((err) => {
      console.error("Failed to start sverklo (global mode):", err);
      process.exit(1);
    });
  } else {
    // No repos registered and no path given — fall through to single-project mode
    // using cwd, matching the original behavior.
    const rootPath = resolve(process.cwd());
    const { startMcpServer } = await import("../src/index.js");
    startMcpServer(rootPath).catch((err) => {
      console.error("Failed to start sverklo:", err);
      process.exit(1);
    });
  }
} else {
  // Single-project mode (backward compatible)
  const rootPath = resolve(positionalArgs[0]);
  const { startMcpServer } = await import("../src/index.js");
  startMcpServer(rootPath).catch((err) => {
    console.error("Failed to start sverklo:", err);
    process.exit(1);
  });
}
