codecortex 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {codecortex-0.2.0/src/codecortex.egg-info → codecortex-0.2.2}/PKG-INFO +69 -5
  2. {codecortex-0.2.0 → codecortex-0.2.2}/README.md +68 -4
  3. {codecortex-0.2.0 → codecortex-0.2.2/src/codecortex.egg-info}/PKG-INFO +69 -5
  4. {codecortex-0.2.0 → codecortex-0.2.2}/src/codecortex.egg-info/SOURCES.txt +1 -0
  5. codecortex-0.2.2/src/codeintel/__init__.py +1 -0
  6. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/cache.py +19 -6
  7. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/http_server.py +7 -3
  8. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/injector.py +5 -1
  9. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/installer.py +11 -1
  10. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/providers/graph.py +10 -3
  11. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/semantic_db.py +12 -0
  12. codecortex-0.2.2/tests/test_cache.py +61 -0
  13. codecortex-0.2.0/src/codeintel/__init__.py +0 -1
  14. {codecortex-0.2.0 → codecortex-0.2.2}/LICENSE +0 -0
  15. {codecortex-0.2.0 → codecortex-0.2.2}/pyproject.toml +0 -0
  16. {codecortex-0.2.0 → codecortex-0.2.2}/setup.cfg +0 -0
  17. {codecortex-0.2.0 → codecortex-0.2.2}/src/codecortex.egg-info/dependency_links.txt +0 -0
  18. {codecortex-0.2.0 → codecortex-0.2.2}/src/codecortex.egg-info/entry_points.txt +0 -0
  19. {codecortex-0.2.0 → codecortex-0.2.2}/src/codecortex.egg-info/requires.txt +0 -0
  20. {codecortex-0.2.0 → codecortex-0.2.2}/src/codecortex.egg-info/top_level.txt +0 -0
  21. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/__main__.py +0 -0
  22. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/config.py +0 -0
  23. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/doctor.py +0 -0
  24. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/gateway.py +0 -0
  25. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/indexer.py +0 -0
  26. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/mapper.py +0 -0
  27. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/onboarding.py +0 -0
  28. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/policy.py +0 -0
  29. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/provider.py +0 -0
  30. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/providers/__init__.py +0 -0
  31. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/providers/lsp.py +0 -0
  32. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/providers/none.py +0 -0
  33. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/providers/semantic.py +0 -0
  34. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/reindexer.py +0 -0
  35. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/reset.py +0 -0
  36. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/searcher.py +0 -0
  37. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/server.py +0 -0
  38. {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/term.py +0 -0
  39. {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_doctor.py +0 -0
  40. {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_e2e.py +0 -0
  41. {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_gateway.py +0 -0
  42. {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_graph_provider.py +0 -0
  43. {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_graph_real.py +0 -0
  44. {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_graph_stdin.py +0 -0
  45. {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_http_server.py +0 -0
  46. {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_integration.py +0 -0
  47. {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_lsp_provider.py +0 -0
  48. {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_lsp_real.py +0 -0
  49. {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_mapper.py +0 -0
  50. {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_never_raise.py +0 -0
  51. {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_onboarding.py +0 -0
  52. {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_reindexer.py +0 -0
  53. {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_reset.py +0 -0
  54. {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_semantic_provider.py +0 -0
  55. {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_term.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecortex
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Local-first, MCP-native code-intelligence server — graph, LSP, and semantic search behind one safe code.query tool for coding agents.
5
5
  Author: Shammai Hamilton
6
6
  License-Expression: MIT
@@ -33,10 +33,65 @@ Dynamic: license-file
33
33
 
34
34
  # codeintel
35
35
 
36
- A unified code-intelligence gateway — graph + LSP + semantic that gives any coding agent a single safe API to search, trace, and understand any codebase.
36
+ **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.
37
37
 
38
38
  [![CI](https://github.com/hamilton-sky/codeintel/actions/workflows/ci.yml/badge.svg)](https://github.com/hamilton-sky/codeintel/actions/workflows/ci.yml)
39
39
 
40
+ ## Why an agent needs it
41
+
42
+ 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:
43
+
44
+ - **"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.
45
+ - **"Where is this symbol defined, and everywhere it's used?"** → the language server, with exact locations.
46
+ - **"Where's the code that does X?"** (when you don't know the name) → semantic search over the repo.
47
+ - **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.
48
+
49
+ 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.
50
+
51
+ ## What your agent can ask
52
+
53
+ It's one call: `code.query(op, target, engine="auto")`. In `auto` mode (the default) codeintel picks the engine per operation:
54
+
55
+ | Ask | `op` | Engine (auto) | Comes back as |
56
+ |---|---|---|---|
57
+ | Find code by meaning ("auth middleware") | `search` | semantic | ranked `path:line │ snippet` hits |
58
+ | A symbol's definition **and** all references | `symbol` | lsp | definition body + reference list |
59
+ | Who calls this? | `callers` | graph | caller symbols + files |
60
+ | What does this call? | `callees` | graph | callee symbols + files |
61
+ | Blast radius of a change | `impact` | graph | callers **and** callees together |
62
+ | Trace a call chain up/downstream | `chain` | graph | ordered hops |
63
+ | Find symbols by pattern | `pattern` | graph | matching nodes + locations |
64
+ | Project shape at a glance | `overview` | graph → lsp | modules, node/edge counts, languages |
65
+ | Everything about one symbol | `context` | graph + lsp | both views merged |
66
+
67
+ Pin one engine with `--engine graph│lsp│semantic`, or fan out with `--engine both` / `all` to merge results.
68
+
69
+ **Example — "who uses `safe_null_result`?"**
70
+
71
+ ```jsonc
72
+ // request
73
+ { "op": "callers", "target": "safe_null_result", "engine": "auto" }
74
+
75
+ // response — always this exact envelope; `result` is ready-to-read markdown
76
+ {
77
+ "ok": true, "op": "callers", "target": "safe_null_result",
78
+ "engine": "graph", "cached": false,
79
+ "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)"
80
+ }
81
+ ```
82
+
83
+ 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.
84
+
85
+ ## What makes it good
86
+
87
+ - **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.)
88
+ - **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`.
89
+ - **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.
90
+ - **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.
91
+ - **Fast on repeat, never stale.** A content-hash cache returns instantly for unchanged code and self-invalidates when a background reindex advances the index — answers stay both quick *and* fresh. The cache is bounded (LRU), so a long-running server holds steady memory.
92
+ - **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.
93
+ - **Honest about its own health.** `codeintel doctor` reports exactly which engines are ready for a repo and the single command to fix each gap — no guessing why a query came back empty.
94
+
40
95
  ## Quickstart
41
96
 
42
97
  ```bash
@@ -146,7 +201,7 @@ backend = "auto" # auto | graph | lsp | semantic
146
201
  semantic = "on" # on | off
147
202
  reindex = "on-demand" # on-demand | never
148
203
  cosine_floor = 0.25 # minimum similarity score for semantic hits
149
- max_chunks = 500 # max chunks to embed per project
204
+ max_chunks = 500 # max chunks to embed per file
150
205
  model = "BAAI/bge-small-en-v1.5" # fastembed embedding model
151
206
  ```
152
207
 
@@ -170,7 +225,16 @@ Not sure what's installed? `codeintel doctor` reports exactly which backends are
170
225
 
171
226
  ## For agents
172
227
 
173
- Start the HTTP server, then POST queries to `/code/query`:
228
+ Register codeintel as an MCP server (`codeintel install`) and the agent gets four tools:
229
+
230
+ | MCP tool | HTTP equivalent | Purpose |
231
+ |---|---|---|
232
+ | `code.query` | `POST /code/query` | The main call — search, trace, understand (the `op` table above) |
233
+ | `code.status` | `GET /code/status` | Which engines are live + whether an index exists |
234
+ | `code.doctor` | `POST /code/doctor` | Per-engine health + repo index status, with a fix for each gap |
235
+ | `code.map` | — | Generate/refresh `CODE_INTEL.md`, a static orientation file for hosts without MCP |
236
+
237
+ Over MCP the agent calls `code.query` directly. Over HTTP, start the server and POST to `/code/query`:
174
238
 
175
239
  ```bash
176
240
  codeintel serve-http & # listens on 127.0.0.1:8766 by default
@@ -202,5 +266,5 @@ The response is always JSON-safe. Check `result["result"] is not None` before us
202
266
  git clone https://github.com/hamilton-sky/codeintel.git
203
267
  cd codeintel
204
268
  pip install -e .[dev]
205
- pytest tests/ -q # full suite, ~1s
269
+ pytest tests/ -q # full suite (~15s — includes live graph/LSP backend tests)
206
270
  ```
@@ -1,9 +1,64 @@
1
1
  # codeintel
2
2
 
3
- A unified code-intelligence gateway — graph + LSP + semantic that gives any coding agent a single safe API to search, trace, and understand any codebase.
3
+ **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.
4
4
 
5
5
  [![CI](https://github.com/hamilton-sky/codeintel/actions/workflows/ci.yml/badge.svg)](https://github.com/hamilton-sky/codeintel/actions/workflows/ci.yml)
6
6
 
7
+ ## Why an agent needs it
8
+
9
+ 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:
10
+
11
+ - **"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.
12
+ - **"Where is this symbol defined, and everywhere it's used?"** → the language server, with exact locations.
13
+ - **"Where's the code that does X?"** (when you don't know the name) → semantic search over the repo.
14
+ - **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.
15
+
16
+ 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.
17
+
18
+ ## What your agent can ask
19
+
20
+ It's one call: `code.query(op, target, engine="auto")`. In `auto` mode (the default) codeintel picks the engine per operation:
21
+
22
+ | Ask | `op` | Engine (auto) | Comes back as |
23
+ |---|---|---|---|
24
+ | Find code by meaning ("auth middleware") | `search` | semantic | ranked `path:line │ snippet` hits |
25
+ | A symbol's definition **and** all references | `symbol` | lsp | definition body + reference list |
26
+ | Who calls this? | `callers` | graph | caller symbols + files |
27
+ | What does this call? | `callees` | graph | callee symbols + files |
28
+ | Blast radius of a change | `impact` | graph | callers **and** callees together |
29
+ | Trace a call chain up/downstream | `chain` | graph | ordered hops |
30
+ | Find symbols by pattern | `pattern` | graph | matching nodes + locations |
31
+ | Project shape at a glance | `overview` | graph → lsp | modules, node/edge counts, languages |
32
+ | Everything about one symbol | `context` | graph + lsp | both views merged |
33
+
34
+ Pin one engine with `--engine graph│lsp│semantic`, or fan out with `--engine both` / `all` to merge results.
35
+
36
+ **Example — "who uses `safe_null_result`?"**
37
+
38
+ ```jsonc
39
+ // request
40
+ { "op": "callers", "target": "safe_null_result", "engine": "auto" }
41
+
42
+ // response — always this exact envelope; `result` is ready-to-read markdown
43
+ {
44
+ "ok": true, "op": "callers", "target": "safe_null_result",
45
+ "engine": "graph", "cached": false,
46
+ "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)"
47
+ }
48
+ ```
49
+
50
+ 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.
51
+
52
+ ## What makes it good
53
+
54
+ - **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.)
55
+ - **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`.
56
+ - **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.
57
+ - **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.
58
+ - **Fast on repeat, never stale.** A content-hash cache returns instantly for unchanged code and self-invalidates when a background reindex advances the index — answers stay both quick *and* fresh. The cache is bounded (LRU), so a long-running server holds steady memory.
59
+ - **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.
60
+ - **Honest about its own health.** `codeintel doctor` reports exactly which engines are ready for a repo and the single command to fix each gap — no guessing why a query came back empty.
61
+
7
62
  ## Quickstart
8
63
 
9
64
  ```bash
@@ -113,7 +168,7 @@ backend = "auto" # auto | graph | lsp | semantic
113
168
  semantic = "on" # on | off
114
169
  reindex = "on-demand" # on-demand | never
115
170
  cosine_floor = 0.25 # minimum similarity score for semantic hits
116
- max_chunks = 500 # max chunks to embed per project
171
+ max_chunks = 500 # max chunks to embed per file
117
172
  model = "BAAI/bge-small-en-v1.5" # fastembed embedding model
118
173
  ```
119
174
 
@@ -137,7 +192,16 @@ Not sure what's installed? `codeintel doctor` reports exactly which backends are
137
192
 
138
193
  ## For agents
139
194
 
140
- Start the HTTP server, then POST queries to `/code/query`:
195
+ Register codeintel as an MCP server (`codeintel install`) and the agent gets four tools:
196
+
197
+ | MCP tool | HTTP equivalent | Purpose |
198
+ |---|---|---|
199
+ | `code.query` | `POST /code/query` | The main call — search, trace, understand (the `op` table above) |
200
+ | `code.status` | `GET /code/status` | Which engines are live + whether an index exists |
201
+ | `code.doctor` | `POST /code/doctor` | Per-engine health + repo index status, with a fix for each gap |
202
+ | `code.map` | — | Generate/refresh `CODE_INTEL.md`, a static orientation file for hosts without MCP |
203
+
204
+ Over MCP the agent calls `code.query` directly. Over HTTP, start the server and POST to `/code/query`:
141
205
 
142
206
  ```bash
143
207
  codeintel serve-http & # listens on 127.0.0.1:8766 by default
@@ -169,5 +233,5 @@ The response is always JSON-safe. Check `result["result"] is not None` before us
169
233
  git clone https://github.com/hamilton-sky/codeintel.git
170
234
  cd codeintel
171
235
  pip install -e .[dev]
172
- pytest tests/ -q # full suite, ~1s
236
+ pytest tests/ -q # full suite (~15s — includes live graph/LSP backend tests)
173
237
  ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecortex
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Local-first, MCP-native code-intelligence server — graph, LSP, and semantic search behind one safe code.query tool for coding agents.
5
5
  Author: Shammai Hamilton
6
6
  License-Expression: MIT
@@ -33,10 +33,65 @@ Dynamic: license-file
33
33
 
34
34
  # codeintel
35
35
 
36
- A unified code-intelligence gateway — graph + LSP + semantic that gives any coding agent a single safe API to search, trace, and understand any codebase.
36
+ **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.
37
37
 
38
38
  [![CI](https://github.com/hamilton-sky/codeintel/actions/workflows/ci.yml/badge.svg)](https://github.com/hamilton-sky/codeintel/actions/workflows/ci.yml)
39
39
 
40
+ ## Why an agent needs it
41
+
42
+ 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:
43
+
44
+ - **"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.
45
+ - **"Where is this symbol defined, and everywhere it's used?"** → the language server, with exact locations.
46
+ - **"Where's the code that does X?"** (when you don't know the name) → semantic search over the repo.
47
+ - **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.
48
+
49
+ 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.
50
+
51
+ ## What your agent can ask
52
+
53
+ It's one call: `code.query(op, target, engine="auto")`. In `auto` mode (the default) codeintel picks the engine per operation:
54
+
55
+ | Ask | `op` | Engine (auto) | Comes back as |
56
+ |---|---|---|---|
57
+ | Find code by meaning ("auth middleware") | `search` | semantic | ranked `path:line │ snippet` hits |
58
+ | A symbol's definition **and** all references | `symbol` | lsp | definition body + reference list |
59
+ | Who calls this? | `callers` | graph | caller symbols + files |
60
+ | What does this call? | `callees` | graph | callee symbols + files |
61
+ | Blast radius of a change | `impact` | graph | callers **and** callees together |
62
+ | Trace a call chain up/downstream | `chain` | graph | ordered hops |
63
+ | Find symbols by pattern | `pattern` | graph | matching nodes + locations |
64
+ | Project shape at a glance | `overview` | graph → lsp | modules, node/edge counts, languages |
65
+ | Everything about one symbol | `context` | graph + lsp | both views merged |
66
+
67
+ Pin one engine with `--engine graph│lsp│semantic`, or fan out with `--engine both` / `all` to merge results.
68
+
69
+ **Example — "who uses `safe_null_result`?"**
70
+
71
+ ```jsonc
72
+ // request
73
+ { "op": "callers", "target": "safe_null_result", "engine": "auto" }
74
+
75
+ // response — always this exact envelope; `result` is ready-to-read markdown
76
+ {
77
+ "ok": true, "op": "callers", "target": "safe_null_result",
78
+ "engine": "graph", "cached": false,
79
+ "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)"
80
+ }
81
+ ```
82
+
83
+ 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.
84
+
85
+ ## What makes it good
86
+
87
+ - **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.)
88
+ - **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`.
89
+ - **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.
90
+ - **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.
91
+ - **Fast on repeat, never stale.** A content-hash cache returns instantly for unchanged code and self-invalidates when a background reindex advances the index — answers stay both quick *and* fresh. The cache is bounded (LRU), so a long-running server holds steady memory.
92
+ - **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.
93
+ - **Honest about its own health.** `codeintel doctor` reports exactly which engines are ready for a repo and the single command to fix each gap — no guessing why a query came back empty.
94
+
40
95
  ## Quickstart
41
96
 
42
97
  ```bash
@@ -146,7 +201,7 @@ backend = "auto" # auto | graph | lsp | semantic
146
201
  semantic = "on" # on | off
147
202
  reindex = "on-demand" # on-demand | never
148
203
  cosine_floor = 0.25 # minimum similarity score for semantic hits
149
- max_chunks = 500 # max chunks to embed per project
204
+ max_chunks = 500 # max chunks to embed per file
150
205
  model = "BAAI/bge-small-en-v1.5" # fastembed embedding model
151
206
  ```
152
207
 
@@ -170,7 +225,16 @@ Not sure what's installed? `codeintel doctor` reports exactly which backends are
170
225
 
171
226
  ## For agents
172
227
 
173
- Start the HTTP server, then POST queries to `/code/query`:
228
+ Register codeintel as an MCP server (`codeintel install`) and the agent gets four tools:
229
+
230
+ | MCP tool | HTTP equivalent | Purpose |
231
+ |---|---|---|
232
+ | `code.query` | `POST /code/query` | The main call — search, trace, understand (the `op` table above) |
233
+ | `code.status` | `GET /code/status` | Which engines are live + whether an index exists |
234
+ | `code.doctor` | `POST /code/doctor` | Per-engine health + repo index status, with a fix for each gap |
235
+ | `code.map` | — | Generate/refresh `CODE_INTEL.md`, a static orientation file for hosts without MCP |
236
+
237
+ Over MCP the agent calls `code.query` directly. Over HTTP, start the server and POST to `/code/query`:
174
238
 
175
239
  ```bash
176
240
  codeintel serve-http & # listens on 127.0.0.1:8766 by default
@@ -202,5 +266,5 @@ The response is always JSON-safe. Check `result["result"] is not None` before us
202
266
  git clone https://github.com/hamilton-sky/codeintel.git
203
267
  cd codeintel
204
268
  pip install -e .[dev]
205
- pytest tests/ -q # full suite, ~1s
269
+ pytest tests/ -q # full suite (~15s — includes live graph/LSP backend tests)
206
270
  ```
@@ -32,6 +32,7 @@ src/codeintel/providers/graph.py
32
32
  src/codeintel/providers/lsp.py
33
33
  src/codeintel/providers/none.py
34
34
  src/codeintel/providers/semantic.py
35
+ tests/test_cache.py
35
36
  tests/test_doctor.py
36
37
  tests/test_e2e.py
37
38
  tests/test_gateway.py
@@ -0,0 +1 @@
1
+ __version__ = "0.2.2"
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import hashlib
4
4
  import os
5
5
  import threading
6
+ from collections import OrderedDict
6
7
  from typing import Optional
7
8
 
8
9
  from codeintel.provider import Result
@@ -22,10 +23,15 @@ def _compute_hash(target: str, project_root: str) -> str:
22
23
 
23
24
 
24
25
  class ContentHashCache:
25
- def __init__(self) -> None:
26
+ # Bounded: the server builds ONE gateway and reuses it across every request, so an unbounded
27
+ # store would grow for the life of the process. Capped as an LRU — past _max_entries the
28
+ # least-recently-used entry is evicted. Sized generously; an agent session touches at most a
29
+ # few hundred distinct (op, target, engine, root) keys, each holding a small Result.
30
+ def __init__(self, max_entries: int = 1024) -> None:
26
31
  self._lock = threading.Lock()
27
- # key (content_hash, Result)
28
- self._store: dict[tuple[str, str, str, str], tuple[str, Result]] = {}
32
+ self._max_entries = max(1, int(max_entries))
33
+ # key (content_hash, Result); OrderedDict preserves insertion/access order for LRU.
34
+ self._store: "OrderedDict[tuple[str, str, str, str], tuple[str, Result]]" = OrderedDict()
29
35
 
30
36
  def get(
31
37
  self,
@@ -41,10 +47,14 @@ class ContentHashCache:
41
47
  if entry is None:
42
48
  return None
43
49
  stored_hash, result = entry
50
+ # _compute_hash may read the target file — keep it OUTSIDE the lock (original intent).
44
51
  current_hash = f"{_compute_hash(target, project_root)}:{freshness}"
45
- if current_hash == stored_hash:
46
- return result
47
- return None
52
+ if current_hash != stored_hash:
53
+ return None
54
+ with self._lock:
55
+ if key in self._store: # may have been evicted between the two locked sections
56
+ self._store.move_to_end(key) # mark most-recently-used
57
+ return result
48
58
 
49
59
  def put(
50
60
  self,
@@ -64,3 +74,6 @@ class ContentHashCache:
64
74
  content_hash = f"{_compute_hash(target, project_root)}:{freshness}"
65
75
  with self._lock:
66
76
  self._store[key] = (content_hash, result)
77
+ self._store.move_to_end(key) # most-recently-used
78
+ while len(self._store) > self._max_entries:
79
+ self._store.popitem(last=False) # evict least-recently-used
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import sys
5
- from http.server import BaseHTTPRequestHandler, HTTPServer
5
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
6
6
 
7
7
  from codeintel.server import code_doctor_handler, code_query_handler, code_status_handler
8
8
 
@@ -57,8 +57,12 @@ class _Handler(BaseHTTPRequestHandler):
57
57
  self._send_json(200, result)
58
58
 
59
59
 
60
- class CodeIntelHTTPServer(HTTPServer):
61
- pass
60
+ class CodeIntelHTTPServer(ThreadingHTTPServer):
61
+ # Threaded so one slow request (e.g. an LSP session warming, or a first-time index) can't
62
+ # block every other agent's query. The gateway is a shared singleton, but its mutable state
63
+ # is lock-guarded (query cache, reindexer, LSP sessions, graph project cache) and the
64
+ # semantic engine is thread-confined with WAL, so concurrent requests are safe.
65
+ daemon_threads = True
62
66
 
63
67
 
64
68
  _LOOPBACK_NAMES = {"localhost"}
@@ -57,8 +57,12 @@ def _read_file(path: str) -> str | None:
57
57
 
58
58
 
59
59
  def _write_file(path: str, content: str) -> None:
60
- with open(path, "w", encoding="utf-8") as f:
60
+ # Atomic: write a sibling temp file then os.replace it into place, so an interrupted
61
+ # write can never leave the user's CLAUDE.md/AGENTS.md truncated or half-written.
62
+ tmp = path + ".codeintel.tmp"
63
+ with open(tmp, "w", encoding="utf-8") as f:
61
64
  f.write(content)
65
+ os.replace(tmp, path)
62
66
 
63
67
 
64
68
  def _update_block(content: str) -> tuple[str, str]:
@@ -1,10 +1,20 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import os
4
5
  import pathlib
5
6
 
6
7
  _AGENTS = ["claude", "codex", "gemini", "zed"]
7
8
 
9
+
10
+ def _atomic_write_text(path: pathlib.Path, text: str) -> None:
11
+ """Write via a sibling temp file + os.replace so an interrupted write can never truncate
12
+ the user's existing agent config (which holds unrelated settings) to a partial/empty file.
13
+ The temp lives in the same directory as ``path`` so the replace stays on one filesystem."""
14
+ tmp = path.with_name(path.name + ".codeintel.tmp")
15
+ tmp.write_text(text, encoding="utf-8")
16
+ os.replace(tmp, path)
17
+
8
18
  _CONFIG: dict[str, dict] = {
9
19
  "claude": {
10
20
  "path": "~/.claude/settings.json",
@@ -80,7 +90,7 @@ class Installer:
80
90
 
81
91
  config_path.parent.mkdir(parents=True, exist_ok=True)
82
92
  _set_nested(data, spec["key"], spec["value"])
83
- config_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
93
+ _atomic_write_text(config_path, json.dumps(data, indent=2))
84
94
 
85
95
  return {
86
96
  "agent": agent,
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  import shutil
5
5
  import subprocess
6
+ import threading
6
7
  import time
7
8
  from typing import Any, Optional
8
9
 
@@ -35,6 +36,7 @@ class GraphProvider:
35
36
 
36
37
  def __init__(self) -> None:
37
38
  self._project_cache: dict[str, Optional[str]] = {}
39
+ self._project_cache_lock = threading.Lock() # concurrent HTTP requests share one provider
38
40
  self._detect_backend()
39
41
 
40
42
  def _detect_backend(self) -> None:
@@ -125,11 +127,16 @@ class GraphProvider:
125
127
  return exact if exact is not None else best_prefix_name
126
128
 
127
129
  def _resolve_project(self, project_root: str) -> Optional[str]:
128
- if project_root in self._project_cache:
129
- return self._project_cache[project_root]
130
+ with self._project_cache_lock:
131
+ if project_root in self._project_cache:
132
+ return self._project_cache[project_root]
133
+ # list_projects shells out — resolve it OUTSIDE the lock so a slow backend can't serialize
134
+ # every concurrent request. A rare duplicate lookup on first contact is harmless (the
135
+ # result is idempotent); we simply never hold the lock across a subprocess.
130
136
  raw = self._run("list_projects", {}, 3000)
131
137
  name = self._match_project(raw, project_root)
132
- self._project_cache[project_root] = name
138
+ with self._project_cache_lock:
139
+ self._project_cache[project_root] = name
133
140
  return name
134
141
 
135
142
  def probe(self, project_root: str, timeout_ms: int = 3000) -> dict:
@@ -31,6 +31,18 @@ class SemanticDb:
31
31
  self._conn = sqlite3.connect(self.db_path)
32
32
  self._conn.enable_load_extension(True)
33
33
  self._conn.row_factory = sqlite3.Row
34
+ # Concurrency: the background Reindexer writes on a daemon thread while a foreground
35
+ # query indexes inline — two separate connections to this one file. With the SQLite
36
+ # default (busy_timeout=0) the loser of that write race gets an immediate
37
+ # "database is locked" and silently drops its work; a busy timeout makes it wait
38
+ # instead, and WAL lets a search read while a reindex writes. (reset.py already
39
+ # cleans up the -wal/-shm siblings WAL creates.) Never-raise: if the pragmas can't
40
+ # be applied, fall back to default locking rather than fail to open the db.
41
+ try:
42
+ self._conn.execute("PRAGMA busy_timeout=5000")
43
+ self._conn.execute("PRAGMA journal_mode=WAL")
44
+ except Exception:
45
+ pass
34
46
  return self._conn
35
47
 
36
48
  def init(self) -> None:
@@ -0,0 +1,61 @@
1
+ """Unit tests for ContentHashCache — the LRU bound added in 0.2.2 so the long-lived server
2
+ can't grow the query cache without limit. Real cache object, no mocks."""
3
+ from __future__ import annotations
4
+
5
+ from codeintel.cache import ContentHashCache
6
+
7
+
8
+ def _result(tag: str) -> dict:
9
+ return {"ok": True, "op": "search", "target": tag, "result": f"r-{tag}",
10
+ "engine": "semantic", "cached": False}
11
+
12
+
13
+ def _put(cache: ContentHashCache, tag: str) -> None:
14
+ cache.put("search", tag, "semantic", "", _result(tag), 0)
15
+
16
+
17
+ def _get(cache: ContentHashCache, tag: str):
18
+ return cache.get("search", tag, "semantic", "", 0)
19
+
20
+
21
+ def test_hit_returns_stored_result():
22
+ c = ContentHashCache()
23
+ _put(c, "x")
24
+ got = _get(c, "x")
25
+ assert got is not None and got["result"] == "r-x"
26
+
27
+
28
+ def test_freshness_bump_busts_the_entry():
29
+ c = ContentHashCache()
30
+ _put(c, "x") # stored at freshness 0
31
+ assert c.get("search", "x", "semantic", "", 1) is None # a completed reindex invalidates it
32
+
33
+
34
+ def test_null_result_is_not_cached():
35
+ c = ContentHashCache()
36
+ c.put("search", "x", "semantic", "", {"ok": True, "result": None}, 0)
37
+ assert _get(c, "x") is None
38
+
39
+
40
+ def test_evicts_least_recently_used_past_capacity():
41
+ c = ContentHashCache(max_entries=3)
42
+ for t in ("t0", "t1", "t2"):
43
+ _put(c, t)
44
+ # Touch t0 so it becomes most-recently-used; t1 is now the LRU.
45
+ assert _get(c, "t0") is not None
46
+ _put(c, "t3") # over capacity → evict the LRU (t1)
47
+
48
+ assert _get(c, "t1") is None # evicted
49
+ assert _get(c, "t0") is not None # kept — it was accessed most recently
50
+ assert _get(c, "t2") is not None # kept
51
+ assert _get(c, "t3") is not None # just inserted
52
+
53
+
54
+ def test_capacity_is_never_exceeded():
55
+ c = ContentHashCache(max_entries=5)
56
+ for i in range(50):
57
+ _put(c, f"k{i}")
58
+ assert len(c._store) == 5
59
+ # The 5 most recent survive; older keys are gone.
60
+ assert _get(c, "k49") is not None
61
+ assert _get(c, "k0") is None
@@ -1 +0,0 @@
1
- __version__ = "0.2.0"
File without changes
File without changes
File without changes
File without changes