interlinked-mapper 0.3.5__tar.gz → 0.3.6__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.6}/PKG-INFO +1 -1
  2. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/analyzer/parser.py +15 -1
  3. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/mcp_server.py +159 -33
  4. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked_mapper.egg-info/PKG-INFO +1 -1
  5. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/pyproject.toml +1 -1
  6. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/__init__.py +0 -0
  7. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/analyzer/__init__.py +0 -0
  8. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/analyzer/dead_code.py +0 -0
  9. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/analyzer/embeddings.py +0 -0
  10. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/analyzer/graph.py +0 -0
  11. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/analyzer/similarity.py +0 -0
  12. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/cli.py +0 -0
  13. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/commander/__init__.py +0 -0
  14. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/commander/llm.py +0 -0
  15. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/commander/query.py +0 -0
  16. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/commander/repl.py +0 -0
  17. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/models.py +0 -0
  18. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/__init__.py +0 -0
  19. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/frontend/dist/assets/index-CyhrxsQU.css +0 -0
  20. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/frontend/dist/assets/index-Dh01aXoE.js +0 -0
  21. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/frontend/dist/index.html +0 -0
  22. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/frontend/index.html +0 -0
  23. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/frontend/index.html.d3-legacy +0 -0
  24. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/frontend/package-lock.json +0 -0
  25. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/frontend/package.json +0 -0
  26. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/frontend/src/App.tsx +0 -0
  27. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/frontend/src/graph/GraphCanvas.tsx +0 -0
  28. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/frontend/src/graph/nodePrograms.ts +0 -0
  29. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/frontend/src/index.css +0 -0
  30. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/frontend/src/main.tsx +0 -0
  31. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/frontend/src/state/graphStore.ts +0 -0
  32. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/frontend/src/state/sseClient.ts +0 -0
  33. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/frontend/src/theme.ts +0 -0
  34. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/frontend/src/types.ts +0 -0
  35. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/frontend/src/vite-env.d.ts +0 -0
  36. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/frontend/tsconfig.json +0 -0
  37. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/frontend/vite.config.ts +0 -0
  38. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/layouts.py +0 -0
  39. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked/visualizer/server.py +0 -0
  40. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked_mapper.egg-info/SOURCES.txt +0 -0
  41. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked_mapper.egg-info/dependency_links.txt +0 -0
  42. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked_mapper.egg-info/entry_points.txt +0 -0
  43. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked_mapper.egg-info/requires.txt +0 -0
  44. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/interlinked_mapper.egg-info/top_level.txt +0 -0
  45. {interlinked_mapper-0.3.5 → interlinked_mapper-0.3.6}/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.6
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,24 +94,30 @@ 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
+ import asyncio
111
112
  print(f"Analyzing {project_path} ...", file=sys.stderr)
112
- graph, engine = build_graph(project_path)
113
+ loop = asyncio.get_running_loop()
114
+ graph, engine = await loop.run_in_executor(None, build_graph, project_path)
113
115
  _state["graph"] = graph
114
116
  _state["engine"] = engine
115
117
  _state["ready"] = True
118
+ # Deferred: similarity + embeddings in single background thread
119
+ # Scheduled AFTER build returns so the tool response goes out first
120
+ _deferred_background_work(graph, engine, project_path)
116
121
  return _state["graph"], _state["engine"]
117
122
 
118
123
  # Pick up API key from env if available
@@ -341,7 +346,7 @@ def create_mcp_server(project_path: str) -> Server:
341
346
  try:
342
347
  # Handle ui_status first — it doesn't need graph or server proxy
343
348
  if name == "interlinked_ui_status":
344
- server_url = _check_server()
349
+ server_url = await _check_server()
345
350
  if server_url:
346
351
  result = json.dumps({
347
352
  "running": True,
@@ -358,20 +363,49 @@ def create_mcp_server(project_path: str) -> Server:
358
363
 
359
364
  # Handle start_ui — needs to spawn the server process
360
365
  if name == "interlinked_start_ui":
366
+ import asyncio
361
367
  port = arguments.get("port", 8420)
362
- result = _start_ui(project_path, port)
368
+ loop = asyncio.get_running_loop()
369
+ result = await loop.run_in_executor(None, _start_ui, project_path, port)
363
370
  return [TextContent(type="text", text=result)]
364
371
 
365
372
  # Try proxying through the running web server first
366
- server_url = _check_server()
373
+ server_url = await _check_server()
367
374
  if server_url:
368
- result = _dispatch_via_server(name, arguments, server_url)
375
+ result = await _async_dispatch_via_server(name, arguments, server_url)
369
376
  if result is not None:
370
377
  return [TextContent(type="text", text=str(result))]
371
378
 
372
- # Fall back to direct graph mode
373
- graph, engine = _ensure_ready()
374
- result = _dispatch_tool(name, arguments, engine, graph, api_key)
379
+ # switch_project: skip _ensure_ready (would double-parse), rebuild directly
380
+ import asyncio
381
+ loop = asyncio.get_running_loop()
382
+
383
+ if name == "interlinked_switch_project":
384
+ from interlinked.visualizer.server import _rebuild_graph
385
+
386
+ def _do_switch():
387
+ g = _state.get("graph")
388
+ if g is None:
389
+ g = CodeGraph()
390
+ return _rebuild_graph(arguments["path"], g, run_similarity=False), g
391
+
392
+ result_dict, graph = await loop.run_in_executor(None, _do_switch)
393
+ engine = _state.get("engine")
394
+ if engine is None:
395
+ engine = QueryEngine(graph)
396
+ engine.reset_filter()
397
+ _state["graph"] = graph
398
+ _state["engine"] = engine
399
+ _state["ready"] = True
400
+ # Deferred: single background thread AFTER response sent
401
+ _deferred_background_work(graph, engine, arguments["path"])
402
+ return [TextContent(type="text", text=json.dumps(result_dict, indent=2))]
403
+
404
+ # All other tools: ensure graph is built first
405
+ graph, engine = await _ensure_ready()
406
+ result = await loop.run_in_executor(
407
+ None, _dispatch_tool, name, arguments, engine, graph, api_key
408
+ )
375
409
  if name == "interlinked_set_api_key":
376
410
  api_key = arguments.get("api_key", "")
377
411
  os.environ["ANTHROPIC_API_KEY"] = api_key
@@ -543,6 +577,97 @@ def _dispatch_via_server(name: str, args: dict[str, Any], server_url: str) -> st
543
577
  return None
544
578
 
545
579
 
580
+ async def _async_dispatch_via_server(name: str, args: dict[str, Any], server_url: str) -> str | None:
581
+ """Async proxy — uses httpx.AsyncClient so the MCP event loop isn't blocked."""
582
+ # Reuse the same endpoint mapping
583
+ _TOOL_TO_ENDPOINT: dict[str, tuple[str, str, dict]] = {
584
+ "interlinked_stats": ("GET", "/api/stats", {}),
585
+ "interlinked_isolate": ("POST", "/api/isolate", {
586
+ "target": args.get("target", ""),
587
+ "level": args.get("level", "function"),
588
+ "depth": args.get("depth", 3),
589
+ "edge_types": args.get("edge_types"),
590
+ }),
591
+ "interlinked_zoom": ("POST", "/api/zoom", {"level": args.get("level", "module")}),
592
+ "interlinked_focus": ("POST", "/api/focus", {
593
+ "node_id": args.get("node_id", ""),
594
+ "depth": args.get("depth", 2),
595
+ }),
596
+ "interlinked_query": ("POST", "/api/query", {"expression": args.get("expression", "")}),
597
+ "interlinked_trace_variable": ("POST", "/api/trace_variable", {
598
+ "variable": args.get("variable", ""),
599
+ "origin": args.get("origin"),
600
+ }),
601
+ "interlinked_propose_function": ("POST", "/api/propose", {
602
+ "name": args.get("name", ""),
603
+ "module": args.get("module", ""),
604
+ "calls": args.get("calls"),
605
+ "called_by": args.get("called_by"),
606
+ }),
607
+ "interlinked_find_duplicates": ("POST", "/api/find_duplicates", {
608
+ "threshold": args.get("threshold", 0.6),
609
+ "scope": args.get("scope"),
610
+ "kind": args.get("kind"),
611
+ }),
612
+ "interlinked_similar_to": ("POST", "/api/similar_to", {
613
+ "target": args.get("target", ""),
614
+ "threshold": args.get("threshold", 0.5),
615
+ }),
616
+ "interlinked_get_context": ("POST", "/api/get_context", {"target": args.get("target", "")}),
617
+ "interlinked_command": ("POST", "/api/command", {"command": args.get("command", "")}),
618
+ "interlinked_switch_project": ("POST", "/api/switch_project", {"path": args.get("path", "")}),
619
+ "interlinked_edges_between": ("POST", "/api/edges_between", {
620
+ "source_scope": args.get("source_scope", ""),
621
+ "target_scope": args.get("target_scope"),
622
+ "edge_types": args.get("edge_types"),
623
+ }),
624
+ "interlinked_reachable": ("POST", "/api/reachable", {
625
+ "source": args.get("source", ""),
626
+ "target": args.get("target", ""),
627
+ "edge_types": args.get("edge_types"),
628
+ "max_depth": args.get("max_depth", 20),
629
+ }),
630
+ "interlinked_reset": ("POST", "/api/reset", {}),
631
+ "interlinked_set_context": ("POST", "/api/set_context", {
632
+ "what": args.get("what", ""),
633
+ "why": args.get("why", ""),
634
+ "where": args.get("where", ""),
635
+ }),
636
+ }
637
+
638
+ if name not in _TOOL_TO_ENDPOINT:
639
+ return None
640
+
641
+ method, path, body = _TOOL_TO_ENDPOINT[name]
642
+ url = f"{server_url}{path}"
643
+
644
+ try:
645
+ async with httpx.AsyncClient() as client:
646
+ timeout = 120.0 if "switch_project" in path else 30.0
647
+ if method == "GET":
648
+ r = await client.get(url, timeout=timeout)
649
+ else:
650
+ r = await client.post(url, json=body, timeout=timeout)
651
+ data = r.json()
652
+
653
+ if "result" in data:
654
+ result = data["result"]
655
+ if isinstance(result, (dict, list)):
656
+ return json.dumps(result, indent=2)
657
+ return str(result)
658
+ if "results" in data:
659
+ results = data["results"]
660
+ if len(results) > 20:
661
+ return f"Found {len(results)} results. Showing first 20:\n" + json.dumps(results[:20], indent=2)
662
+ return json.dumps(results, indent=2)
663
+ if "error" in data:
664
+ return f"Error: {data['error']}"
665
+ return json.dumps(data, indent=2)
666
+ except Exception as e:
667
+ print(f"Async server proxy failed for {name}: {e}", file=sys.stderr)
668
+ return None
669
+
670
+
546
671
  def _dispatch_tool(
547
672
  name: str, args: dict[str, Any],
548
673
  engine: QueryEngine, graph: CodeGraph, api_key: str,
@@ -624,8 +749,9 @@ def _dispatch_tool(
624
749
  from interlinked.visualizer.server import _rebuild_graph
625
750
  result = _rebuild_graph(args["path"], graph, run_similarity=False)
626
751
  engine.reset_filter()
627
- # Background similarity + embeddings matches REST API pattern
628
- _start_background_analysis(graph, engine, args["path"])
752
+ # Deferred: similarity + embeddings in a single background thread
753
+ # (matches REST API: never run competing CPU threads)
754
+ _deferred_background_work(graph, engine, args["path"])
629
755
  return json.dumps(result, indent=2)
630
756
 
631
757
  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.6
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.6"
8
8
  description = "A Python program topology explorer — visualize the shape of your codebase"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}