codecortex 0.2.2__py3-none-any.whl → 0.4.0__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecortex
3
- Version: 0.2.2
3
+ Version: 0.4.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
@@ -183,7 +183,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
183
183
  | `codeintel setup [project_root] [--index] [--warm] [--install-uv]` | Check backends + optionally index this repo; ends with a health report |
184
184
  | `codeintel index [project_root]` | Index a project for semantic search |
185
185
  | `codeintel serve` | Start the MCP server (stdio transport) |
186
- | `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) |
187
187
  | `codeintel query --op OP --target TARGET [--engine auto]` | Run a single query and print the result |
188
188
  | `codeintel status [project_root]` | Show engine availability and index age |
189
189
  | `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap |
@@ -197,17 +197,31 @@ Human-facing commands (`doctor`, `status`, `query`, `setup`, `reset`) honor `--n
197
197
  Create `.codeintel.toml` at your project root to override defaults:
198
198
 
199
199
  ```toml
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
204
- max_chunks = 500 # max chunks to embed per file
205
- 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
206
207
  ```
207
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_LOG_LEVEL` | `DEBUG`\|`INFO`\|`WARNING`(default)\|`ERROR` for the server logger |
217
+ | `CODEINTEL_LOG_FORMAT=json` | Structured (JSON-per-line) logs for ELK / Splunk / Datadog |
218
+ | `CODEINTEL_HTTP_ACCESS_LOG=1` | One log line per HTTP request (method, path, status, latency) |
219
+ | `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` |
220
+ | `CODEINTEL_REINDEX=off` | Disable the background reindexer; queries then index inline to stay fresh |
221
+
208
222
  ## Privacy & dependencies
209
223
 
210
- **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.
224
+ **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.
211
225
 
212
226
  **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).
213
227
 
@@ -240,6 +254,8 @@ Over MCP the agent calls `code.query` directly. Over HTTP, start the server and
240
254
  codeintel serve-http & # listens on 127.0.0.1:8766 by default
241
255
  ```
242
256
 
257
+ 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.
258
+
243
259
  ```python
244
260
  import urllib.request, json
245
261
 
@@ -260,6 +276,25 @@ if result["result"] is not None:
260
276
 
261
277
  The response is always JSON-safe. Check `result["result"] is not None` before use. Never catch an exception from the gateway — it never raises.
262
278
 
279
+ ## Operations & deployment
280
+
281
+ Running codeintel as a shared service? It ships with what ops teams expect:
282
+
283
+ | Endpoint | Auth | Purpose |
284
+ |---|---|---|
285
+ | `GET /healthz` | none | Liveness — always `200` (for load balancers / `livenessProbe`) |
286
+ | `GET /readyz` | none | Readiness — `200` once the gateway is up (`readinessProbe`) |
287
+ | `GET /metrics` | token | Prometheus exposition — request counts, latency, in-flight, build info |
288
+
289
+ Plus **bearer-token auth**, **structured JSON logs** (`CODEINTEL_LOG_FORMAT=json`) with optional per-request access logs, **graceful `SIGTERM`** shutdown, a bounded connection pool, and a non-root **Dockerfile** with a healthcheck.
290
+
291
+ **Full guide → [docs/deploy.md](docs/deploy.md)**: systemd, Docker / Compose, Kubernetes (liveness + readiness probes, token from a Secret), reverse-proxy TLS, a Prometheus scrape config, and a security checklist.
292
+
293
+ ```bash
294
+ docker build -t codeintel . && docker run -p 127.0.0.1:8766:8766 \
295
+ -e CODEINTEL_HTTP_TOKEN="$(openssl rand -hex 32)" codeintel
296
+ ```
297
+
263
298
  ## Development
264
299
 
265
300
  ```bash
@@ -0,0 +1,33 @@
1
+ codecortex-0.4.0.dist-info/licenses/LICENSE,sha256=DIRvhlH8EulEUOYQxKDFFiimJrlVT5-kCKxQIgi2m7c,1073
2
+ codeintel/__init__.py,sha256=42STGor_9nKYXumfeV5tiyD_M8VdcddX7CEexmibPBk,22
3
+ codeintel/__main__.py,sha256=m0MQ0qKFD6VAfrkXP__d_8rbVb1RtUykuzMaa1Wzq6E,15573
4
+ codeintel/cache.py,sha256=UESsUtVqzuIqNQOnXG78ckQfbAz8pz_w1AE9gzeHOww,3158
5
+ codeintel/config.py,sha256=r8tsX4yeiqBgqpnFmUkuMczs6Hbwt8iasEuYSceW200,3761
6
+ codeintel/doctor.py,sha256=jiPuZzS801xfWwTyeVO9UHUxQqAJ-YqTlX-_epZfoyI,6659
7
+ codeintel/gateway.py,sha256=3MEuD5SC2bIB3MLeZWj4q9FJYGfkVfRjmB1n8ISTp_U,9209
8
+ codeintel/http_server.py,sha256=u4UFORQ6dKTjtDKY8_SWSaXqz7mLbOLrj9V70ubpNDI,12969
9
+ codeintel/indexer.py,sha256=brvvoCSdjO5v6VZrRe-8_VrakzQpp1ByyANFhUGTjQQ,10243
10
+ codeintel/injector.py,sha256=IHZTLajQURE2LLxL8dFE9pvJV1RMOAbE7pVRQbFVvgs,2755
11
+ codeintel/installer.py,sha256=PSgK6KA14VEQs2pQKCmjf9JDxJ2mpoEsaWrpFGWSy68,3488
12
+ codeintel/logconfig.py,sha256=i15tVUgHvXE3UoMutMhsaj8578-A0anYSn8-YFZIbxY,1859
13
+ codeintel/mapper.py,sha256=JN-lMO1CXIyDKSwHfzp0JuK396FR41XhFgtAip77G0E,7493
14
+ codeintel/metrics.py,sha256=nULopQhkuzMzlLjXvYfRzHOUCjnW-VHmtpYtrKF9tqA,3961
15
+ codeintel/onboarding.py,sha256=UxNJnlENxEAzG3JnEXu_Yue-4GZiEX2HG2BZVVA6jMI,8925
16
+ codeintel/policy.py,sha256=mUAlk9oYoszQLEcvoroFLRd9DSTzho7YcbM7xb6RS-k,935
17
+ codeintel/provider.py,sha256=GYNhOpJteq3zTp4R_vTp1Wz2k6TPjx6PicGhsq_CT9s,2013
18
+ codeintel/reindexer.py,sha256=sgCvb65scbmTtRd5LKhqxQtWolPOAmwKXJbmXWxvmVc,5667
19
+ codeintel/reset.py,sha256=8Mpak4Tj5e-yTcM3_EnZJzbJPwPV2o8xpxGjv-wk9EE,3675
20
+ codeintel/searcher.py,sha256=R9PV7jSx-GO--qmh65FXlYxpbKbjCnxV-EF9hGe8hZU,4588
21
+ codeintel/semantic_db.py,sha256=9umQ8UUUtHHy7Ngi0B-e8eGCy62XFIOkcWnd3wc3MMQ,3503
22
+ codeintel/server.py,sha256=W5iw1fmWJsu4rvJAH7_cH5dEJEtyOdepkkpthhGj4TY,7629
23
+ codeintel/term.py,sha256=csbSYD8euC13FnnpThy8kylDgZeejQ6j6Ol6Gu21Clo,6819
24
+ codeintel/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
+ codeintel/providers/graph.py,sha256=nsVsSQueFENSlyXlRf9r4-5aU-f18AnMM_9xOGb8rok,20180
26
+ codeintel/providers/lsp.py,sha256=w4xCPmZcuro4W9MIGU-4pEtuVA2YrvEapXHTjVVd0gA,16840
27
+ codeintel/providers/none.py,sha256=VI84XQkT7opQm6rBAEn0CdM8CNVlKGWYmjAhyeWbHeg,783
28
+ codeintel/providers/semantic.py,sha256=LGIbJLt5zkSv1frbloEeAfy7oplY1xl-FmhxujTEmk0,6311
29
+ codecortex-0.4.0.dist-info/METADATA,sha256=6RMMkOImBeRdKfuvu_IiF3Dfn80oqWG-wUcf8E4SJTw,18154
30
+ codecortex-0.4.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
31
+ codecortex-0.4.0.dist-info/entry_points.txt,sha256=zrYfo95-8JkK0G2e5f9d94sYqqim7ZrdCW8knIfSy2U,54
32
+ codecortex-0.4.0.dist-info/top_level.txt,sha256=DF3TH1hHLWrrnX-7HhfMX0OnS2EXgqPRhPbiL_oSekw,10
33
+ codecortex-0.4.0.dist-info/RECORD,,
codeintel/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.2.2"
1
+ __version__ = "0.4.0"
codeintel/__main__.py CHANGED
@@ -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:
codeintel/cache.py CHANGED
@@ -13,7 +13,7 @@ def _compute_hash(target: str, project_root: str) -> str:
13
13
  try:
14
14
  root = os.path.realpath(project_root) if project_root else ""
15
15
  path = os.path.realpath(target)
16
- if root and path.startswith(root + os.sep) or path == root:
16
+ if (root and path.startswith(root + os.sep)) or path == root:
17
17
  if os.path.isfile(path):
18
18
  with open(path, "rb") as fh:
19
19
  return hashlib.sha256(fh.read()).hexdigest()
codeintel/config.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  import pathlib
4
5
  import sys
5
6
 
@@ -11,17 +12,28 @@ else: # pragma: no cover
11
12
  except ImportError:
12
13
  import tomli as tomllib # type: ignore[no-redef]
13
14
 
15
+ logger = logging.getLogger("codeintel")
16
+
14
17
  _DEFAULTS: dict = {
15
18
  "backend": "auto",
16
19
  "semantic": "on",
17
20
  "reindex": "on-demand",
18
21
  "window": 20,
19
22
  "stride": 10,
20
- "max_chunks": 500,
23
+ "max_chunks": 500, # per file
24
+ "max_total_chunks": 100000, # safety ceiling on chunks embedded in one index pass
21
25
  "cosine_floor": 0.25,
22
26
  "model": "BAAI/bge-small-en-v1.5",
23
27
  }
24
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
+
25
37
 
26
38
  def _read_toml(path: pathlib.Path) -> dict:
27
39
  try:
@@ -31,12 +43,55 @@ def _read_toml(path: pathlib.Path) -> dict:
31
43
  return {}
32
44
 
33
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
+
34
88
  def load_config(project_root: str | None = None) -> dict:
35
- """Return merged config: defaults < global < project."""
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."""
36
91
  root = pathlib.Path(project_root) if project_root is not None else pathlib.Path.cwd()
37
92
 
38
93
  global_cfg = _read_toml(pathlib.Path.home() / ".codeintel" / "config.toml")
39
94
  project_cfg = _read_toml(root / ".codeintel.toml")
40
95
 
41
96
  merged = {**_DEFAULTS, **global_cfg, **project_cfg}
42
- return merged
97
+ return _coerce(merged)
codeintel/gateway.py CHANGED
@@ -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")
codeintel/http_server.py CHANGED
@@ -1,17 +1,67 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import hmac
3
4
  import json
5
+ import logging
6
+ import os
7
+ import signal
4
8
  import sys
9
+ import threading
10
+ import time
5
11
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
12
+ from urllib.parse import parse_qs, urlparse
6
13
 
14
+ from codeintel import __version__
15
+ from codeintel.logconfig import configure_logging
16
+ from codeintel.metrics import Metrics
17
+ from codeintel.provider import log_swallowed
7
18
  from codeintel.server import code_doctor_handler, code_query_handler, code_status_handler
8
19
 
9
- _MAX_BODY_BYTES = 1_048_576 # 1 MiB
20
+ _MAX_BODY_BYTES = 1_048_576 # 1 MiB
21
+ _REQUEST_TIMEOUT_S = 60 # per-request socket read timeout — drops an idle/half-open client
22
+ _MAX_CONCURRENT_REQUESTS = 64 # cap live worker threads so a burst can't exhaust threads/FDs
23
+ _DRAIN_TIMEOUT_S = 15 # on shutdown, wait up to this long for in-flight requests to finish
24
+ _ACCESS_LOG = os.environ.get("CODEINTEL_HTTP_ACCESS_LOG", "").strip().lower() in ("1", "true", "on", "yes")
25
+ _PROM_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8"
26
+
27
+ _logger = logging.getLogger("codeintel")
10
28
 
11
29
 
12
30
  class _Handler(BaseHTTPRequestHandler):
31
+ # Applied to the request socket by the base handler's setup(); a slow/half-open client
32
+ # connection is dropped instead of pinning a worker thread forever (basic slowloris guard).
33
+ timeout = _REQUEST_TIMEOUT_S
34
+
13
35
  def log_message(self, format: str, *args: object) -> None:
14
- pass # suppress default stderr noise
36
+ pass # suppress default stderr noise; opt-in access logging is handled in _observe
37
+
38
+ def handle_one_request(self) -> None:
39
+ self._t0 = time.monotonic()
40
+ m = getattr(self.server, "metrics", None)
41
+ if m is not None:
42
+ m.inc_in_flight()
43
+ try:
44
+ super().handle_one_request()
45
+ finally:
46
+ if m is not None:
47
+ m.dec_in_flight()
48
+
49
+ def _observe(self, status: int) -> None:
50
+ try:
51
+ dur = time.monotonic() - getattr(self, "_t0", time.monotonic())
52
+ except Exception:
53
+ dur = 0.0
54
+ m = getattr(self.server, "metrics", None)
55
+ if m is not None:
56
+ try:
57
+ m.record(self.command or "?", urlparse(self.path).path, status, dur)
58
+ except Exception:
59
+ pass
60
+ if _ACCESS_LOG:
61
+ try:
62
+ _logger.info("%s %s -> %s (%.1fms)", self.command, self.path, status, dur * 1000.0)
63
+ except Exception:
64
+ pass
15
65
 
16
66
  def _send_json(self, status: int, data: dict) -> None:
17
67
  body = json.dumps(data).encode()
@@ -20,8 +70,37 @@ class _Handler(BaseHTTPRequestHandler):
20
70
  self.send_header("Content-Length", str(len(body)))
21
71
  self.end_headers()
22
72
  self.wfile.write(body)
73
+ self._observe(status)
74
+
75
+ def _send_text(self, status: int, text: str, content_type: str) -> None:
76
+ body = text.encode()
77
+ self.send_response(status)
78
+ self.send_header("Content-Type", content_type)
79
+ self.send_header("Content-Length", str(len(body)))
80
+ self.end_headers()
81
+ self.wfile.write(body)
82
+ self._observe(status)
83
+
84
+ def _authorized(self) -> bool:
85
+ """When the server was started with a token, require ``Authorization: Bearer <token>``
86
+ (constant-time compare). No token configured → auth is disabled (the loopback default)."""
87
+ token = getattr(self.server, "auth_token", None)
88
+ if not token:
89
+ return True
90
+ header = self.headers.get("Authorization", "")
91
+ prefix = "Bearer "
92
+ if not header.startswith(prefix):
93
+ return False
94
+ presented = header[len(prefix):].strip()
95
+ # Compare as UTF-8 bytes: hmac.compare_digest raises TypeError on a non-ASCII str, so a
96
+ # malformed header (e.g. `Bearer résumé`) would otherwise crash the handler thread. Encoding
97
+ # is always safe and keeps the comparison constant-time.
98
+ return hmac.compare_digest(presented.encode("utf-8"), token.encode("utf-8"))
23
99
 
24
100
  def do_POST(self) -> None:
101
+ if not self._authorized():
102
+ self._send_json(401, {"error": "unauthorized"})
103
+ return
25
104
  if self.path not in ("/code/query", "/code/doctor"):
26
105
  self._send_json(404, {"error": "not-found"})
27
106
  return
@@ -50,19 +129,91 @@ class _Handler(BaseHTTPRequestHandler):
50
129
  self._send_json(200, result)
51
130
 
52
131
  def do_GET(self) -> None:
53
- if self.path != "/code/status":
54
- self._send_json(404, {"error": "not-found"})
132
+ parsed = urlparse(self.path)
133
+ path = parsed.path
134
+
135
+ # Liveness / readiness are UNAUTHENTICATED by convention (kubelet/load balancers don't send
136
+ # tokens) and reveal nothing sensitive — only up/ready.
137
+ if path == "/healthz":
138
+ self._send_json(200, {"status": "ok"})
139
+ return
140
+ if path == "/readyz":
141
+ try:
142
+ from codeintel.server import _get_gateway
143
+ _get_gateway() # builds once (cached); readiness = the gateway is constructible
144
+ self._send_json(200, {"status": "ready"})
145
+ except Exception as exc:
146
+ log_swallowed("readyz", exc)
147
+ self._send_json(503, {"status": "not-ready"})
148
+ return
149
+
150
+ # Everything else is auth-gated when a token is configured (metrics can reveal usage
151
+ # patterns; status reveals engine/index state).
152
+ if not self._authorized():
153
+ self._send_json(401, {"error": "unauthorized"})
55
154
  return
56
- result = code_status_handler({})
57
- self._send_json(200, result)
155
+
156
+ if path == "/metrics":
157
+ m = getattr(self.server, "metrics", None)
158
+ try:
159
+ text = m.render() if m is not None else ""
160
+ except Exception as exc:
161
+ log_swallowed("metrics.render", exc) # never leave the client with no response
162
+ text = ""
163
+ self._send_text(200, text, _PROM_CONTENT_TYPE)
164
+ return
165
+ if path == "/code/status":
166
+ project_root = (parse_qs(parsed.query).get("project_root") or [""])[0]
167
+ self._send_json(200, code_status_handler({"project_root": project_root}))
168
+ return
169
+ self._send_json(404, {"error": "not-found"})
58
170
 
59
171
 
60
172
  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.
173
+ # Threaded so one slow request (an LSP session warming, or a first-time index) can't block
174
+ # every other agent's query. The gateway is a shared singleton, but its mutable state is
175
+ # lock-guarded (query cache, reindexer, LSP sessions, graph project cache) and the semantic
176
+ # engine is thread-confined with WAL, so concurrent requests are safe.
65
177
  daemon_threads = True
178
+ auth_token: str | None = None # set by run() when a token is provided
179
+
180
+ def __init__(self, *args, **kwargs) -> None:
181
+ super().__init__(*args, **kwargs)
182
+ self.metrics = Metrics(version=__version__)
183
+ # Stdlib ThreadingHTTPServer spawns one thread per connection with no ceiling, so many
184
+ # slow/half-open clients could exhaust threads/FDs. Bound live workers; past the cap we
185
+ # refuse fast with 503 rather than spawn an unbounded thread. (For a genuinely hostile
186
+ # network, still front this with a reverse proxy — http.server is not hardened for that.)
187
+ self._slots = threading.BoundedSemaphore(_MAX_CONCURRENT_REQUESTS)
188
+
189
+ def process_request(self, request, client_address) -> None:
190
+ if not self._slots.acquire(blocking=False):
191
+ try:
192
+ self.metrics.inc_overload() # surface overload in /metrics (rejected_total)
193
+ except Exception:
194
+ pass
195
+ try:
196
+ request.sendall(
197
+ b"HTTP/1.1 503 Service Unavailable\r\n"
198
+ b"Content-Type: application/json\r\nContent-Length: 22\r\n"
199
+ b'Connection: close\r\n\r\n{"error":"overloaded"}'
200
+ )
201
+ except Exception:
202
+ pass
203
+ self.shutdown_request(request)
204
+ return
205
+ super().process_request(request, client_address) # spawns the worker thread
206
+
207
+ def process_request_thread(self, request, client_address) -> None:
208
+ try:
209
+ super().process_request_thread(request, client_address)
210
+ finally:
211
+ self._slots.release()
212
+
213
+ def handle_error(self, request, client_address) -> None:
214
+ # A stalled/reset client raises here (socket timeout, broken pipe) — expected noise, not a
215
+ # server bug (handlers are never-raise). Keep it off stderr; surface with CODEINTEL_DEBUG=1.
216
+ log_swallowed("http.handle_error", sys.exc_info()[1] or Exception("unknown"))
66
217
 
67
218
 
68
219
  _LOOPBACK_NAMES = {"localhost"}
@@ -83,15 +234,61 @@ def _is_loopback(host: str) -> bool:
83
234
  return False # a non-IP hostname is never treated as loopback
84
235
 
85
236
 
86
- def run(host: str = "127.0.0.1", port: int = 8766, *, allow_remote: bool = False) -> None:
87
- if not _is_loopback(host) and not allow_remote:
88
- print(f"refusing to bind non-loopback host {host!r} without --allow-remote — this would "
89
- f"expose an UNAUTHENTICATED code-intel endpoint (your indexed repo) to the network",
90
- file=sys.stderr)
91
- raise SystemExit(2)
237
+ def _no_auth_override() -> bool:
238
+ return os.environ.get("CODEINTEL_ALLOW_NO_AUTH", "").strip().lower() in ("1", "true", "on", "yes")
239
+
240
+
241
+ def run(
242
+ host: str = "127.0.0.1",
243
+ port: int = 8766,
244
+ *,
245
+ allow_remote: bool = False,
246
+ token: str | None = None,
247
+ ) -> None:
248
+ configure_logging()
249
+ server_token = (token or "").strip() or None
92
250
  if not _is_loopback(host):
93
- print(f"WARNING: serving codeintel on {host}:{port} with NO authentication — anyone who can "
94
- f"reach this port can read your indexed repo", file=sys.stderr)
251
+ if not allow_remote:
252
+ print(f"refusing to bind non-loopback host {host!r} without --allow-remote — this would "
253
+ f"expose a code-intel endpoint (your indexed repo) to the network", file=sys.stderr)
254
+ raise SystemExit(2)
255
+ if not server_token and not _no_auth_override():
256
+ # Fail closed: never serve an UNAUTHENTICATED endpoint on the network by accident (a
257
+ # containerized `serve-http --host 0.0.0.0` with no token stops here unless overridden).
258
+ print(f"refusing to serve on {host}:{port} with NO authentication — set --token or "
259
+ f"CODEINTEL_HTTP_TOKEN (recommended), or set CODEINTEL_ALLOW_NO_AUTH=1 to override "
260
+ f"for a trusted network", file=sys.stderr)
261
+ raise SystemExit(2)
262
+
95
263
  server = CodeIntelHTTPServer((host, port), _Handler)
96
- print(f"Listening on http://{host}:{port}")
97
- server.serve_forever()
264
+ server.auth_token = server_token
265
+ if not _is_loopback(host) and not server_token:
266
+ print(f"WARNING: serving codeintel on {host}:{port} with NO authentication "
267
+ f"(CODEINTEL_ALLOW_NO_AUTH set) — anyone who can reach this port can read your "
268
+ f"indexed repo", file=sys.stderr)
269
+ auth_note = " (bearer-token auth required)" if server_token else ""
270
+ print(f"Listening on http://{host}:{port}{auth_note}")
271
+
272
+ def _graceful(signum, frame) -> None:
273
+ # shutdown() blocks until serve_forever() returns, so it must run OFF the serving thread —
274
+ # spawn a helper thread to avoid deadlocking the signal handler on the main thread.
275
+ threading.Thread(target=server.shutdown, daemon=True).start()
276
+
277
+ try:
278
+ signal.signal(signal.SIGTERM, _graceful)
279
+ signal.signal(signal.SIGINT, _graceful)
280
+ except (ValueError, OSError):
281
+ pass # not the main thread — signal handlers unavailable; rely on the caller to stop us
282
+
283
+ try:
284
+ server.serve_forever()
285
+ finally:
286
+ # Drain: serve_forever() has stopped accepting new connections; wait (bounded) for in-flight
287
+ # requests to finish before closing. Worker threads are daemons, so server_close() will NOT
288
+ # join them — this loop is what makes the shutdown actually graceful (a rolling restart
289
+ # doesn't cut a live response mid-flight).
290
+ deadline = time.monotonic() + _DRAIN_TIMEOUT_S
291
+ while server.metrics.in_flight() > 0 and time.monotonic() < deadline:
292
+ time.sleep(0.05)
293
+ server.server_close()
294
+ print("codeintel http server stopped", file=sys.stderr)
codeintel/indexer.py CHANGED
@@ -37,12 +37,14 @@ class Indexer:
37
37
  window: int = 20,
38
38
  stride: int = 10,
39
39
  max_chunks: int = 500,
40
+ max_total_chunks: int = 100000,
40
41
  ) -> None:
41
42
  self.db = db
42
43
  self.model_name = model_name
43
44
  self.window = window
44
45
  self.stride = stride
45
- self.max_chunks = max_chunks
46
+ self.max_chunks = max_chunks # per file
47
+ self.max_total_chunks = max_total_chunks # safety ceiling per index pass (memory backstop)
46
48
  self._embedder = None
47
49
 
48
50
  def _get_embedder(self):
@@ -131,6 +133,13 @@ class Indexer:
131
133
  new_chunks: list[tuple[str, str, str, int, str]] = []
132
134
 
133
135
  for filepath in self._walk_files(root):
136
+ if len(new_chunks) >= self.max_total_chunks:
137
+ logger.warning(
138
+ "index: reached max_total_chunks=%d this pass — stopping "
139
+ "(raise it in .codeintel.toml to embed more of a very large repo)",
140
+ self.max_total_chunks,
141
+ )
142
+ break
134
143
  try:
135
144
  with open(filepath, encoding="utf-8", errors="replace") as f:
136
145
  lines = f.readlines()
@@ -145,6 +154,8 @@ class Indexer:
145
154
  chunk_count = 0
146
155
 
147
156
  for chunk_start in range(0, len(lines), self.stride):
157
+ if len(new_chunks) >= self.max_total_chunks:
158
+ break # global ceiling reached mid-file; the file-loop guard logs and stops
148
159
  if chunk_count >= self.max_chunks:
149
160
  logger.debug(
150
161
  "chunk cap hit for %s, truncating at %d",
codeintel/logconfig.py ADDED
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ import sys
7
+
8
+
9
+ class _JsonFormatter(logging.Formatter):
10
+ """Minimal structured formatter for log aggregators (ELK / Splunk / Datadog). One JSON object
11
+ per line; no external dependency."""
12
+
13
+ def format(self, record: logging.LogRecord) -> str:
14
+ payload = {
15
+ "ts": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"),
16
+ "level": record.levelname,
17
+ "logger": record.name,
18
+ "msg": record.getMessage(),
19
+ }
20
+ if record.exc_info:
21
+ payload["exc"] = self.formatException(record.exc_info)
22
+ return json.dumps(payload, ensure_ascii=False)
23
+
24
+
25
+ _configured = False
26
+
27
+
28
+ def configure_logging() -> None:
29
+ """Configure the ``codeintel`` logger for a long-running server process. Idempotent, and
30
+ scoped to the ``codeintel`` logger only — importing codeintel as a library never hijacks the
31
+ root logger. Honors:
32
+
33
+ * ``CODEINTEL_LOG_LEVEL`` (default ``WARNING``) — DEBUG | INFO | WARNING | ERROR
34
+ * ``CODEINTEL_LOG_FORMAT`` (default plain) — set to ``json`` for structured output
35
+ """
36
+ global _configured
37
+ if _configured:
38
+ return
39
+ level_name = os.environ.get("CODEINTEL_LOG_LEVEL", "WARNING").strip().upper()
40
+ level = getattr(logging, level_name, logging.WARNING)
41
+ if not isinstance(level, int):
42
+ level = logging.WARNING
43
+
44
+ handler = logging.StreamHandler(sys.stderr)
45
+ if os.environ.get("CODEINTEL_LOG_FORMAT", "").strip().lower() == "json":
46
+ handler.setFormatter(_JsonFormatter())
47
+ else:
48
+ handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s"))
49
+
50
+ logger = logging.getLogger("codeintel")
51
+ logger.setLevel(level)
52
+ logger.handlers[:] = [handler]
53
+ logger.propagate = False
54
+ _configured = True
codeintel/metrics.py ADDED
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from typing import Dict, Tuple
5
+
6
+ # Path labels are restricted to known routes so a caller cannot explode Prometheus label
7
+ # cardinality (a memory-exhaustion vector) by hammering random URLs — everything else folds
8
+ # into path="other". Method and status are already low-cardinality; version is our own string.
9
+ _KNOWN_PATHS = frozenset(
10
+ {"/code/query", "/code/doctor", "/code/status", "/healthz", "/readyz", "/metrics"}
11
+ )
12
+
13
+
14
+ class Metrics:
15
+ """Thread-safe, dependency-free request metrics rendered in Prometheus text exposition
16
+ format. Shared across the threaded HTTP server's worker threads via one instance on the
17
+ server object; every mutation is lock-guarded."""
18
+
19
+ def __init__(self, version: str = "") -> None:
20
+ self._lock = threading.Lock()
21
+ self._version = version
22
+ self._count: Dict[Tuple[str, str, int], int] = {} # (method, path, status) -> n
23
+ self._lat_sum: Dict[str, float] = {} # path -> total seconds
24
+ self._lat_count: Dict[str, int] = {} # path -> n
25
+ self._in_flight = 0
26
+ self._overload = 0 # requests refused at the concurrency cap
27
+
28
+ @staticmethod
29
+ def _norm(path: str) -> str:
30
+ return path if path in _KNOWN_PATHS else "other"
31
+
32
+ def inc_in_flight(self) -> None:
33
+ with self._lock:
34
+ self._in_flight += 1
35
+
36
+ def dec_in_flight(self) -> None:
37
+ with self._lock:
38
+ self._in_flight = max(0, self._in_flight - 1)
39
+
40
+ def in_flight(self) -> int:
41
+ with self._lock:
42
+ return self._in_flight
43
+
44
+ def inc_overload(self) -> None:
45
+ with self._lock:
46
+ self._overload += 1
47
+
48
+ def record(self, method: str, path: str, status: int, duration_s: float) -> None:
49
+ p = self._norm(str(path))
50
+ key = (str(method or "?"), p, int(status))
51
+ with self._lock:
52
+ self._count[key] = self._count.get(key, 0) + 1
53
+ self._lat_sum[p] = self._lat_sum.get(p, 0.0) + max(0.0, float(duration_s))
54
+ self._lat_count[p] = self._lat_count.get(p, 0) + 1
55
+
56
+ def render(self) -> str:
57
+ with self._lock:
58
+ count = dict(self._count)
59
+ lat_sum = dict(self._lat_sum)
60
+ lat_count = dict(self._lat_count)
61
+ in_flight = self._in_flight
62
+ overload = self._overload
63
+ version = self._version
64
+
65
+ out: list[str] = []
66
+ out.append("# HELP codeintel_requests_total Total HTTP requests handled.")
67
+ out.append("# TYPE codeintel_requests_total counter")
68
+ for (method, path, status), n in sorted(count.items()):
69
+ out.append(
70
+ f'codeintel_requests_total{{method="{method}",path="{path}",status="{status}"}} {n}'
71
+ )
72
+ out.append("# HELP codeintel_request_duration_seconds Request handling duration.")
73
+ out.append("# TYPE codeintel_request_duration_seconds summary")
74
+ for path in sorted(lat_count):
75
+ out.append(f'codeintel_request_duration_seconds_sum{{path="{path}"}} {lat_sum[path]:.6f}')
76
+ out.append(f'codeintel_request_duration_seconds_count{{path="{path}"}} {lat_count[path]}')
77
+ out.append("# HELP codeintel_requests_in_flight Requests currently being handled.")
78
+ out.append("# TYPE codeintel_requests_in_flight gauge")
79
+ out.append(f"codeintel_requests_in_flight {in_flight}")
80
+ out.append("# HELP codeintel_requests_rejected_total Requests refused at the concurrency cap.")
81
+ out.append("# TYPE codeintel_requests_rejected_total counter")
82
+ out.append(f"codeintel_requests_rejected_total {overload}")
83
+ if version:
84
+ out.append("# HELP codeintel_build_info Build metadata (constant 1).")
85
+ out.append("# TYPE codeintel_build_info gauge")
86
+ out.append(f'codeintel_build_info{{version="{version}"}} 1')
87
+ return "\n".join(out) + "\n"
codeintel/onboarding.py CHANGED
@@ -72,6 +72,7 @@ def _bounded_index(project_root: str, *, timeout_s: float, out) -> dict:
72
72
  db, model_name=str(cfg.get("model") or "BAAI/bge-small-en-v1.5"),
73
73
  window=int(cfg.get("window", 20)), stride=int(cfg.get("stride", 10)),
74
74
  max_chunks=int(cfg.get("max_chunks", 500)),
75
+ max_total_chunks=int(cfg.get("max_total_chunks", 100000)),
75
76
  ).index(project_root)
76
77
  finally:
77
78
  db.close()
codeintel/provider.py CHANGED
@@ -1,8 +1,27 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
4
+ import os
5
+ import traceback
3
6
  from typing import Any, Optional, Protocol, runtime_checkable
4
7
  from typing_extensions import NotRequired, TypedDict
5
8
 
9
+ _logger = logging.getLogger("codeintel")
10
+ _DEBUG = os.environ.get("CODEINTEL_DEBUG", "").strip().lower() in ("1", "true", "on", "yes")
11
+
12
+
13
+ def log_swallowed(where: str, exc: BaseException) -> None:
14
+ """Record an exception the never-raise contract is about to swallow. Quiet by default so the
15
+ contract stays silent in normal use; set ``CODEINTEL_DEBUG=1`` to surface a full traceback when
16
+ diagnosing why a query came back as a safe-null. Never raises — logging failures are ignored."""
17
+ try:
18
+ if _DEBUG:
19
+ _logger.warning("codeintel swallowed error in %s: %s\n%s", where, exc, traceback.format_exc())
20
+ else:
21
+ _logger.debug("codeintel swallowed error in %s: %s", where, exc)
22
+ except Exception:
23
+ pass
24
+
6
25
 
7
26
  class Result(TypedDict):
8
27
  ok: bool
@@ -7,7 +7,7 @@ import threading
7
7
  import time
8
8
  from typing import Any, Optional
9
9
 
10
- from codeintel.provider import Result, safe_null_result
10
+ from codeintel.provider import Result, log_swallowed, safe_null_result
11
11
 
12
12
 
13
13
  def _cypher_literal(s: Any) -> str:
@@ -35,7 +35,8 @@ class GraphProvider:
35
35
  """
36
36
 
37
37
  def __init__(self) -> None:
38
- self._project_cache: dict[str, Optional[str]] = {}
38
+ self._project_cache: dict[str, Optional[str]] = {} # resolved names (stable, kept)
39
+ self._negative_until: dict[str, float] = {} # failed lookups, short TTL only
39
40
  self._project_cache_lock = threading.Lock() # concurrent HTTP requests share one provider
40
41
  self._detect_backend()
41
42
 
@@ -129,14 +130,22 @@ class GraphProvider:
129
130
  def _resolve_project(self, project_root: str) -> Optional[str]:
130
131
  with self._project_cache_lock:
131
132
  if project_root in self._project_cache:
132
- return self._project_cache[project_root]
133
+ return self._project_cache[project_root] # positive: a repo's name is stable
134
+ neg_until = self._negative_until.get(project_root)
135
+ if neg_until is not None and time.monotonic() < neg_until:
136
+ return None # a recently-failed lookup, still within its short TTL
133
137
  # 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.
138
+ # every concurrent request. A rare duplicate lookup on first contact is harmless.
136
139
  raw = self._run("list_projects", {}, 3000)
137
140
  name = self._match_project(raw, project_root)
138
141
  with self._project_cache_lock:
139
- self._project_cache[project_root] = name
142
+ if name is not None:
143
+ self._project_cache[project_root] = name
144
+ self._negative_until.pop(project_root, None)
145
+ else:
146
+ # Cache the MISS only briefly, so a repo indexed into the graph AFTER this failed
147
+ # lookup is picked up within the TTL rather than staying stuck until a restart.
148
+ self._negative_until[project_root] = time.monotonic() + 30.0
140
149
  return name
141
150
 
142
151
  def probe(self, project_root: str, timeout_ms: int = 3000) -> dict:
@@ -400,7 +409,8 @@ class GraphProvider:
400
409
  "engine": "graph",
401
410
  "cached": False,
402
411
  }
403
- except Exception:
412
+ except Exception as exc:
413
+ log_swallowed("GraphProvider.build_result", exc)
404
414
  return safe_null_result(op, target, engine="graph", reason="error")
405
415
 
406
416
  def _dispatch(
@@ -12,7 +12,7 @@ from typing import Any, Optional
12
12
  from mcp import ClientSession
13
13
  from mcp.client.stdio import stdio_client
14
14
 
15
- from codeintel.provider import Result, safe_null_result
15
+ from codeintel.provider import Result, log_swallowed, safe_null_result
16
16
 
17
17
  _COOLDOWN_SECONDS = 60
18
18
  _DEFAULT_TIMEOUT_S = 5.0
@@ -231,7 +231,8 @@ class LspProvider:
231
231
  "engine": "lsp",
232
232
  "cached": False,
233
233
  }
234
- except Exception:
234
+ except Exception as exc:
235
+ log_swallowed("LspProvider.build_result", exc)
235
236
  return safe_null_result(op, target, engine="lsp", reason="error")
236
237
 
237
238
  def _dispatch(
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import os
3
4
  import pathlib
4
5
 
5
- from codeintel.provider import Result, safe_null_result
6
+ from codeintel.provider import Result, log_swallowed, safe_null_result
6
7
 
7
8
  try:
8
9
  import fastembed # noqa: F401
@@ -101,15 +102,26 @@ class SemanticProvider:
101
102
  db = SemanticDb(str(_DB_PATH))
102
103
  db.init()
103
104
 
104
- Indexer(
105
- db,
106
- model_name=model,
107
- window=int(cfg.get("window", 20)),
108
- stride=int(cfg.get("stride", 10)),
109
- max_chunks=int(cfg.get("max_chunks", 500)),
110
- ).index(project_root)
111
-
112
105
  searcher = Searcher(db, model_name=model)
106
+
107
+ # A full index pass walks and hashes every file — too expensive to run on every query.
108
+ # The background Reindexer (gated by the CODEINTEL_REINDEX env) already keeps a warm repo
109
+ # fresh, so we only pay the inline pass on a COLD repo (nothing indexed yet). The one
110
+ # exception: when that background reindexer is turned off, the inline pass is the only
111
+ # thing keeping the index current, so we run it every query to preserve freshness.
112
+ background_reindex_off = (
113
+ os.environ.get("CODEINTEL_REINDEX", "on").strip().lower() == "off"
114
+ )
115
+ if background_reindex_off or not searcher.has_index(project_root):
116
+ Indexer(
117
+ db,
118
+ model_name=model,
119
+ window=int(cfg.get("window", 20)),
120
+ stride=int(cfg.get("stride", 10)),
121
+ max_chunks=int(cfg.get("max_chunks", 500)),
122
+ max_total_chunks=int(cfg.get("max_total_chunks", 100000)),
123
+ ).index(project_root)
124
+
113
125
  if not searcher.has_index(project_root):
114
126
  return safe_null_result(
115
127
  op, target, engine="semantic", reason="no-index",
@@ -135,5 +147,6 @@ class SemanticProvider:
135
147
  "cached": False,
136
148
  }
137
149
  return result
138
- except Exception:
150
+ except Exception as exc:
151
+ log_swallowed("SemanticProvider.build_result", exc)
139
152
  return safe_null_result(op, target, engine="semantic", reason="provider-error")
codeintel/reindexer.py CHANGED
@@ -71,8 +71,21 @@ class Reindexer:
71
71
  return
72
72
  self._last_fired[project_root] = now
73
73
 
74
+ # Passed the debounce gate — honor a per-project `reindex = "never"` opt-out before doing
75
+ # expensive work, so that config key actually disables background reindexing (not only the
76
+ # inline path). Checked post-debounce, so config is read at most once per window.
77
+ if self._reindex_disabled(project_root):
78
+ return
79
+
74
80
  self._executor.submit(self._do_reindex, project_root)
75
81
 
82
+ def _reindex_disabled(self, project_root: str) -> bool:
83
+ try:
84
+ from codeintel.config import load_config
85
+ return str(load_config(project_root).get("reindex") or "").strip().lower() == "never"
86
+ except Exception:
87
+ return False
88
+
76
89
  def _do_reindex(self, project_root: str) -> None:
77
90
  try:
78
91
  self._semantic_reindex(project_root)
@@ -85,17 +98,27 @@ class Reindexer:
85
98
  def _semantic_reindex(self, project_root: str) -> None:
86
99
  import pathlib
87
100
 
101
+ from codeintel.config import load_config
88
102
  from codeintel.semantic_db import SemanticDb, default_db_path
89
103
  from codeintel.indexer import Indexer
90
104
 
91
- # Same per-machine cache the SemanticProvider reads — index and search must
92
- # never diverge onto different files.
105
+ # Same per-machine cache the SemanticProvider reads — index and search must never diverge
106
+ # onto different files. Honor the project's config so the background pass indexes exactly
107
+ # like the inline and CLI paths (same model, window/stride, and chunk ceilings).
108
+ cfg = load_config(project_root)
93
109
  db_path = default_db_path()
94
110
  pathlib.Path(db_path).parent.mkdir(parents=True, exist_ok=True)
95
111
  db = SemanticDb(db_path)
96
112
  try:
97
113
  db.init()
98
- Indexer(db).index(project_root)
114
+ Indexer(
115
+ db,
116
+ model_name=str(cfg.get("model") or "BAAI/bge-small-en-v1.5"),
117
+ window=int(cfg.get("window", 20)),
118
+ stride=int(cfg.get("stride", 10)),
119
+ max_chunks=int(cfg.get("max_chunks", 500)),
120
+ max_total_chunks=int(cfg.get("max_total_chunks", 100000)),
121
+ ).index(project_root)
99
122
  finally:
100
123
  db.close()
101
124
 
codeintel/server.py CHANGED
@@ -103,15 +103,21 @@ def code_status_handler(args: dict) -> dict:
103
103
  if not engines:
104
104
  engines.append("none")
105
105
 
106
- # Report real freshness/model instead of hardcoded nulls (SPEC §7).
106
+ # Report real freshness/model instead of hardcoded nulls (SPEC §7). When a project_root is
107
+ # supplied, `indexed` is scoped to THAT repo (does it have indexed chunks?) rather than the
108
+ # misleading "any semantic db file exists on this machine".
109
+ project_root = str(args.get("project_root", "") or "")
107
110
  indexed = False
108
111
  model = None
109
112
  try:
110
- import os
111
113
  from codeintel.semantic_db import DEFAULT_MODEL, default_db_path
112
114
  if semantic_available:
113
115
  model = DEFAULT_MODEL
114
- indexed = os.path.exists(default_db_path())
116
+ if project_root:
117
+ indexed = bool(SemanticProvider().probe(project_root).get("repo_indexed"))
118
+ else:
119
+ import os
120
+ indexed = os.path.exists(default_db_path())
115
121
  except Exception:
116
122
  pass
117
123
 
@@ -186,6 +192,8 @@ def code_map_handler(args: dict) -> dict:
186
192
 
187
193
 
188
194
  def run() -> None:
195
+ from codeintel.logconfig import configure_logging
196
+ configure_logging() # logs to stderr; stdout is the MCP protocol channel
189
197
  mcp = MCPServer(name="codeintel")
190
198
 
191
199
  async def _code_query(
@@ -199,8 +207,8 @@ def run() -> None:
199
207
  {"op": op, "target": target, "project_root": project_root, "engine": engine, "role": role}
200
208
  )
201
209
 
202
- async def _code_status() -> dict:
203
- return code_status_handler({})
210
+ async def _code_status(project_root: str = "") -> dict:
211
+ return code_status_handler({"project_root": project_root})
204
212
 
205
213
  async def _code_doctor(project_root: str = "", deep: bool = False) -> dict:
206
214
  return code_doctor_handler({"project_root": project_root, "deep": deep})
@@ -1,31 +0,0 @@
1
- codecortex-0.2.2.dist-info/licenses/LICENSE,sha256=DIRvhlH8EulEUOYQxKDFFiimJrlVT5-kCKxQIgi2m7c,1073
2
- codeintel/__init__.py,sha256=m6kyaNpwBcP1XYcqrelX2oS3PJuOnElOcRdBa9pEb8c,22
3
- codeintel/__main__.py,sha256=3MpEhdpAfHiO0rwIB7hxXM_mDN3RovPQBLXNH36aLUg,15189
4
- codeintel/cache.py,sha256=VhAc_xEA3FtC_dSLXHPe646FKvL-cgPWm27S9i6VuYE,3156
5
- codeintel/config.py,sha256=Q7Mgq3UleJO6cpBKr1S7I1SeRbyL_rDekmCi8iasy9s,1133
6
- codeintel/doctor.py,sha256=jiPuZzS801xfWwTyeVO9UHUxQqAJ-YqTlX-_epZfoyI,6659
7
- codeintel/gateway.py,sha256=1c0rtjGGcgaa63Klk79LtdeOTl2ld8cvcnFlRSpKNsg,8929
8
- codeintel/http_server.py,sha256=J44uZqpXFJ06X-wDOZZvLEM1wftuokKCvS09Z62eUyU,3942
9
- codeintel/indexer.py,sha256=QKQtVu79DsEf1qSj9pLSdrspy5ajgzuNmupaZFL128s,9578
10
- codeintel/injector.py,sha256=IHZTLajQURE2LLxL8dFE9pvJV1RMOAbE7pVRQbFVvgs,2755
11
- codeintel/installer.py,sha256=PSgK6KA14VEQs2pQKCmjf9JDxJ2mpoEsaWrpFGWSy68,3488
12
- codeintel/mapper.py,sha256=JN-lMO1CXIyDKSwHfzp0JuK396FR41XhFgtAip77G0E,7493
13
- codeintel/onboarding.py,sha256=TOpRKkFvN2UesA7lW233s5PUJTFj98i7sby953EGWIQ,8846
14
- codeintel/policy.py,sha256=mUAlk9oYoszQLEcvoroFLRd9DSTzho7YcbM7xb6RS-k,935
15
- codeintel/provider.py,sha256=SlIModATCANJA4VyJs_Rz24bpQfzHG8ljHfpS1LWa8c,1214
16
- codeintel/reindexer.py,sha256=zFNeLgPRZPfeczc5Cx3aPYkJpyrjd6AHyvuqsrLGf8E,4443
17
- codeintel/reset.py,sha256=8Mpak4Tj5e-yTcM3_EnZJzbJPwPV2o8xpxGjv-wk9EE,3675
18
- codeintel/searcher.py,sha256=R9PV7jSx-GO--qmh65FXlYxpbKbjCnxV-EF9hGe8hZU,4588
19
- codeintel/semantic_db.py,sha256=9umQ8UUUtHHy7Ngi0B-e8eGCy62XFIOkcWnd3wc3MMQ,3503
20
- codeintel/server.py,sha256=9eo531Hj1XZYz3I6Zx2DQZ-fsYPYtmIRJH2TTe5yRjo,7031
21
- codeintel/term.py,sha256=csbSYD8euC13FnnpThy8kylDgZeejQ6j6Ol6Gu21Clo,6819
22
- codeintel/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
- codeintel/providers/graph.py,sha256=JUAnAo7UioUMZJ3PUGrKT4svWQ3mmaTa14JMKAGVvNk,19401
24
- codeintel/providers/lsp.py,sha256=3jn2bMfo5jiaL4_X3kIjzzxLsL475xIZsT-okWy-vgg,16759
25
- codeintel/providers/none.py,sha256=VI84XQkT7opQm6rBAEn0CdM8CNVlKGWYmjAhyeWbHeg,783
26
- codeintel/providers/semantic.py,sha256=Yka9Puyd8LqY2jfvjoJzAsSMZvb3S-cViQC6fuesdUk,5397
27
- codecortex-0.2.2.dist-info/METADATA,sha256=kL47WPI6VYU46EBctBloS984tmQ8ZRGNyfKNxRwYzAs,15376
28
- codecortex-0.2.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
29
- codecortex-0.2.2.dist-info/entry_points.txt,sha256=zrYfo95-8JkK0G2e5f9d94sYqqim7ZrdCW8knIfSy2U,54
30
- codecortex-0.2.2.dist-info/top_level.txt,sha256=DF3TH1hHLWrrnX-7HhfMX0OnS2EXgqPRhPbiL_oSekw,10
31
- codecortex-0.2.2.dist-info/RECORD,,