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.
- codecortex-0.2.0.dist-info/METADATA +206 -0
- codecortex-0.2.0.dist-info/RECORD +31 -0
- codecortex-0.2.0.dist-info/WHEEL +5 -0
- codecortex-0.2.0.dist-info/entry_points.txt +2 -0
- codecortex-0.2.0.dist-info/licenses/LICENSE +21 -0
- codecortex-0.2.0.dist-info/top_level.txt +1 -0
- codeintel/__init__.py +1 -0
- codeintel/__main__.py +361 -0
- codeintel/cache.py +66 -0
- codeintel/config.py +42 -0
- codeintel/doctor.py +161 -0
- codeintel/gateway.py +228 -0
- codeintel/http_server.py +93 -0
- codeintel/indexer.py +250 -0
- codeintel/injector.py +81 -0
- codeintel/installer.py +103 -0
- codeintel/mapper.py +192 -0
- codeintel/onboarding.py +197 -0
- codeintel/policy.py +30 -0
- codeintel/provider.py +52 -0
- codeintel/providers/__init__.py +0 -0
- codeintel/providers/graph.py +415 -0
- codeintel/providers/lsp.py +407 -0
- codeintel/providers/none.py +30 -0
- codeintel/providers/semantic.py +139 -0
- codeintel/reindexer.py +112 -0
- codeintel/reset.py +100 -0
- codeintel/searcher.py +143 -0
- codeintel/semantic_db.py +78 -0
- codeintel/server.py +216 -0
- codeintel/term.py +162 -0
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
|
codeintel/semantic_db.py
ADDED
|
@@ -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)
|