contextl 1.2.22 → 1.2.24
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.
- package/package.json +2 -2
- package/python/import_parser.py +29 -18
- package/python/mcp_server.py +4 -1
- package/python/query_engine.py +16 -8
- package/requirements.txt +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "contextl",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.24",
|
|
4
4
|
"description": "contextl — finds the most relevant files in your codebase for any change request. MCP server for AI coding agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"files": [
|
|
32
32
|
"bin/",
|
|
33
33
|
"python/",
|
|
34
|
-
"README.md"
|
|
34
|
+
"README.md", "requirements.txt"
|
|
35
35
|
],
|
|
36
36
|
|
|
37
37
|
"engines": {
|
package/python/import_parser.py
CHANGED
|
@@ -553,24 +553,35 @@ def _find_file_in_repo(
|
|
|
553
553
|
return None
|
|
554
554
|
|
|
555
555
|
if extension == ".py":
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
556
|
+
def _resolve_python(cand: str) -> str | None:
|
|
557
|
+
py_ext = cand + ".py"
|
|
558
|
+
if py_ext in file_index:
|
|
559
|
+
return py_ext
|
|
560
|
+
py_idx = cand + "/__init__.py"
|
|
561
|
+
if py_idx in file_index:
|
|
562
|
+
return py_idx
|
|
563
|
+
suffix = "/" + py_ext
|
|
564
|
+
for path in file_index:
|
|
565
|
+
if path.endswith(suffix) or path == py_ext:
|
|
566
|
+
return path
|
|
567
|
+
|
|
568
|
+
# Fallback for Python cross-language imports (e.g. from tests to .ts/.js core logic)
|
|
569
|
+
for ext in [".ts", ".tsx", ".js", ".jsx", ".java", ".go", ".rs", ".cpp", ".cc", ".cxx", ".h", ".hpp", ".c"]:
|
|
570
|
+
alt_suffix = "/" + cand + ext
|
|
571
|
+
alt_matches = [p for p in file_index if p.endswith(alt_suffix) or p == alt_suffix.lstrip("/")]
|
|
572
|
+
if len(alt_matches) == 1:
|
|
573
|
+
return alt_matches[0]
|
|
574
|
+
return None
|
|
575
|
+
|
|
576
|
+
res = _resolve_python(candidate_str)
|
|
577
|
+
if res is not None:
|
|
578
|
+
return res
|
|
579
|
+
|
|
580
|
+
# Try stripping the last component (in case it's a symbol-level import like `from a.b import C`)
|
|
581
|
+
if "/" in candidate_str:
|
|
582
|
+
stripped = candidate_str.rsplit("/", 1)[0]
|
|
583
|
+
return _resolve_python(stripped)
|
|
584
|
+
|
|
574
585
|
return None
|
|
575
586
|
for ext in [".tsx", ".ts", ".jsx", ".js"]:
|
|
576
587
|
with_ext = candidate_str + ext
|
package/python/mcp_server.py
CHANGED
|
@@ -106,7 +106,10 @@ def _get_graph(repo_path: str):
|
|
|
106
106
|
now = time.time()
|
|
107
107
|
entry = _cache.get(resolved)
|
|
108
108
|
|
|
109
|
-
# Fast path: bypass filesystem walk if checked within last 5 seconds
|
|
109
|
+
# Fast path: bypass filesystem walk if checked within last 5 seconds.
|
|
110
|
+
# This intentionally creates a 5s "blind window" where new/deleted/modified
|
|
111
|
+
# files are not instantly detected, drastically reducing disk I/O on large repos
|
|
112
|
+
# when the MCP client rapid-fires multiple queries concurrently.
|
|
110
113
|
if entry and (now - entry.last_checked_time < 5.0):
|
|
111
114
|
return entry.scan, entry.repo_graph
|
|
112
115
|
|
package/python/query_engine.py
CHANGED
|
@@ -17,6 +17,7 @@ No LLM. No embeddings. Pure text + graph.
|
|
|
17
17
|
import math
|
|
18
18
|
import re
|
|
19
19
|
from dataclasses import dataclass, field
|
|
20
|
+
from impact_analysis import _is_test_file
|
|
20
21
|
|
|
21
22
|
from scanner import scan_repo
|
|
22
23
|
from import_parser import parse_imports
|
|
@@ -93,12 +94,16 @@ def _keyword_score(query_terms: list[str], file_path: str, idf_weights: dict[str
|
|
|
93
94
|
score = 0.0
|
|
94
95
|
|
|
95
96
|
for term in query_terms:
|
|
96
|
-
# Exact match
|
|
97
|
+
# Exact match on filename
|
|
97
98
|
if term in filename_toks:
|
|
98
99
|
score += 2.0 * idf_weights.get(term, 1.0) # Massive signal
|
|
99
100
|
matched.append(term)
|
|
100
|
-
# Substring match on filename
|
|
101
|
-
elif len(term) >= 2 and any(
|
|
101
|
+
# Substring or abbreviation match on filename
|
|
102
|
+
elif len(term) >= 2 and any(
|
|
103
|
+
term in ft or ft in term or ft.startswith(term) or term.startswith(ft) or
|
|
104
|
+
all(c in iter(ft) for c in term)
|
|
105
|
+
for ft in filename_toks
|
|
106
|
+
):
|
|
102
107
|
score += 1.5 * idf_weights.get(term, 1.0)
|
|
103
108
|
matched.append(term)
|
|
104
109
|
# Exact match on path
|
|
@@ -106,8 +111,12 @@ def _keyword_score(query_terms: list[str], file_path: str, idf_weights: dict[str
|
|
|
106
111
|
score += 0.8 * idf_weights.get(term, 1.0) # Strong signal
|
|
107
112
|
if term not in matched:
|
|
108
113
|
matched.append(term)
|
|
109
|
-
# Substring match on path
|
|
110
|
-
elif len(term) >= 2 and any(
|
|
114
|
+
# Substring or abbreviation match on path
|
|
115
|
+
elif len(term) >= 2 and any(
|
|
116
|
+
term in pt or pt in term or pt.startswith(term) or term.startswith(pt) or
|
|
117
|
+
all(c in iter(pt) for c in term)
|
|
118
|
+
for pt in path_toks
|
|
119
|
+
):
|
|
111
120
|
score += 0.4 * idf_weights.get(term, 1.0)
|
|
112
121
|
if term not in matched:
|
|
113
122
|
matched.append(term)
|
|
@@ -247,7 +256,7 @@ def _content_score(query_terms: list[str], file_path: str, file_content: str, id
|
|
|
247
256
|
def _apply_neighbor_bonus(
|
|
248
257
|
scores: dict[str, float],
|
|
249
258
|
repo_graph: RepoGraph,
|
|
250
|
-
boost: float = 0.
|
|
259
|
+
boost: float = 0.2,
|
|
251
260
|
depth: int = 1,
|
|
252
261
|
) -> dict[str, float]:
|
|
253
262
|
"""
|
|
@@ -382,8 +391,7 @@ def query(
|
|
|
382
391
|
))
|
|
383
392
|
|
|
384
393
|
# Down-rank test files unless the user specifically searched for tests
|
|
385
|
-
is_test_query = "test" in
|
|
386
|
-
from impact_analysis import _is_test_file
|
|
394
|
+
is_test_query = "test" in q.lower() or "spec" in q.lower()
|
|
387
395
|
for r in results:
|
|
388
396
|
if _is_test_file(r.path) and not is_test_query:
|
|
389
397
|
r.total_score *= 0.5
|
package/requirements.txt
ADDED