codecortex 0.2.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.

Potentially problematic release.


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

codeintel/reset.py ADDED
@@ -0,0 +1,100 @@
1
+ """Recovery command — drop the semantic index cache for one project, or nuke the whole
2
+ per-machine db. This is the escape hatch for a corrupt index, so it must work even when the
3
+ db file itself can't be opened: never raises, never prompts. The CLI owns confirmation; this
4
+ module is pure (dry-run by default via ``apply=False``).
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import sqlite3
10
+
11
+ import sqlite_vec
12
+
13
+ from codeintel.semantic_db import default_db_path
14
+
15
+
16
+ def _reset_scoped(project_root: str, path: str, apply: bool) -> dict:
17
+ real = os.path.realpath(str(project_root))
18
+ if not os.path.exists(path):
19
+ return {"ok": True, "mode": "scoped", "target": real, "count": 0,
20
+ "applied": bool(apply), "detail": "no index db found — nothing to reset"}
21
+
22
+ conn = None
23
+ try:
24
+ conn = sqlite3.connect(path)
25
+ conn.execute("PRAGMA busy_timeout=2000")
26
+ try:
27
+ # Only needed to delete from the vec0 virtual table below — the count query
28
+ # against the plain chunk_hashes table works fine without it.
29
+ conn.enable_load_extension(True)
30
+ sqlite_vec.load(conn)
31
+ except Exception:
32
+ pass
33
+
34
+ row = conn.execute(
35
+ "SELECT COUNT(*) FROM chunk_hashes WHERE project_root=?", (real,)
36
+ ).fetchone()
37
+ count = int(row[0]) if row else 0
38
+
39
+ if apply:
40
+ conn.execute(
41
+ "DELETE FROM code_embeddings WHERE chunk_id IN "
42
+ "(SELECT chunk_id FROM chunk_hashes WHERE project_root=?)", (real,))
43
+ conn.execute("DELETE FROM chunk_hashes WHERE project_root=?", (real,))
44
+ conn.commit()
45
+
46
+ verb = "removed" if apply else "found"
47
+ return {"ok": True, "mode": "scoped", "target": real, "count": count,
48
+ "applied": bool(apply),
49
+ "detail": f"{verb} {count} indexed chunk(s) for this project"}
50
+ except Exception as exc:
51
+ return {"ok": True, "mode": "scoped", "target": real, "count": 0,
52
+ "applied": bool(apply),
53
+ "detail": f"reset-error: db unreadable/locked ({type(exc).__name__}: {exc})"}
54
+ finally:
55
+ if conn is not None:
56
+ try:
57
+ conn.close()
58
+ except Exception:
59
+ pass
60
+
61
+
62
+ def _reset_all(path: str, apply: bool) -> dict:
63
+ candidates = [path, path + "-wal", path + "-shm"]
64
+ if apply:
65
+ removed = 0
66
+ for p in candidates:
67
+ try:
68
+ os.remove(p)
69
+ removed += 1
70
+ except FileNotFoundError:
71
+ pass
72
+ except Exception:
73
+ pass
74
+ count = removed
75
+ detail = f"removed {removed} index file(s)"
76
+ else:
77
+ count = sum(1 for p in candidates if os.path.exists(p))
78
+ detail = f"{count} index file(s) would be removed"
79
+
80
+ return {"ok": True, "mode": "all", "target": "ALL", "count": count,
81
+ "applied": bool(apply), "detail": detail}
82
+
83
+
84
+ def run_reset(
85
+ project_root: str,
86
+ *,
87
+ all_projects: bool = False,
88
+ apply: bool = False,
89
+ db_path: str | None = None,
90
+ ) -> dict:
91
+ """Drop indexed rows for ``project_root`` (or, with ``all_projects``, remove the whole
92
+ cache db file plus its -wal/-shm siblings). ``apply=False`` is a dry-run: count only,
93
+ delete nothing. Never raises."""
94
+ try:
95
+ path = db_path if db_path is not None else default_db_path()
96
+ if all_projects:
97
+ return _reset_all(path, apply)
98
+ return _reset_scoped(project_root, path, apply)
99
+ except Exception as exc:
100
+ return {"ok": True, "applied": apply, "detail": f"reset-error: {type(exc).__name__}: {exc}"}
codeintel/searcher.py ADDED
@@ -0,0 +1,143 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import struct
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from codeintel.semantic_db import SemanticDb
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ _SNIPPET_LINES = 5
15
+
16
+
17
+ class Searcher:
18
+ def __init__(
19
+ self,
20
+ db: SemanticDb,
21
+ model_name: str = "BAAI/bge-small-en-v1.5",
22
+ ) -> None:
23
+ self.db = db
24
+ self.model_name = model_name
25
+ self._embedder = None
26
+
27
+ def _get_embedder(self):
28
+ if self._embedder is None:
29
+ from fastembed import TextEmbedding
30
+ self._embedder = TextEmbedding(model_name=self.model_name)
31
+ return self._embedder
32
+
33
+ def _embed_query(self, query: str) -> bytes | None:
34
+ try:
35
+ embedder = self._get_embedder()
36
+ vecs = list(embedder.embed([query]))
37
+ if not vecs:
38
+ return None
39
+ vec = vecs[0]
40
+ return struct.pack(f"{len(vec)}f", *vec)
41
+ except Exception as exc:
42
+ logger.warning("query embedding failed: %s", exc)
43
+ return None
44
+
45
+ def _row_count(self, project_root_real: str) -> int:
46
+ try:
47
+ conn = self.db.conn()
48
+ row = conn.execute(
49
+ "SELECT COUNT(*) FROM chunk_hashes WHERE project_root = ?",
50
+ (project_root_real,),
51
+ ).fetchone()
52
+ return row[0] if row else 0
53
+ except Exception as exc:
54
+ logger.warning("rowcount check failed: %s", exc)
55
+ return 0
56
+
57
+ def has_index(self, project_root: str) -> bool:
58
+ """True when this project has at least one indexed chunk — lets the provider
59
+ distinguish 'nothing indexed yet' (no-index) from 'matches below floor'."""
60
+ return self._row_count(os.path.realpath(project_root)) > 0
61
+
62
+ def _read_snippet(self, file_path: Path, chunk_start: int) -> str:
63
+ try:
64
+ with open(file_path, encoding="utf-8", errors="replace") as f:
65
+ lines = f.readlines()
66
+ snippet_lines = lines[chunk_start: chunk_start + _SNIPPET_LINES]
67
+ return "".join(snippet_lines).rstrip()
68
+ except FileNotFoundError:
69
+ return "[file not found]"
70
+ except Exception as exc:
71
+ logger.debug("snippet read failed for %s:%d: %s", file_path, chunk_start, exc)
72
+ return "[file not found]"
73
+
74
+ def search(
75
+ self,
76
+ query: str,
77
+ project_root: str,
78
+ k: int = 10,
79
+ cosine_floor: float = 0.25,
80
+ ) -> list[dict]:
81
+ if not query or not query.strip():
82
+ return []
83
+
84
+ k = max(1, k)
85
+ project_root_real = os.path.realpath(project_root)
86
+
87
+ # Scope the KNN to THIS project — a search in repo B must never surface repo A's
88
+ # chunks (wrong-file, wrong-content hits) from the shared cache.
89
+ if self._row_count(project_root_real) == 0:
90
+ return []
91
+
92
+ query_vec = self._embed_query(query)
93
+ if query_vec is None:
94
+ return []
95
+
96
+ try:
97
+ conn = self.db.conn()
98
+ rows = conn.execute(
99
+ """
100
+ SELECT
101
+ ce.chunk_id,
102
+ ch.chunk_start,
103
+ ch.file_path,
104
+ vec_distance_cosine(ce.embedding, ?) AS dist
105
+ FROM code_embeddings ce
106
+ JOIN chunk_hashes ch ON ce.chunk_id = ch.chunk_id
107
+ WHERE ch.project_root = ?
108
+ ORDER BY dist
109
+ LIMIT ?
110
+ """,
111
+ (query_vec, project_root_real, k),
112
+ ).fetchall()
113
+ except Exception as exc:
114
+ logger.warning("KNN query failed: %s", exc)
115
+ return []
116
+
117
+ root = Path(project_root)
118
+ results: list[dict] = []
119
+
120
+ for row in rows:
121
+ try:
122
+ dist = float(row["dist"])
123
+ score = 1.0 - dist
124
+ if score < cosine_floor:
125
+ continue
126
+
127
+ chunk_start = int(row["chunk_start"])
128
+ rel_path = str(row["file_path"])
129
+ abs_path = root / rel_path
130
+
131
+ snippet = self._read_snippet(abs_path, chunk_start)
132
+
133
+ results.append({
134
+ "path": rel_path,
135
+ "line": chunk_start,
136
+ "snippet": snippet,
137
+ "score": round(score, 6),
138
+ })
139
+ except Exception as exc:
140
+ logger.debug("result row processing failed: %s", exc)
141
+ continue
142
+
143
+ return results
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ import pathlib
4
+ import sqlite3
5
+
6
+ import sqlite_vec
7
+
8
+ DEFAULT_MODEL = "BAAI/bge-small-en-v1.5"
9
+
10
+
11
+ def default_db_path() -> str:
12
+ """The single, per-machine semantic index cache. Every entry point (the
13
+ SemanticProvider, the Reindexer, and the CLI) MUST resolve to this one path — rows
14
+ are partitioned by ``project_root`` inside it — so ``index`` and ``search`` can never
15
+ diverge onto different files for the same repo.
16
+ """
17
+ return str(pathlib.Path.home() / ".codeintel" / "semantic.db")
18
+
19
+
20
+ class SemanticDb:
21
+ """DB layer: opens a SQLite connection, loads sqlite-vec, and owns schema creation."""
22
+
23
+ dimension: int = 384
24
+
25
+ def __init__(self, db_path: str) -> None:
26
+ self.db_path = db_path
27
+ self._conn: sqlite3.Connection | None = None
28
+
29
+ def conn(self) -> sqlite3.Connection:
30
+ if self._conn is None:
31
+ self._conn = sqlite3.connect(self.db_path)
32
+ self._conn.enable_load_extension(True)
33
+ self._conn.row_factory = sqlite3.Row
34
+ return self._conn
35
+
36
+ def init(self) -> None:
37
+ c = self.conn()
38
+ try:
39
+ sqlite_vec.load(c)
40
+ except Exception as exc:
41
+ raise RuntimeError(f"sqlite-vec extension failed to load: {exc}") from exc
42
+
43
+ # Migration: caches created before the project_root partition column lack it. The
44
+ # index is a regenerable cache, so on a schema mismatch we drop and rebuild rather
45
+ # than ALTER — the next index pass repopulates it.
46
+ try:
47
+ cols = [r[1] for r in c.execute("PRAGMA table_info(chunk_hashes)").fetchall()]
48
+ if cols and "project_root" not in cols:
49
+ c.executescript(
50
+ "DROP TABLE IF EXISTS code_embeddings;"
51
+ "DROP TABLE IF EXISTS chunk_hashes;"
52
+ )
53
+ except Exception:
54
+ pass
55
+
56
+ c.executescript(f"""
57
+ CREATE VIRTUAL TABLE IF NOT EXISTS code_embeddings USING vec0(
58
+ chunk_id TEXT PRIMARY KEY,
59
+ embedding FLOAT[{self.dimension}]
60
+ );
61
+
62
+ CREATE TABLE IF NOT EXISTS chunk_hashes (
63
+ chunk_id TEXT PRIMARY KEY,
64
+ project_root TEXT NOT NULL,
65
+ file_path TEXT NOT NULL,
66
+ chunk_start INT NOT NULL,
67
+ content_hash TEXT NOT NULL
68
+ );
69
+
70
+ CREATE INDEX IF NOT EXISTS idx_chunk_project
71
+ ON chunk_hashes(project_root);
72
+ """)
73
+ c.commit()
74
+
75
+ def close(self) -> None:
76
+ if self._conn is not None:
77
+ self._conn.close()
78
+ self._conn = None
codeintel/server.py ADDED
@@ -0,0 +1,216 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+
5
+ import anyio
6
+ from mcp.server.mcpserver.server import MCPServer
7
+
8
+ from codeintel.gateway import Gateway
9
+ from codeintel.policy import TieringPolicy
10
+ from codeintel.provider import safe_null_result
11
+ from codeintel.providers.graph import GraphProvider
12
+ from codeintel.providers.lsp import LspProvider
13
+ from codeintel.providers.semantic import SemanticProvider
14
+ from codeintel.reindexer import Reindexer
15
+
16
+ _REINDEXER = Reindexer()
17
+
18
+
19
+ def _build_gateway() -> Gateway:
20
+ graph = None
21
+ lsp = None
22
+ try:
23
+ gp = GraphProvider()
24
+ if gp.available:
25
+ graph = gp
26
+ except Exception:
27
+ pass
28
+ try:
29
+ lp = LspProvider()
30
+ if lp.available:
31
+ lsp = lp
32
+ except Exception:
33
+ pass
34
+ semantic = SemanticProvider()
35
+ policy = TieringPolicy(enabled=False)
36
+ return Gateway(graph=graph, lsp=lsp, semantic=semantic, policy=policy, reindexer=_REINDEXER)
37
+
38
+
39
+ _GATEWAY: Gateway | None = None
40
+ _GATEWAY_LOCK = threading.Lock()
41
+
42
+
43
+ def _get_gateway() -> Gateway:
44
+ """Build the gateway ONCE and reuse it across requests, so the content-hash cache and
45
+ the async-warming LSP session persist between an agent's calls. Rebuilding it per
46
+ request (the old behavior) left the cache permanently cold and the LSP engine stuck
47
+ re-warming a fresh ``uvx serena`` subprocess on every single call."""
48
+ global _GATEWAY
49
+ if _GATEWAY is None:
50
+ with _GATEWAY_LOCK:
51
+ if _GATEWAY is None:
52
+ _GATEWAY = _build_gateway()
53
+ return _GATEWAY
54
+
55
+
56
+ def _reset_gateway() -> None:
57
+ """Test hook: drop the cached gateway so the next call rebuilds it."""
58
+ global _GATEWAY
59
+ with _GATEWAY_LOCK:
60
+ _GATEWAY = None
61
+
62
+
63
+ def code_query_handler(args: dict) -> dict:
64
+ try:
65
+ op = args.get("op", "")
66
+ target = args.get("target", "")
67
+ project_root = args.get("project_root", "")
68
+ engine = args.get("engine", None)
69
+ role = args.get("role", "")
70
+ gw = _get_gateway()
71
+ return gw.query(op=op, target=target, engine=engine, role=role, project_root=project_root)
72
+ except Exception:
73
+ return safe_null_result("", "", reason="handler-error")
74
+
75
+
76
+ def code_status_handler(args: dict) -> dict:
77
+ try:
78
+ graph_available = False
79
+ lsp_available = False
80
+ semantic_available = False
81
+ try:
82
+ gp = GraphProvider()
83
+ graph_available = bool(gp.available)
84
+ except Exception:
85
+ pass
86
+ try:
87
+ lp = LspProvider()
88
+ lsp_available = bool(lp.available)
89
+ except Exception:
90
+ pass
91
+ try:
92
+ sp = SemanticProvider()
93
+ semantic_available = bool(sp.available)
94
+ except Exception:
95
+ pass
96
+ engines: list[str] = []
97
+ if graph_available:
98
+ engines.append("graph")
99
+ if lsp_available:
100
+ engines.append("lsp")
101
+ if semantic_available:
102
+ engines.append("semantic")
103
+ if not engines:
104
+ engines.append("none")
105
+
106
+ # Report real freshness/model instead of hardcoded nulls (SPEC §7).
107
+ indexed = False
108
+ model = None
109
+ try:
110
+ import os
111
+ from codeintel.semantic_db import DEFAULT_MODEL, default_db_path
112
+ if semantic_available:
113
+ model = DEFAULT_MODEL
114
+ indexed = os.path.exists(default_db_path())
115
+ except Exception:
116
+ pass
117
+
118
+ return {
119
+ "ok": True,
120
+ "engines": engines,
121
+ "graph": graph_available,
122
+ "lsp": lsp_available,
123
+ "semantic": semantic_available,
124
+ "indexed": indexed,
125
+ "model": model,
126
+ }
127
+ except Exception:
128
+ return {
129
+ "ok": True,
130
+ "engines": ["none"],
131
+ "graph": False,
132
+ "lsp": False,
133
+ "semantic": False,
134
+ "indexed": False,
135
+ "model": None,
136
+ }
137
+
138
+
139
+ def code_doctor_handler(args: dict) -> dict:
140
+ try:
141
+ from codeintel import doctor as _doctor
142
+
143
+ project_root = str(args.get("project_root", "") or "")
144
+ deep = bool(args.get("deep", False))
145
+ # Reuse the singleton gateway's providers so the report reflects the LIVE warmed LSP
146
+ # session state an agent's real queries hit (and the graph project cache).
147
+ gw = _get_gateway()
148
+ return _doctor.run_doctor(
149
+ project_root, deep=deep, graph=gw.graph, lsp=gw.lsp, semantic=gw.semantic
150
+ )
151
+ except Exception:
152
+ return {
153
+ "ok": True, "project_root": "", "deep": False,
154
+ "summary": {"ready": 0, "total": 3, "healthy": False},
155
+ "engines": {}, "note": "doctor-error",
156
+ }
157
+
158
+
159
+ def code_map_handler(args: dict) -> dict:
160
+ try:
161
+ from codeintel.mapper import MapGenerator
162
+ from codeintel.injector import Injector
163
+
164
+ project_root = str(args.get("project_root", "") or "")
165
+ budget = int(args.get("budget", 32768) or 32768)
166
+ inject = bool(args.get("inject", False))
167
+
168
+ try:
169
+ provider = GraphProvider()
170
+ except Exception:
171
+ provider = None
172
+
173
+ gen = MapGenerator(provider)
174
+ content = gen.generate(project_root, budget_bytes=budget)
175
+ path = gen.write(project_root, content)
176
+ size = len(content.encode("utf-8"))
177
+
178
+ inject_result = None
179
+ if inject:
180
+ inj_path, inj_action = Injector().inject(project_root)
181
+ inject_result = {"path": inj_path, "action": inj_action}
182
+
183
+ return {"ok": True, "path": path, "size_bytes": size, "inject": inject_result}
184
+ except Exception:
185
+ return {"ok": True, "path": None, "size_bytes": 0, "note": "map-error"}
186
+
187
+
188
+ def run() -> None:
189
+ mcp = MCPServer(name="codeintel")
190
+
191
+ async def _code_query(
192
+ op: str = "",
193
+ target: str = "",
194
+ project_root: str = "",
195
+ engine: str = "",
196
+ role: str = "",
197
+ ) -> dict:
198
+ return code_query_handler(
199
+ {"op": op, "target": target, "project_root": project_root, "engine": engine, "role": role}
200
+ )
201
+
202
+ async def _code_status() -> dict:
203
+ return code_status_handler({})
204
+
205
+ async def _code_doctor(project_root: str = "", deep: bool = False) -> dict:
206
+ return code_doctor_handler({"project_root": project_root, "deep": deep})
207
+
208
+ async def _code_map(project_root: str = "", budget: int = 32768, inject: bool = False) -> dict:
209
+ return code_map_handler({"project_root": project_root, "budget": budget, "inject": inject})
210
+
211
+ mcp.add_tool(_code_query, name="code.query", description="Query the code intelligence engine")
212
+ mcp.add_tool(_code_status, name="code.status", description="Return engine status")
213
+ mcp.add_tool(_code_doctor, name="code.doctor", description="Diagnose engine health + repo index status with remediation")
214
+ mcp.add_tool(_code_map, name="code.map", description="Generate or refresh CODE_INTEL.md orientation file")
215
+
216
+ anyio.run(mcp.run_stdio_async)