Metadata-Version: 2.4
Name: codecortex
Version: 0.15.5
Summary: Local-first, MCP-native code-intelligence server — graph, LSP, and semantic search behind one safe code.query tool for coding agents.
Author: Shammai Hamilton
License-Expression: MIT
Project-URL: Homepage, https://github.com/hamilton-sky/codeintel
Project-URL: Repository, https://github.com/hamilton-sky/codeintel
Project-URL: Issues, https://github.com/hamilton-sky/codeintel/issues
Project-URL: Changelog, https://github.com/hamilton-sky/codeintel/blob/main/CHANGELOG.md
Keywords: mcp,model-context-protocol,code-intelligence,code-search,llm,agents,lsp,semantic-search,knowledge-graph,static-analysis,developer-tools
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Environment :: Console
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mcp>=1.0
Requires-Dist: sqlite-vec>=0.1
Requires-Dist: fastembed>=0.3
Requires-Dist: tree-sitter-language-pack>=1.0
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-cov>=5; extra == "dev"
Requires-Dist: numpy>=1.24; extra == "dev"
Requires-Dist: ruff>=0.16; extra == "dev"
Requires-Dist: mypy>=1.11; extra == "dev"
Dynamic: license-file

# codeintel

**One MCP tool that lets a coding agent search, trace, and *understand* a codebase — structurally, not by grepping.** codeintel unifies three engines — a call/import **graph**, an **LSP** for exact symbols, and **semantic** embedding search — behind a single `code.query` call that routes to the right engine, caches the answer, and **never throws**. The agent always gets back a clean, well-formed result to reason over.

[![CI](https://github.com/hamilton-sky/codeintel/actions/workflows/ci.yml/badge.svg)](https://github.com/hamilton-sky/codeintel/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/codecortex.svg)](https://pypi.org/project/codecortex/)
[![Python](https://img.shields.io/pypi/pyversions/codecortex.svg)](https://pypi.org/project/codecortex/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

> **Status: beta (`0.x`), and young.** The `code.query` surface is one call and has been stable
> since `0.8`, the suite is thorough, and every release is gated by a canary that runs a real query
> against a built wheel. But this is a new project with a single maintainer, and each time it has
> been pointed at an unfamiliar codebase it has found real bugs. **Use it locally, on a developer
> machine, for a single user** — that is the case it is built and tested for. Before relying on it
> for anything beyond that, read **[Project status](#project-status)**.

![codeintel's own call graph — an interactive, self-contained HTML view with force / radial / layered / module layouts, complexity-sized nodes, and click-to-inspect metrics.](docs/images/graph-codeintel.png)

> *codeintel visualizing its own codebase.* One command — `codeintel graph <repo> --html` — turns any indexed repo into a **self-contained, interactive call graph** you can open offline or share as a file. Layouts, complexity-sized nodes, click-to-inspect metrics, and JSON/Markdown/SVG/PNG export. See **[docs/graph-viewer.md](docs/graph-viewer.md)**.

Prefer plain text? `codeintel map` writes a **readable architecture overview** to `CODE_INTEL.md` — node/edge counts, ranked symbols by caller count, and entry points — for skimming or for MCP hosts that can't render a graph:

<img src="docs/images/code-intel-map.png" width="520" alt="CODE_INTEL.md — the codeintel map: an architecture overview with node/edge counts and symbols ranked by caller count.">

**What `CODE_INTEL.md` is for.** It's a *static, committable* snapshot of a codebase's shape — meant to be read (by a person or an agent) **first**, instead of reconstructing structure by grepping. It covers the cases the live `code.query` tool doesn't:

- **Agents & hosts that don't speak MCP.** Not every agent supports MCP, and the server isn't always running. `codeintel map` writes a plain file any agent can read; `codeintel map --inject` also drops a pointer into `CLAUDE.md` / `AGENTS.md`, so an agent picks up the codebase's structure automatically at the start of a session.
- **A committed, diffable overview.** It lives *in the repo* — reviewable in a PR, browsable on GitHub, available offline. Re-run `codeintel map` after `codeintel index` to refresh it.
- **The load-bearing code at a glance.** Ranking symbols by caller count surfaces what most of the codebase depends on (the risky-to-change core) plus the entry points — the first things a newcomer, or an agent, should understand before touching anything.

See **[docs/map-file.md](docs/map-file.md)** for the format and the `--inject` flow.

## Why an agent needs it

Without structural tools, an agent dropped into unfamiliar code falls back on `grep` and reads whole files to reconstruct relationships by hand — burning tokens, missing call sites, and guessing at blast radius before it edits anything. codeintel answers those questions directly instead:

- **"What calls this? What breaks if I change it?"** → the real call graph, which catches cross-file and module-level callers a text search silently misses.
- **"Where is this symbol defined, and everywhere it's used?"** → the language server, with exact locations.
- **"Where's the code that does X?"** (when you don't know the name) → semantic search over the repo.
- **Always a clean answer.** Every call returns the same JSON envelope. A missing or broken backend degrades to a safe `null` *with a reason* — so the agent falls back to grep instead of crashing on an exception it can't reason its way out of.

Net effect: fewer, sharper tool calls, less re-reading, and an agent that can see *structure* — callers, impact, call chains — that plain search can't.

## What your agent can ask

It's one call: `code.query(op, target, engine="auto")`. In `auto` mode (the default) codeintel picks the engine per operation:

| Ask | `op` | Engine (auto) | Comes back as |
|---|---|---|---|
| Find code by meaning ("auth middleware") | `search` | semantic | ranked `path:line │ snippet` hits |
| A symbol's definition **and** all references | `symbol` | lsp | definition body + reference list |
| Who calls this? | `callers` | graph | caller symbols + files |
| What does this call? | `callees` | graph | callee symbols + files |
| Blast radius of a change | `impact` | graph | callers **and** callees together |
| Trace a call chain up/downstream | `chain` | graph | ordered, risk-labeled hops |
| Find symbols by pattern | `pattern` | graph | matching nodes + locations |
| Project shape at a glance | `overview` | graph → lsp | modules, node/edge counts, languages |
| Everything about one symbol | `context` | graph + lsp | both views merged |
| **Impact of your uncommitted edits** | `changed` | graph | changed files → impacted symbols |
| Refactor-risk hotspots | `hotspots` | graph | highest complexity / fan-in symbols |
| Unreferenced (dead) code | `deadcode` | graph | **withdrawn** — measured wrong in both directions on real repos; safe-nulls with `reason: "op-withdrawn"` unless you opt in — [why, and what to use instead](#deadcode-is-withdrawn) |

Pin one engine with `--engine graph│lsp│semantic`, or fan out with `--engine both` / `all` to merge results.

#### `deadcode` is withdrawn

`deadcode` is withdrawn: it returns a safe-null (`reason: "op-withdrawn"`) instead of running. It
was measured wrong in **both** directions on real repositories — on one it named five candidates of
which four were live code (a rollup plugin hook; two entries of a `Record<string, fn>` reached by a
runtime string; a `predicate` passed inline to the call that consumes it), and on another it
reported "(none found)" for a 4,883-function codebase that had at least three genuinely unreferenced
private helpers. It is the one op whose output is an instruction to delete code, so it needs the
highest evidence bar of any op here, and currently has the least. It returns when a labelled corpus
measures its precision and recall — not before.

**Use `callers` on a specific symbol instead.** "Does anything call this?" is exactly the question
`deadcode` was trying to answer in bulk, and `callers` answers it accurately, one symbol at a time.

**Escape hatch, if you understand the risk.** Set `CODEINTEL_ENABLE_UNVERIFIED_OPS=1` to run it
anyway. It still re-reads the source before reporting a hit, which removes the *common* false
positives — but that verification is exactly what was measured wrong on the repositories above, so
**review every hit before deleting anything, and never wire it into an agent that deletes without a
human in the loop.**

**Example — "who uses `safe_null_result`?"**

```jsonc
// request
{ "op": "callers", "target": "safe_null_result", "engine": "auto" }

// response — always this exact envelope; `result` is ready-to-read markdown
{
  "ok": true, "op": "callers", "target": "safe_null_result",
  "engine": "graph", "cached": false,
  "result": "## Callers of safe_null_result (7)\n- …gateway [USAGE] (src/codeintel/gateway.py)\n- …providers.graph [USAGE] (src/codeintel/providers/graph.py)\n- …server [USAGE] (src/codeintel/server.py)\n- … (4 more)"
}
```

The agent hands `result` straight to the model. If the graph backend isn't installed, the identical call returns `"result": null, "reason": "engine-unavailable"` — no exception, and the agent just falls back to its own search.

## What makes it good

- **Local-first and private.** One process on your machine — no cloud service, no API keys, no telemetry, no per-query network. Safe to point at a private repo, even with `--engine all`. (The one-time exception: `fastembed` downloads its embedding model once, then runs fully offline.)
- **It never throws.** Every call returns the same JSON envelope; a missing or broken backend degrades to `null` *with a reason*. No exceptions, no 500s, no malformed output for the agent to trip over — so you never wrap `code.query` in a `try`.
- **One tool, not three.** Register a single MCP server and it auto-routes each question to graph, LSP, or semantic — instead of wiring up three backends with three response shapes and three failure modes.
- **Degrades instead of breaking.** No graph backend installed? That engine returns `null` and the agent falls back to grep. The semantic engine needs nothing external, so codeintel is useful the moment it's installed and only gets sharper as you add backends.
- **Fast on repeat, and the cache never lies.** A content-hash cache returns instantly for unchanged code and self-invalidates when a background reindex advances the index, so you never read a cached answer for code that moved on. The cache is bounded (LRU), so a long-running server holds steady memory. (The *cache* is always consistent with the index; how current the index itself is depends on the engine — see [Keeping answers fresh](#keeping-answers-fresh).)
- **Concurrency-safe.** The HTTP transport handles requests on threads, so one slow query (an LSP session warming, a first-time index) can't block every other agent.
- **Honest about its own health.** `codeintel doctor` answers three separate questions per engine — *installed?* *runnable?* *is this repo indexed?* — with the single command to fix each gap, so "installed" is never mistaken for "working". And a readiness claim is one a query can actually honor: install a missing backend mid-session and the running server picks it up on the next call, rather than reporting the engine healthy while quietly routing around it until you restart the host.

## Quickstart

```bash
pip install codecortex
```

This installs the `codeintel` CLI; the **semantic** engine works out of the box. (On PyPI the
distribution is `codecortex` because `codeintel` was taken; the CLI and import stay `codeintel`.)

**One command prepares the rest and indexes your repo:**

```bash
codeintel setup --all /path/to/your/project
```

This installs `uv` (for the LSP engine), warms serena, downloads the embedding model, indexes the
repo, and prints a health report ending in a **Next:** list — exactly what's ready and the one
remaining step. It's idempotent, so re-running is safe. The **graph** engine (`codebase-memory-mcp`)
is an *optional* external binary that adds who-calls / impact / hotspots / `changed`; codeintel is
fully usable without it.

Or from source:

```bash
git clone https://github.com/hamilton-sky/codeintel.git
cd codeintel
pip install -e .
```

Register with your AI agent(s), then query:

```bash
codeintel install            # registers with the agents you actually have installed
codeintel query --op search --target "authentication middleware"
```

### Enable native Codex integration

`codeintel` is an MCP server, so Codex can call its tools directly rather than invoking the CLI.
After installing the package, explicitly register it with Codex:

```bash
codeintel install --agent codex
```

This safely adds a `[mcp_servers.codeintel]` entry to `~/.codex/config.toml` (or `$CODEX_HOME/config.toml`
when that is set) without changing your other Codex settings. Registration is deliberately opt-in:
installing a Python package should not silently modify an agent's configuration. Start a new Codex
task (or restart Codex) after registration; the refreshed task will have native `code.query`,
`code.status`, `code.doctor`, and `code.map` MCP tools available.

For a fully prepared local setup, run:

```bash
codeintel setup --all /path/to/your/project && codeintel install --agent codex
```

### Enable native Claude Code integration

After installing the package, explicitly register it with Claude Code:

```bash
codeintel install --agent claude
```

This adds the `codeintel` MCP server to `~/.claude.json` (or `$CLAUDE_CONFIG_DIR/.claude.json`)
while preserving your existing configuration — that is the file Claude Code reads for user-scope
MCP servers, and you can confirm the entry with `claude mcp list`. Start a new Claude Code session
after registration so it can load the native `code.query`, `code.status`, `code.doctor`, and
`code.map` MCP tools.

> **Upgrading from ≤ 0.11.1?** Earlier versions wrote this block to `~/.claude/settings.json`,
> which Claude Code ignores for MCP registration — so codeintel never actually loaded. Re-run
> `codeintel install --agent claude`; it registers in the right place and points out the stale
> entry so you can delete it.

For a fully prepared local setup, run:

```bash
codeintel setup --all /path/to/your/project && codeintel install --agent claude
```

### Registration is verified, not assumed

**It only touches agents you have.** `codeintel install` defaults to `--agent auto`: it registers
the hosts whose config root already exists on this machine and *names the ones it skipped*.
Installing a Python package should not create `~/.gemini/` and `~/.config/zed/` for someone who has
neither. Force a specific host with `--agent claude|codex|gemini|zed`, or every supported host with
`--agent all`.

**It registers an absolute path.** The bare name `codeintel` is resolved by the *host*, not by the
shell you ran `install` in — and a GUI-launched desktop agent does not source your shell profile, so
a command your terminal finds is routinely invisible to the app. That is the one failure a handshake
run in your terminal cannot catch, because it inherits the PATH that works. Pass
`--relative-command` for the bare name. If a later upgrade moves the binary, re-running
`codeintel install` repairs the stale path in place, leaving the rest of your config untouched.

Then it launches the exact command it registered and drives a real MCP handshake —
`initialize` → `tools/list` — and reports what came back:

```text
v claude: registered at /Users/you/.claude.json

v verified: codeintel 0.11.2 — 4 tools (code.query, code.status, code.doctor, code.map)
```

If the command is not on `PATH`, or the server fails to start, install says so and exits non-zero
instead of reporting a success your agent cannot use. Pass `--no-verify` to skip the handshake.

The same principle gates releases. Because every result is a safe envelope with `ok: true` and the
CLI never throws, an exit-code smoke test passes against a build that boots cleanly and answers
nothing — so **[`scripts/release_canary.py`](scripts/release_canary.py)** runs before every publish
against the built wheel in a clean environment: it registers Codex and Claude Code into a throwaway
`HOME`, launches the command those config files name, and asserts on the **answer text** of a real
`code.query` over a fixture repo. A release that writes a config no host reads, or that returns
`ok: true` with nothing in it, fails there instead of on your machine.

> Full reference — what each host reads, the absolute-path rationale, and troubleshooting:
> **[docs/install.md](docs/install.md)**.

## How it works

A `Gateway` receives every query and dispatches it to one of three providers — graph (structural relationships), LSP (precise symbol resolution), or semantic (embedding-based search) — based on the operation type. Each provider is fully isolated: if it is unavailable or raises an exception, the gateway catches it and returns a safe-null envelope. The caller always gets a well-formed response with no exception to catch.

```mermaid
flowchart LR
    A["AI agent · MCP"] --> GW
    H["Harness · HTTP"] --> GW
    C["Developer · CLI"] --> GW
    GW["Gateway<br/>route · cache · safe-null"] -->|"auto: search"| SP[SemanticProvider]
    GW -->|"auto: impact / callers / …"| GP[GraphProvider]
    GW -->|"auto: symbol"| LP[LspProvider]
    GP --> GB[("codebase-memory-mcp")]
    LP --> LB[("language server")]
    SP --> SB[("fastembed + sqlite-vec")]
```

> Full walkthrough: **[docs/architecture.md](docs/architecture.md)** · **[docs/query-flow.md](docs/query-flow.md)**.

## Safe-null contract

Every `Gateway.query()` call returns a dict with exactly these keys:

```json
{"ok": true, "op": "search", "target": "auth", "result": null, "engine": "semantic", "cached": false}
```

`ok` is always `true`. `result` is `null` when no provider has an answer — never an exception, never a 500. Callers must check `result is not None` before using the value.

The optional keys are the ones worth reading when an answer surprises you:

| Key | Meaning |
|---|---|
| `reason` | Why `result` is null — `engine-unavailable`, `no-result`, `not-in-graph` (the symbol isn't in the index — usually a stale index), `project-not-indexed`, `unsupported-op`, `root-not-allowed-for-role` (RBAC) |
| `hint` | The specific command that resolves this `reason`, when there is one |
| `engine` | Which engine actually answered — not necessarily the one you asked for, under `auto` |
| `cached` | Whether it came from the content-hash cache |
| `reindexing` | Present and `true` when a reindex was running, i.e. the answer reflects the last *completed* index. See [Keeping answers fresh](#keeping-answers-fresh) |

`codeintel query --json` prints this envelope from the CLI.

## Engines

| Engine | Key ops | Install prereq |
|---|---|---|
| `graph` | `impact`, `callers`, `callees`, `chain`, `pattern`, `overview`, `context` | `codebase-memory-mcp` **0.9.x** on PATH (`pip install 'codebase-memory-mcp==0.9.*'`) — 0.10.x changed its response format and returns nothing for every op but resolution; see [docs/graph.md](docs/graph.md) |
| `lsp` | `symbol`, `overview`, `context` | `uvx` on PATH — serena is fetched from GitHub on first use; see [docs/lsp.md](docs/lsp.md) |
| `semantic` | `search`, `context` | `fastembed` + `sqlite-vec` (installed with the package) — see [docs/semantic.md](docs/semantic.md) |

Run `codeintel doctor` at any time to see which engines are actually ready for a repo and how to fix the ones that aren't.

### Keeping answers fresh

The three engines have genuinely different freshness models, and it's worth knowing which you're
reading:

| Engine | Freshness |
|---|---|
| `lsp` | **Live.** Reads your files at query time — always current, no refresh needed. |
| `semantic` | **Incremental.** A background reindex re-embeds only what changed; `codeintel status` shows the index age. |
| `graph` | **Snapshot.** Built by `codeintel index` and stale until the next one. |

So a `callers`/`impact`/`hotspots` answer is only as current as your last index. If a result
describes code you just changed — or a symbol you just added comes back
`reason: "not-in-graph"` — that's the signal to re-run:

```bash
codeintel index /path/to/repo
```

The reply names the fix when it can: a missing symbol now returns a `hint` with the exact command
rather than a bare reason.

One more honest caveat. For targets that are **symbol names or free text** (most of them —
`callers`, `impact`, `hotspots`, `search`), there is no file whose content hash could change, so a
cached answer is invalidated only when a background reindex completes, and those are debounced
(~30s). An edit followed immediately by the same query can therefore return the pre-edit answer.
Targets that are real file paths are content-hashed and refresh as soon as the bytes change.

Pass `--engine auto` (the default) and codeintel chooses the best engine per operation. Pass `--engine both` or `--engine all` to fan out to multiple engines and merge results.

## Documentation

Full system docs live in [`docs/`](docs/) — start with the index:

- **[Architecture](docs/architecture.md)** — layers, the `CodeProvider` protocol, the safe-null contract, caching, freshness (ASCII + Mermaid).
- **[Install & registration](docs/install.md)** — what each agent host actually reads, why the registered command is an absolute path, and the three levels of proof that registration worked.
- **[Query flow](docs/query-flow.md)** — request lifecycle, engine selection, fan-out & merge, and why it never throws.
- **[Map file](docs/map-file.md)** — the static `CODE_INTEL.md` orientation layer for hosts with no MCP support.
- **[Benchmarks](docs/benchmarks.md)** — real numbers at scale: 25 k chunks indexed in ~8 min, ~235 ms warm queries, 60 MB index.
- Engine references: **[graph](docs/graph.md)** · **[lsp](docs/lsp.md)** · **[semantic](docs/semantic.md)**.

## CLI reference

| Command | Purpose |
|---|---|
| `codeintel help` | Every command grouped by task, with descriptions and examples (also the bare `codeintel`). A mistyped command suggests what you meant. |
| `codeintel install [--agent auto\|claude\|codex\|gemini\|zed\|all] [--no-verify] [--relative-command]` | Register codeintel with the agents installed on this machine (`auto`, the default), then prove it by completing a real MCP handshake against the registered command |
| `codeintel setup [project_root] [--all] [--index] [--warm] [--install-uv] [--install-deps] [--json]` | Prepare backends + index this repo (`--all` = one command: do everything automatable, idempotent); ends with a health report + **Next:** steps |
| `codeintel index [project_root]` | Index a project for semantic search |
| `codeintel serve` | Start the MCP server (stdio transport) |
| `codeintel serve-http [--host HOST] [--port 8766] [--allow-remote] [--token TOKEN]` | Start the HTTP transport (loopback-only unless `--allow-remote`; `--token` requires a bearer token on every request) |
| `codeintel query --op OP --target TARGET [--engine auto] [--project-root DIR] [--json]` | Run a single query and print the result |
| `codeintel status [project_root]` | Show engine availability and index age |
| `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap |
| `codeintel map [project_root]` | Generate the `CODE_INTEL.md` orientation file |
| `codeintel graph [project_root] [--html] [--out FILE] [--limit N]` | Emit the call graph as `{nodes,edges}` JSON, or `--html` a self-contained interactive viewer — see [docs/graph-viewer.md](docs/graph-viewer.md) |
| `codeintel reset [project_root] [--all] [--yes] [--json]` | Clear the semantic index (this repo, or `--all`) to recover from a corrupt/stale DB |
| `codeintel gen-token` | Print a secure random bearer token (for `serve-http` / RBAC `auth.toml`) |

Human-facing commands (`doctor`, `status`, `query`, `setup`, `reset`) honor `--no-color` / `NO_COLOR` and `--ascii`, and auto-degrade to plain text when piped.

**Exit codes**, so a `make` target or CI step can gate on `$?`:

| | |
|---|---|
| `0` | The command did its job. For `query` this includes an empty result — "nothing found" is an answer, not a failure. |
| `1` | The command could not do its job: a file it exists to write wasn't written (`map`, `graph`), an index didn't happen (`index`), a project root doesn't exist, or an engine is unhealthy (`doctor`, `setup`). |
| `2` | Bad usage — an unknown command or a missing required flag. |

## Config

Create `.codeintel.toml` at your project root to override defaults:

```toml
backend          = "auto"                   # auto | graph | lsp | semantic
semantic         = "on"                     # on | off
reindex          = "on-demand"              # on-demand | never
cosine_floor     = 0.25                     # minimum similarity score for semantic hits (0–1)
max_chunks       = 500                      # max chunks to embed per file
max_total_chunks = 100000                   # safety ceiling on chunks embedded in one index pass
model            = "BAAI/bge-small-en-v1.5" # fastembed embedding model
```

Config is **validated on load** — an out-of-range number, a misspelled enum, or a wrong type falls back to that key's default (with a logged warning) instead of breaking every query.

**Environment variables:**

| Variable | Effect |
|---|---|
| `CODEINTEL_HTTP_TOKEN` | Bearer token required by `serve-http` (equivalent to `--token`) |
| `CODEINTEL_AUTH_CONFIG` | Path to an RBAC token→role config (default `~/.codeintel/auth.toml`) — per-token roles + op scopes |
| `CODEINTEL_LOG_LEVEL` | `DEBUG`\|`INFO`\|`WARNING`(default)\|`ERROR` for the server logger |
| `CODEINTEL_LOG_FORMAT=json` | Structured (JSON-per-line) logs for ELK / Splunk / Datadog |
| `CODEINTEL_HTTP_ACCESS_LOG=1` | One log line per HTTP request (method, path, status, latency) |
| `CODEINTEL_DEBUG=1` | Log the full traceback of any error the never-throw contract swallows (silent by default) — the switch for diagnosing an unexpected `null` |
| `CODEINTEL_REINDEX=off` | Disable the background reindexer; queries then index inline to stay fresh |
| `CODEINTEL_HOME` | Where the index cache lives (default `~/.codeintel`). Set this when the process has **no resolvable home directory** — a container running as a UID with no passwd entry and no `$HOME`, which is common when an agent runs in one. Without it, `Path.home()` raises and every command fails somewhere far from the cause. |

## Privacy & dependencies

**codeintel is local-first** — one local process, no cloud service, no API keys, no telemetry, and no per-query network. Its own code makes zero outbound HTTP calls, and the HTTP transport binds to `127.0.0.1` only by default — binding a non-loopback host requires `--allow-remote`, and `--token` (or `CODEINTEL_HTTP_TOKEN`) then gates every request behind a bearer token. The server bounds concurrent connections, but for exposure to a hostile network you should still front it with a reverse proxy (TLS, rate-limiting) — the built-in `http.server` is not hardened for the open internet.

**Bundled (installed with the package, run locally):** `mcp` (the tool interface) · `sqlite-vec` (the semantic index, a local DB file) · `fastembed` (the local embedding model).

**Optional external backends** — auto-detected on `PATH`; if one is absent, that engine returns a safe-null and the agent simply degrades to grep:

| Engine | Needs on `PATH` | Third-party? |
|---|---|---|
| `graph` | `codebase-memory-mcp` | yes — external CLI |
| `lsp` | `uvx` (fetches & runs serena from GitHub on first use) | yes — [oraios/serena](https://github.com/oraios/serena) |
| `semantic` | *nothing external* | no — fully in-house |

Not sure what's installed? `codeintel doctor` reports exactly which backends are present, whether this repo is indexed, and the command to fix each gap.

**The only network touch is first-run setup:** `fastembed` downloads the `BAAI/bge-small-en-v1.5` weights once (cached under `~/.cache`, fully offline thereafter); the optional backends also install on first use *if you opt in*. After that, **no code or data leaves your machine** — which is what makes `--engine all` safe to run on a private repo.

## For agents

Register codeintel as an MCP server (`codeintel install`) and the agent gets four tools:

| MCP tool | HTTP equivalent | Purpose |
|---|---|---|
| `code.query` | `POST /code/query` | The main call — search, trace, understand (the `op` table above) |
| `code.status` | `GET /code/status` | Per-engine `installed` / `runnable` / `repo_indexed`, probed against the live engines a query actually hits |
| `code.doctor` | `POST /code/doctor` | Per-engine health + repo index status, with a fix for each gap |
| `code.map` | — | Generate/refresh `CODE_INTEL.md`, a static orientation file for hosts without MCP |

Over MCP the agent calls `code.query` directly. Over HTTP, start the server and POST to `/code/query`:

```bash
codeintel serve-http &   # listens on 127.0.0.1:8766 by default
```

For a shared or remote deployment, start it with `--allow-remote --token "$CODEINTEL_HTTP_TOKEN"` and send `Authorization: Bearer <token>` on each request — a missing or wrong token gets a clean `401`. Requests are handled concurrently, so one slow query never blocks another.

```python
import urllib.request, json

def code_query(op: str, target: str, engine: str = "auto") -> dict:
    body = json.dumps({"op": op, "target": target, "engine": engine}).encode()
    req = urllib.request.Request(
        "http://127.0.0.1:8766/code/query",
        data=body,
        headers={"Content-Type": "application/json"},
    )
    with urllib.request.urlopen(req) as resp:
        return json.loads(resp.read())

result = code_query("search", "authentication middleware")
if result["result"] is not None:
    print(result["result"])   # ranked semantic matches
```

The response is always JSON-safe. Check `result["result"] is not None` before use. Never catch an exception from the gateway — it never raises.

## Operations & deployment

Running codeintel as a shared service? It ships with what ops teams expect:

| Endpoint | Auth | Purpose |
|---|---|---|
| `GET /healthz` | none | Liveness — always `200` (for load balancers / `livenessProbe`) |
| `GET /readyz` | none | Readiness — `200` once the gateway is up (`readinessProbe`) |
| `GET /metrics` | token | Prometheus exposition — request counts, latency, in-flight, build info |

Plus **bearer-token auth** — or **RBAC** (per-token roles + op scopes via `auth.toml`; a disallowed op returns `403`, and the role is server-authoritative so a client can't escalate) — **structured JSON logs** (`CODEINTEL_LOG_FORMAT=json`) with optional per-request access logs, **graceful `SIGTERM`** shutdown, a bounded connection pool, and a non-root **Dockerfile** with a healthcheck.

**Full guide → [docs/deploy.md](docs/deploy.md)**: systemd, Docker / Compose, Kubernetes (liveness + readiness probes, token from a Secret), reverse-proxy TLS, **RBAC + SSO-via-auth-proxy**, a Prometheus scrape config, and a security checklist.

```bash
docker build -t codeintel . && docker run -p 127.0.0.1:8766:8766 \
  -e CODEINTEL_HTTP_TOKEN="$(openssl rand -hex 32)" codeintel
```

## Project status

An honest picture, so you can decide what to trust this with.

**What's solid.** The suite is large and real — more test code than source, a coverage floor
enforced in CI, and fault-injection tests behind the never-raise contract. CI runs lint, `mypy`,
and the full suite on Python 3.11/3.12/3.13, then builds the wheel, installs it into a clean
environment, and runs a **release canary** that registers the build with Codex and Claude Code in a
throwaway `HOME`, boots the server those configs name, and asserts on the answer text of a real
`code.query`. A release that installs into a file no host reads, or that boots and answers nothing,
fails before it ships. The `code.query` envelope has been stable since `0.8`.

**What's young.** The project is pre-1.0 and moves fast. The honest signal is in the
[CHANGELOG](CHANGELOG.md): `0.15.0` and `0.15.1` were written almost entirely from pointing the
tool at four repositories it had never seen, and several of those defects had survived multiple
adversarial review rounds. **The rate at which new codebases surface new bugs has not yet flattened.**
Expect to be the first person to hit something, and please report it — see below.

**Use it for.** Local, single-user code intelligence on a developer machine. That is the designed
case, it is the tested case, and the blast radius of a wrong answer is a wasted tool call: the
safe-null contract means a failing engine degrades to `null` with a reason, so your agent falls
back to grep rather than crashing.

**Be careful with.**

| Area | Why |
|---|---|
| `deadcode` | Withdrawn by default (`reason: "op-withdrawn"`) — measured wrong in both directions on real repos. Use `callers` on a specific symbol instead — [details](#deadcode-is-withdrawn). |
| Non-loopback serving | `serve-http` is stdlib `http.server`. It binds loopback by default for a reason; front it with a reverse proxy and see [docs/deploy.md](docs/deploy.md). |
| RBAC between **untrusting** tenants | It separates privilege levels among callers you already trust. It is not a wall against an adversary with write access to their own root — see the warning in [docs/deploy.md](docs/deploy.md). |
| Unattended automation | Anything that acts on a result without a human reading it deserves a pilot first. |

**On the test numbers.** The suite is large and the coverage floor is enforced, but read the figure
with its caveat. In the **main test job** the graph and LSP backends are absent, so their live tests
skip and those engines run against hand-authored mocks rather than the real wire contract. Separate
jobs cover the contract itself: `graph-contract` installs the pinned `codebase-memory-mcp` and runs
the live graph tests — and **fails if they skipped**, because a silently-skipped contract test is
how a total backend outage stayed green here once — while the nightly corpus job runs that same
real backend against pinned third-party repositories. `lsp-contract` runs the live serena tests but
is **`continue-on-error`**: serena is fetched from an upstream git HEAD this project does not
control, so a breakage there must be visible without blocking an unrelated release. Read that as
the LSP wire contract being *watched* rather than *gated*. The release canary — the only check that
asserts on real answer text from a built wheel — still covers **the semantic engine only**.

Line coverage measures how much of the intended behavior runs, not how much of reality it has met.

**The honest one-paragraph version.** codeintel has been run on very few repositories its author did
not write, and that is where its bugs have come from — every fix in `0.15.x` came from pointing it
at an unfamiliar codebase. Its characteristic failure mode is **answering confidently from the
wrong index rather than failing loudly**, which the never-raise contract makes harder to notice: a
wrong answer and a right one are the same shape. Run `codeintel doctor` before trusting a repo-wide
answer — and `deadcode` in particular is withdrawn rather than merely caveated (see above) — and if
something looks off please [report it](#reporting-a-problem) — an issue from someone who is not the
author is the single most useful thing this project can receive right now.

**Engine coverage depends on external binaries.** Semantic search works out of the box. The graph
engine needs `codebase-memory-mcp` and the LSP engine needs `uvx` on `PATH` — without them those
engines safe-null and you get a fraction of the capability table above. `codeintel setup --all`
installs what it can and `codeintel doctor` tells you exactly what is missing and how to fix it.
Run `doctor` first if the tool seems quieter than the docs suggest.

**Maintenance.** One maintainer, MIT licensed, issues and PRs welcome. There is no support
guarantee — factor that into anything load-bearing.

## Reporting a problem

`codeintel doctor --json` prints a complete, machine-readable picture of what's installed, what's
runnable, and whether this repo is indexed — per engine, with the remediation for each gap. Paste
it into an issue and the report is actionable immediately instead of needing a round trip:

```bash
codeintel doctor --json
```

It reports only local engine and index state. Over the HTTP transport the `registrations` field —
which names agent config files on the machine running the server — is deliberately omitted.

If a *result* looks wrong rather than a command failing, send the envelope rather than the
rendered text:

```bash
codeintel query --op callers --target yourSymbol --json
```

`engine` says which engine answered, `cached` whether it came from the cache, `reindexing` whether
the index was mid-rebuild, and `reason`/`hint` why an empty answer was empty. Those five fields are
usually the whole diagnosis.

## Development

```bash
git clone https://github.com/hamilton-sky/codeintel.git
cd codeintel
pip install -e .[dev]

pytest tests/ -q            # ~620 tests, ~30s; fails under 83% coverage
ruff check src tests        # lint
mypy                        # types (src/ only)
```

**Your local run is not CI's run.** A dev machine usually has `codebase-memory-mcp` and `uvx`
installed; CI has neither, so the live graph/LSP tests skip there *and* the never-raise envelopes
take different `reason`/`hint` paths. A bug reachable only on the no-backend path passes at your
desk and fails in CI. To see CI's shape before you push:

```bash
env PATH="$(dirname "$(which python)"):/usr/bin:/bin" pytest -q
```

**Release gate.** The unit suite runs against the source tree, so it cannot see a packaging break, a
missing entry point, a host config written where nobody reads it, or a server that boots and answers
nothing. Run the canary against the built wheel in a clean environment — the same check CI runs
before publishing:

```bash
python -m build && python -m venv /tmp/canary && /tmp/canary/bin/python -m pip install dist/*.whl && /tmp/canary/bin/python scripts/release_canary.py
```

It exits non-zero on the first failed check and cleans up the temporary `HOME` it installs into.
