cortexcode 0.1.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.
- cortexcode/__init__.py +3 -0
- cortexcode/analysis.py +331 -0
- cortexcode/cli.py +845 -0
- cortexcode/context.py +298 -0
- cortexcode/dashboard.py +152 -0
- cortexcode/docs.py +1266 -0
- cortexcode/git_diff.py +157 -0
- cortexcode/indexer.py +1860 -0
- cortexcode/lsp_server.py +315 -0
- cortexcode/mcp_server.py +455 -0
- cortexcode/plugins.py +188 -0
- cortexcode/semantic_search.py +237 -0
- cortexcode/vuln_scan.py +241 -0
- cortexcode/watcher.py +122 -0
- cortexcode/workspace.py +180 -0
- cortexcode-0.1.0.dist-info/METADATA +448 -0
- cortexcode-0.1.0.dist-info/RECORD +21 -0
- cortexcode-0.1.0.dist-info/WHEEL +5 -0
- cortexcode-0.1.0.dist-info/entry_points.txt +2 -0
- cortexcode-0.1.0.dist-info/licenses/LICENSE +21 -0
- cortexcode-0.1.0.dist-info/top_level.txt +1 -0
cortexcode/lsp_server.py
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""LSP Server — Language Server Protocol support for CortexCode.
|
|
2
|
+
|
|
3
|
+
Provides hover, go-to-definition, and document symbols via LSP,
|
|
4
|
+
so any LSP-compatible editor can use CortexCode index data.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
cortexcode lsp # Start LSP server on stdin/stdout
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _read_message(stream) -> dict | None:
|
|
18
|
+
"""Read an LSP message from stream (Content-Length header + JSON body)."""
|
|
19
|
+
headers = {}
|
|
20
|
+
while True:
|
|
21
|
+
line = stream.readline()
|
|
22
|
+
if not line:
|
|
23
|
+
return None
|
|
24
|
+
line = line.strip()
|
|
25
|
+
if not line:
|
|
26
|
+
break
|
|
27
|
+
if ":" in line:
|
|
28
|
+
key, value = line.split(":", 1)
|
|
29
|
+
headers[key.strip().lower()] = value.strip()
|
|
30
|
+
|
|
31
|
+
content_length = int(headers.get("content-length", 0))
|
|
32
|
+
if content_length == 0:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
body = stream.read(content_length)
|
|
36
|
+
return json.loads(body)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _write_message(stream, msg: dict) -> None:
|
|
40
|
+
"""Write an LSP message to stream."""
|
|
41
|
+
body = json.dumps(msg)
|
|
42
|
+
header = f"Content-Length: {len(body)}\r\n\r\n"
|
|
43
|
+
stream.write(header + body)
|
|
44
|
+
stream.flush()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _make_response(req_id: Any, result: Any) -> dict:
|
|
48
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": result}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CortexCodeLSP:
|
|
52
|
+
"""Minimal LSP server backed by CortexCode index."""
|
|
53
|
+
|
|
54
|
+
def __init__(self):
|
|
55
|
+
self.index: dict | None = None
|
|
56
|
+
self.index_path: Path | None = None
|
|
57
|
+
self.root_path: str = ""
|
|
58
|
+
self._symbol_cache: dict[str, list[dict]] = {} # file -> symbols
|
|
59
|
+
|
|
60
|
+
def _load_index(self):
|
|
61
|
+
"""Find and load the CortexCode index."""
|
|
62
|
+
if self.root_path:
|
|
63
|
+
candidate = Path(self.root_path) / ".cortexcode" / "index.json"
|
|
64
|
+
if candidate.exists():
|
|
65
|
+
self.index_path = candidate
|
|
66
|
+
try:
|
|
67
|
+
self.index = json.loads(candidate.read_text(encoding="utf-8"))
|
|
68
|
+
self._build_symbol_cache()
|
|
69
|
+
except (json.JSONDecodeError, OSError):
|
|
70
|
+
self.index = None
|
|
71
|
+
|
|
72
|
+
def _build_symbol_cache(self):
|
|
73
|
+
"""Build a lookup from file path to symbols."""
|
|
74
|
+
self._symbol_cache = {}
|
|
75
|
+
if not self.index:
|
|
76
|
+
return
|
|
77
|
+
root = self.index.get("project_root", "")
|
|
78
|
+
for rel_path, file_data in self.index.get("files", {}).items():
|
|
79
|
+
if not isinstance(file_data, dict):
|
|
80
|
+
continue
|
|
81
|
+
# Store by both relative and absolute path
|
|
82
|
+
abs_path = str(Path(root) / rel_path).replace("\\", "/")
|
|
83
|
+
self._symbol_cache[abs_path] = file_data.get("symbols", [])
|
|
84
|
+
self._symbol_cache[rel_path.replace("\\", "/")] = file_data.get("symbols", [])
|
|
85
|
+
|
|
86
|
+
def _find_symbol_at(self, uri: str, line: int, col: int) -> dict | None:
|
|
87
|
+
"""Find a symbol at a given position."""
|
|
88
|
+
file_path = _uri_to_path(uri)
|
|
89
|
+
|
|
90
|
+
# Try to read the word at position from the file
|
|
91
|
+
word = self._get_word_at_position(file_path, line, col)
|
|
92
|
+
if not word:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
# Search all files for this symbol
|
|
96
|
+
if self.index:
|
|
97
|
+
for rel_path, file_data in self.index.get("files", {}).items():
|
|
98
|
+
if not isinstance(file_data, dict):
|
|
99
|
+
continue
|
|
100
|
+
for sym in file_data.get("symbols", []):
|
|
101
|
+
if sym.get("name") == word:
|
|
102
|
+
return {**sym, "file": rel_path}
|
|
103
|
+
# Check methods
|
|
104
|
+
for m in sym.get("methods", []):
|
|
105
|
+
if m.get("name") == word:
|
|
106
|
+
return {**m, "file": rel_path}
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
def _get_word_at_position(self, file_path: str, line: int, col: int) -> str | None:
|
|
110
|
+
"""Extract the word under the cursor."""
|
|
111
|
+
try:
|
|
112
|
+
path = Path(file_path)
|
|
113
|
+
if not path.exists():
|
|
114
|
+
return None
|
|
115
|
+
lines = path.read_text(encoding="utf-8", errors="ignore").split("\n")
|
|
116
|
+
if line >= len(lines):
|
|
117
|
+
return None
|
|
118
|
+
text = lines[line]
|
|
119
|
+
# Find word boundaries
|
|
120
|
+
start = col
|
|
121
|
+
while start > 0 and (text[start - 1].isalnum() or text[start - 1] == "_"):
|
|
122
|
+
start -= 1
|
|
123
|
+
end = col
|
|
124
|
+
while end < len(text) and (text[end].isalnum() or text[end] == "_"):
|
|
125
|
+
end += 1
|
|
126
|
+
word = text[start:end]
|
|
127
|
+
return word if word else None
|
|
128
|
+
except (OSError, IndexError):
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
def handle(self, msg: dict) -> dict | None:
|
|
132
|
+
"""Handle an LSP request/notification."""
|
|
133
|
+
method = msg.get("method", "")
|
|
134
|
+
params = msg.get("params", {})
|
|
135
|
+
req_id = msg.get("id")
|
|
136
|
+
|
|
137
|
+
if method == "initialize":
|
|
138
|
+
self.root_path = params.get("rootPath", "") or ""
|
|
139
|
+
root_uri = params.get("rootUri", "")
|
|
140
|
+
if root_uri and not self.root_path:
|
|
141
|
+
self.root_path = _uri_to_path(root_uri)
|
|
142
|
+
self._load_index()
|
|
143
|
+
|
|
144
|
+
return _make_response(req_id, {
|
|
145
|
+
"capabilities": {
|
|
146
|
+
"hoverProvider": True,
|
|
147
|
+
"definitionProvider": True,
|
|
148
|
+
"documentSymbolProvider": True,
|
|
149
|
+
"textDocumentSync": 1,
|
|
150
|
+
},
|
|
151
|
+
"serverInfo": {
|
|
152
|
+
"name": "cortexcode-lsp",
|
|
153
|
+
"version": "0.1.0",
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
elif method == "initialized":
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
elif method == "shutdown":
|
|
161
|
+
return _make_response(req_id, None)
|
|
162
|
+
|
|
163
|
+
elif method == "exit":
|
|
164
|
+
sys.exit(0)
|
|
165
|
+
|
|
166
|
+
elif method == "textDocument/hover":
|
|
167
|
+
return self._handle_hover(req_id, params)
|
|
168
|
+
|
|
169
|
+
elif method == "textDocument/definition":
|
|
170
|
+
return self._handle_definition(req_id, params)
|
|
171
|
+
|
|
172
|
+
elif method == "textDocument/documentSymbol":
|
|
173
|
+
return self._handle_document_symbols(req_id, params)
|
|
174
|
+
|
|
175
|
+
elif req_id is not None:
|
|
176
|
+
return _make_response(req_id, None)
|
|
177
|
+
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
def _handle_hover(self, req_id, params: dict) -> dict:
|
|
181
|
+
"""Handle textDocument/hover."""
|
|
182
|
+
uri = params.get("textDocument", {}).get("uri", "")
|
|
183
|
+
pos = params.get("position", {})
|
|
184
|
+
line = pos.get("line", 0)
|
|
185
|
+
col = pos.get("character", 0)
|
|
186
|
+
|
|
187
|
+
sym = self._find_symbol_at(uri, line, col)
|
|
188
|
+
if not sym:
|
|
189
|
+
return _make_response(req_id, None)
|
|
190
|
+
|
|
191
|
+
# Build hover content
|
|
192
|
+
parts = [f"**{sym.get('name')}** ({sym.get('type', 'symbol')})"]
|
|
193
|
+
if sym.get("params"):
|
|
194
|
+
parts.append(f"Parameters: `{', '.join(sym['params'])}`")
|
|
195
|
+
if sym.get("return_type"):
|
|
196
|
+
parts.append(f"Returns: `{sym['return_type']}`")
|
|
197
|
+
if sym.get("file"):
|
|
198
|
+
parts.append(f"Defined in: `{sym['file']}:{sym.get('line', '?')}`")
|
|
199
|
+
if sym.get("calls"):
|
|
200
|
+
parts.append(f"Calls: {', '.join(f'`{c}`' for c in sym['calls'][:5])}")
|
|
201
|
+
if sym.get("doc"):
|
|
202
|
+
parts.append(f"\n{sym['doc']}")
|
|
203
|
+
if sym.get("framework"):
|
|
204
|
+
parts.append(f"Framework: {sym['framework']}")
|
|
205
|
+
|
|
206
|
+
content = "\n\n".join(parts)
|
|
207
|
+
|
|
208
|
+
return _make_response(req_id, {
|
|
209
|
+
"contents": {"kind": "markdown", "value": content},
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
def _handle_definition(self, req_id, params: dict) -> dict:
|
|
213
|
+
"""Handle textDocument/definition."""
|
|
214
|
+
uri = params.get("textDocument", {}).get("uri", "")
|
|
215
|
+
pos = params.get("position", {})
|
|
216
|
+
line = pos.get("line", 0)
|
|
217
|
+
col = pos.get("character", 0)
|
|
218
|
+
|
|
219
|
+
sym = self._find_symbol_at(uri, line, col)
|
|
220
|
+
if not sym or not sym.get("file"):
|
|
221
|
+
return _make_response(req_id, None)
|
|
222
|
+
|
|
223
|
+
root = self.index.get("project_root", "") if self.index else ""
|
|
224
|
+
target_path = str(Path(root) / sym["file"])
|
|
225
|
+
target_uri = _path_to_uri(target_path)
|
|
226
|
+
target_line = max(0, sym.get("line", 1) - 1)
|
|
227
|
+
|
|
228
|
+
return _make_response(req_id, {
|
|
229
|
+
"uri": target_uri,
|
|
230
|
+
"range": {
|
|
231
|
+
"start": {"line": target_line, "character": 0},
|
|
232
|
+
"end": {"line": target_line, "character": 0},
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
def _handle_document_symbols(self, req_id, params: dict) -> dict:
|
|
237
|
+
"""Handle textDocument/documentSymbol."""
|
|
238
|
+
uri = params.get("textDocument", {}).get("uri", "")
|
|
239
|
+
file_path = _uri_to_path(uri).replace("\\", "/")
|
|
240
|
+
|
|
241
|
+
symbols = []
|
|
242
|
+
|
|
243
|
+
# Try to find symbols for this file
|
|
244
|
+
found_syms = self._symbol_cache.get(file_path, [])
|
|
245
|
+
if not found_syms:
|
|
246
|
+
# Try matching by filename
|
|
247
|
+
for key, syms in self._symbol_cache.items():
|
|
248
|
+
if file_path.endswith(key) or key.endswith(file_path.split("/")[-1]):
|
|
249
|
+
found_syms = syms
|
|
250
|
+
break
|
|
251
|
+
|
|
252
|
+
symbol_kind_map = {
|
|
253
|
+
"function": 12, # Function
|
|
254
|
+
"method": 6, # Method
|
|
255
|
+
"class": 5, # Class
|
|
256
|
+
"interface": 11, # Interface
|
|
257
|
+
"type": 26, # TypeParameter
|
|
258
|
+
"enum": 10, # Enum
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for sym in found_syms:
|
|
262
|
+
line = max(0, sym.get("line", 1) - 1)
|
|
263
|
+
kind = symbol_kind_map.get(sym.get("type", ""), 13) # 13 = Variable
|
|
264
|
+
|
|
265
|
+
symbols.append({
|
|
266
|
+
"name": sym.get("name", "?"),
|
|
267
|
+
"kind": kind,
|
|
268
|
+
"range": {
|
|
269
|
+
"start": {"line": line, "character": 0},
|
|
270
|
+
"end": {"line": line, "character": 0},
|
|
271
|
+
},
|
|
272
|
+
"selectionRange": {
|
|
273
|
+
"start": {"line": line, "character": 0},
|
|
274
|
+
"end": {"line": line, "character": 0},
|
|
275
|
+
},
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
return _make_response(req_id, symbols)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _uri_to_path(uri: str) -> str:
|
|
282
|
+
"""Convert file URI to path."""
|
|
283
|
+
if uri.startswith("file:///"):
|
|
284
|
+
path = uri[8:] # Remove file:///
|
|
285
|
+
# Handle Windows drive letters
|
|
286
|
+
if len(path) > 2 and path[1] == ":" or (path[0] == "/" and len(path) > 3 and path[2] == ":"):
|
|
287
|
+
path = path.lstrip("/")
|
|
288
|
+
return path.replace("/", "\\") if "\\" in path or ":" in path[:3] else path
|
|
289
|
+
return uri
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _path_to_uri(path: str) -> str:
|
|
293
|
+
"""Convert path to file URI."""
|
|
294
|
+
path = path.replace("\\", "/")
|
|
295
|
+
if not path.startswith("/"):
|
|
296
|
+
path = "/" + path
|
|
297
|
+
return "file://" + path
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def run_lsp_server():
|
|
301
|
+
"""Run the LSP server on stdin/stdout."""
|
|
302
|
+
server = CortexCodeLSP()
|
|
303
|
+
|
|
304
|
+
# Use binary mode for reading
|
|
305
|
+
stdin = sys.stdin
|
|
306
|
+
stdout = sys.stdout
|
|
307
|
+
|
|
308
|
+
while True:
|
|
309
|
+
msg = _read_message(stdin)
|
|
310
|
+
if msg is None:
|
|
311
|
+
break
|
|
312
|
+
|
|
313
|
+
response = server.handle(msg)
|
|
314
|
+
if response is not None:
|
|
315
|
+
_write_message(stdout, response)
|