contextl 1.2.43 → 1.2.45

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contextl",
3
- "version": "1.2.43",
3
+ "version": "1.2.45",
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",
@@ -2,9 +2,11 @@ import os
2
2
  import json
3
3
  import platform
4
4
  import sys
5
+ import tempfile
6
+ import shutil
5
7
  from pathlib import Path
6
8
 
7
- def install_mcp():
9
+ def install_mcp(include_vscode_workspace: bool = False):
8
10
  home = Path.home()
9
11
  system = platform.system()
10
12
 
@@ -42,6 +44,16 @@ def install_mcp():
42
44
  # 4. Cursor
43
45
  targets.append(home / ".cursor" / "mcp.json")
44
46
 
47
+ # 5. Claude Code
48
+ targets.append(home / ".claude.json")
49
+
50
+ # 6. Windsurf
51
+ targets.append(home / ".codeium" / "windsurf" / "mcp_config.json")
52
+
53
+ # 7. VS Code (Workspace level)
54
+ if include_vscode_workspace:
55
+ targets.append(Path.cwd() / ".vscode" / "mcp.json")
56
+
45
57
  success_count = 0
46
58
 
47
59
  for path in targets:
@@ -71,9 +83,14 @@ def install_mcp():
71
83
  "args": []
72
84
  }
73
85
 
74
- # Write config
75
- with open(path, "w", encoding="utf-8") as f:
86
+ # Backup existing config
87
+ shutil.copy(path, path.with_suffix(path.suffix + ".bak"))
88
+
89
+ # Write config atomically
90
+ fd, tmp_path = tempfile.mkstemp(dir=path.parent, suffix=".tmp")
91
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
76
92
  json.dump(config, f, indent=2)
93
+ os.replace(tmp_path, path)
77
94
 
78
95
  print(f"[SUCCESS] Successfully injected ContextL MCP Server into: {path}")
79
96
  success_count += 1
@@ -82,6 +82,41 @@ def _path_tokens(path: str) -> list[str]:
82
82
  tokens.extend(re.findall(r"[a-z0-9]+", sub))
83
83
  return [t for t in tokens if t not in STOP_WORDS and len(t) > 1]
84
84
 
85
+ COMMON_ABBREVS = {
86
+ "db": "database",
87
+ "auth": "authentication",
88
+ "config": "configuration",
89
+ "pkg": "package",
90
+ "app": "application",
91
+ "svc": "service",
92
+ "repo": "repository",
93
+ "mgr": "manager",
94
+ "impl": "implementation",
95
+ "dir": "directory",
96
+ "req": "request",
97
+ "res": "response",
98
+ "env": "environment",
99
+ "src": "source",
100
+ "lib": "library",
101
+ "bin": "binary",
102
+ "init": "initialize",
103
+ "util": "utility",
104
+ "utils": "utilities",
105
+ "msg": "message",
106
+ "err": "error",
107
+ "ctx": "context",
108
+ "fn": "function",
109
+ "cmd": "command",
110
+ "ctrl": "controller",
111
+ "mid": "middleware"
112
+ }
113
+
114
+ def _is_match(term: str, target: str) -> bool:
115
+ term_canon = COMMON_ABBREVS.get(term, term)
116
+ if term_canon in target or target in term_canon or target.startswith(term_canon) or term_canon.startswith(target):
117
+ return True
118
+ return False
119
+
85
120
 
86
121
  def _keyword_score(query_terms: list[str], file_path: str, idf_weights: dict[str, float]) -> tuple[float, list[str]]:
87
122
  """
@@ -100,10 +135,7 @@ def _keyword_score(query_terms: list[str], file_path: str, idf_weights: dict[str
100
135
  score += 4.0 * idf_weights.get(term, 1.0) # Massive signal
101
136
  matched.append(term)
102
137
  # Substring or abbreviation match on filename
103
- elif len(term) >= 2 and any(
104
- term in ft or ft in term or ft.startswith(term) or term.startswith(ft)
105
- for ft in filename_toks
106
- ):
138
+ elif len(term) >= 2 and any(_is_match(term, ft) for ft in filename_toks):
107
139
  score += 3.0 * idf_weights.get(term, 1.0)
108
140
  matched.append(term)
109
141
  # Exact match on path
@@ -112,10 +144,7 @@ def _keyword_score(query_terms: list[str], file_path: str, idf_weights: dict[str
112
144
  if term not in matched:
113
145
  matched.append(term)
114
146
  # 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)
117
- for pt in path_toks
118
- ):
147
+ elif len(term) >= 2 and any(_is_match(term, pt) for pt in path_toks):
119
148
  score += 1.5 * idf_weights.get(term, 1.0)
120
149
  if term not in matched:
121
150
  matched.append(term)
@@ -330,13 +359,8 @@ def query(
330
359
  path_tokens = set(_path_tokens(path))
331
360
  combined_tokens = content_tokens.union(path_tokens)
332
361
  for term in query_terms:
333
- if term in combined_tokens:
362
+ if any(_is_match(term, ct) for ct in combined_tokens):
334
363
  document_frequency[term] += 1
335
- elif len(term) >= 2:
336
- for ct in combined_tokens:
337
- if term in ct or (len(ct) >= 2 and ct in term):
338
- document_frequency[term] += 1
339
- break
340
364
 
341
365
  avgdl = total_tokens_count / total_files if total_files > 0 else 1.0
342
366