cortex-local 2.2.0__tar.gz → 2.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.
- {cortex_local-2.2.0 → cortex_local-2.2.2}/PKG-INFO +1 -1
- {cortex_local-2.2.0 → cortex_local-2.2.2}/cortex/core/chunker.py +21 -3
- cortex_local-2.2.2/cortex/core/formatters.py +114 -0
- {cortex_local-2.2.0 → cortex_local-2.2.2}/cortex/mcp_server.py +5 -55
- {cortex_local-2.2.0 → cortex_local-2.2.2}/README.md +0 -0
- {cortex_local-2.2.0 → cortex_local-2.2.2}/cortex/__init__.py +0 -0
- {cortex_local-2.2.0 → cortex_local-2.2.2}/cortex/cli.py +0 -0
- {cortex_local-2.2.0 → cortex_local-2.2.2}/cortex/config.py +0 -0
- {cortex_local-2.2.0 → cortex_local-2.2.2}/cortex/core/__init__.py +0 -0
- {cortex_local-2.2.0 → cortex_local-2.2.2}/cortex/core/cache.py +0 -0
- {cortex_local-2.2.0 → cortex_local-2.2.2}/cortex/core/code_graph.py +0 -0
- {cortex_local-2.2.0 → cortex_local-2.2.2}/cortex/core/global_graph.py +0 -0
- {cortex_local-2.2.0 → cortex_local-2.2.2}/cortex/core/graph.py +0 -0
- {cortex_local-2.2.0 → cortex_local-2.2.2}/cortex/core/notes_graph.py +0 -0
- {cortex_local-2.2.0 → cortex_local-2.2.2}/cortex/embedder.py +0 -0
- {cortex_local-2.2.0 → cortex_local-2.2.2}/cortex/indexer.py +0 -0
- {cortex_local-2.2.0 → cortex_local-2.2.2}/cortex/state.py +0 -0
- {cortex_local-2.2.0 → cortex_local-2.2.2}/cortex/watcher.py +0 -0
- {cortex_local-2.2.0 → cortex_local-2.2.2}/pyproject.toml +0 -0
|
@@ -7,14 +7,14 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
REPOS_DIR = Path(os.environ.get("REPOS_DIR", "/tmp/repos"))
|
|
9
9
|
|
|
10
|
-
CODE_EXTS = {".js", ".ts", ".tsx", ".jsx", ".svelte", ".py", ".java", ".go", ".rs", ".css", ".html", ".kt", ".kts", ".gd", ".yml", ".yaml"}
|
|
11
|
-
SKIP_DIRS = {"node_modules", ".git", "dist", "build", ".next", ".svelte-kit", "__pycache__", ".gradle", "target"}
|
|
10
|
+
CODE_EXTS = {".js", ".ts", ".tsx", ".jsx", ".svelte", ".py", ".java", ".go", ".rs", ".css", ".html", ".kt", ".kts", ".gd", ".yml", ".yaml", ".cs"}
|
|
11
|
+
SKIP_DIRS = {"node_modules", ".git", "dist", "build", ".next", ".svelte-kit", "__pycache__", ".gradle", "target", "bin", "obj"}
|
|
12
12
|
LANG_MAP = {
|
|
13
13
|
".js": "javascript", ".ts": "typescript", ".tsx": "typescript",
|
|
14
14
|
".jsx": "javascript", ".svelte": "svelte", ".py": "python",
|
|
15
15
|
".java": "java", ".go": "go", ".rs": "rust", ".css": "css", ".html": "html",
|
|
16
16
|
".kt": "kotlin", ".kts": "kotlin", ".gd": "gdscript",
|
|
17
|
-
".yml": "yaml", ".yaml": "yaml",
|
|
17
|
+
".yml": "yaml", ".yaml": "yaml", ".cs": "csharp",
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
def _is_excluded(rel: str, patterns: list) -> bool:
|
|
@@ -97,6 +97,15 @@ _FRAMEWORK_IMPORTS = [
|
|
|
97
97
|
("use actix", "actix"),
|
|
98
98
|
("use axum", "axum"),
|
|
99
99
|
("use tokio", "tokio"),
|
|
100
|
+
# C#
|
|
101
|
+
("using Microsoft.AspNetCore", "aspnetcore"),
|
|
102
|
+
("using Microsoft.Extensions", "aspnetcore"),
|
|
103
|
+
("using Microsoft.EntityFrameworkCore", "entityframework"),
|
|
104
|
+
("using Newtonsoft.Json", "newtonsoft"),
|
|
105
|
+
("using System.Text.Json", "system-text-json"),
|
|
106
|
+
("using Xunit", "xunit"),
|
|
107
|
+
("using NUnit.Framework", "nunit"),
|
|
108
|
+
("using Microsoft.Azure", "azure-sdk"),
|
|
100
109
|
]
|
|
101
110
|
|
|
102
111
|
try:
|
|
@@ -124,6 +133,15 @@ try:
|
|
|
124
133
|
".kt": {"function_declaration", "class_declaration", "object_declaration"},
|
|
125
134
|
".kts": {"function_declaration", "class_declaration", "object_declaration"},
|
|
126
135
|
}
|
|
136
|
+
try:
|
|
137
|
+
import tree_sitter_c_sharp as _tscs
|
|
138
|
+
_TS_LANGUAGES[".cs"] = Language(_tscs.language())
|
|
139
|
+
_TS_SEMANTIC[".cs"] = {
|
|
140
|
+
"method_declaration", "class_declaration", "interface_declaration",
|
|
141
|
+
"constructor_declaration", "struct_declaration", "enum_declaration",
|
|
142
|
+
}
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
127
145
|
_TS_AVAILABLE = True
|
|
128
146
|
except Exception:
|
|
129
147
|
_TS_AVAILABLE = False
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Shared result formatters for MCP search tools — used by server and local modes."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def format_notes_parts(results) -> tuple:
|
|
7
|
+
"""Build parts list from note search results.
|
|
8
|
+
|
|
9
|
+
Returns (parts, matched_files, matched_scores). Caller is responsible for:
|
|
10
|
+
- computing ppr_block from matched_files/matched_scores and appending to parts
|
|
11
|
+
- joining parts with '\\n\\n---\\n\\n'
|
|
12
|
+
- updating stats['context_tokens_notes']
|
|
13
|
+
"""
|
|
14
|
+
parts = []
|
|
15
|
+
matched_files = []
|
|
16
|
+
matched_scores = []
|
|
17
|
+
for r in results:
|
|
18
|
+
p = r.payload
|
|
19
|
+
tags = p.get("tags", [])
|
|
20
|
+
tag_str = f" `{'`, `'.join(tags)}`" if tags else ""
|
|
21
|
+
parts.append(
|
|
22
|
+
f"**{p['file']} > {p['heading']}** (score: {round(r.score, 3)}){tag_str}\n\n{p['text']}"
|
|
23
|
+
)
|
|
24
|
+
matched_files.append(p.get("file", ""))
|
|
25
|
+
matched_scores.append(r.score)
|
|
26
|
+
return parts, matched_files, matched_scores
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def format_code_results(deduped) -> str:
|
|
30
|
+
"""Build formatted string from scored/deduped code search results.
|
|
31
|
+
|
|
32
|
+
Input: list of (boosted_score, qdrant_point, file_meta) tuples.
|
|
33
|
+
Caller is responsible for updating stats['context_tokens_code'].
|
|
34
|
+
"""
|
|
35
|
+
parts = []
|
|
36
|
+
for boosted_score, r, file_meta in deduped:
|
|
37
|
+
p = r.payload
|
|
38
|
+
centrality = file_meta.get("centrality")
|
|
39
|
+
community = file_meta.get("community_id")
|
|
40
|
+
header = (
|
|
41
|
+
f"**{p['repo']}/{p['file']}** "
|
|
42
|
+
f"lines {p['start_line']}-{p['end_line']} "
|
|
43
|
+
f"({p.get('language', '')}) — score: {round(boosted_score, 3)}"
|
|
44
|
+
)
|
|
45
|
+
if centrality is not None:
|
|
46
|
+
header += f" · centrality: {centrality}"
|
|
47
|
+
if community is not None:
|
|
48
|
+
header += f" · community: {community}"
|
|
49
|
+
url = p.get("github_url", "")
|
|
50
|
+
parts.append(f"{header}\n{url}\n\n{p['text']}" if url else f"{header}\n\n{p['text']}")
|
|
51
|
+
return "\n\n---\n\n".join(parts)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def fmt_stats(v: str, stats: dict, current: bool = False) -> str:
|
|
55
|
+
"""Format stats block for one version. Safe for both server and local stats dicts."""
|
|
56
|
+
total_searches = stats.get("search_code_calls", 0) + stats.get("search_notes_calls", 0)
|
|
57
|
+
ppr_rate = (
|
|
58
|
+
f"{stats['ppr_fires'] / stats['search_notes_calls'] * 100:.1f}%"
|
|
59
|
+
if stats.get("search_notes_calls", 0) > 0 else "—"
|
|
60
|
+
)
|
|
61
|
+
avg_ppr = round(stats["ppr_results_added"] / stats["ppr_fires"], 1) if stats.get("ppr_fires", 0) > 0 else "—"
|
|
62
|
+
total_cache = stats.get("graph_cache_hits", 0) + stats.get("graph_cache_misses", 0)
|
|
63
|
+
cache_rate = f"{stats['graph_cache_hits'] / total_cache * 100:.1f}%" if total_cache > 0 else "—"
|
|
64
|
+
avg_lift = round(stats["centrality_lift_total"] / stats["centrality_lift_count"], 4) if stats.get("centrality_lift_count", 0) > 0 else 0
|
|
65
|
+
total_results = stats.get("total_results_code", 0) + stats.get("total_results_notes", 0)
|
|
66
|
+
avg_results = round(total_results / total_searches, 1) if total_searches > 0 else "—"
|
|
67
|
+
notes_pct = f"{stats['search_notes_calls'] / total_searches * 100:.0f}%" if total_searches > 0 else "—"
|
|
68
|
+
|
|
69
|
+
label = f"{v} (current)" if current else v
|
|
70
|
+
lines = [label]
|
|
71
|
+
|
|
72
|
+
started = stats.get("started_at", "")
|
|
73
|
+
if current and started:
|
|
74
|
+
try:
|
|
75
|
+
delta = datetime.now(timezone.utc) - datetime.fromisoformat(started)
|
|
76
|
+
h, rem = divmod(int(delta.total_seconds()), 3600)
|
|
77
|
+
m = rem // 60
|
|
78
|
+
lines.append(f" uptime: {h}h {m}m")
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
elif started:
|
|
82
|
+
last = stats.get("last_saved", "")
|
|
83
|
+
period = started[:10]
|
|
84
|
+
if last:
|
|
85
|
+
period += f" -> {last[:10]}"
|
|
86
|
+
lines.append(f" period: {period}")
|
|
87
|
+
|
|
88
|
+
lines += [
|
|
89
|
+
f" searches: code={stats.get('search_code_calls', 0)} notes={stats.get('search_notes_calls', 0)} total={total_searches} (notes {notes_pct})",
|
|
90
|
+
f" results: code={stats.get('total_results_code', 0)} notes={stats.get('total_results_notes', 0)} avg/search={avg_results}",
|
|
91
|
+
f" centrality lift avg: {avg_lift} (across {stats.get('centrality_lift_count', 0)} results)",
|
|
92
|
+
f" PPR: {stats.get('ppr_fires', 0)} fires ({ppr_rate} of note searches) · +{stats.get('ppr_results_added', 0)} results · avg {avg_ppr}/fire",
|
|
93
|
+
]
|
|
94
|
+
_ppr_diag = {k: stats.get(k, 0) for k in ("ppr_nx_missing", "ppr_graph_missing", "ppr_no_matches", "ppr_below_threshold", "ppr_exception")}
|
|
95
|
+
if any(_ppr_diag.values()):
|
|
96
|
+
lines.append(
|
|
97
|
+
f" PPR misses: nx={_ppr_diag['ppr_nx_missing']} graph={_ppr_diag['ppr_graph_missing']}"
|
|
98
|
+
f" no_match={_ppr_diag['ppr_no_matches']} below_threshold={_ppr_diag['ppr_below_threshold']}"
|
|
99
|
+
f" exception={_ppr_diag['ppr_exception']}"
|
|
100
|
+
)
|
|
101
|
+
ctx_notes = stats.get("context_tokens_notes", 0)
|
|
102
|
+
ctx_code = stats.get("context_tokens_code", 0)
|
|
103
|
+
ctx_total = ctx_notes + ctx_code
|
|
104
|
+
|
|
105
|
+
def _k(n):
|
|
106
|
+
return f"{n // 1000}k" if n >= 1000 else str(n)
|
|
107
|
+
|
|
108
|
+
lines += [
|
|
109
|
+
f" context injected: notes={_k(ctx_notes)} code={_k(ctx_code)} total={_k(ctx_total)} tokens (~chars/4 est.)",
|
|
110
|
+
f" graph cache: {cache_rate} ({stats.get('graph_cache_hits', 0)}/{total_cache})",
|
|
111
|
+
f" embed cache skipped: notes={stats.get('embed_cache_notes', 0)} code={stats.get('embed_cache_code', 0)}",
|
|
112
|
+
f" reindexes: {stats.get('reindex_count', 0)}",
|
|
113
|
+
]
|
|
114
|
+
return "\n".join(lines)
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import threading
|
|
5
5
|
import time
|
|
6
|
-
from datetime import datetime, timezone
|
|
7
6
|
from pathlib import Path
|
|
8
7
|
|
|
9
8
|
|
|
@@ -19,6 +18,7 @@ from .config import (
|
|
|
19
18
|
from .embedder import embed, sparse_embed
|
|
20
19
|
from .state import _stats, get_graph_meta, save_stats, invalidate_graph_cache
|
|
21
20
|
from .indexer import index_path
|
|
21
|
+
from .core.formatters import format_notes_parts, format_code_results, fmt_stats
|
|
22
22
|
|
|
23
23
|
mcp = FastMCP("cortex")
|
|
24
24
|
|
|
@@ -107,16 +107,7 @@ def _search_notes(query: str, limit: int = 5):
|
|
|
107
107
|
if not results:
|
|
108
108
|
return "No results found.", [], []
|
|
109
109
|
_stats["total_results_notes"] += len(results)
|
|
110
|
-
parts =
|
|
111
|
-
matched_files = []
|
|
112
|
-
matched_scores = []
|
|
113
|
-
for r in results:
|
|
114
|
-
p = r.payload
|
|
115
|
-
tags = p.get("tags", [])
|
|
116
|
-
tag_str = f" `{'`, `'.join(tags)}`" if tags else ""
|
|
117
|
-
parts.append(f"**{p['file']} > {p['heading']}** (score: {round(r.score, 3)}){tag_str}\n\n{p['text']}")
|
|
118
|
-
matched_files.append(p.get("file", ""))
|
|
119
|
-
matched_scores.append(r.score)
|
|
110
|
+
parts, matched_files, matched_scores = format_notes_parts(results)
|
|
120
111
|
ppr = _ppr_block(matched_files, matched_scores)
|
|
121
112
|
if ppr:
|
|
122
113
|
parts.append(ppr)
|
|
@@ -170,23 +161,7 @@ def _search_code(query: str, limit: int = 5):
|
|
|
170
161
|
{"repo": r.payload.get("repo", ""), "file": r.payload.get("file", ""), "score": round(bs, 3)}
|
|
171
162
|
for bs, r, _ in deduped
|
|
172
163
|
]
|
|
173
|
-
|
|
174
|
-
for boosted_score, r, file_meta in deduped:
|
|
175
|
-
p = r.payload
|
|
176
|
-
centrality = file_meta.get("centrality")
|
|
177
|
-
community = file_meta.get("community_id")
|
|
178
|
-
header = (
|
|
179
|
-
f"**{p['repo']}/{p['file']}** "
|
|
180
|
-
f"lines {p['start_line']}-{p['end_line']} "
|
|
181
|
-
f"({p.get('language', '')}) — score: {round(boosted_score, 3)}"
|
|
182
|
-
)
|
|
183
|
-
if centrality is not None:
|
|
184
|
-
header += f" · centrality: {centrality}"
|
|
185
|
-
if community is not None:
|
|
186
|
-
header += f" · community: {community}"
|
|
187
|
-
url = p.get("github_url", "")
|
|
188
|
-
parts.append(f"{header}\n{url}\n\n{p['text']}" if url else f"{header}\n\n{p['text']}")
|
|
189
|
-
out = "\n\n---\n\n".join(parts)
|
|
164
|
+
out = format_code_results(deduped)
|
|
190
165
|
_stats["context_tokens_code"] += len(out) // 4
|
|
191
166
|
return out, log_entries
|
|
192
167
|
|
|
@@ -290,34 +265,9 @@ def get_stats(all: bool = False) -> str:
|
|
|
290
265
|
return "No persisted stats found."
|
|
291
266
|
blocks = []
|
|
292
267
|
for v in sorted(versions):
|
|
293
|
-
|
|
294
|
-
blocks.append(_fmt_stats(v, s, current=(v == VERSION)))
|
|
268
|
+
blocks.append(fmt_stats(v, versions[v], current=(v == VERSION)))
|
|
295
269
|
return "\n\n".join(blocks)
|
|
296
|
-
return
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
def _fmt_stats(v: str, s: dict, current: bool = False) -> str:
|
|
300
|
-
total = s.get("search_code_calls", 0) + s.get("search_notes_calls", 0)
|
|
301
|
-
ppr_rate = f"{s['ppr_fires'] / s['search_notes_calls'] * 100:.1f}%" if s.get("search_notes_calls") else "—"
|
|
302
|
-
lift_avg = round(s["centrality_lift_total"] / s["centrality_lift_count"], 4) if s.get("centrality_lift_count") else 0
|
|
303
|
-
label = f"{v} (current)" if current else v
|
|
304
|
-
lines = [label]
|
|
305
|
-
started = s.get("started_at", "")
|
|
306
|
-
if current and started:
|
|
307
|
-
try:
|
|
308
|
-
delta = datetime.now(timezone.utc) - datetime.fromisoformat(started)
|
|
309
|
-
h, rem = divmod(int(delta.total_seconds()), 3600)
|
|
310
|
-
m = rem // 60
|
|
311
|
-
lines.append(f" uptime: {h}h {m}m")
|
|
312
|
-
except Exception:
|
|
313
|
-
pass
|
|
314
|
-
lines += [
|
|
315
|
-
f" searches: code={s.get('search_code_calls', 0)} notes={s.get('search_notes_calls', 0)} total={total}",
|
|
316
|
-
f" centrality lift avg: {lift_avg} (across {s.get('centrality_lift_count', 0)} results)",
|
|
317
|
-
f" PPR: {s.get('ppr_fires', 0)} fires ({ppr_rate} of note searches) · +{s.get('ppr_results_added', 0)} results",
|
|
318
|
-
f" reindexes: {s.get('reindex_count', 0)}",
|
|
319
|
-
]
|
|
320
|
-
return "\n".join(lines)
|
|
270
|
+
return fmt_stats(VERSION, _stats, current=True)
|
|
321
271
|
|
|
322
272
|
|
|
323
273
|
@mcp.tool()
|
|
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
|