contextl 1.2.11 → 1.2.12
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 +1 -1
- package/python/main.py +9 -9
- package/python/mcp_server.py +11 -11
- package/python/{dead_code.py → standalone.py} +13 -9
package/package.json
CHANGED
package/python/main.py
CHANGED
|
@@ -198,9 +198,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
198
198
|
search_parser.add_argument("--top", "-n", type=int, default=5, help="Number of results to return (default: 5)")
|
|
199
199
|
search_parser.add_argument("--json", action="store_true", help="Output clean JSON instead of human-readable text")
|
|
200
200
|
|
|
201
|
-
# 2.
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
# 2. Standalone Files
|
|
202
|
+
standalone_parser = subparsers.add_parser("standalone", help="Find files with 0 in-degree dependencies (unimported)")
|
|
203
|
+
standalone_parser.add_argument("repo_path", help="Path to the repository root")
|
|
204
204
|
|
|
205
205
|
# 3. Impact Analysis
|
|
206
206
|
impact_parser = subparsers.add_parser("impact", help="Analyze the impact of changing a file")
|
|
@@ -218,7 +218,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
218
218
|
def main():
|
|
219
219
|
# To maintain backward compatibility with old `contextl <repo> <query>`
|
|
220
220
|
# we manually inject "search" if the first argument isn't a known command.
|
|
221
|
-
if len(sys.argv) >= 2 and sys.argv[1] not in ["search", "
|
|
221
|
+
if len(sys.argv) >= 2 and sys.argv[1] not in ["search", "standalone", "impact", "obsidian", "-h", "--help"]:
|
|
222
222
|
sys.argv.insert(1, "search")
|
|
223
223
|
|
|
224
224
|
parser = build_parser()
|
|
@@ -243,14 +243,14 @@ def main():
|
|
|
243
243
|
else:
|
|
244
244
|
print(_format_human(args.query, args.repo_path, results, repo_graph, elapsed))
|
|
245
245
|
|
|
246
|
-
elif args.command == "
|
|
247
|
-
from
|
|
246
|
+
elif args.command == "standalone":
|
|
247
|
+
from standalone import find_standalone_files
|
|
248
248
|
scan = scan_repo(args.repo_path)
|
|
249
249
|
parse = parse_imports(scan)
|
|
250
250
|
repo_graph = build_graph(scan, parse)
|
|
251
|
-
|
|
252
|
-
print(f"Found {len(
|
|
253
|
-
for d in sorted(
|
|
251
|
+
standalone = find_standalone_files(repo_graph)
|
|
252
|
+
print(f"Found {len(standalone)} standalone files:")
|
|
253
|
+
for d in sorted(standalone):
|
|
254
254
|
print(f" {d}")
|
|
255
255
|
|
|
256
256
|
elif args.command == "impact":
|
package/python/mcp_server.py
CHANGED
|
@@ -35,7 +35,7 @@ from graph_builder import build_graph
|
|
|
35
35
|
from query_engine import query as engine_query
|
|
36
36
|
from main import _confidence, _reasoning
|
|
37
37
|
from impact_analysis import analyze_impact
|
|
38
|
-
from
|
|
38
|
+
from standalone import find_standalone_files
|
|
39
39
|
from obsidian_export import export_obsidian_vault
|
|
40
40
|
|
|
41
41
|
|
|
@@ -145,11 +145,11 @@ async def list_tools() -> list[types.Tool]:
|
|
|
145
145
|
},
|
|
146
146
|
),
|
|
147
147
|
types.Tool(
|
|
148
|
-
name="
|
|
148
|
+
name="find_standalone_files",
|
|
149
149
|
description=(
|
|
150
150
|
"Find files in the repository that are never imported by any other file "
|
|
151
151
|
"(in-degree of 0 in the dependency graph). Automatically filters out "
|
|
152
|
-
"standard entry points and test files. Use this to identify
|
|
152
|
+
"standard entry points and test files. Use this to identify standalone code "
|
|
153
153
|
"and unused files that can potentially be deleted."
|
|
154
154
|
),
|
|
155
155
|
inputSchema={
|
|
@@ -203,8 +203,8 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
|
|
203
203
|
if name == "analyze_impact":
|
|
204
204
|
return await _handle_impact(arguments)
|
|
205
205
|
|
|
206
|
-
if name == "
|
|
207
|
-
return await
|
|
206
|
+
if name == "find_standalone_files":
|
|
207
|
+
return await _handle_standalone_files(arguments)
|
|
208
208
|
|
|
209
209
|
if name == "export_obsidian_vault":
|
|
210
210
|
return await _handle_export_obsidian(arguments)
|
|
@@ -321,30 +321,30 @@ def _run_impact(repo_path: str, target_file: str, max_depth: int) -> dict:
|
|
|
321
321
|
}
|
|
322
322
|
|
|
323
323
|
|
|
324
|
-
async def
|
|
324
|
+
async def _handle_standalone_files(args: dict) -> list[types.TextContent]:
|
|
325
325
|
repo_path = args.get("repo_path", "")
|
|
326
326
|
|
|
327
327
|
loop = asyncio.get_event_loop()
|
|
328
328
|
try:
|
|
329
|
-
result = await loop.run_in_executor(None,
|
|
329
|
+
result = await loop.run_in_executor(None, _run_standalone_files, repo_path)
|
|
330
330
|
except Exception as e:
|
|
331
331
|
result = {"error": str(e), "repo": repo_path}
|
|
332
332
|
|
|
333
333
|
return [types.TextContent(type="text", text=json.dumps(result, indent=2))]
|
|
334
334
|
|
|
335
335
|
|
|
336
|
-
def
|
|
336
|
+
def _run_standalone_files(repo_path: str) -> dict:
|
|
337
337
|
"""Synchronous pipeline — called from a thread executor."""
|
|
338
338
|
scan = scan_repo(repo_path)
|
|
339
339
|
parse = parse_imports(scan)
|
|
340
340
|
repo_graph = build_graph(scan, parse)
|
|
341
|
-
|
|
341
|
+
standalone_files = find_standalone_files(repo_graph)
|
|
342
342
|
|
|
343
343
|
return {
|
|
344
344
|
"repo": scan.root,
|
|
345
345
|
"total_files_scanned": scan.total_files,
|
|
346
|
-
"
|
|
347
|
-
"
|
|
346
|
+
"total_standalone_files": len(standalone_files),
|
|
347
|
+
"standalone_files": standalone_files
|
|
348
348
|
}
|
|
349
349
|
|
|
350
350
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Repository Intelligence Engine
|
|
3
|
-
Step 7:
|
|
3
|
+
Step 7: Standalone Files Analysis
|
|
4
4
|
|
|
5
5
|
Finds files in the dependency graph that have an in-degree of 0
|
|
6
6
|
(meaning they are never imported by any other file), while filtering
|
|
@@ -19,29 +19,33 @@ ENTRY_POINT_MARKERS = {
|
|
|
19
19
|
"globals.css", "global.css", "tailwind.css",
|
|
20
20
|
"index.ts", "index.js", "index.tsx", "index.jsx",
|
|
21
21
|
"main.ts", "main.js", "main.py",
|
|
22
|
+
"app.ts", "app.js", "app.tsx", "app.jsx", "app.py",
|
|
23
|
+
"mcp_server.py", "server.ts", "server.js", "server.py",
|
|
22
24
|
"__main__.py", "__init__.py", "setup.py", "conftest.py",
|
|
23
25
|
"next.config", "tailwind.config", "postcss.config", "vite.config", "webpack.config"
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
def _is_entry_point(path: str) -> bool:
|
|
27
29
|
lowered = path.lower()
|
|
30
|
+
if lowered.endswith(".d.ts"):
|
|
31
|
+
return True
|
|
28
32
|
for marker in ENTRY_POINT_MARKERS:
|
|
29
33
|
if marker in lowered:
|
|
30
34
|
return True
|
|
31
35
|
return False
|
|
32
36
|
|
|
33
|
-
def
|
|
37
|
+
def find_standalone_files(repo_graph: RepoGraph) -> list[str]:
|
|
34
38
|
"""
|
|
35
39
|
Find files with in-degree 0, excluding entry points and tests.
|
|
36
40
|
"""
|
|
37
|
-
|
|
41
|
+
standalone_files = []
|
|
38
42
|
|
|
39
43
|
for path, node in repo_graph.nodes.items():
|
|
40
44
|
if node.in_degree == 0:
|
|
41
45
|
if not _is_test_file(path) and not _is_entry_point(path):
|
|
42
|
-
|
|
46
|
+
standalone_files.append(path)
|
|
43
47
|
|
|
44
|
-
return sorted(
|
|
48
|
+
return sorted(standalone_files)
|
|
45
49
|
|
|
46
50
|
if __name__ == "__main__":
|
|
47
51
|
import sys
|
|
@@ -50,7 +54,7 @@ if __name__ == "__main__":
|
|
|
50
54
|
from graph_builder import build_graph
|
|
51
55
|
|
|
52
56
|
if len(sys.argv) < 2:
|
|
53
|
-
print("Usage: python
|
|
57
|
+
print("Usage: python standalone.py <repo_path>")
|
|
54
58
|
sys.exit(1)
|
|
55
59
|
|
|
56
60
|
repo_path = sys.argv[1]
|
|
@@ -58,8 +62,8 @@ if __name__ == "__main__":
|
|
|
58
62
|
parse = parse_imports(scan)
|
|
59
63
|
repo_graph = build_graph(scan, parse)
|
|
60
64
|
|
|
61
|
-
|
|
65
|
+
standalone = find_standalone_files(repo_graph)
|
|
62
66
|
|
|
63
|
-
print(f"Found {len(
|
|
64
|
-
for f in
|
|
67
|
+
print(f"Found {len(standalone)} standalone files:")
|
|
68
|
+
for f in standalone:
|
|
65
69
|
print(f" - {f}")
|