codecortex 0.2.1__tar.gz → 0.3.0__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.

Potentially problematic release.


This version of codecortex might be problematic. Click here for more details.

Files changed (60) hide show
  1. {codecortex-0.2.1/src/codecortex.egg-info → codecortex-0.3.0}/PKG-INFO +32 -9
  2. {codecortex-0.2.1 → codecortex-0.3.0}/README.md +31 -8
  3. {codecortex-0.2.1 → codecortex-0.3.0/src/codecortex.egg-info}/PKG-INFO +32 -9
  4. {codecortex-0.2.1 → codecortex-0.3.0}/src/codecortex.egg-info/SOURCES.txt +4 -0
  5. codecortex-0.3.0/src/codeintel/__init__.py +1 -0
  6. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/__main__.py +6 -3
  7. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/cache.py +20 -7
  8. codecortex-0.3.0/src/codeintel/config.py +97 -0
  9. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/gateway.py +9 -6
  10. codecortex-0.3.0/src/codeintel/http_server.py +175 -0
  11. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/indexer.py +12 -1
  12. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/onboarding.py +1 -0
  13. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/provider.py +19 -0
  14. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/providers/graph.py +23 -6
  15. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/providers/lsp.py +3 -2
  16. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/providers/semantic.py +23 -10
  17. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/reindexer.py +26 -3
  18. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/server.py +11 -5
  19. codecortex-0.3.0/tests/test_cache.py +61 -0
  20. codecortex-0.3.0/tests/test_config.py +63 -0
  21. codecortex-0.3.0/tests/test_hardening.py +182 -0
  22. codecortex-0.3.0/tests/test_http_auth.py +121 -0
  23. codecortex-0.2.1/src/codeintel/__init__.py +0 -1
  24. codecortex-0.2.1/src/codeintel/config.py +0 -42
  25. codecortex-0.2.1/src/codeintel/http_server.py +0 -93
  26. {codecortex-0.2.1 → codecortex-0.3.0}/LICENSE +0 -0
  27. {codecortex-0.2.1 → codecortex-0.3.0}/pyproject.toml +0 -0
  28. {codecortex-0.2.1 → codecortex-0.3.0}/setup.cfg +0 -0
  29. {codecortex-0.2.1 → codecortex-0.3.0}/src/codecortex.egg-info/dependency_links.txt +0 -0
  30. {codecortex-0.2.1 → codecortex-0.3.0}/src/codecortex.egg-info/entry_points.txt +0 -0
  31. {codecortex-0.2.1 → codecortex-0.3.0}/src/codecortex.egg-info/requires.txt +0 -0
  32. {codecortex-0.2.1 → codecortex-0.3.0}/src/codecortex.egg-info/top_level.txt +0 -0
  33. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/doctor.py +0 -0
  34. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/injector.py +0 -0
  35. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/installer.py +0 -0
  36. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/mapper.py +0 -0
  37. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/policy.py +0 -0
  38. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/providers/__init__.py +0 -0
  39. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/providers/none.py +0 -0
  40. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/reset.py +0 -0
  41. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/searcher.py +0 -0
  42. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/semantic_db.py +0 -0
  43. {codecortex-0.2.1 → codecortex-0.3.0}/src/codeintel/term.py +0 -0
  44. {codecortex-0.2.1 → codecortex-0.3.0}/tests/test_doctor.py +0 -0
  45. {codecortex-0.2.1 → codecortex-0.3.0}/tests/test_e2e.py +0 -0
  46. {codecortex-0.2.1 → codecortex-0.3.0}/tests/test_gateway.py +0 -0
  47. {codecortex-0.2.1 → codecortex-0.3.0}/tests/test_graph_provider.py +0 -0
  48. {codecortex-0.2.1 → codecortex-0.3.0}/tests/test_graph_real.py +0 -0
  49. {codecortex-0.2.1 → codecortex-0.3.0}/tests/test_graph_stdin.py +0 -0
  50. {codecortex-0.2.1 → codecortex-0.3.0}/tests/test_http_server.py +0 -0
  51. {codecortex-0.2.1 → codecortex-0.3.0}/tests/test_integration.py +0 -0
  52. {codecortex-0.2.1 → codecortex-0.3.0}/tests/test_lsp_provider.py +0 -0
  53. {codecortex-0.2.1 → codecortex-0.3.0}/tests/test_lsp_real.py +0 -0
  54. {codecortex-0.2.1 → codecortex-0.3.0}/tests/test_mapper.py +0 -0
  55. {codecortex-0.2.1 → codecortex-0.3.0}/tests/test_never_raise.py +0 -0
  56. {codecortex-0.2.1 → codecortex-0.3.0}/tests/test_onboarding.py +0 -0
  57. {codecortex-0.2.1 → codecortex-0.3.0}/tests/test_reindexer.py +0 -0
  58. {codecortex-0.2.1 → codecortex-0.3.0}/tests/test_reset.py +0 -0
  59. {codecortex-0.2.1 → codecortex-0.3.0}/tests/test_semantic_provider.py +0 -0
  60. {codecortex-0.2.1 → codecortex-0.3.0}/tests/test_term.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecortex
3
- Version: 0.2.1
3
+ Version: 0.3.0
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
@@ -82,6 +82,16 @@ Pin one engine with `--engine graph│lsp│semantic`, or fan out with `--engine
82
82
 
83
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
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
+
85
95
  ## Quickstart
86
96
 
87
97
  ```bash
@@ -173,7 +183,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
173
183
  | `codeintel setup [project_root] [--index] [--warm] [--install-uv]` | Check backends + optionally index this repo; ends with a health report |
174
184
  | `codeintel index [project_root]` | Index a project for semantic search |
175
185
  | `codeintel serve` | Start the MCP server (stdio transport) |
176
- | `codeintel serve-http [--host HOST] [--port 8766] [--allow-remote]` | Start the HTTP transport (loopback-only unless `--allow-remote`) |
186
+ | `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) |
177
187
  | `codeintel query --op OP --target TARGET [--engine auto]` | Run a single query and print the result |
178
188
  | `codeintel status [project_root]` | Show engine availability and index age |
179
189
  | `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap |
@@ -187,17 +197,28 @@ Human-facing commands (`doctor`, `status`, `query`, `setup`, `reset`) honor `--n
187
197
  Create `.codeintel.toml` at your project root to override defaults:
188
198
 
189
199
  ```toml
190
- backend = "auto" # auto | graph | lsp | semantic
191
- semantic = "on" # on | off
192
- reindex = "on-demand" # on-demand | never
193
- cosine_floor = 0.25 # minimum similarity score for semantic hits
194
- max_chunks = 500 # max chunks to embed per file
195
- model = "BAAI/bge-small-en-v1.5" # fastembed embedding model
200
+ backend = "auto" # auto | graph | lsp | semantic
201
+ semantic = "on" # on | off
202
+ reindex = "on-demand" # on-demand | never
203
+ cosine_floor = 0.25 # minimum similarity score for semantic hits (0–1)
204
+ max_chunks = 500 # max chunks to embed per file
205
+ max_total_chunks = 100000 # safety ceiling on chunks embedded in one index pass
206
+ model = "BAAI/bge-small-en-v1.5" # fastembed embedding model
196
207
  ```
197
208
 
209
+ 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.
210
+
211
+ **Environment variables:**
212
+
213
+ | Variable | Effect |
214
+ |---|---|
215
+ | `CODEINTEL_HTTP_TOKEN` | Bearer token required by `serve-http` (equivalent to `--token`) |
216
+ | `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` |
217
+ | `CODEINTEL_REINDEX=off` | Disable the background reindexer; queries then index inline to stay fresh |
218
+
198
219
  ## Privacy & dependencies
199
220
 
200
- **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.
221
+ **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.
201
222
 
202
223
  **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).
203
224
 
@@ -230,6 +251,8 @@ Over MCP the agent calls `code.query` directly. Over HTTP, start the server and
230
251
  codeintel serve-http & # listens on 127.0.0.1:8766 by default
231
252
  ```
232
253
 
254
+ 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.
255
+
233
256
  ```python
234
257
  import urllib.request, json
235
258
 
@@ -49,6 +49,16 @@ Pin one engine with `--engine graph│lsp│semantic`, or fan out with `--engine
49
49
 
50
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
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
+
52
62
  ## Quickstart
53
63
 
54
64
  ```bash
@@ -140,7 +150,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
140
150
  | `codeintel setup [project_root] [--index] [--warm] [--install-uv]` | Check backends + optionally index this repo; ends with a health report |
141
151
  | `codeintel index [project_root]` | Index a project for semantic search |
142
152
  | `codeintel serve` | Start the MCP server (stdio transport) |
143
- | `codeintel serve-http [--host HOST] [--port 8766] [--allow-remote]` | Start the HTTP transport (loopback-only unless `--allow-remote`) |
153
+ | `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) |
144
154
  | `codeintel query --op OP --target TARGET [--engine auto]` | Run a single query and print the result |
145
155
  | `codeintel status [project_root]` | Show engine availability and index age |
146
156
  | `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap |
@@ -154,17 +164,28 @@ Human-facing commands (`doctor`, `status`, `query`, `setup`, `reset`) honor `--n
154
164
  Create `.codeintel.toml` at your project root to override defaults:
155
165
 
156
166
  ```toml
157
- backend = "auto" # auto | graph | lsp | semantic
158
- semantic = "on" # on | off
159
- reindex = "on-demand" # on-demand | never
160
- cosine_floor = 0.25 # minimum similarity score for semantic hits
161
- max_chunks = 500 # max chunks to embed per file
162
- model = "BAAI/bge-small-en-v1.5" # fastembed embedding model
167
+ backend = "auto" # auto | graph | lsp | semantic
168
+ semantic = "on" # on | off
169
+ reindex = "on-demand" # on-demand | never
170
+ cosine_floor = 0.25 # minimum similarity score for semantic hits (0–1)
171
+ max_chunks = 500 # max chunks to embed per file
172
+ max_total_chunks = 100000 # safety ceiling on chunks embedded in one index pass
173
+ model = "BAAI/bge-small-en-v1.5" # fastembed embedding model
163
174
  ```
164
175
 
176
+ 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.
177
+
178
+ **Environment variables:**
179
+
180
+ | Variable | Effect |
181
+ |---|---|
182
+ | `CODEINTEL_HTTP_TOKEN` | Bearer token required by `serve-http` (equivalent to `--token`) |
183
+ | `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` |
184
+ | `CODEINTEL_REINDEX=off` | Disable the background reindexer; queries then index inline to stay fresh |
185
+
165
186
  ## Privacy & dependencies
166
187
 
167
- **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.
188
+ **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.
168
189
 
169
190
  **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).
170
191
 
@@ -197,6 +218,8 @@ Over MCP the agent calls `code.query` directly. Over HTTP, start the server and
197
218
  codeintel serve-http & # listens on 127.0.0.1:8766 by default
198
219
  ```
199
220
 
221
+ 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.
222
+
200
223
  ```python
201
224
  import urllib.request, json
202
225
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecortex
3
- Version: 0.2.1
3
+ Version: 0.3.0
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
@@ -82,6 +82,16 @@ Pin one engine with `--engine graph│lsp│semantic`, or fan out with `--engine
82
82
 
83
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
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
+
85
95
  ## Quickstart
86
96
 
87
97
  ```bash
@@ -173,7 +183,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
173
183
  | `codeintel setup [project_root] [--index] [--warm] [--install-uv]` | Check backends + optionally index this repo; ends with a health report |
174
184
  | `codeintel index [project_root]` | Index a project for semantic search |
175
185
  | `codeintel serve` | Start the MCP server (stdio transport) |
176
- | `codeintel serve-http [--host HOST] [--port 8766] [--allow-remote]` | Start the HTTP transport (loopback-only unless `--allow-remote`) |
186
+ | `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) |
177
187
  | `codeintel query --op OP --target TARGET [--engine auto]` | Run a single query and print the result |
178
188
  | `codeintel status [project_root]` | Show engine availability and index age |
179
189
  | `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap |
@@ -187,17 +197,28 @@ Human-facing commands (`doctor`, `status`, `query`, `setup`, `reset`) honor `--n
187
197
  Create `.codeintel.toml` at your project root to override defaults:
188
198
 
189
199
  ```toml
190
- backend = "auto" # auto | graph | lsp | semantic
191
- semantic = "on" # on | off
192
- reindex = "on-demand" # on-demand | never
193
- cosine_floor = 0.25 # minimum similarity score for semantic hits
194
- max_chunks = 500 # max chunks to embed per file
195
- model = "BAAI/bge-small-en-v1.5" # fastembed embedding model
200
+ backend = "auto" # auto | graph | lsp | semantic
201
+ semantic = "on" # on | off
202
+ reindex = "on-demand" # on-demand | never
203
+ cosine_floor = 0.25 # minimum similarity score for semantic hits (0–1)
204
+ max_chunks = 500 # max chunks to embed per file
205
+ max_total_chunks = 100000 # safety ceiling on chunks embedded in one index pass
206
+ model = "BAAI/bge-small-en-v1.5" # fastembed embedding model
196
207
  ```
197
208
 
209
+ 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.
210
+
211
+ **Environment variables:**
212
+
213
+ | Variable | Effect |
214
+ |---|---|
215
+ | `CODEINTEL_HTTP_TOKEN` | Bearer token required by `serve-http` (equivalent to `--token`) |
216
+ | `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` |
217
+ | `CODEINTEL_REINDEX=off` | Disable the background reindexer; queries then index inline to stay fresh |
218
+
198
219
  ## Privacy & dependencies
199
220
 
200
- **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.
221
+ **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.
201
222
 
202
223
  **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).
203
224
 
@@ -230,6 +251,8 @@ Over MCP the agent calls `code.query` directly. Over HTTP, start the server and
230
251
  codeintel serve-http & # listens on 127.0.0.1:8766 by default
231
252
  ```
232
253
 
254
+ 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.
255
+
233
256
  ```python
234
257
  import urllib.request, json
235
258
 
@@ -32,12 +32,16 @@ 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
36
+ tests/test_config.py
35
37
  tests/test_doctor.py
36
38
  tests/test_e2e.py
37
39
  tests/test_gateway.py
38
40
  tests/test_graph_provider.py
39
41
  tests/test_graph_real.py
40
42
  tests/test_graph_stdin.py
43
+ tests/test_hardening.py
44
+ tests/test_http_auth.py
41
45
  tests/test_http_server.py
42
46
  tests/test_integration.py
43
47
  tests/test_lsp_provider.py
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -50,7 +50,8 @@ def main() -> None:
50
50
  http_parser = subparsers.add_parser("serve-http", help="Start the HTTP transport server")
51
51
  http_parser.add_argument("--port", type=int, default=8766, help="Port to listen on (default: 8766)")
52
52
  http_parser.add_argument("--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)")
53
- http_parser.add_argument("--allow-remote", action="store_true", help="Permit binding a non-loopback host (exposes an UNAUTHENTICATED endpoint)")
53
+ http_parser.add_argument("--allow-remote", action="store_true", help="Permit binding a non-loopback host (use with --token, or the endpoint is UNAUTHENTICATED)")
54
+ http_parser.add_argument("--token", default=None, help="Require this bearer token on every request (or set CODEINTEL_HTTP_TOKEN). Strongly recommended with --allow-remote.")
54
55
 
55
56
  # install subcommand
56
57
  install_parser = subparsers.add_parser("install", help="Register codeintel with AI agents")
@@ -120,6 +121,7 @@ def main() -> None:
120
121
  window=int(cfg.get("window", 20)),
121
122
  stride=int(cfg.get("stride", 10)),
122
123
  max_chunks=int(cfg.get("max_chunks", 500)),
124
+ max_total_chunks=int(cfg.get("max_total_chunks", 100000)),
123
125
  ).index(project_root)
124
126
  if count > 0:
125
127
  print(f"Indexed {count} chunks")
@@ -225,7 +227,7 @@ def main() -> None:
225
227
  from codeintel import server
226
228
 
227
229
  project_root = args.project_root or os.getcwd()
228
- status = server.code_status_handler({})
230
+ status = server.code_status_handler({"project_root": project_root})
229
231
 
230
232
  print("Engine status:")
231
233
  for engine in ["graph", "lsp", "semantic"]:
@@ -318,7 +320,8 @@ def main() -> None:
318
320
  elif args.command == "serve-http":
319
321
  try:
320
322
  from codeintel.http_server import run
321
- run(host=args.host, port=args.port, allow_remote=args.allow_remote)
323
+ token = args.token or os.environ.get("CODEINTEL_HTTP_TOKEN")
324
+ run(host=args.host, port=args.port, allow_remote=args.allow_remote, token=token)
322
325
  except KeyboardInterrupt:
323
326
  pass
324
327
  except Exception as exc:
@@ -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
@@ -12,7 +13,7 @@ def _compute_hash(target: str, project_root: str) -> str:
12
13
  try:
13
14
  root = os.path.realpath(project_root) if project_root else ""
14
15
  path = os.path.realpath(target)
15
- if root and path.startswith(root + os.sep) or path == root:
16
+ if (root and path.startswith(root + os.sep)) or path == root:
16
17
  if os.path.isfile(path):
17
18
  with open(path, "rb") as fh:
18
19
  return hashlib.sha256(fh.read()).hexdigest()
@@ -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
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import pathlib
5
+ import sys
6
+
7
+ if sys.version_info >= (3, 11):
8
+ import tomllib
9
+ else: # pragma: no cover
10
+ try:
11
+ import tomllib # type: ignore[no-redef]
12
+ except ImportError:
13
+ import tomli as tomllib # type: ignore[no-redef]
14
+
15
+ logger = logging.getLogger("codeintel")
16
+
17
+ _DEFAULTS: dict = {
18
+ "backend": "auto",
19
+ "semantic": "on",
20
+ "reindex": "on-demand",
21
+ "window": 20,
22
+ "stride": 10,
23
+ "max_chunks": 500, # per file
24
+ "max_total_chunks": 100000, # safety ceiling on chunks embedded in one index pass
25
+ "cosine_floor": 0.25,
26
+ "model": "BAAI/bge-small-en-v1.5",
27
+ }
28
+
29
+ # Values restricted to a fixed set — anything else falls back to the default.
30
+ _ENUMS: dict = {
31
+ "backend": {"auto", "graph", "lsp", "semantic"},
32
+ "semantic": {"on", "off"},
33
+ "reindex": {"on-demand", "never"},
34
+ }
35
+ _POSITIVE_INTS = ("window", "stride", "max_chunks", "max_total_chunks")
36
+
37
+
38
+ def _read_toml(path: pathlib.Path) -> dict:
39
+ try:
40
+ with path.open("rb") as fh:
41
+ return tomllib.load(fh)
42
+ except (FileNotFoundError, OSError, tomllib.TOMLDecodeError):
43
+ return {}
44
+
45
+
46
+ def _coerce(cfg: dict) -> dict:
47
+ """Coerce/clamp a merged config to safe values, warning on and dropping bad ones. Never raises:
48
+ a malformed ``.codeintel.toml`` (a string where a number belongs, an out-of-range floor, an
49
+ unknown enum) must degrade to the default for that key, not break every query that loads it."""
50
+ out = dict(_DEFAULTS)
51
+ for key, default in _DEFAULTS.items():
52
+ if key not in cfg:
53
+ continue
54
+ val = cfg[key]
55
+ try:
56
+ if key in _ENUMS:
57
+ s = str(val).strip().lower()
58
+ if s in _ENUMS[key]:
59
+ out[key] = s
60
+ else:
61
+ logger.warning("config: %s=%r invalid (expected %s) — using %r",
62
+ key, val, sorted(_ENUMS[key]), default)
63
+ elif key == "model":
64
+ out[key] = str(val).strip() or default
65
+ elif key == "cosine_floor":
66
+ f = float(val)
67
+ if f != f: # NaN (TOML allows `nan`) — reject so it can't silently disable the floor
68
+ raise ValueError("nan")
69
+ out[key] = min(1.0, max(0.0, f))
70
+ elif key in _POSITIVE_INTS:
71
+ n = int(val)
72
+ if n > 0:
73
+ out[key] = n
74
+ else:
75
+ logger.warning("config: %s=%r must be > 0 — using %r", key, val, default)
76
+ except (TypeError, ValueError, OverflowError):
77
+ # int(float('inf')) raises OverflowError (TOML allows `inf`); float("x") → ValueError;
78
+ # int([]) → TypeError. Any non-usable value logs and keeps the default, so the
79
+ # docstring's never-raise promise holds even for the CLI paths that don't wrap this.
80
+ logger.warning("config: %s=%r not usable — using default %r", key, val, default)
81
+ # Keep any extra keys the user set (forward-compat) without validating them.
82
+ for k, v in cfg.items():
83
+ if k not in _DEFAULTS:
84
+ out[k] = v
85
+ return out
86
+
87
+
88
+ def load_config(project_root: str | None = None) -> dict:
89
+ """Return the merged, validated config: defaults < global < project. Values that fail
90
+ validation fall back to their default (logged), so a bad config file never breaks a query."""
91
+ root = pathlib.Path(project_root) if project_root is not None else pathlib.Path.cwd()
92
+
93
+ global_cfg = _read_toml(pathlib.Path.home() / ".codeintel" / "config.toml")
94
+ project_cfg = _read_toml(root / ".codeintel.toml")
95
+
96
+ merged = {**_DEFAULTS, **global_cfg, **project_cfg}
97
+ return _coerce(merged)
@@ -5,7 +5,7 @@ from typing import Any
5
5
 
6
6
  from codeintel.cache import ContentHashCache
7
7
  from codeintel.policy import TieringPolicy
8
- from codeintel.provider import Result, safe_null_result
8
+ from codeintel.provider import Result, log_swallowed, safe_null_result
9
9
  from codeintel.providers.none import NoneProvider
10
10
  from codeintel.reindexer import Reindexer
11
11
 
@@ -128,7 +128,8 @@ class Gateway:
128
128
  if r is not None:
129
129
  return r
130
130
  return safe_null_result(op_str, target_str, engine=engine_str, reason="no-result")
131
- except Exception:
131
+ except Exception as exc:
132
+ log_swallowed(f"Gateway._dispatch_single[{engine_str}.{op_str}]", exc)
132
133
  return safe_null_result(op_str, target_str, engine=engine_str, reason="provider-error")
133
134
 
134
135
  def query(
@@ -206,14 +207,15 @@ class Gateway:
206
207
  provider = self._provider_for(engine_str)
207
208
  result = self._dispatch_single(provider, op_str, target_str, budget, project_root, engine_str)
208
209
 
209
- # overview auto-fallback (F4 Story 2): when auto-routed to graph but graph is
210
- # unavailable, try lsp a file/symbol overview is something lsp can also serve.
210
+ # overview auto-fallback (F4 Story 2): when auto-routed to graph but graph can't serve
211
+ # it — the backend is unavailable, OR this repo simply isn't in the graph try lsp,
212
+ # which can produce a file/symbol overview without the graph index.
211
213
  if (
212
214
  was_auto
213
215
  and op_str == "overview"
214
216
  and engine_str == "graph"
215
217
  and result.get("result") is None
216
- and result.get("reason") == "engine-unavailable"
218
+ and result.get("reason") in ("engine-unavailable", "project-not-indexed")
217
219
  ):
218
220
  lsp_result = self._dispatch_single(
219
221
  self.lsp, op_str, target_str, budget, project_root, "lsp"
@@ -224,5 +226,6 @@ class Gateway:
224
226
  self._cache.put(op_str, target_str, engine_str, root_str, result, freshness)
225
227
  return result
226
228
 
227
- except Exception:
229
+ except Exception as exc:
230
+ log_swallowed("Gateway.query", exc)
228
231
  return safe_null_result(op or "", target or "", reason="gateway-error")