codecortex 0.2.1__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.

Potentially problematic release.


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

Files changed (55) hide show
  1. {codecortex-0.2.1/src/codecortex.egg-info → codecortex-0.2.2}/PKG-INFO +11 -1
  2. {codecortex-0.2.1 → codecortex-0.2.2}/README.md +10 -0
  3. {codecortex-0.2.1 → codecortex-0.2.2/src/codecortex.egg-info}/PKG-INFO +11 -1
  4. {codecortex-0.2.1 → codecortex-0.2.2}/src/codecortex.egg-info/SOURCES.txt +1 -0
  5. codecortex-0.2.2/src/codeintel/__init__.py +1 -0
  6. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/cache.py +19 -6
  7. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/http_server.py +7 -3
  8. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/providers/graph.py +10 -3
  9. codecortex-0.2.2/tests/test_cache.py +61 -0
  10. codecortex-0.2.1/src/codeintel/__init__.py +0 -1
  11. {codecortex-0.2.1 → codecortex-0.2.2}/LICENSE +0 -0
  12. {codecortex-0.2.1 → codecortex-0.2.2}/pyproject.toml +0 -0
  13. {codecortex-0.2.1 → codecortex-0.2.2}/setup.cfg +0 -0
  14. {codecortex-0.2.1 → codecortex-0.2.2}/src/codecortex.egg-info/dependency_links.txt +0 -0
  15. {codecortex-0.2.1 → codecortex-0.2.2}/src/codecortex.egg-info/entry_points.txt +0 -0
  16. {codecortex-0.2.1 → codecortex-0.2.2}/src/codecortex.egg-info/requires.txt +0 -0
  17. {codecortex-0.2.1 → codecortex-0.2.2}/src/codecortex.egg-info/top_level.txt +0 -0
  18. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/__main__.py +0 -0
  19. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/config.py +0 -0
  20. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/doctor.py +0 -0
  21. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/gateway.py +0 -0
  22. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/indexer.py +0 -0
  23. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/injector.py +0 -0
  24. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/installer.py +0 -0
  25. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/mapper.py +0 -0
  26. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/onboarding.py +0 -0
  27. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/policy.py +0 -0
  28. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/provider.py +0 -0
  29. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/providers/__init__.py +0 -0
  30. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/providers/lsp.py +0 -0
  31. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/providers/none.py +0 -0
  32. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/providers/semantic.py +0 -0
  33. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/reindexer.py +0 -0
  34. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/reset.py +0 -0
  35. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/searcher.py +0 -0
  36. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/semantic_db.py +0 -0
  37. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/server.py +0 -0
  38. {codecortex-0.2.1 → codecortex-0.2.2}/src/codeintel/term.py +0 -0
  39. {codecortex-0.2.1 → codecortex-0.2.2}/tests/test_doctor.py +0 -0
  40. {codecortex-0.2.1 → codecortex-0.2.2}/tests/test_e2e.py +0 -0
  41. {codecortex-0.2.1 → codecortex-0.2.2}/tests/test_gateway.py +0 -0
  42. {codecortex-0.2.1 → codecortex-0.2.2}/tests/test_graph_provider.py +0 -0
  43. {codecortex-0.2.1 → codecortex-0.2.2}/tests/test_graph_real.py +0 -0
  44. {codecortex-0.2.1 → codecortex-0.2.2}/tests/test_graph_stdin.py +0 -0
  45. {codecortex-0.2.1 → codecortex-0.2.2}/tests/test_http_server.py +0 -0
  46. {codecortex-0.2.1 → codecortex-0.2.2}/tests/test_integration.py +0 -0
  47. {codecortex-0.2.1 → codecortex-0.2.2}/tests/test_lsp_provider.py +0 -0
  48. {codecortex-0.2.1 → codecortex-0.2.2}/tests/test_lsp_real.py +0 -0
  49. {codecortex-0.2.1 → codecortex-0.2.2}/tests/test_mapper.py +0 -0
  50. {codecortex-0.2.1 → codecortex-0.2.2}/tests/test_never_raise.py +0 -0
  51. {codecortex-0.2.1 → codecortex-0.2.2}/tests/test_onboarding.py +0 -0
  52. {codecortex-0.2.1 → codecortex-0.2.2}/tests/test_reindexer.py +0 -0
  53. {codecortex-0.2.1 → codecortex-0.2.2}/tests/test_reset.py +0 -0
  54. {codecortex-0.2.1 → codecortex-0.2.2}/tests/test_semantic_provider.py +0 -0
  55. {codecortex-0.2.1 → 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.1
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
@@ -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
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecortex
3
- Version: 0.2.1
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
@@ -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
@@ -32,6 +32,7 @@ src/codeintel/providers/graph.py
32
32
  src/codeintel/providers/lsp.py
33
33
  src/codeintel/providers/none.py
34
34
  src/codeintel/providers/semantic.py
35
+ tests/test_cache.py
35
36
  tests/test_doctor.py
36
37
  tests/test_e2e.py
37
38
  tests/test_gateway.py
@@ -0,0 +1 @@
1
+ __version__ = "0.2.2"
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import hashlib
4
4
  import os
5
5
  import threading
6
+ from collections import OrderedDict
6
7
  from typing import Optional
7
8
 
8
9
  from codeintel.provider import Result
@@ -22,10 +23,15 @@ def _compute_hash(target: str, project_root: str) -> str:
22
23
 
23
24
 
24
25
  class ContentHashCache:
25
- def __init__(self) -> None:
26
+ # Bounded: the server builds ONE gateway and reuses it across every request, so an unbounded
27
+ # store would grow for the life of the process. Capped as an LRU — past _max_entries the
28
+ # least-recently-used entry is evicted. Sized generously; an agent session touches at most a
29
+ # few hundred distinct (op, target, engine, root) keys, each holding a small Result.
30
+ def __init__(self, max_entries: int = 1024) -> None:
26
31
  self._lock = threading.Lock()
27
- # key (content_hash, Result)
28
- self._store: dict[tuple[str, str, str, str], tuple[str, Result]] = {}
32
+ self._max_entries = max(1, int(max_entries))
33
+ # key (content_hash, Result); OrderedDict preserves insertion/access order for LRU.
34
+ self._store: "OrderedDict[tuple[str, str, str, str], tuple[str, Result]]" = OrderedDict()
29
35
 
30
36
  def get(
31
37
  self,
@@ -41,10 +47,14 @@ class ContentHashCache:
41
47
  if entry is None:
42
48
  return None
43
49
  stored_hash, result = entry
50
+ # _compute_hash may read the target file — keep it OUTSIDE the lock (original intent).
44
51
  current_hash = f"{_compute_hash(target, project_root)}:{freshness}"
45
- if current_hash == stored_hash:
46
- return result
47
- return None
52
+ if current_hash != stored_hash:
53
+ return None
54
+ with self._lock:
55
+ if key in self._store: # may have been evicted between the two locked sections
56
+ self._store.move_to_end(key) # mark most-recently-used
57
+ return result
48
58
 
49
59
  def put(
50
60
  self,
@@ -64,3 +74,6 @@ class ContentHashCache:
64
74
  content_hash = f"{_compute_hash(target, project_root)}:{freshness}"
65
75
  with self._lock:
66
76
  self._store[key] = (content_hash, result)
77
+ self._store.move_to_end(key) # most-recently-used
78
+ while len(self._store) > self._max_entries:
79
+ self._store.popitem(last=False) # evict least-recently-used
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import sys
5
- from http.server import BaseHTTPRequestHandler, HTTPServer
5
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
6
6
 
7
7
  from codeintel.server import code_doctor_handler, code_query_handler, code_status_handler
8
8
 
@@ -57,8 +57,12 @@ class _Handler(BaseHTTPRequestHandler):
57
57
  self._send_json(200, result)
58
58
 
59
59
 
60
- class CodeIntelHTTPServer(HTTPServer):
61
- pass
60
+ class CodeIntelHTTPServer(ThreadingHTTPServer):
61
+ # Threaded so one slow request (e.g. an LSP session warming, or a first-time index) can't
62
+ # block every other agent's query. The gateway is a shared singleton, but its mutable state
63
+ # is lock-guarded (query cache, reindexer, LSP sessions, graph project cache) and the
64
+ # semantic engine is thread-confined with WAL, so concurrent requests are safe.
65
+ daemon_threads = True
62
66
 
63
67
 
64
68
  _LOOPBACK_NAMES = {"localhost"}
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  import shutil
5
5
  import subprocess
6
+ import threading
6
7
  import time
7
8
  from typing import Any, Optional
8
9
 
@@ -35,6 +36,7 @@ class GraphProvider:
35
36
 
36
37
  def __init__(self) -> None:
37
38
  self._project_cache: dict[str, Optional[str]] = {}
39
+ self._project_cache_lock = threading.Lock() # concurrent HTTP requests share one provider
38
40
  self._detect_backend()
39
41
 
40
42
  def _detect_backend(self) -> None:
@@ -125,11 +127,16 @@ class GraphProvider:
125
127
  return exact if exact is not None else best_prefix_name
126
128
 
127
129
  def _resolve_project(self, project_root: str) -> Optional[str]:
128
- if project_root in self._project_cache:
129
- return self._project_cache[project_root]
130
+ with self._project_cache_lock:
131
+ if project_root in self._project_cache:
132
+ return self._project_cache[project_root]
133
+ # list_projects shells out — resolve it OUTSIDE the lock so a slow backend can't serialize
134
+ # every concurrent request. A rare duplicate lookup on first contact is harmless (the
135
+ # result is idempotent); we simply never hold the lock across a subprocess.
130
136
  raw = self._run("list_projects", {}, 3000)
131
137
  name = self._match_project(raw, project_root)
132
- self._project_cache[project_root] = name
138
+ with self._project_cache_lock:
139
+ self._project_cache[project_root] = name
133
140
  return name
134
141
 
135
142
  def probe(self, project_root: str, timeout_ms: int = 3000) -> dict:
@@ -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.1"
File without changes
File without changes
File without changes
File without changes