interlinked-mapper 0.3.5__tar.gz → 0.3.7__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.
Files changed (45) hide show
  1. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/PKG-INFO +1 -1
  2. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/analyzer/parser.py +15 -1
  3. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/mcp_server.py +159 -35
  4. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked_mapper.egg-info/PKG-INFO +1 -1
  5. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/pyproject.toml +1 -1
  6. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/__init__.py +0 -0
  7. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/analyzer/__init__.py +0 -0
  8. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/analyzer/dead_code.py +0 -0
  9. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/analyzer/embeddings.py +0 -0
  10. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/analyzer/graph.py +0 -0
  11. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/analyzer/similarity.py +0 -0
  12. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/cli.py +0 -0
  13. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/commander/__init__.py +0 -0
  14. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/commander/llm.py +0 -0
  15. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/commander/query.py +0 -0
  16. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/commander/repl.py +0 -0
  17. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/models.py +0 -0
  18. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/__init__.py +0 -0
  19. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/frontend/dist/assets/index-CyhrxsQU.css +0 -0
  20. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/frontend/dist/assets/index-Dh01aXoE.js +0 -0
  21. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/frontend/dist/index.html +0 -0
  22. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/frontend/index.html +0 -0
  23. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/frontend/index.html.d3-legacy +0 -0
  24. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/frontend/package-lock.json +0 -0
  25. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/frontend/package.json +0 -0
  26. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/frontend/src/App.tsx +0 -0
  27. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/frontend/src/graph/GraphCanvas.tsx +0 -0
  28. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/frontend/src/graph/nodePrograms.ts +0 -0
  29. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/frontend/src/index.css +0 -0
  30. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/frontend/src/main.tsx +0 -0
  31. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/frontend/src/state/graphStore.ts +0 -0
  32. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/frontend/src/state/sseClient.ts +0 -0
  33. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/frontend/src/theme.ts +0 -0
  34. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/frontend/src/types.ts +0 -0
  35. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/frontend/src/vite-env.d.ts +0 -0
  36. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/frontend/tsconfig.json +0 -0
  37. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/frontend/vite.config.ts +0 -0
  38. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/layouts.py +0 -0
  39. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked/visualizer/server.py +0 -0
  40. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked_mapper.egg-info/SOURCES.txt +0 -0
  41. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked_mapper.egg-info/dependency_links.txt +0 -0
  42. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked_mapper.egg-info/entry_points.txt +0 -0
  43. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked_mapper.egg-info/requires.txt +0 -0
  44. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/interlinked_mapper.egg-info/top_level.txt +0 -0
  45. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: interlinked-mapper
3
- Version: 0.3.5
3
+ Version: 0.3.7
4
4
  Summary: A Python program topology explorer — visualize the shape of your codebase
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/austerecryptid/interlinked
@@ -187,7 +187,21 @@ def parse_project(root: str | Path) -> tuple[list[NodeData], list[EdgeData]]:
187
187
  nodes: list[NodeData] = []
188
188
  edges: list[EdgeData] = []
189
189
 
190
- py_files = sorted(root.rglob("*.py"))
190
+ # Skip directories that contain third-party or non-project Python files.
191
+ # External references are resolved via import/AST analysis, not by parsing venv.
192
+ _SKIP_DIRS = frozenset({
193
+ ".venv", "venv", "env", ".env",
194
+ "node_modules",
195
+ ".git",
196
+ "__pycache__",
197
+ ".tox", ".nox", ".eggs",
198
+ ".mypy_cache", ".ruff_cache", ".pytest_cache",
199
+ "build", "dist", ".build",
200
+ })
201
+ py_files = sorted(
202
+ p for p in root.rglob("*.py")
203
+ if not any(part in _SKIP_DIRS for part in p.relative_to(root).parts)
204
+ )
191
205
 
192
206
  # Pass 1: extract all symbols and raw (unresolved) edges
193
207
  trees: list[tuple[ast.Module, str, str]] = []
@@ -41,36 +41,39 @@ from interlinked.analyzer.dead_code import detect_dead_code
41
41
  from interlinked.commander.query import QueryEngine
42
42
 
43
43
 
44
- def _start_background_analysis(graph: CodeGraph, engine: QueryEngine, project_path: str) -> None:
45
- """Run similarity + embeddings in background threads (non-blocking).
44
+ def _deferred_background_work(graph: CodeGraph, engine: QueryEngine, project_path: str) -> None:
45
+ """Run similarity + embeddings in a SINGLE background thread (non-blocking).
46
46
 
47
- Mirrors the pattern used by the REST API server: parse returns fast,
48
- then similarity fingerprinting and embedding builds run in the background.
47
+ Mirrors the REST API pattern: never spawn competing CPU threads that
48
+ fight over the GIL with the main event loop. Everything heavy runs
49
+ sequentially in one thread.
49
50
  """
50
51
  import threading
51
52
 
52
- # Similarity in background thread
53
- def _run_similarity():
53
+ def _work():
54
+ # 1. Similarity fingerprinting (CPU-bound)
54
55
  try:
55
56
  from interlinked.analyzer.similarity import analyze_similarity
56
57
  analyze_similarity(graph)
57
58
  except Exception:
58
59
  pass
59
- threading.Thread(target=_run_similarity, daemon=True, name="mcp-similarity").start()
60
60
 
61
- # Embeddings in background thread (via build_async which spawns its own thread)
62
- try:
63
- from interlinked.visualizer.server import _start_embedding_build, _collect_function_sources
64
- _start_embedding_build(graph, project_path, engine)
65
- except Exception:
66
- pass
61
+ # 2. Embedding build (spawns its own single thread internally via build_async)
62
+ try:
63
+ from interlinked.visualizer.server import _start_embedding_build
64
+ _start_embedding_build(graph, project_path, engine)
65
+ except Exception:
66
+ pass
67
+
68
+ threading.Thread(target=_work, daemon=True, name="mcp-background").start()
67
69
 
68
70
 
69
71
  def build_graph(project_path: str) -> tuple[CodeGraph, QueryEngine]:
70
72
  """Parse a project and build the graph + query engine.
71
73
 
72
74
  Returns immediately after parsing + dead code detection.
73
- Similarity and embeddings run in background threads.
75
+ Similarity and embeddings are NOT started here — they are deferred
76
+ to avoid GIL contention with the caller.
74
77
  """
75
78
  nodes, edges = parse_project(project_path)
76
79
  graph = CodeGraph()
@@ -78,10 +81,6 @@ def build_graph(project_path: str) -> tuple[CodeGraph, QueryEngine]:
78
81
  detect_dead_code(graph)
79
82
 
80
83
  engine = QueryEngine(graph)
81
-
82
- # Kick off similarity + embeddings in background (non-blocking)
83
- _start_background_analysis(graph, engine, project_path)
84
-
85
84
  return graph, engine
86
85
 
87
86
 
@@ -95,21 +94,25 @@ def create_mcp_server(project_path: str) -> Server:
95
94
  # Lazy state — built on first tool call (only if no web server is running)
96
95
  _state: dict[str, Any] = {"graph": None, "engine": None, "ready": False}
97
96
 
98
- def _check_server(port: int = 8420) -> str | None:
97
+ async def _check_server(port: int = 8420) -> str | None:
99
98
  """Check if the web visualizer is running right now. Returns base URL or None."""
100
99
  url = f"http://127.0.0.1:{port}"
101
100
  try:
102
- r = httpx.get(f"{url}/api/stats", timeout=1.0)
103
- if r.status_code == 200:
104
- return url
101
+ async with httpx.AsyncClient() as client:
102
+ r = await client.get(f"{url}/api/stats", timeout=1.0)
103
+ if r.status_code == 200:
104
+ return url
105
105
  except Exception:
106
106
  pass
107
107
  return None
108
108
 
109
- def _ensure_ready() -> tuple[CodeGraph, QueryEngine]:
109
+ async def _ensure_ready() -> tuple[CodeGraph, QueryEngine]:
110
110
  if not _state["ready"]:
111
- print(f"Analyzing {project_path} ...", file=sys.stderr)
112
- graph, engine = build_graph(project_path)
111
+ # Start empty — require switch_project to load a project.
112
+ # Windsurf spawns MCP with unpredictable cwd, so we never
113
+ # auto-parse to avoid scanning / or ~.
114
+ graph = CodeGraph()
115
+ engine = QueryEngine(graph)
113
116
  _state["graph"] = graph
114
117
  _state["engine"] = engine
115
118
  _state["ready"] = True
@@ -251,7 +254,7 @@ def create_mcp_server(project_path: str) -> Server:
251
254
  ),
252
255
  Tool(
253
256
  name="interlinked_switch_project",
254
- description="Switch to analyzing a different Python project. Re-parses the new project and rebuilds the entire graph.",
257
+ description="Switch to analyzing a different Python project. Re-parses the new project and rebuilds the entire graph. Must be called before any other tool — the server starts with no project loaded.",
255
258
  inputSchema={
256
259
  "type": "object",
257
260
  "properties": {
@@ -341,7 +344,7 @@ def create_mcp_server(project_path: str) -> Server:
341
344
  try:
342
345
  # Handle ui_status first — it doesn't need graph or server proxy
343
346
  if name == "interlinked_ui_status":
344
- server_url = _check_server()
347
+ server_url = await _check_server()
345
348
  if server_url:
346
349
  result = json.dumps({
347
350
  "running": True,
@@ -358,20 +361,49 @@ def create_mcp_server(project_path: str) -> Server:
358
361
 
359
362
  # Handle start_ui — needs to spawn the server process
360
363
  if name == "interlinked_start_ui":
364
+ import asyncio
361
365
  port = arguments.get("port", 8420)
362
- result = _start_ui(project_path, port)
366
+ loop = asyncio.get_running_loop()
367
+ result = await loop.run_in_executor(None, _start_ui, project_path, port)
363
368
  return [TextContent(type="text", text=result)]
364
369
 
365
370
  # Try proxying through the running web server first
366
- server_url = _check_server()
371
+ server_url = await _check_server()
367
372
  if server_url:
368
- result = _dispatch_via_server(name, arguments, server_url)
373
+ result = await _async_dispatch_via_server(name, arguments, server_url)
369
374
  if result is not None:
370
375
  return [TextContent(type="text", text=str(result))]
371
376
 
372
- # Fall back to direct graph mode
373
- graph, engine = _ensure_ready()
374
- result = _dispatch_tool(name, arguments, engine, graph, api_key)
377
+ # switch_project: skip _ensure_ready (would double-parse), rebuild directly
378
+ import asyncio
379
+ loop = asyncio.get_running_loop()
380
+
381
+ if name == "interlinked_switch_project":
382
+ from interlinked.visualizer.server import _rebuild_graph
383
+
384
+ def _do_switch():
385
+ g = _state.get("graph")
386
+ if g is None:
387
+ g = CodeGraph()
388
+ return _rebuild_graph(arguments["path"], g, run_similarity=False), g
389
+
390
+ result_dict, graph = await loop.run_in_executor(None, _do_switch)
391
+ engine = _state.get("engine")
392
+ if engine is None:
393
+ engine = QueryEngine(graph)
394
+ engine.reset_filter()
395
+ _state["graph"] = graph
396
+ _state["engine"] = engine
397
+ _state["ready"] = True
398
+ # Deferred: single background thread AFTER response sent
399
+ _deferred_background_work(graph, engine, arguments["path"])
400
+ return [TextContent(type="text", text=json.dumps(result_dict, indent=2))]
401
+
402
+ # All other tools: ensure graph is built first
403
+ graph, engine = await _ensure_ready()
404
+ result = await loop.run_in_executor(
405
+ None, _dispatch_tool, name, arguments, engine, graph, api_key
406
+ )
375
407
  if name == "interlinked_set_api_key":
376
408
  api_key = arguments.get("api_key", "")
377
409
  os.environ["ANTHROPIC_API_KEY"] = api_key
@@ -543,6 +575,97 @@ def _dispatch_via_server(name: str, args: dict[str, Any], server_url: str) -> st
543
575
  return None
544
576
 
545
577
 
578
+ async def _async_dispatch_via_server(name: str, args: dict[str, Any], server_url: str) -> str | None:
579
+ """Async proxy — uses httpx.AsyncClient so the MCP event loop isn't blocked."""
580
+ # Reuse the same endpoint mapping
581
+ _TOOL_TO_ENDPOINT: dict[str, tuple[str, str, dict]] = {
582
+ "interlinked_stats": ("GET", "/api/stats", {}),
583
+ "interlinked_isolate": ("POST", "/api/isolate", {
584
+ "target": args.get("target", ""),
585
+ "level": args.get("level", "function"),
586
+ "depth": args.get("depth", 3),
587
+ "edge_types": args.get("edge_types"),
588
+ }),
589
+ "interlinked_zoom": ("POST", "/api/zoom", {"level": args.get("level", "module")}),
590
+ "interlinked_focus": ("POST", "/api/focus", {
591
+ "node_id": args.get("node_id", ""),
592
+ "depth": args.get("depth", 2),
593
+ }),
594
+ "interlinked_query": ("POST", "/api/query", {"expression": args.get("expression", "")}),
595
+ "interlinked_trace_variable": ("POST", "/api/trace_variable", {
596
+ "variable": args.get("variable", ""),
597
+ "origin": args.get("origin"),
598
+ }),
599
+ "interlinked_propose_function": ("POST", "/api/propose", {
600
+ "name": args.get("name", ""),
601
+ "module": args.get("module", ""),
602
+ "calls": args.get("calls"),
603
+ "called_by": args.get("called_by"),
604
+ }),
605
+ "interlinked_find_duplicates": ("POST", "/api/find_duplicates", {
606
+ "threshold": args.get("threshold", 0.6),
607
+ "scope": args.get("scope"),
608
+ "kind": args.get("kind"),
609
+ }),
610
+ "interlinked_similar_to": ("POST", "/api/similar_to", {
611
+ "target": args.get("target", ""),
612
+ "threshold": args.get("threshold", 0.5),
613
+ }),
614
+ "interlinked_get_context": ("POST", "/api/get_context", {"target": args.get("target", "")}),
615
+ "interlinked_command": ("POST", "/api/command", {"command": args.get("command", "")}),
616
+ "interlinked_switch_project": ("POST", "/api/switch_project", {"path": args.get("path", "")}),
617
+ "interlinked_edges_between": ("POST", "/api/edges_between", {
618
+ "source_scope": args.get("source_scope", ""),
619
+ "target_scope": args.get("target_scope"),
620
+ "edge_types": args.get("edge_types"),
621
+ }),
622
+ "interlinked_reachable": ("POST", "/api/reachable", {
623
+ "source": args.get("source", ""),
624
+ "target": args.get("target", ""),
625
+ "edge_types": args.get("edge_types"),
626
+ "max_depth": args.get("max_depth", 20),
627
+ }),
628
+ "interlinked_reset": ("POST", "/api/reset", {}),
629
+ "interlinked_set_context": ("POST", "/api/set_context", {
630
+ "what": args.get("what", ""),
631
+ "why": args.get("why", ""),
632
+ "where": args.get("where", ""),
633
+ }),
634
+ }
635
+
636
+ if name not in _TOOL_TO_ENDPOINT:
637
+ return None
638
+
639
+ method, path, body = _TOOL_TO_ENDPOINT[name]
640
+ url = f"{server_url}{path}"
641
+
642
+ try:
643
+ async with httpx.AsyncClient() as client:
644
+ timeout = 120.0 if "switch_project" in path else 30.0
645
+ if method == "GET":
646
+ r = await client.get(url, timeout=timeout)
647
+ else:
648
+ r = await client.post(url, json=body, timeout=timeout)
649
+ data = r.json()
650
+
651
+ if "result" in data:
652
+ result = data["result"]
653
+ if isinstance(result, (dict, list)):
654
+ return json.dumps(result, indent=2)
655
+ return str(result)
656
+ if "results" in data:
657
+ results = data["results"]
658
+ if len(results) > 20:
659
+ return f"Found {len(results)} results. Showing first 20:\n" + json.dumps(results[:20], indent=2)
660
+ return json.dumps(results, indent=2)
661
+ if "error" in data:
662
+ return f"Error: {data['error']}"
663
+ return json.dumps(data, indent=2)
664
+ except Exception as e:
665
+ print(f"Async server proxy failed for {name}: {e}", file=sys.stderr)
666
+ return None
667
+
668
+
546
669
  def _dispatch_tool(
547
670
  name: str, args: dict[str, Any],
548
671
  engine: QueryEngine, graph: CodeGraph, api_key: str,
@@ -624,8 +747,9 @@ def _dispatch_tool(
624
747
  from interlinked.visualizer.server import _rebuild_graph
625
748
  result = _rebuild_graph(args["path"], graph, run_similarity=False)
626
749
  engine.reset_filter()
627
- # Background similarity + embeddings matches REST API pattern
628
- _start_background_analysis(graph, engine, args["path"])
750
+ # Deferred: similarity + embeddings in a single background thread
751
+ # (matches REST API: never run competing CPU threads)
752
+ _deferred_background_work(graph, engine, args["path"])
629
753
  return json.dumps(result, indent=2)
630
754
 
631
755
  elif name == "interlinked_edges_between":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: interlinked-mapper
3
- Version: 0.3.5
3
+ Version: 0.3.7
4
4
  Summary: A Python program topology explorer — visualize the shape of your codebase
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/austerecryptid/interlinked
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "interlinked-mapper"
7
- version = "0.3.5"
7
+ version = "0.3.7"
8
8
  description = "A Python program topology explorer — visualize the shape of your codebase"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}