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