gitcortex 0.4.0

Git-aware code knowledge graph — incremental AST indexing on every commit, MCP server for AI assistants
use std::{fs, path::Path};

use anyhow::{Context, Result};

const AGENTS_SECTION: &str = r#"# GitCortex - Codex Guide

## What This Repo Is

GitCortex (`gcx`) is a local-first, branch-aware code knowledge graph for Git repositories. It indexes changed files with tree-sitter, stores graph data in embedded KuzuDB, and exposes graph queries to AI coding assistants through MCP and the `gcx` CLI.

## Workspace Layout

- `crates/gitcortex-core`: shared schema, graph types, and `GraphStore` trait. No I/O, no async.
- `crates/gitcortex-indexer`: sync tree-sitter parsers and incremental indexing.
- `crates/gitcortex-store`: KuzuDB backend implementing `GraphStore`.
- `crates/gitcortex-mcp`: MCP server and async boundary.
- `crates/gitcortex-cli`: `gcx` CLI commands.
- `crates/gitcortex-viz`: Rust-side viz embedding support.
- `viz`: React/Vite graph UI.
- `tests/integration/fixtures`: small language fixtures.
- `.gitcortex`: repo-level GitCortex config and generated context.

## Architecture Rules

- `gitcortex-core` must stay pure: no filesystem, subprocesses, network, or async.
- `gitcortex-indexer` and `gitcortex-store` are sync-only.
- `tokio` belongs only in `gitcortex-mcp` and CLI/server boundaries.
- Store behavior goes through the `GraphStore` trait; do not make indexer or MCP depend on concrete Kuzu details unless that layer owns the concern.
- Library code in `core`, `indexer`, and `store` should not introduce `.unwrap()` or `.expect()`. Propagate errors with the crate error type.
- Hook and incremental-index paths must stay fast. Avoid broad scans, unbounded allocations, and expensive work on every git operation.
- Keep changes surgical. Do not refactor adjacent code unless it is required for the task.

## GitCortex Graph Use

This repo is intended to be indexed by GitCortex. Use the compact GitCortex MCP server first for structural questions; it exposes one dispatch tool (`gcx`) so Codex gets graph access without loading the full tool-schema surface. Read source files after the graph narrows the search.

Useful commands:

```bash
gcx query search <fragment>
gcx query lookup-symbol <name>
gcx query find-callers <name>
gcx query find-callees <name>
gcx query list-definitions <path>
gcx query wiki <name>
gcx query tour --limit 12
gcx blast-radius --base main --head HEAD --format text
```

If graph queries fail with Kuzu schema or lock errors, fall back to `rg` and note that the local graph store likely needs `gcx clean && gcx init`.

## Development Commands

Prefer `just` entry points when available:

```bash
just fmt
just fmt-check
just clippy
just test
just ci
just build
```

Direct equivalents:

```bash
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
cd viz && npm run lint
cd viz && npm run test --if-present
cd viz && npm run build
```

If `gitcortex-mcp` build fails because `viz/dist` files are missing, build the frontend first:

```bash
cd viz && npm ci && npm run build
```

## Review Stance

When asked for a review, lead with concrete bugs and risks. Include file and line references, severity, and suggested fixes. Skip broad summaries unless there are no findings.
"#;

pub fn install(repo_root: &Path) -> Result<()> {
    write_agents_md(repo_root)?;
    write_codex_config(repo_root)?;
    Ok(())
}

fn write_agents_md(repo_root: &Path) -> Result<()> {
    let path = repo_root.join("AGENTS.md");

    if path.exists() {
        let existing = fs::read_to_string(&path).context("read AGENTS.md")?;
        if existing.contains("GitCortex - Codex Guide") {
            return Ok(());
        }
        fs::write(&path, format!("{existing}\n\n{AGENTS_SECTION}")).context("update AGENTS.md")?;
    } else {
        fs::write(&path, AGENTS_SECTION).context("write AGENTS.md")?;
    }
    Ok(())
}

fn write_codex_config(repo_root: &Path) -> Result<()> {
    let dir = repo_root.join(".codex");
    fs::create_dir_all(&dir)?;
    let path = dir.join("config.toml");
    const CODEX_MCP: &str = r#"[mcp_servers.gitcortex]
command = "gcx"
args = ["serve", "--compact"]
startup_timeout_sec = 30
"#;

    if path.exists() {
        let existing = fs::read_to_string(&path).context("read .codex/config.toml")?;
        if existing.contains("[mcp_servers.gitcortex]") {
            return Ok(());
        }
        fs::write(&path, format!("{}\n\n{CODEX_MCP}", existing.trim_end()))
            .context("update .codex/config.toml")?;
    } else {
        fs::write(&path, CODEX_MCP).context("write .codex/config.toml")?;
    }
    Ok(())
}