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.
- {codecortex-0.2.0/src/codecortex.egg-info → codecortex-0.2.2}/PKG-INFO +69 -5
- {codecortex-0.2.0 → codecortex-0.2.2}/README.md +68 -4
- {codecortex-0.2.0 → codecortex-0.2.2/src/codecortex.egg-info}/PKG-INFO +69 -5
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codecortex.egg-info/SOURCES.txt +1 -0
- codecortex-0.2.2/src/codeintel/__init__.py +1 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/cache.py +19 -6
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/http_server.py +7 -3
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/injector.py +5 -1
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/installer.py +11 -1
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/providers/graph.py +10 -3
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/semantic_db.py +12 -0
- codecortex-0.2.2/tests/test_cache.py +61 -0
- codecortex-0.2.0/src/codeintel/__init__.py +0 -1
- {codecortex-0.2.0 → codecortex-0.2.2}/LICENSE +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/pyproject.toml +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/setup.cfg +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codecortex.egg-info/dependency_links.txt +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codecortex.egg-info/entry_points.txt +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codecortex.egg-info/requires.txt +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codecortex.egg-info/top_level.txt +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/__main__.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/config.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/doctor.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/gateway.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/indexer.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/mapper.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/onboarding.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/policy.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/provider.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/providers/__init__.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/providers/lsp.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/providers/none.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/providers/semantic.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/reindexer.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/reset.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/searcher.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/server.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/src/codeintel/term.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_doctor.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_e2e.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_gateway.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_graph_provider.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_graph_real.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_graph_stdin.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_http_server.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_integration.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_lsp_provider.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_lsp_real.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_mapper.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_never_raise.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_onboarding.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_reindexer.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_reset.py +0 -0
- {codecortex-0.2.0 → codecortex-0.2.2}/tests/test_semantic_provider.py +0 -0
- {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.
|
|
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
|
-
|
|
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
|
[](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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
[](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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
[](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
|
|
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
|
-
|
|
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
|
|
269
|
+
pytest tests/ -q # full suite (~15s — includes live graph/LSP backend tests)
|
|
206
270
|
```
|
|
@@ -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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
|
46
|
-
return
|
|
47
|
-
|
|
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,
|
|
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(
|
|
61
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
129
|
-
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|